├── .githooks ├── pre-push └── pre-push.d │ └── 100-test-coverage.bash ├── .github └── workflows │ ├── go.yml │ ├── scheduled-dependency-check.yaml │ └── stale.yml ├── .gitignore ├── .golangci.yml ├── .nancy-ignore ├── .tool-versions ├── Automation_Grants.md ├── LICENSE ├── Makefile ├── OVERVIEW.md ├── README.md ├── SIMULATOR.md ├── benchmarks.txt ├── cmd └── simulator │ ├── main.go │ └── services.go ├── docs ├── LOG_TRIGGERS.md ├── PROTOCOL_v21.md ├── README.md ├── diagrams │ ├── coordinated_ticker.md │ ├── generic_ticker_sequence.md │ ├── log_trigger_ticker.md │ └── sampling_ticker.md └── images │ ├── automation_log_triggers_input.jpg │ ├── automation_log_window.jpg │ ├── automation_ocr3_block.jpg │ ├── automation_ocr3_highlevel_block.jpg │ └── conditional_coordinator_block_progression_diagram.jpg ├── go.mod ├── go.sum ├── internal └── util │ ├── array.go │ ├── array_test.go │ ├── rand.go │ ├── rand_test.go │ ├── recoverable.go │ ├── recoverable_test.go │ ├── result.go │ └── result_test.go ├── pkg ├── util │ ├── cache.go │ ├── cache_test.go │ ├── cleaner.go │ ├── worker.go │ └── worker_test.go ├── v2 │ ├── basetypes.go │ ├── config │ │ ├── config.go │ │ └── config_test.go │ ├── coordinator │ │ ├── coordinator.go │ │ ├── coordinator_test.go │ │ ├── factory.go │ │ └── mocks │ │ │ ├── encoder.generated.go │ │ │ └── log_provider.generated.go │ ├── crypto.go │ ├── delegate.go │ ├── delegate_test.go │ ├── doc.go │ ├── encode.go │ ├── encode_test.go │ ├── encoding │ │ └── basic.go │ ├── factory.go │ ├── logger.go │ ├── logger_test.go │ ├── mocks │ │ └── logger.generated.go │ ├── observation.go │ ├── observation_test.go │ ├── observer │ │ ├── polling │ │ │ ├── factory.go │ │ │ └── observer.go │ │ └── services.go │ ├── ocr.go │ ├── ratio │ │ ├── ratio.go │ │ └── ratio_test.go │ ├── runner │ │ ├── result.go │ │ └── runner.go │ ├── shuffle.go │ └── shuffle_test.go └── v3 │ ├── LICENSE │ ├── config │ ├── config.go │ └── config_test.go │ ├── coordinator │ ├── coordinator.go │ └── coordinator_test.go │ ├── fixtures │ ├── expected_encoded_observation.txt │ └── expected_encoded_outcome.txt │ ├── flows │ ├── conditional.go │ ├── conditional_test.go │ ├── factory.go │ ├── factory_test.go │ ├── logtrigger.go │ ├── logtrigger_test.go │ ├── recovery.go │ ├── recovery_test.go │ ├── retry.go │ └── retry_test.go │ ├── observation.go │ ├── observation_test.go │ ├── observer.go │ ├── observer_test.go │ ├── outcome.go │ ├── outcome_test.go │ ├── plugin │ ├── coordinated_block_proposals.go │ ├── coordinated_block_proposals_test.go │ ├── delegate.go │ ├── factory.go │ ├── hooks │ │ ├── add_block_history.go │ │ ├── add_block_history_test.go │ │ ├── add_conditional_proposals.go │ │ ├── add_conditional_proposals_test.go │ │ ├── add_from_staging.go │ │ ├── add_from_staging_test.go │ │ ├── add_log_proposals.go │ │ ├── add_log_proposals_test.go │ │ ├── add_to_proposalq.go │ │ ├── add_to_proposalq_test.go │ │ ├── remove_from_metadata.go │ │ ├── remove_from_metadata_test.go │ │ ├── remove_from_staging.go │ │ └── remove_from_staging_test.go │ ├── ocr3.go │ ├── ocr3_test.go │ ├── performable.go │ ├── performable_test.go │ └── plugin.go │ ├── postprocessors │ ├── combine.go │ ├── combine_test.go │ ├── eligible.go │ ├── eligible_test.go │ ├── ineligible.go │ ├── ineligible_test.go │ ├── metadata.go │ ├── metadata_test.go │ ├── retry.go │ └── retry_test.go │ ├── preprocessors │ ├── proposal_filterer.go │ └── proposal_filterer_test.go │ ├── prommetrics │ └── metrics.go │ ├── random │ ├── shuffler.go │ ├── shuffler_test.go │ ├── src.go │ └── src_test.go │ ├── runner │ ├── result.go │ ├── result_test.go │ ├── runner.go │ └── runner_test.go │ ├── service │ ├── recoverable.go │ └── recoverable_test.go │ ├── stores │ ├── metadata_store.go │ ├── metadata_store_test.go │ ├── proposal_queue.go │ ├── proposal_queue_test.go │ ├── result_store.go │ ├── result_store_test.go │ ├── retry_queue.go │ └── retry_queue_test.go │ ├── telemetry │ └── log.go │ ├── tickers │ ├── tick.go │ ├── time.go │ └── time_test.go │ └── types │ ├── basetypes.go │ ├── interfaces.go │ └── mocks │ ├── block_subscriber.generated.go │ ├── conditionalupkeepprovider.generated.go │ ├── coordinator.generated.go │ ├── encoder.generated.go │ ├── logeventprovider.generated.go │ ├── metadatastore.generated.go │ ├── payloadbuilder.generated.go │ ├── ratio.generated.go │ ├── recoverableprovider.generated.go │ ├── result_store.generated.go │ ├── runnable.generated.go │ ├── transmit_event_provider.generated.go │ └── upkeep_state_updater.generated.go ├── sonar-project.properties └── tools ├── simulator ├── config │ ├── duration.go │ ├── duration_test.go │ ├── event.go │ ├── keyring.go │ ├── keyring_test.go │ ├── simulation.go │ └── simulation_test.go ├── io │ ├── log.go │ └── monitor.go ├── node │ ├── active.go │ ├── add.go │ ├── group.go │ ├── report.go │ ├── statistics.go │ └── stats.go ├── plans │ ├── only_log_trigger.json │ ├── simplan_failed_rpc.json │ └── simplan_fast_check.json ├── run │ ├── output.go │ ├── profile.go │ └── runbook.go ├── simulate │ ├── chain │ │ ├── block.go │ │ ├── broadcaster.go │ │ ├── broadcaster_test.go │ │ ├── generate.go │ │ ├── generate_test.go │ │ ├── history.go │ │ ├── history_test.go │ │ ├── listener.go │ │ └── listener_test.go │ ├── db │ │ ├── ocr3.go │ │ └── upkeep.go │ ├── hydrator.go │ ├── loader │ │ ├── logtrigger.go │ │ ├── logtrigger_test.go │ │ ├── ocr3config.go │ │ ├── ocr3config_test.go │ │ ├── ocr3transmit.go │ │ ├── ocr3transmit_test.go │ │ ├── upkeep.go │ │ └── upkeep_test.go │ ├── net │ │ ├── network.go │ │ ├── network_test.go │ │ ├── service.go │ │ └── service_test.go │ ├── ocr │ │ ├── config.go │ │ ├── config_test.go │ │ ├── report.go │ │ ├── report_test.go │ │ ├── transmit.go │ │ └── transmit_test.go │ └── upkeep │ │ ├── active.go │ │ ├── active_test.go │ │ ├── log.go │ │ ├── log_test.go │ │ ├── perform.go │ │ ├── perform_test.go │ │ ├── pipeline.go │ │ ├── pipeline_test.go │ │ ├── source.go │ │ ├── source_test.go │ │ ├── util.go │ │ └── util_test.go ├── telemetry │ ├── base.go │ ├── contract.go │ ├── log.go │ ├── progress.go │ └── rpc.go └── util │ ├── encode.go │ ├── rand.go │ └── sort.go └── testprotocol ├── README.md └── modify ├── byte.go ├── byte_test.go ├── defaults.go └── struct.go /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -u 6 | 7 | # This script should be saved in a git repo as a hook file, e.g. .git/hooks/pre-push. 8 | # It looks for scripts in the .git/hooks/pre-push.d directory and executes them in order, 9 | # passing along stdin. If any script exits with a non-zero status, this script exits. 10 | 11 | script_dir=$(dirname "$0") 12 | hook_name=$(basename "$0") 13 | 14 | hooks_dir="$script_dir/$hook_name.d" 15 | 16 | if [ -d "$hooks_dir" ]; then 17 | stdin=$(cat /dev/stdin) 18 | 19 | for hook in "$hooks_dir"/*; do 20 | # echo "Running hook $hook_name/$hook" 21 | printf '%s' "$stdin" | $hook "$@" 22 | 23 | exit_code=$? 24 | 25 | if [ $exit_code != 0 ]; then 26 | exit $exit_code 27 | fi 28 | done 29 | fi 30 | 31 | exit 0 32 | -------------------------------------------------------------------------------- /.githooks/pre-push.d/100-test-coverage.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make coverage 4 | -------------------------------------------------------------------------------- /.github/workflows/scheduled-dependency-check.yaml: -------------------------------------------------------------------------------- 1 | name: Weekly Dependency Check 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 */7 * *' 6 | 7 | jobs: 8 | dependency-check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 12 | 13 | - name: Set up Go 14 | uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 15 | with: 16 | go-version-file: "go.mod" 17 | 18 | - name: Write Go Dep list 19 | run: go list -json -m all > go.list 20 | 21 | - name: Nancy Scan 22 | uses: sonatype-nexus-community/nancy-github-action@726e338312e68ecdd4b4195765f174d3b3ce1533 # v1.0.3 -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Manage stale PRs 2 | 3 | on: 4 | schedule: 5 | - cron: "30 0 * * *" # will be triggered daily at 00:30 UTC. 6 | permissions: {} 7 | jobs: 8 | stale-prs: 9 | permissions: 10 | actions: write 11 | contents: write 12 | issues: write 13 | pull-requests: write 14 | uses: smartcontractkit/.github/.github/workflows/reusable-stale-prs-issues.yml@de0ec7feedae310c287330a2bb2b9e61db035114 # 2025-06-05 15 | with: 16 | days-before-pr-stale: 30 # days 17 | days-before-pr-close: 7 # days 18 | secrets: 19 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Output of any log files generated while testing 15 | *.log 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # OS specific 21 | .DS_Store 22 | 23 | # Editor specific 24 | .vscode 25 | .idea/ 26 | 27 | # executable output folder 28 | bin/ 29 | 30 | # default simulation output folders 31 | reports/ 32 | simulation_plan_logs/ 33 | simulation_plans/ 34 | 35 | # Linter output 36 | golangci-lint/ 37 | -------------------------------------------------------------------------------- /.nancy-ignore: -------------------------------------------------------------------------------- 1 | # The go-libp2p dependency is not used directly in this plugin so the following 2 | # issue can be ignored within the scope of this repo. Container should consult 3 | # https://ossindex.sonatype.org/vulnerability/CVE-2022-23492?component-type=golang&component-name=github.com%2Flibp2p%2Fgo-libp2p&utm_source=nancy-client&utm_medium=integration&utm_content=1.0.42 4 | # for more info on the following vulnerability. 5 | CVE-2022-23492 6 | 7 | # Replaced or removed dependencies to eliminate vulnerabilities 8 | CVE-2021-3121 9 | CVE-2022-36640 10 | 11 | # Skip indirect/transitive dependencies where code path not hit, or affected library features are not utilized. 12 | CVE-2022-23328 13 | sonatype-2021-0076 14 | CVE-2022-37450 15 | sonatype-2019-0772 16 | sonatype-2022-3945 17 | CVE-2021-42219 18 | 19 | # golang.org/x/net is not used directly by anything in this repo, however 20 | # go-ethereum may use it. container applications should be cautioned 21 | CVE-2022-41723 -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.20 2 | mockery 2.43.2 3 | -------------------------------------------------------------------------------- /Automation_Grants.md: -------------------------------------------------------------------------------- 1 | Automation-License-grants 2 | 3 | Additional Use Grant(s): 4 | 5 | You may make use of Chainlink Automation (which is available subject to the license here the “Licensed Work”) solely for purposes listed below: 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2023 SmartContract ChainLink Limited SEZC 3 | 4 | Portions of this software are licensed as follows: 5 | 6 | *All content residing under (1) https://github.com/smartcontractkit/chainlink/tree/develop/contracts/src/v0.8/automation/2_1; (2) https://github.com/smartcontractkit/chainlink-automation/tree/main/pkg/v3 are licensed under “Business Source License 1.1” with a Change Date of September 12, 2027 and Change License to “MIT License” 7 | 8 | * Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined below. 9 | 10 | The MIT License (MIT) 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOBASE=$(shell pwd) 2 | GOBIN=$(GOBASE)/bin 3 | 4 | GOPACKAGES = $(shell go list ./pkg/... && go list ./internal/... && go list ./cmd/...) 5 | 6 | dependencies: 7 | go mod download 8 | 9 | generate: 10 | go generate -x $(GOPACKAGES) 11 | 12 | test: dependencies 13 | @go test -v $(GOPACKAGES) 14 | 15 | race: dependencies 16 | @go test -race $(GOPACKAGES) 17 | 18 | coverage: 19 | @go test -coverprofile cover.out $(GOPACKAGES) && \ 20 | go test github.com/smartcontractkit/chainlink-automation/pkg/v3/... -coverprofile coverV3.out -covermode count && \ 21 | go tool cover -func=coverV3.out | grep total | grep -Eo '[0-9]+\.[0-9]+' 22 | 23 | benchmark: dependencies fmt 24 | @go test $(GOPACKAGES) -bench=. -benchmem -run=^# 25 | 26 | parallel: dependencies fmt 27 | @go test github.com/smartcontractkit/chainlink-automation/internal/keepers -bench=BenchmarkCacheParallelism -benchtime 20s -mutexprofile mutex.out -run=^# 28 | 29 | simulator: dependencies fmt 30 | go build -o $(GOBIN)/simulator ./cmd/simulator/*.go || exit 31 | 32 | fmt: 33 | gofmt -w . 34 | 35 | default: build 36 | 37 | .PHONY: lint 38 | lint: ## Run golangci-lint for all issues. 39 | [ -d "./golangci-lint" ] || mkdir ./golangci-lint && \ 40 | docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:v1.56.2 golangci-lint run --max-issues-per-linter 0 --max-same-issues 0 > ./golangci-lint/$(shell date +%Y-%m-%d_%H:%M:%S).txt 41 | 42 | .PHONY: dependencies test fmt benchmark simulate 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ocr2keepers Oracle Plugin 2 | Initialize the plugin by creating a new Delegate 3 | 4 | ```go 5 | delegate, err := ocr2keepers.NewDelegate(delegateConfig) 6 | ``` 7 | 8 | ## Links 9 | 10 | - [Overview](./OVERVIEW.md) 11 | - [Simulator](./SIMULATOR.md) 12 | 13 | ## Unit Testing 14 | Unit testing is used extensively and the primary goal is to keep test coverage above 70%. 15 | 16 | Test coverage should be rerun with every commit either by running a git hook or `make coverage` to help maintain a high level of test coverage. 17 | 18 | It is recommended that you install the git hooks so that the automated tooling is part of your workflow. Simply run: 19 | 20 | ``` 21 | cp -r .githooks/ .git/hooks 22 | ``` 23 | 24 | Explore test coverage per file with 25 | ``` 26 | $ go tool cover -html=cover.out 27 | ``` 28 | 29 | ## Benchmarking 30 | Benchmarking helps identify general function inefficiencies both with memory and processor time. Only benchmark functions that are likely to run multiple times, asynchronously, or be processor/memory intensive. 31 | 32 | Using benchmarking consistently requires that os, arch, and cpu be kept consistent. Do not overwrite the `benchmark.txt` file unless your specs are identical. 33 | 34 | To run benchmarking: 35 | ``` 36 | $ make benchmark | new.txt 37 | ``` 38 | 39 | To view a diff in benchmarks: 40 | ``` 41 | $ go install golang.org/x/perf/cmd/benchstat@latest 42 | $ benchstat benchmarks.txt new.txt 43 | ``` 44 | 45 | ## Logging 46 | To reduce dependencies on the main chainlink repo, all loggers are based on the default go log.Logger. When using the NewDelegate function, a new logger is created with `[keepers-plugin] ` prepending all logs and includes short file names/numbers. This logger writes its output to the ocr logger provided to the delegate as `Debug` logs. 47 | 48 | The strategy of logging in this repo is to have two types of outcomes from logs: 49 | 1. actionable - errors and panics (which should be handled by the chainlink node itself) 50 | 2. debug info - extra log info about inner workings of the plugin (optional based on provided ocr logger settings) 51 | 52 | If an error cannot be handled, it should be bubbled up. If it cannot be bubbled up, it should panic. The plugin shouldn't be concerned with managing runtime errors, log severity, or panic recovery unless it cannot be handled by the chainlink node process. An example might be a background service that is created a plugin startup but not managed by the chainlink node. If there is such a service, it should handle its own recovery within the context of a Start/Stop service. 53 | -------------------------------------------------------------------------------- /cmd/simulator/services.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | ) 9 | 10 | func contextWithInterrupt(ctx context.Context) (context.Context, context.CancelFunc) { 11 | ctx, cancel := context.WithCancel(ctx) 12 | 13 | go func() { 14 | chSigTerm := make(chan os.Signal, 1) 15 | 16 | signal.Notify(chSigTerm, os.Interrupt, syscall.SIGTERM) 17 | 18 | ServiceLoop: 19 | for { 20 | select { 21 | case <-chSigTerm: 22 | cancel() 23 | 24 | break ServiceLoop 25 | case <-ctx.Done(): 26 | break ServiceLoop 27 | } 28 | } 29 | }() 30 | 31 | return ctx, cancel 32 | } 33 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Automation Plugin 2 | 3 | The documents in this folder describe the architecture and design of Autmation plugin. 4 | 5 | **Version:** `v2.1` 6 | 7 | ## Links 8 | 9 | - [Protocol Overview](./PROTOCOL_v21.md) 10 | - [Log Triggers](./LOG_TRIGGERS.md) 11 | - [Diagrams Source](https://miro.com/app/board/uXjVPntyh4E=/) -------------------------------------------------------------------------------- /docs/diagrams/coordinated_ticker.md: -------------------------------------------------------------------------------- 1 | # Coordinated Ticker 2 | 3 | ```mermaid 4 | sequenceDiagram 5 | participant TK as Ticker[T any] 6 | participant OB as Observer[T any] 7 | participant R as Registry 8 | %% TODO: participant Coordinator 9 | participant RN as Runner 10 | participant Q as Queue 11 | 12 | TK->>OB: Process(ctx, T) 13 | OB->>R: GetActiveUpkeeps(T) 14 | R-->>OB: []UpkeepPayload, error 15 | 16 | Note over OB,R: NOTE: GetActiveUpkeeps only returns upkeeps for type T 17 | 18 | OB->>RN: CheckUpkeeps(ctx, []UpkeepPayload) 19 | RN->>RN: makeBatches() 20 | loop for each batch execute concurrently 21 | RN->>R: CheckUpkeeps(ctx, []UpkeepPayload) 22 | R-->>RN: []CheckResult, error 23 | end 24 | RN-->>OB: []CheckResult, error 25 | 26 | OB->>OB: getLast() 27 | OB->>Q: Clear(T) 28 | Note over OB,Q: NOTE: clear all results for last tick from queue 29 | 30 | loop for each result 31 | alt is eligible 32 | OB->>Q: Add(CheckResult) 33 | end 34 | end 35 | 36 | OB->>OB: saveLast(T) 37 | ``` -------------------------------------------------------------------------------- /docs/diagrams/generic_ticker_sequence.md: -------------------------------------------------------------------------------- 1 | # Generic Ticker Sequence 2 | 3 | A generic sequence applies middleware to pre-process upkeep payloads either to 4 | filter or modify upkeep payloads before running the check pipeline on each. 5 | Finally, a single post-processing is applied with the array of results from the 6 | check pipeline. 7 | 8 | The concept of retries can also be done by routing results from the 9 | post-processor back to the pre-processing middleware and inject them back into 10 | the check process. 11 | 12 | ```mermaid 13 | sequenceDiagram 14 | participant TK as Ticker[T any] 15 | participant OB as Observer[T any] 16 | participant R as Registry 17 | participant MD as PreProcessor 18 | participant RN as Runner 19 | participant P as PostProcessor 20 | 21 | TK->>OB: Process(ctx, T) 22 | OB->>R: GetActiveUpkeeps(T) 23 | R-->>OB: []UpkeepPayload, error 24 | 25 | Note over OB,R: NOTE: GetActiveUpkeeps only returns upkeeps for type T 26 | 27 | loop for each PreProcessMiddleware 28 | OB->>MD: Run([]UpkeepPayload) 29 | MD-->>OB: []UpkeepPayload, error 30 | end 31 | 32 | OB->>RN: CheckUpkeeps(ctx, []UpkeepPayload) 33 | RN->>RN: makeBatches() 34 | loop for each batch execute concurrently 35 | RN->>R: CheckUpkeeps(ctx, []UpkeepPayload) 36 | R-->>RN: []CheckResult, error 37 | end 38 | RN-->>OB: []CheckResult, error 39 | 40 | OB->>P: Run([]CheckResult) 41 | P-->>OB: error 42 | ``` -------------------------------------------------------------------------------- /docs/diagrams/log_trigger_ticker.md: -------------------------------------------------------------------------------- 1 | # Log Trigger 2 | 3 | A ticker and observer are paired on ticker data type. The registry provides 4 | upkeep data and the check pipeline. The runner provides caching and 5 | parallelization and has the same interface as the check pipeline. The encoder 6 | determines eligibility and finally eligible results are added to a queue. 7 | 8 | On startup, an observer builds a mapping of log events to upkeeps and begins 9 | watching the registry for upkeep configuration changes or new/cancelled upkeeps. 10 | Upkeep changes not shown in the following diagram for simplicity. 11 | 12 | ```mermaid 13 | sequenceDiagram 14 | participant TK as Ticker[T any] 15 | participant OB as Observer[T any] 16 | participant R as Registry 17 | %% TODO: participant Coordinator 18 | participant RN as Runner 19 | participant Q as Queue 20 | 21 | TK->>OB: Process(ctx, T) 22 | OB->>R: GetActiveUpkeeps(T) 23 | R-->>OB: []UpkeepPayload, error 24 | 25 | Note over OB,R: NOTE: GetActiveUpkeeps only returns upkeeps for type T 26 | 27 | OB->>RN: CheckUpkeeps(ctx, []UpkeepPayload) 28 | RN->>RN: makeBatches() 29 | loop for each batch execute concurrently 30 | RN->>R: CheckUpkeeps(ctx, []UpkeepPayload) 31 | R-->>RN: []CheckResult, error 32 | end 33 | RN-->>OB: []CheckResult, error 34 | 35 | loop for each result 36 | alt is eligible 37 | OB->>Q: Add(CheckResult) 38 | end 39 | end 40 | ``` -------------------------------------------------------------------------------- /docs/diagrams/sampling_ticker.md: -------------------------------------------------------------------------------- 1 | # Sampling Ticker 2 | 3 | The sampling ticker checks a random sample of upkeeps and applies the ticker 4 | and upkeep ids to a staging queue. The plugin pulls values from this queue to 5 | achieve quorum on the results. 6 | 7 | ```mermaid 8 | sequenceDiagram 9 | participant TK as Ticker[T any] 10 | participant OB as Observer[T any] 11 | participant R as Registry 12 | participant RN as Runner 13 | participant C as Coordinator 14 | participant S as SampleStager 15 | 16 | TK->>OB: Process(ctx, T) 17 | OB->>R: GetActiveUpkeeps(T) 18 | R-->>OB: []UpkeepPayload, error 19 | 20 | Note over OB,R: NOTE: GetActiveUpkeeps only returns upkeeps for type T 21 | 22 | OB->>OB: sampleSlicePayload() 23 | 24 | OB->>RN: CheckUpkeeps(ctx, []UpkeepPayload) 25 | RN->>RN: makeBatches() 26 | loop for each batch execute concurrently 27 | RN->>R: CheckUpkeeps(ctx, []UpkeepPayload) 28 | R-->>RN: []CheckResult, error 29 | end 30 | RN-->>OB: []CheckResult, error 31 | 32 | loop for each result 33 | OB->>C: IsPending(CheckResult) 34 | C-->>OB: bool, error 35 | 36 | alt is eligible and is not pending 37 | OB->>OB: addResult(UpkeepIdentifier) 38 | end 39 | end 40 | 41 | Note over OB,S: NOTE: sampled results are pushed to the stager 42 | OB->>S: Next(T, []UpkeepResult) 43 | ``` -------------------------------------------------------------------------------- /docs/images/automation_log_triggers_input.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-automation/9adf4454bd0a39cf51f036f782f1634cb2cb3bbe/docs/images/automation_log_triggers_input.jpg -------------------------------------------------------------------------------- /docs/images/automation_log_window.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-automation/9adf4454bd0a39cf51f036f782f1634cb2cb3bbe/docs/images/automation_log_window.jpg -------------------------------------------------------------------------------- /docs/images/automation_ocr3_block.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-automation/9adf4454bd0a39cf51f036f782f1634cb2cb3bbe/docs/images/automation_ocr3_block.jpg -------------------------------------------------------------------------------- /docs/images/automation_ocr3_highlevel_block.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-automation/9adf4454bd0a39cf51f036f782f1634cb2cb3bbe/docs/images/automation_ocr3_highlevel_block.jpg -------------------------------------------------------------------------------- /docs/images/conditional_coordinator_block_progression_diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartcontractkit/chainlink-automation/9adf4454bd0a39cf51f036f782f1634cb2cb3bbe/docs/images/conditional_coordinator_block_progression_diagram.jpg -------------------------------------------------------------------------------- /internal/util/array.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "sync" 4 | 5 | type SyncedArray[T any] struct { 6 | data []T 7 | mu sync.RWMutex 8 | } 9 | 10 | func NewSyncedArray[T any]() *SyncedArray[T] { 11 | return &SyncedArray[T]{ 12 | data: []T{}, 13 | } 14 | } 15 | 16 | func (a *SyncedArray[T]) Append(vals ...T) *SyncedArray[T] { 17 | a.mu.Lock() 18 | defer a.mu.Unlock() 19 | 20 | a.data = append(a.data, vals...) 21 | return a 22 | } 23 | 24 | func (a *SyncedArray[T]) Values() []T { 25 | a.mu.RLock() 26 | defer a.mu.RUnlock() 27 | return a.data 28 | } 29 | 30 | func Unflatten[T any](b []T, size int) (groups [][]T) { 31 | for i := 0; i < len(b); i += size { 32 | j := i + size 33 | if j > len(b) { 34 | j = len(b) 35 | } 36 | groups = append(groups, b[i:j]) 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /internal/util/array_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewSyncedArray(t *testing.T) { 12 | t.Run("simple append operations update the array as expected", func(t *testing.T) { 13 | s := NewSyncedArray[int]() 14 | 15 | s.Append(1, 2, 3) 16 | 17 | assert.Equal(t, []int{1, 2, 3}, s.Values()) 18 | 19 | s.Append(4, 5, 6) 20 | 21 | assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, s.Values()) 22 | }) 23 | 24 | t.Run("parallel append operations update the array as expected", func(t *testing.T) { 25 | s := NewSyncedArray[int]() 26 | 27 | var wg sync.WaitGroup 28 | wg.Add(6) 29 | 30 | go func() { 31 | s.Append(5, 4, 3, 2) 32 | wg.Done() 33 | }() 34 | go func() { 35 | s.Append(1) 36 | wg.Done() 37 | }() 38 | go func() { 39 | s.Append(999, 999, 111) 40 | wg.Done() 41 | }() 42 | go func() { 43 | s.Append(7, 6, 5) 44 | wg.Done() 45 | }() 46 | go func() { 47 | s.Append(9, 1, 0) 48 | wg.Done() 49 | }() 50 | go func() { 51 | s.Append(4, 8, 2) 52 | wg.Done() 53 | }() 54 | 55 | wg.Wait() 56 | 57 | sort.Ints(s.Values()) 58 | 59 | assert.Equal(t, []int{0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 111, 999, 999}, s.Values()) 60 | }) 61 | } 62 | 63 | func TestUnflatten(t *testing.T) { 64 | groups := Unflatten[int]([]int{0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9, 111, 999, 999}, 3) 65 | 66 | assert.Equal(t, 6, len(groups)) 67 | assert.Equal(t, []int{0, 1, 1}, groups[0]) 68 | assert.Equal(t, []int{2, 2, 3}, groups[1]) 69 | assert.Equal(t, []int{4, 4, 5}, groups[2]) 70 | assert.Equal(t, []int{5, 6, 7}, groups[3]) 71 | assert.Equal(t, []int{8, 9, 111}, groups[4]) 72 | assert.Equal(t, []int{999, 999}, groups[5]) 73 | } 74 | -------------------------------------------------------------------------------- /internal/util/rand.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | crand "crypto/rand" 7 | "encoding/binary" 8 | "math/rand" 9 | ) 10 | 11 | var ( 12 | newCipherFn = aes.NewCipher 13 | randReadFn = crand.Read 14 | ) 15 | 16 | type keyedCryptoRandSource struct { 17 | stream cipher.Stream 18 | } 19 | 20 | func NewKeyedCryptoRandSource(key [16]byte) rand.Source { 21 | var iv [16]byte // zero IV is fine here 22 | block, err := newCipherFn(key[:]) 23 | if err != nil { 24 | // assertion 25 | panic(err) 26 | } 27 | return &keyedCryptoRandSource{cipher.NewCTR(block, iv[:])} 28 | } 29 | 30 | const int63Mask = 1<<63 - 1 31 | 32 | func (crs *keyedCryptoRandSource) Int63() int64 { 33 | var buf [8]byte 34 | crs.stream.XORKeyStream(buf[:], buf[:]) 35 | return int64(binary.LittleEndian.Uint64(buf[:]) & int63Mask) 36 | } 37 | 38 | func (crs *keyedCryptoRandSource) Seed(seed int64) { 39 | panic("keyedCryptoRandSource.Seed: Not supported") 40 | } 41 | 42 | type cryptoRandSource struct{} 43 | 44 | func NewCryptoRandSource() rand.Source { 45 | return cryptoRandSource{} 46 | } 47 | 48 | func (_ cryptoRandSource) Int63() int64 { 49 | var b [8]byte 50 | _, err := randReadFn(b[:]) 51 | if err != nil { 52 | panic(err) 53 | } 54 | return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1)) 55 | } 56 | 57 | func (_ cryptoRandSource) Seed(_ int64) { 58 | panic("cryptoRandSource.Seed: Not supported") 59 | } 60 | 61 | type Shuffler[T any] struct { 62 | Source rand.Source 63 | } 64 | 65 | func (s Shuffler[T]) Shuffle(a []T) []T { 66 | r := rand.New(s.Source) 67 | r.Shuffle(len(a), func(i, j int) { 68 | a[i], a[j] = a[j], a[i] 69 | }) 70 | return a 71 | } 72 | 73 | func ShuffleString(s string, rSrc [16]byte) string { 74 | shuffled := []rune(s) 75 | rand.New(NewKeyedCryptoRandSource(rSrc)).Shuffle(len(shuffled), func(i, j int) { 76 | shuffled[i], shuffled[j] = shuffled[j], shuffled[i] 77 | }) 78 | return string(shuffled) 79 | } 80 | -------------------------------------------------------------------------------- /internal/util/recoverable.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "runtime/debug" 8 | "sync" 9 | "time" 10 | 11 | "github.com/smartcontractkit/chainlink-common/pkg/services" 12 | ) 13 | 14 | var ( 15 | errServiceStopped = fmt.Errorf("service stopped") 16 | coolDown = 10 * time.Second 17 | ) 18 | 19 | type Doable interface { 20 | Do() error 21 | Stop() 22 | } 23 | 24 | func NewRecoverableService(svc Doable, logger *log.Logger) *RecoverableService { 25 | return &RecoverableService{ 26 | service: svc, 27 | stopped: make(chan error, 1), 28 | log: logger, 29 | stopCh: make(chan struct{}), 30 | } 31 | } 32 | 33 | type RecoverableService struct { 34 | mu sync.Mutex 35 | running bool 36 | service Doable 37 | stopped chan error 38 | log *log.Logger 39 | stopCh services.StopChan 40 | } 41 | 42 | func (m *RecoverableService) Start() { 43 | m.mu.Lock() 44 | defer m.mu.Unlock() 45 | 46 | if m.running { 47 | return 48 | } 49 | 50 | go m.serviceStart() 51 | m.run() 52 | m.running = true 53 | } 54 | 55 | func (m *RecoverableService) Stop() { 56 | m.mu.Lock() 57 | defer m.mu.Unlock() 58 | 59 | if !m.running { 60 | return 61 | } 62 | 63 | m.service.Stop() 64 | close(m.stopCh) 65 | m.running = false 66 | } 67 | 68 | func (m *RecoverableService) serviceStart() { 69 | for { 70 | select { 71 | case err := <-m.stopped: 72 | // restart the service 73 | if err != nil && errors.Is(err, errServiceStopped) { 74 | <-time.After(coolDown) 75 | m.run() 76 | } 77 | case <-m.stopCh: 78 | return 79 | } 80 | } 81 | } 82 | 83 | func (m *RecoverableService) run() { 84 | go func(s Doable, l *log.Logger, chStop chan error) { 85 | defer func() { 86 | if err := recover(); err != nil { 87 | if l != nil { 88 | l.Println(err) 89 | l.Println(string(debug.Stack())) 90 | } 91 | 92 | chStop <- errServiceStopped 93 | } 94 | }() 95 | 96 | err := s.Do() 97 | 98 | if l != nil && err != nil { 99 | l.Println(err) 100 | } 101 | 102 | chStop <- err 103 | }(m.service, m.log, m.stopped) 104 | } 105 | -------------------------------------------------------------------------------- /internal/util/recoverable_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type testService struct { 12 | DoFn func() error 13 | StopFn func() 14 | } 15 | 16 | func (t *testService) Do() error { 17 | return t.DoFn() 18 | } 19 | 20 | func (t *testService) Stop() { 21 | t.StopFn() 22 | } 23 | 24 | func TestNewRecoverableService(t *testing.T) { 25 | t.Run("creates and starts a recoverable service", func(t *testing.T) { 26 | svc := NewRecoverableService(&testService{ 27 | DoFn: func() error { 28 | return nil 29 | }, 30 | StopFn: func() { 31 | 32 | }, 33 | }, log.Default()) 34 | 35 | svc.Start() 36 | assert.True(t, svc.running) 37 | svc.Stop() 38 | assert.False(t, svc.running) 39 | }) 40 | 41 | t.Run("should not be able to start an already running service", func(t *testing.T) { 42 | svc := NewRecoverableService(&testService{ 43 | DoFn: func() error { 44 | t.Fatal("do should not be called when the service is already running") 45 | return nil 46 | }, 47 | StopFn: func() { 48 | 49 | }, 50 | }, log.Default()) 51 | 52 | svc.running = true 53 | 54 | svc.Start() 55 | assert.True(t, svc.running) 56 | svc.Stop() 57 | assert.False(t, svc.running) 58 | }) 59 | 60 | t.Run("should not be able to stop an already stopped service", func(t *testing.T) { 61 | svc := NewRecoverableService(&testService{ 62 | DoFn: func() error { 63 | return nil 64 | }, 65 | StopFn: func() { 66 | t.Fatal("do should not be called when the service is already running") 67 | }, 68 | }, log.Default()) 69 | 70 | svc.running = false 71 | 72 | svc.Stop() 73 | assert.False(t, svc.running) 74 | }) 75 | 76 | t.Run("a running service is stopped by the underlying service returning an error", func(t *testing.T) { 77 | callCount := 0 78 | oldCoolDown := coolDown 79 | coolDown = time.Millisecond * 10 80 | defer func() { 81 | coolDown = oldCoolDown 82 | }() 83 | ch := make(chan struct{}) 84 | svc := NewRecoverableService(&testService{ 85 | DoFn: func() error { 86 | callCount++ 87 | if callCount == 1 { 88 | return errServiceStopped 89 | } else if callCount > 1 { 90 | ch <- struct{}{} 91 | } 92 | return nil 93 | }, 94 | StopFn: func() { 95 | }, 96 | }, log.Default()) 97 | 98 | svc.Start() 99 | 100 | <-ch 101 | 102 | svc.Stop() 103 | 104 | assert.Equal(t, callCount, 2) 105 | }) 106 | 107 | t.Run("a running service is stopped by the underlying service causing a panic", func(t *testing.T) { 108 | callCount := 0 109 | oldCoolDown := coolDown 110 | coolDown = time.Millisecond * 10 111 | defer func() { 112 | coolDown = oldCoolDown 113 | }() 114 | ch := make(chan struct{}) 115 | svc := NewRecoverableService(&testService{ 116 | DoFn: func() error { 117 | callCount++ 118 | if callCount == 1 { 119 | panic("something worth panicking over") 120 | } else if callCount > 1 { 121 | ch <- struct{}{} 122 | } 123 | return nil 124 | }, 125 | StopFn: func() { 126 | }, 127 | }, log.Default()) 128 | 129 | svc.Start() 130 | 131 | <-ch 132 | 133 | svc.Stop() 134 | 135 | assert.Equal(t, callCount, 2) 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /internal/util/result.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type Results struct { 4 | Successes int 5 | Failures int 6 | Err error 7 | } 8 | 9 | func (r *Results) Total() int { 10 | return r.Successes + r.Failures 11 | } 12 | 13 | func (r *Results) SuccessRate() float64 { 14 | if r.Total() == 0 { 15 | return 0 16 | } 17 | 18 | return float64(r.Successes) / float64(r.Total()) 19 | } 20 | 21 | func (r *Results) FailureRate() float64 { 22 | if r.Total() == 0 { 23 | return 0 24 | } 25 | 26 | return float64(r.Failures) / float64(r.Total()) 27 | } 28 | -------------------------------------------------------------------------------- /internal/util/result_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestResults_SuccessRate(t *testing.T) { 10 | result := &Results{ 11 | Failures: 10, 12 | Successes: 90, 13 | } 14 | 15 | assert.Equal(t, result.SuccessRate(), .9) 16 | 17 | result = &Results{ 18 | Successes: 0, 19 | } 20 | 21 | assert.Equal(t, result.SuccessRate(), float64(0)) 22 | } 23 | 24 | func TestResults_FailureRate(t *testing.T) { 25 | result := &Results{ 26 | Failures: 10, 27 | Successes: 90, 28 | } 29 | 30 | assert.Equal(t, result.FailureRate(), .1) 31 | 32 | result = &Results{ 33 | Failures: 0, 34 | } 35 | 36 | assert.Equal(t, result.FailureRate(), float64(0)) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/util/cleaner.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type Cleanable[T any] interface { 9 | ClearExpired() 10 | } 11 | 12 | func NewIntervalCacheCleaner[T any](interval time.Duration) *IntervalCacheCleaner[T] { 13 | return &IntervalCacheCleaner[T]{ 14 | interval: interval, 15 | stop: make(chan struct{}), 16 | } 17 | } 18 | 19 | type IntervalCacheCleaner[T any] struct { 20 | interval time.Duration 21 | stopper sync.Once 22 | stop chan struct{} 23 | } 24 | 25 | func (ic *IntervalCacheCleaner[T]) Run(c Cleanable[T]) { 26 | ticker := time.NewTicker(ic.interval) 27 | for { 28 | select { 29 | case <-ticker.C: 30 | c.ClearExpired() 31 | case <-ic.stop: 32 | ticker.Stop() 33 | return 34 | } 35 | } 36 | } 37 | 38 | func (ic *IntervalCacheCleaner[T]) Stop() { 39 | ic.stopper.Do(func() { 40 | close(ic.stop) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/v2/basetypes.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type UpkeepIdentifier []byte 10 | 11 | type BlockKey string 12 | 13 | type UpkeepKey []byte 14 | 15 | type UpkeepResult interface{} 16 | 17 | func upkeepKeysToString(keys []UpkeepKey) string { 18 | keysStr := make([]string, len(keys)) 19 | for i, key := range keys { 20 | keysStr[i] = string(key) 21 | } 22 | 23 | return strings.Join(keysStr, ", ") 24 | } 25 | 26 | type PerformLog struct { 27 | Key UpkeepKey 28 | TransmitBlock BlockKey 29 | Confirmations int64 30 | TransactionHash string 31 | } 32 | 33 | type StaleReportLog struct { 34 | Key UpkeepKey 35 | TransmitBlock BlockKey 36 | Confirmations int64 37 | TransactionHash string 38 | } 39 | 40 | type BlockHistory []BlockKey 41 | 42 | func (bh BlockHistory) Latest() (BlockKey, error) { 43 | if len(bh) == 0 { 44 | return BlockKey(""), fmt.Errorf("empty block history") 45 | } 46 | 47 | return bh[0], nil 48 | } 49 | 50 | func (bh BlockHistory) Keys() []BlockKey { 51 | return bh 52 | } 53 | 54 | func (bh *BlockHistory) UnmarshalJSON(b []byte) error { 55 | var raw []string 56 | 57 | if err := json.Unmarshal(b, &raw); err != nil { 58 | return err 59 | } 60 | 61 | output := make([]BlockKey, len(raw)) 62 | for i, value := range raw { 63 | output[i] = BlockKey(value) 64 | } 65 | 66 | *bh = output 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/v2/coordinator/factory.go: -------------------------------------------------------------------------------- 1 | package coordinator 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | ocr2keepers "github.com/smartcontractkit/chainlink-automation/pkg/v2" 8 | "github.com/smartcontractkit/chainlink-automation/pkg/v2/config" 9 | ) 10 | 11 | // CoordinatorFactory provides a single method to create a new coordinator 12 | type CoordinatorFactory struct { 13 | Logger *log.Logger 14 | Encoder Encoder 15 | Logs LogProvider 16 | CacheClean time.Duration 17 | } 18 | 19 | // NewCoordinator returns a new coordinator with provided dependencies and 20 | // config. The new coordinator is not automatically started. 21 | func (f *CoordinatorFactory) NewCoordinator(c config.OffchainConfig) (ocr2keepers.Coordinator, error) { 22 | return NewReportCoordinator( 23 | time.Duration(c.PerformLockoutWindow)*time.Millisecond, 24 | f.CacheClean, 25 | f.Logs, 26 | c.MinConfirmations, 27 | f.Logger, 28 | f.Encoder, 29 | ), nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/v2/coordinator/mocks/log_provider.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | 10 | ocr2keepers "github.com/smartcontractkit/chainlink-automation/pkg/v2" 11 | ) 12 | 13 | // LogProvider is an autogenerated mock type for the LogProvider type 14 | type LogProvider struct { 15 | mock.Mock 16 | } 17 | 18 | // PerformLogs provides a mock function with given fields: _a0 19 | func (_m *LogProvider) PerformLogs(_a0 context.Context) ([]ocr2keepers.PerformLog, error) { 20 | ret := _m.Called(_a0) 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for PerformLogs") 24 | } 25 | 26 | var r0 []ocr2keepers.PerformLog 27 | var r1 error 28 | if rf, ok := ret.Get(0).(func(context.Context) ([]ocr2keepers.PerformLog, error)); ok { 29 | return rf(_a0) 30 | } 31 | if rf, ok := ret.Get(0).(func(context.Context) []ocr2keepers.PerformLog); ok { 32 | r0 = rf(_a0) 33 | } else { 34 | if ret.Get(0) != nil { 35 | r0 = ret.Get(0).([]ocr2keepers.PerformLog) 36 | } 37 | } 38 | 39 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 40 | r1 = rf(_a0) 41 | } else { 42 | r1 = ret.Error(1) 43 | } 44 | 45 | return r0, r1 46 | } 47 | 48 | // StaleReportLogs provides a mock function with given fields: _a0 49 | func (_m *LogProvider) StaleReportLogs(_a0 context.Context) ([]ocr2keepers.StaleReportLog, error) { 50 | ret := _m.Called(_a0) 51 | 52 | if len(ret) == 0 { 53 | panic("no return value specified for StaleReportLogs") 54 | } 55 | 56 | var r0 []ocr2keepers.StaleReportLog 57 | var r1 error 58 | if rf, ok := ret.Get(0).(func(context.Context) ([]ocr2keepers.StaleReportLog, error)); ok { 59 | return rf(_a0) 60 | } 61 | if rf, ok := ret.Get(0).(func(context.Context) []ocr2keepers.StaleReportLog); ok { 62 | r0 = rf(_a0) 63 | } else { 64 | if ret.Get(0) != nil { 65 | r0 = ret.Get(0).([]ocr2keepers.StaleReportLog) 66 | } 67 | } 68 | 69 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 70 | r1 = rf(_a0) 71 | } else { 72 | r1 = ret.Error(1) 73 | } 74 | 75 | return r0, r1 76 | } 77 | 78 | // NewLogProvider creates a new instance of LogProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 79 | // The first argument is typically a *testing.T value. 80 | func NewLogProvider(t interface { 81 | mock.TestingT 82 | Cleanup(func()) 83 | }) *LogProvider { 84 | mock := &LogProvider{} 85 | mock.Mock.Test(t) 86 | 87 | t.Cleanup(func() { mock.AssertExpectations(t) }) 88 | 89 | return mock 90 | } 91 | -------------------------------------------------------------------------------- /pkg/v2/crypto.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/smartcontractkit/libocr/offchainreporting2plus/types" 7 | "golang.org/x/crypto/sha3" 8 | ) 9 | 10 | // Generates a randomness source derived from the report timestamp (config, epoch, round) so 11 | // that it's the same across the network for the same round 12 | func getRandomKeySource(rt types.ReportTimestamp) [16]byte { 13 | // similar key building as libocr transmit selector 14 | hash := sha3.NewLegacyKeccak256() 15 | hash.Write(rt.ConfigDigest[:]) 16 | temp := make([]byte, 8) 17 | binary.LittleEndian.PutUint64(temp, uint64(rt.Epoch)) 18 | hash.Write(temp) 19 | binary.LittleEndian.PutUint64(temp, uint64(rt.Round)) 20 | hash.Write(temp) 21 | 22 | var keyRandSource [16]byte 23 | copy(keyRandSource[:], hash.Sum(nil)) 24 | return keyRandSource 25 | } 26 | -------------------------------------------------------------------------------- /pkg/v2/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ocr2keepers provides an implementation of the OCR2 oracle plugin. 3 | Sub-packages include chain specific configurations starting with EVM based 4 | chains. To create a new Delegate that can start and stop a Keepers OCR2 Oracle 5 | plugin, run the following: 6 | 7 | del, err := ocr2keepers.NewDelegate(config) 8 | 9 | # Multi-chain Support 10 | 11 | Chain specific supported can be added by providing implementations for both 12 | Registry and ReportEncoder. A registry is used to collect all upkeeps registered 13 | and to check the perform status of each. A report encoder produces a byte array 14 | from a set of upkeeps to be performed such that the resulting bytes can be 15 | transacted on chain. 16 | 17 | # Types 18 | 19 | Most types are wrappers for byte arrays, but their internal structure is 20 | important when creating new Registry and ReportEncoder implementations. It is 21 | assumed that a block on any chain has an identifier, be it numeric or textual. 22 | A BlockKey wraps a byte array for the reason that each implementation handle 23 | encoding of the block identifier internally. 24 | 25 | Likewise, an UpkeepKey is a wrapper for a byte array such that the encoded data 26 | should be the combination of both a block and upkeep id. In most chains, an 27 | upkeep id will be numeric (*big.Big), but is not required to be. It is up to 28 | the implementation for detail. The main idea is that the plugin assumes an 29 | UpkeepKey to be an upkeep id at a specific point in time on a block chain. 30 | */ 31 | package ocr2keepers 32 | -------------------------------------------------------------------------------- /pkg/v2/encode.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // encode is a convenience method that uses json encoding to 9 | // encode any value to an array of bytes 10 | // gob encoding was compared to json encoding where gob encoding 11 | // was shown to be 8x slower 12 | func encode[T any](value T) ([]byte, error) { 13 | var b bytes.Buffer 14 | 15 | if err := json.NewEncoder(&b).Encode(value); err != nil { 16 | return nil, err 17 | } 18 | 19 | return b.Bytes(), nil 20 | } 21 | 22 | // decode is a convenience method that uses json encoding to 23 | // decode any value from an array of bytes 24 | func decode[T any](b []byte, value *T) error { 25 | bts := bytes.NewReader(b) 26 | dec := json.NewDecoder(bts) 27 | return dec.Decode(value) 28 | } 29 | 30 | func limitedLengthEncode(obs Observation, limit int) ([]byte, error) { 31 | if len(obs.UpkeepIdentifiers) == 0 { 32 | return encode(obs) 33 | } 34 | 35 | var res []byte 36 | for i := range obs.UpkeepIdentifiers { 37 | b, err := encode(Observation{ 38 | BlockKey: obs.BlockKey, 39 | UpkeepIdentifiers: obs.UpkeepIdentifiers[:i+1], 40 | }) 41 | if err != nil { 42 | return nil, err 43 | } 44 | if len(b) > limit { 45 | break 46 | } 47 | res = b 48 | } 49 | 50 | return res, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/v2/encode_test.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func BenchmarkDecode(b *testing.B) { 10 | key1 := UpkeepKey([]byte("1239487928374|18768923479234987")) 11 | key2 := UpkeepKey([]byte("1239487928374|18768923479234989")) 12 | key3 := UpkeepKey([]byte("1239487928375|18768923479234987")) 13 | 14 | encoded := mustEncodeKeys([]UpkeepKey{key1, key2, key3}) 15 | 16 | b.ResetTimer() 17 | for n := 0; n < b.N; n++ { 18 | var keys []UpkeepKey 19 | 20 | b.StartTimer() 21 | err := decode(encoded, &keys) 22 | b.StopTimer() 23 | 24 | if err != nil { 25 | b.FailNow() 26 | } 27 | } 28 | } 29 | 30 | func Test_encode(t *testing.T) { 31 | t.Run("successfully encodes a string", func(t *testing.T) { 32 | b, err := encode([]string{"1", "2", "3"}) 33 | assert.Nil(t, err) 34 | // the encoder appends a new line to the output string 35 | assert.Equal(t, b, []byte(`["1","2","3"] 36 | `)) 37 | }) 38 | 39 | t.Run("fails to encode a channel", func(t *testing.T) { 40 | b, err := encode(make(chan int)) 41 | assert.NotNil(t, err) 42 | assert.Nil(t, b) 43 | }) 44 | } 45 | 46 | func mustEncodeKeys(keys []UpkeepKey) []byte { 47 | b, _ := encode(keys) 48 | return b 49 | } 50 | -------------------------------------------------------------------------------- /pkg/v2/logger.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/smartcontractkit/libocr/commontypes" 8 | "github.com/smartcontractkit/libocr/offchainreporting2plus/types" 9 | ) 10 | 11 | // Generate types from third-party repos: 12 | // 13 | //go:generate mockery --name Logger --structname MockLogger --srcpkg "github.com/smartcontractkit/libocr/commontypes" --case underscore --filename logger.generated.go 14 | 15 | type logWriter struct { 16 | l commontypes.Logger 17 | } 18 | 19 | func (l *logWriter) Write(p []byte) (n int, err error) { 20 | l.l.Debug(string(p), nil) 21 | n = len(p) 22 | return 23 | } 24 | 25 | type ocrLogContextKey struct{} 26 | 27 | type ocrLogContext struct { 28 | Epoch uint32 29 | Round uint8 30 | StartTime time.Time 31 | } 32 | 33 | func newOcrLogContext(rt types.ReportTimestamp) ocrLogContext { 34 | return ocrLogContext{ 35 | Epoch: rt.Epoch, 36 | Round: rt.Round, 37 | StartTime: time.Now(), 38 | } 39 | } 40 | 41 | func (c ocrLogContext) String() string { 42 | return fmt.Sprintf("[epoch=%d, round=%d, completion=%dms]", c.Epoch, c.Round, time.Since(c.StartTime)/time.Millisecond) 43 | } 44 | 45 | func (c ocrLogContext) Short() string { 46 | return fmt.Sprintf("[epoch=%d, round=%d]", c.Epoch, c.Round) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/v2/logger_test.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/mock" 8 | 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v2/mocks" 10 | ) 11 | 12 | func TestLogWriter(t *testing.T) { 13 | m := mocks.NewMockLogger(t) 14 | lw := &logWriter{l: m} 15 | input := []byte("test") 16 | 17 | m.On("Debug", string(input), mock.Anything) 18 | 19 | n, err := lw.Write(input) 20 | assert.NoError(t, err) 21 | assert.Equal(t, len(input), n) 22 | 23 | m.AssertExpectations(t) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/v2/mocks/logger.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | commontypes "github.com/smartcontractkit/libocr/commontypes" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockLogger is an autogenerated mock type for the Logger type 11 | type MockLogger struct { 12 | mock.Mock 13 | } 14 | 15 | // Critical provides a mock function with given fields: msg, fields 16 | func (_m *MockLogger) Critical(msg string, fields commontypes.LogFields) { 17 | _m.Called(msg, fields) 18 | } 19 | 20 | // Debug provides a mock function with given fields: msg, fields 21 | func (_m *MockLogger) Debug(msg string, fields commontypes.LogFields) { 22 | _m.Called(msg, fields) 23 | } 24 | 25 | // Error provides a mock function with given fields: msg, fields 26 | func (_m *MockLogger) Error(msg string, fields commontypes.LogFields) { 27 | _m.Called(msg, fields) 28 | } 29 | 30 | // Info provides a mock function with given fields: msg, fields 31 | func (_m *MockLogger) Info(msg string, fields commontypes.LogFields) { 32 | _m.Called(msg, fields) 33 | } 34 | 35 | // Trace provides a mock function with given fields: msg, fields 36 | func (_m *MockLogger) Trace(msg string, fields commontypes.LogFields) { 37 | _m.Called(msg, fields) 38 | } 39 | 40 | // Warn provides a mock function with given fields: msg, fields 41 | func (_m *MockLogger) Warn(msg string, fields commontypes.LogFields) { 42 | _m.Called(msg, fields) 43 | } 44 | 45 | // NewMockLogger creates a new instance of MockLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 46 | // The first argument is typically a *testing.T value. 47 | func NewMockLogger(t interface { 48 | mock.TestingT 49 | Cleanup(func()) 50 | }) *MockLogger { 51 | mock := &MockLogger{} 52 | mock.Mock.Test(t) 53 | 54 | t.Cleanup(func() { mock.AssertExpectations(t) }) 55 | 56 | return mock 57 | } 58 | -------------------------------------------------------------------------------- /pkg/v2/observer/polling/factory.go: -------------------------------------------------------------------------------- 1 | package polling 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "math/cmplx" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/smartcontractkit/libocr/offchainreporting2plus/types" 12 | 13 | ocr2keepers "github.com/smartcontractkit/chainlink-automation/pkg/v2" 14 | "github.com/smartcontractkit/chainlink-automation/pkg/v2/config" 15 | ) 16 | 17 | // PollingObserverFactory ... 18 | type PollingObserverFactory struct { 19 | Logger *log.Logger 20 | Source UpkeepProvider 21 | Heads HeadProvider 22 | Runner Runner 23 | Encoder Encoder 24 | } 25 | 26 | // NewConditionalObserver ... 27 | func (f *PollingObserverFactory) NewConditionalObserver(oc config.OffchainConfig, c types.ReportingPluginConfig, coord ocr2keepers.Coordinator) (ocr2keepers.ConditionalObserver, error) { 28 | var ( 29 | p float64 30 | err error 31 | sample sampleRatio 32 | ) 33 | 34 | p, err = strconv.ParseFloat(oc.TargetProbability, 32) 35 | if err != nil { 36 | return nil, fmt.Errorf("%w: failed to parse configured probability", err) 37 | } 38 | 39 | sample, err = sampleFromProbability(oc.TargetInRounds, c.N-c.F, float32(p)) 40 | if err != nil { 41 | return nil, fmt.Errorf("%w: failed to create plugin", err) 42 | } 43 | 44 | ob := NewPollingObserver( 45 | f.Logger, 46 | f.Source, 47 | f.Heads, 48 | f.Runner, 49 | f.Encoder, 50 | sample, 51 | time.Duration(oc.SamplingJobDuration)*time.Millisecond, 52 | coord, 53 | oc.MercuryLookup, 54 | ) 55 | 56 | return ob, nil 57 | } 58 | 59 | func sampleFromProbability(rounds, nodes int, probability float32) (sampleRatio, error) { 60 | var ratio sampleRatio 61 | 62 | if rounds <= 0 { 63 | return ratio, fmt.Errorf("number of rounds must be greater than 0") 64 | } 65 | 66 | if nodes <= 0 { 67 | return ratio, fmt.Errorf("number of nodes must be greater than 0") 68 | } 69 | 70 | if probability > 1 || probability <= 0 { 71 | return ratio, fmt.Errorf("probability must be less than 1 and greater than 0") 72 | } 73 | 74 | r := complex(float64(rounds), 0) 75 | n := complex(float64(nodes), 0) 76 | p := complex(float64(probability), 0) 77 | 78 | g := -1.0 * (p - 1.0) 79 | x := cmplx.Pow(cmplx.Pow(g, 1.0/r), 1.0/n) 80 | rat := cmplx.Abs(-1.0 * (x - 1.0)) 81 | rat = math.Round(rat/0.01) * 0.01 82 | ratio = sampleRatio(float32(rat)) 83 | 84 | return ratio, nil 85 | } 86 | 87 | type sampleRatio float32 88 | 89 | func (r sampleRatio) OfInt(count int) int { 90 | // rounds the result using basic rounding op 91 | return int(math.Round(float64(r) * float64(count))) 92 | } 93 | 94 | func (r sampleRatio) String() string { 95 | return fmt.Sprintf("%.8f", float32(r)) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/v2/observer/services.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | type SimpleService struct { 4 | F func() error 5 | C func() 6 | } 7 | 8 | func (sw *SimpleService) Do() error { 9 | return sw.F() 10 | } 11 | 12 | func (sw *SimpleService) Stop() { 13 | sw.C() 14 | } 15 | -------------------------------------------------------------------------------- /pkg/v2/ratio/ratio.go: -------------------------------------------------------------------------------- 1 | package ratio 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | type SampleRatio float32 9 | 10 | func (r SampleRatio) OfInt(count int) int { 11 | // rounds the result using basic rounding op 12 | return int(math.Round(float64(r) * float64(count))) 13 | } 14 | 15 | func (r SampleRatio) String() string { 16 | return fmt.Sprintf("%.8f", float32(r)) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/v2/ratio/ratio_test.go: -------------------------------------------------------------------------------- 1 | package ratio 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSampleRatio_OfInt(t *testing.T) { 10 | tests := []struct { 11 | Name string 12 | Ratio float32 13 | Of int 14 | ExpectedResult int 15 | }{ 16 | { 17 | Name: "30% of 100", 18 | Ratio: 0.3, 19 | Of: 100, 20 | ExpectedResult: 30, 21 | }, 22 | { 23 | Name: "33% of 10", 24 | Ratio: 0.33, 25 | Of: 10, 26 | ExpectedResult: 3, 27 | }, 28 | { 29 | Name: "Zero", 30 | Ratio: 0.3, 31 | Of: 0, 32 | ExpectedResult: 0, 33 | }, 34 | { 35 | Name: "Rounding", 36 | Ratio: 0.9, 37 | Of: 1, 38 | ExpectedResult: 1, 39 | }, 40 | { 41 | Name: "All", 42 | Ratio: 1.0, 43 | Of: 2, 44 | ExpectedResult: 2, 45 | }, 46 | } 47 | 48 | for _, test := range tests { 49 | t.Run(test.Name, func(t *testing.T) { 50 | assert.Equal(t, test.ExpectedResult, SampleRatio(test.Ratio).OfInt(test.Of)) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/v2/runner/result.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "sync" 5 | 6 | ocr2keepers "github.com/smartcontractkit/chainlink-automation/pkg/v2" 7 | ) 8 | 9 | type Result struct { 10 | // this struct type isn't expressly defined to run in a single thread or 11 | // multiple threads so internally a mutex provides the thread safety 12 | // guarantees in the case it is used in a multi-threaded way 13 | mu sync.RWMutex 14 | successes int 15 | failures int 16 | err error 17 | values []ocr2keepers.UpkeepResult 18 | } 19 | 20 | func NewResult() *Result { 21 | return &Result{ 22 | values: make([]ocr2keepers.UpkeepResult, 0), 23 | } 24 | } 25 | 26 | func (r *Result) Successes() int { 27 | r.mu.RLock() 28 | defer r.mu.RUnlock() 29 | 30 | return r.successes 31 | } 32 | 33 | func (r *Result) AddSuccesses(v int) { 34 | r.mu.Lock() 35 | defer r.mu.Unlock() 36 | 37 | r.successes += v 38 | } 39 | 40 | func (r *Result) Failures() int { 41 | r.mu.RLock() 42 | defer r.mu.RUnlock() 43 | 44 | return r.failures 45 | } 46 | 47 | func (r *Result) AddFailures(v int) { 48 | r.mu.Lock() 49 | defer r.mu.Unlock() 50 | 51 | r.failures += v 52 | } 53 | 54 | func (r *Result) Err() error { 55 | r.mu.RLock() 56 | defer r.mu.RUnlock() 57 | 58 | return r.err 59 | } 60 | 61 | func (r *Result) SetErr(err error) { 62 | r.mu.Lock() 63 | defer r.mu.Unlock() 64 | 65 | r.err = err 66 | } 67 | 68 | func (r *Result) Total() int { 69 | r.mu.RLock() 70 | defer r.mu.RUnlock() 71 | 72 | return r.successes + r.failures 73 | } 74 | 75 | func (r *Result) unsafeTotal() int { 76 | return r.successes + r.failures 77 | } 78 | 79 | func (r *Result) SuccessRate() float64 { 80 | r.mu.RLock() 81 | defer r.mu.RUnlock() 82 | 83 | if r.unsafeTotal() == 0 { 84 | return 0 85 | } 86 | 87 | return float64(r.successes) / float64(r.unsafeTotal()) 88 | } 89 | 90 | func (r *Result) FailureRate() float64 { 91 | r.mu.RLock() 92 | defer r.mu.RUnlock() 93 | 94 | if r.unsafeTotal() == 0 { 95 | return 0 96 | } 97 | 98 | return float64(r.failures) / float64(r.unsafeTotal()) 99 | } 100 | 101 | func (r *Result) Add(res ocr2keepers.UpkeepResult) { 102 | r.mu.Lock() 103 | defer r.mu.Unlock() 104 | 105 | r.values = append(r.values, res) 106 | } 107 | 108 | func (r *Result) Values() []ocr2keepers.UpkeepResult { 109 | r.mu.RLock() 110 | defer r.mu.RUnlock() 111 | 112 | return r.values 113 | } 114 | -------------------------------------------------------------------------------- /pkg/v2/shuffle.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/smartcontractkit/chainlink-automation/internal/util" 7 | ) 8 | 9 | func filterDedupeShuffleObservations(upkeepKeys [][]UpkeepKey, keyRandSource [16]byte, filters ...func(UpkeepKey) (bool, error)) ([]UpkeepKey, error) { 10 | uniqueKeys, err := filterAndDedupe(upkeepKeys, filters...) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | rand.New(util.NewKeyedCryptoRandSource(keyRandSource)).Shuffle(len(uniqueKeys), func(i, j int) { 16 | uniqueKeys[i], uniqueKeys[j] = uniqueKeys[j], uniqueKeys[i] 17 | }) 18 | 19 | return uniqueKeys, nil 20 | } 21 | 22 | func filterAndDedupe(inputs [][]UpkeepKey, filters ...func(UpkeepKey) (bool, error)) ([]UpkeepKey, error) { 23 | var max int 24 | for _, input := range inputs { 25 | max += len(input) 26 | } 27 | 28 | output := make([]UpkeepKey, 0, max) 29 | matched := make(map[string]struct{}) 30 | 31 | for _, input := range inputs { 32 | InnerLoop: 33 | for _, val := range input { 34 | for _, filter := range filters { 35 | if ok, err := filter(val); ok || err != nil { 36 | continue InnerLoop 37 | } 38 | } 39 | 40 | key := string(val) 41 | _, ok := matched[key] 42 | if !ok { 43 | matched[key] = struct{}{} 44 | output = append(output, val) 45 | } 46 | } 47 | } 48 | 49 | return output, nil 50 | } 51 | 52 | func shuffleObservations(upkeepIdentifiers []UpkeepIdentifier, source [16]byte) []UpkeepIdentifier { 53 | rand.New(util.NewKeyedCryptoRandSource(source)).Shuffle(len(upkeepIdentifiers), func(i, j int) { 54 | upkeepIdentifiers[i], upkeepIdentifiers[j] = upkeepIdentifiers[j], upkeepIdentifiers[i] 55 | }) 56 | 57 | return upkeepIdentifiers 58 | } 59 | -------------------------------------------------------------------------------- /pkg/v2/shuffle_test.go: -------------------------------------------------------------------------------- 1 | package ocr2keepers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | func TestFilterAndDedupe(t *testing.T) { 11 | inputs := [][]UpkeepKey{ 12 | {UpkeepKey("123|1234"), UpkeepKey("124|1234")}, 13 | {UpkeepKey("123|1234")}, 14 | {UpkeepKey("125|1234"), UpkeepKey("124|1235")}, 15 | } 16 | 17 | expected := []UpkeepKey{ 18 | UpkeepKey("123|1234"), 19 | UpkeepKey("124|1234"), 20 | UpkeepKey("125|1234"), 21 | UpkeepKey("124|1235"), 22 | } 23 | 24 | mf := new(MockFilter) 25 | 26 | mf.On("IsPending", mock.AnythingOfType("UpkeepKey")).Return(false, nil).Times(5) 27 | 28 | results, err := filterAndDedupe(inputs, mf.IsPending) 29 | 30 | assert.NoError(t, err) 31 | assert.Equal(t, expected, results) 32 | 33 | mf.AssertExpectations(t) 34 | } 35 | 36 | type MockFilter struct { 37 | mock.Mock 38 | } 39 | 40 | func (_m *MockFilter) IsPending(key UpkeepKey) (bool, error) { 41 | ret := _m.Called(key) 42 | 43 | var r0 bool 44 | if rf, ok := ret.Get(0).(func() bool); ok { 45 | r0 = rf() 46 | } else { 47 | if ret.Get(0) != nil { 48 | r0 = ret.Get(0).(bool) 49 | } 50 | } 51 | 52 | return r0, ret.Error(1) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/v3/flows/factory_test.go: -------------------------------------------------------------------------------- 1 | package flows 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | common "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 13 | ) 14 | 15 | func TestConditionalTriggerFlows(t *testing.T) { 16 | flows := ConditionalTriggerFlows( 17 | nil, 18 | nil, 19 | nil, 20 | &mockSubscriber{ 21 | SubscribeFn: func() (int, chan common.BlockHistory, error) { 22 | return 0, nil, nil 23 | }, 24 | }, 25 | nil, 26 | nil, 27 | nil, 28 | &mockRunner{ 29 | CheckUpkeepsFn: func(ctx context.Context, payload ...common.UpkeepPayload) ([]common.CheckResult, error) { 30 | return nil, nil 31 | }, 32 | }, 33 | nil, 34 | nil, 35 | nil, 36 | log.New(io.Discard, "", 0), 37 | ) 38 | assert.Equal(t, 2, len(flows)) 39 | } 40 | 41 | func TestLogTriggerFlows(t *testing.T) { 42 | flows := LogTriggerFlows( 43 | nil, 44 | nil, 45 | nil, 46 | &mockRunner{ 47 | CheckUpkeepsFn: func(ctx context.Context, payload ...common.UpkeepPayload) ([]common.CheckResult, error) { 48 | return nil, nil 49 | }, 50 | }, 51 | nil, 52 | nil, 53 | nil, 54 | time.Minute, 55 | time.Minute, 56 | time.Minute, 57 | nil, 58 | nil, 59 | nil, 60 | log.New(io.Discard, "", 0), 61 | ) 62 | assert.Equal(t, 3, len(flows)) 63 | } 64 | 65 | type mockRunner struct { 66 | CheckUpkeepsFn func(context.Context, ...common.UpkeepPayload) ([]common.CheckResult, error) 67 | } 68 | 69 | func (r *mockRunner) CheckUpkeeps(ctx context.Context, p ...common.UpkeepPayload) ([]common.CheckResult, error) { 70 | return r.CheckUpkeepsFn(ctx, p...) 71 | } 72 | 73 | type mockSubscriber struct { 74 | SubscribeFn func() (int, chan common.BlockHistory, error) 75 | UnsubscribeFn func(int) error 76 | StartFn func(ctx context.Context) error 77 | CloseFn func() error 78 | } 79 | 80 | func (r *mockSubscriber) Subscribe() (int, chan common.BlockHistory, error) { 81 | return r.SubscribeFn() 82 | } 83 | func (r *mockSubscriber) Unsubscribe(i int) error { 84 | return r.UnsubscribeFn(i) 85 | } 86 | func (r *mockSubscriber) Start(ctx context.Context) error { 87 | return r.StartFn(ctx) 88 | } 89 | func (r *mockSubscriber) Close() error { 90 | return r.CloseFn() 91 | } 92 | -------------------------------------------------------------------------------- /pkg/v3/flows/logtrigger.go: -------------------------------------------------------------------------------- 1 | package flows 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/postprocessors" 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/service" 12 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 13 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/tickers" 14 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 15 | common "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 16 | ) 17 | 18 | var ( 19 | ErrNotRetryable = fmt.Errorf("payload is not retryable") 20 | ) 21 | 22 | const ( 23 | // This is the ticker interval for log trigger flow 24 | LogCheckInterval = 1 * time.Second 25 | // Limit for processing a whole observer flow given a payload. The main component of this 26 | // is the checkPipeline which involves some RPC, DB and Mercury calls, this is limited 27 | // to 20 seconds for now 28 | ObservationProcessLimit = 20 * time.Second 29 | ) 30 | 31 | // log trigger flow is the happy path entry point for log triggered upkeeps 32 | func newLogTriggerFlow( 33 | preprocessors []ocr2keepersv3.PreProcessor[common.UpkeepPayload], 34 | rs types.ResultStore, 35 | rn ocr2keepersv3.Runner, 36 | logProvider common.LogEventProvider, 37 | logInterval time.Duration, 38 | retryQ types.RetryQueue, 39 | stateUpdater common.UpkeepStateUpdater, 40 | logger *log.Logger, 41 | ) service.Recoverable { 42 | post := postprocessors.NewCombinedPostprocessor( 43 | postprocessors.NewEligiblePostProcessor(rs, telemetry.WrapLogger(logger, "log-trigger-eligible-postprocessor")), 44 | postprocessors.NewRetryablePostProcessor(retryQ, telemetry.WrapLogger(logger, "log-trigger-retryable-postprocessor")), 45 | postprocessors.NewIneligiblePostProcessor(stateUpdater, telemetry.WrapLogger(logger, "retry-ineligible-postprocessor")), 46 | ) 47 | 48 | obs := ocr2keepersv3.NewRunnableObserver( 49 | preprocessors, 50 | post, 51 | rn, 52 | ObservationProcessLimit, 53 | log.New(logger.Writer(), fmt.Sprintf("[%s | log-trigger-observer]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 54 | ) 55 | 56 | timeTick := tickers.NewTimeTicker[[]common.UpkeepPayload](logInterval, obs, func(ctx context.Context, _ time.Time) (tickers.Tick[[]common.UpkeepPayload], error) { 57 | return logTick{logger: logger, logProvider: logProvider}, nil 58 | }, log.New(logger.Writer(), fmt.Sprintf("[%s | log-trigger-ticker]", telemetry.ServiceName), telemetry.LogPkgStdFlags)) 59 | 60 | return timeTick 61 | } 62 | 63 | type logTick struct { 64 | logProvider common.LogEventProvider 65 | logger *log.Logger 66 | } 67 | 68 | func (et logTick) Value(ctx context.Context) ([]common.UpkeepPayload, error) { 69 | if et.logProvider == nil { 70 | return nil, nil 71 | } 72 | 73 | logs, err := et.logProvider.GetLatestPayloads(ctx) 74 | 75 | et.logger.Printf("%d logs returned by log provider", len(logs)) 76 | 77 | return logs, err 78 | } 79 | -------------------------------------------------------------------------------- /pkg/v3/flows/logtrigger_test.go: -------------------------------------------------------------------------------- 1 | package flows 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | 14 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 15 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/service" 16 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/stores" 17 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types/mocks" 18 | common "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 19 | ) 20 | 21 | func TestLogTriggerFlow(t *testing.T) { 22 | logger := log.New(io.Discard, "", log.LstdFlags) 23 | 24 | times := 2 25 | 26 | runner := new(mocks.MockRunnable) 27 | rStore := new(mocks.MockResultStore) 28 | coord := new(mocks.MockCoordinator) 29 | retryQ := stores.NewRetryQueue(logger) 30 | upkeepStateUpdater := new(mocks.MockUpkeepStateUpdater) 31 | lp := new(mocks.MockLogEventProvider) 32 | 33 | lp.On("GetLatestPayloads", mock.Anything).Return([]common.UpkeepPayload{ 34 | { 35 | UpkeepID: common.UpkeepIdentifier([32]byte{1}), 36 | WorkID: "0x1", 37 | }, 38 | { 39 | UpkeepID: common.UpkeepIdentifier([32]byte{2}), 40 | WorkID: "0x2", 41 | }, 42 | }, nil).Times(times) 43 | coord.On("PreProcess", mock.Anything, mock.Anything).Return([]common.UpkeepPayload{ 44 | { 45 | UpkeepID: common.UpkeepIdentifier([32]byte{1}), 46 | WorkID: "0x1", 47 | }, 48 | { 49 | UpkeepID: common.UpkeepIdentifier([32]byte{2}), 50 | WorkID: "0x2", 51 | }, 52 | }, nil).Times(times) 53 | runner.On("CheckUpkeeps", mock.Anything, mock.Anything, mock.Anything).Return([]common.CheckResult{ 54 | { 55 | UpkeepID: common.UpkeepIdentifier([32]byte{1}), 56 | WorkID: "0x1", 57 | Eligible: true, 58 | }, 59 | { 60 | UpkeepID: common.UpkeepIdentifier([32]byte{2}), 61 | WorkID: "0x2", 62 | Retryable: true, 63 | }, 64 | }, nil).Times(times) 65 | // within the 3 ticks, it should retry twice and the third time it should be eligible and add to result store 66 | rStore.On("Add", mock.Anything).Times(times) 67 | upkeepStateUpdater.On("SetUpkeepState", mock.Anything, mock.Anything, mock.Anything).Return(nil) 68 | // set the ticker time lower to reduce the test time 69 | logInterval := 50 * time.Millisecond 70 | 71 | svc := newLogTriggerFlow([]ocr2keepersv3.PreProcessor[common.UpkeepPayload]{coord}, 72 | rStore, runner, lp, logInterval, retryQ, upkeepStateUpdater, logger) 73 | 74 | var wg sync.WaitGroup 75 | wg.Add(1) 76 | 77 | go func(svc service.Recoverable, ctx context.Context) { 78 | assert.NoError(t, svc.Start(ctx)) 79 | wg.Done() 80 | }(svc, context.Background()) 81 | 82 | time.Sleep(logInterval*time.Duration(times) + logInterval/2) 83 | 84 | assert.NoError(t, svc.Close(), "no error expected on shut down") 85 | 86 | lp.AssertExpectations(t) 87 | coord.AssertExpectations(t) 88 | runner.AssertExpectations(t) 89 | rStore.AssertExpectations(t) 90 | 91 | wg.Wait() 92 | } 93 | -------------------------------------------------------------------------------- /pkg/v3/flows/retry.go: -------------------------------------------------------------------------------- 1 | package flows 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/postprocessors" 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/service" 12 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 13 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/tickers" 14 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 15 | common "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 16 | ) 17 | 18 | const ( 19 | // These are the max number of payloads dequeued on every tick from the retry queue in the retry flow 20 | RetryBatchSize = 10 21 | // This is the ticker interval for retry flow 22 | RetryCheckInterval = 5 * time.Second 23 | ) 24 | 25 | func NewRetryFlow( 26 | coord ocr2keepersv3.PreProcessor[common.UpkeepPayload], 27 | resultStore types.ResultStore, 28 | runner ocr2keepersv3.Runner, 29 | retryQ types.RetryQueue, 30 | retryTickerInterval time.Duration, 31 | stateUpdater common.UpkeepStateUpdater, 32 | logger *log.Logger, 33 | ) service.Recoverable { 34 | preprocessors := []ocr2keepersv3.PreProcessor[common.UpkeepPayload]{coord} 35 | post := postprocessors.NewCombinedPostprocessor( 36 | postprocessors.NewEligiblePostProcessor(resultStore, telemetry.WrapLogger(logger, "retry-eligible-postprocessor")), 37 | postprocessors.NewRetryablePostProcessor(retryQ, telemetry.WrapLogger(logger, "retry-retryable-postprocessor")), 38 | postprocessors.NewIneligiblePostProcessor(stateUpdater, telemetry.WrapLogger(logger, "retry-ineligible-postprocessor")), 39 | ) 40 | 41 | obs := ocr2keepersv3.NewRunnableObserver( 42 | preprocessors, 43 | post, 44 | runner, 45 | ObservationProcessLimit, 46 | log.New(logger.Writer(), fmt.Sprintf("[%s | retry-observer]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 47 | ) 48 | 49 | timeTick := tickers.NewTimeTicker[[]common.UpkeepPayload](retryTickerInterval, obs, func(ctx context.Context, _ time.Time) (tickers.Tick[[]common.UpkeepPayload], error) { 50 | return retryTick{logger: logger, q: retryQ, batchSize: RetryBatchSize}, nil 51 | }, log.New(logger.Writer(), fmt.Sprintf("[%s | retry-ticker]", telemetry.ServiceName), telemetry.LogPkgStdFlags)) 52 | 53 | return timeTick 54 | } 55 | 56 | type retryTick struct { 57 | logger *log.Logger 58 | q types.RetryQueue 59 | batchSize int 60 | } 61 | 62 | func (t retryTick) Value(ctx context.Context) ([]common.UpkeepPayload, error) { 63 | if t.q == nil { 64 | return nil, nil 65 | } 66 | 67 | payloads, err := t.q.Dequeue(t.batchSize) 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to dequeue from retry queue: %w", err) 70 | } 71 | t.logger.Printf("%d payloads returned by retry queue", len(payloads)) 72 | 73 | return payloads, err 74 | } 75 | -------------------------------------------------------------------------------- /pkg/v3/flows/retry_test.go: -------------------------------------------------------------------------------- 1 | package flows 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | 14 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/service" 15 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/stores" 16 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 17 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types/mocks" 18 | common "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 19 | ) 20 | 21 | func TestRetryFlow(t *testing.T) { 22 | logger := log.New(io.Discard, "", log.LstdFlags) 23 | 24 | times := 3 25 | 26 | runner := new(mocks.MockRunnable) 27 | rStore := new(mocks.MockResultStore) 28 | coord := new(mocks.MockCoordinator) 29 | upkeepStateUpdater := new(mocks.MockUpkeepStateUpdater) 30 | retryQ := stores.NewRetryQueue(logger) 31 | 32 | coord.On("PreProcess", mock.Anything, mock.Anything).Return([]common.UpkeepPayload{ 33 | { 34 | UpkeepID: common.UpkeepIdentifier([32]byte{1}), 35 | WorkID: "0x1", 36 | }, 37 | { 38 | UpkeepID: common.UpkeepIdentifier([32]byte{2}), 39 | WorkID: "0x2", 40 | }, 41 | }, nil).Times(times) 42 | runner.On("CheckUpkeeps", mock.Anything, mock.Anything, mock.Anything).Return([]common.CheckResult{ 43 | { 44 | UpkeepID: common.UpkeepIdentifier([32]byte{1}), 45 | WorkID: "0x1", 46 | Eligible: true, 47 | }, 48 | { 49 | UpkeepID: common.UpkeepIdentifier([32]byte{2}), 50 | WorkID: "0x2", 51 | Retryable: true, 52 | }, 53 | }, nil).Times(times) 54 | // within the 3 ticks, it should retry twice and the third time it should be eligible and add to result store 55 | rStore.On("Add", mock.Anything).Times(times) 56 | upkeepStateUpdater.On("SetUpkeepState", mock.Anything, mock.Anything, mock.Anything).Return(nil) 57 | // set the ticker time lower to reduce the test time 58 | retryInterval := 50 * time.Millisecond 59 | 60 | svc := NewRetryFlow(coord, rStore, runner, retryQ, retryInterval, upkeepStateUpdater, logger) 61 | 62 | var wg sync.WaitGroup 63 | wg.Add(1) 64 | 65 | err := retryQ.Enqueue(types.RetryRecord{ 66 | Payload: common.UpkeepPayload{ 67 | UpkeepID: common.UpkeepIdentifier([32]byte{1}), 68 | WorkID: "0x1", 69 | }, 70 | }, types.RetryRecord{ 71 | Payload: common.UpkeepPayload{ 72 | UpkeepID: common.UpkeepIdentifier([32]byte{2}), 73 | WorkID: "0x2", 74 | }, 75 | }) 76 | assert.NoError(t, err) 77 | 78 | go func(svc service.Recoverable, ctx context.Context) { 79 | assert.NoError(t, svc.Start(ctx)) 80 | wg.Done() 81 | }(svc, context.Background()) 82 | 83 | time.Sleep(retryInterval*time.Duration(times) + retryInterval/2) 84 | 85 | assert.NoError(t, svc.Close(), "no error expected on shut down") 86 | 87 | coord.AssertExpectations(t) 88 | runner.AssertExpectations(t) 89 | rStore.AssertExpectations(t) 90 | 91 | wg.Wait() 92 | } 93 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/add_block_history.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 8 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 10 | ) 11 | 12 | type AddBlockHistoryHook struct { 13 | metadata types.MetadataStore 14 | logger *log.Logger 15 | } 16 | 17 | func NewAddBlockHistoryHook(ms types.MetadataStore, logger *log.Logger) AddBlockHistoryHook { 18 | return AddBlockHistoryHook{ 19 | metadata: ms, 20 | logger: log.New(logger.Writer(), fmt.Sprintf("[%s | build hook:add-block-history]", telemetry.ServiceName), telemetry.LogPkgStdFlags)} 21 | } 22 | 23 | func (h *AddBlockHistoryHook) RunHook(obs *ocr2keepersv3.AutomationObservation, limit int) { 24 | blockHistory := h.metadata.GetBlockHistory() 25 | if len(blockHistory) > limit { 26 | blockHistory = blockHistory[:limit] 27 | } 28 | obs.BlockHistory = blockHistory 29 | h.logger.Printf("adding %d blocks to observation", len(blockHistory)) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/add_block_history_test.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types/mocks" 12 | types "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 13 | ) 14 | 15 | func TestAddBlockHistoryHook_RunHook(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | existingBlocks types.BlockHistory 19 | blockHistory types.BlockHistory 20 | limit int 21 | expectedOutput types.BlockHistory 22 | }{ 23 | { 24 | name: "Add block history to observation", 25 | blockHistory: types.BlockHistory{ 26 | {Number: 1}, 27 | {Number: 2}, 28 | {Number: 3}, 29 | }, 30 | limit: 10, 31 | expectedOutput: types.BlockHistory{{Number: 1}, {Number: 2}, {Number: 3}}, 32 | }, 33 | { 34 | name: "Empty block history", 35 | blockHistory: types.BlockHistory{}, 36 | limit: 10, 37 | expectedOutput: types.BlockHistory{}, 38 | }, 39 | { 40 | name: "Overwrites existing block history", 41 | existingBlocks: types.BlockHistory{ 42 | {Number: 1}, 43 | {Number: 2}, 44 | {Number: 3}, 45 | }, 46 | blockHistory: types.BlockHistory{}, 47 | limit: 10, 48 | expectedOutput: types.BlockHistory{}, 49 | }, 50 | { 51 | name: "limits blocks added", 52 | existingBlocks: types.BlockHistory{}, 53 | blockHistory: types.BlockHistory{ 54 | {Number: 1}, 55 | {Number: 2}, 56 | {Number: 3}, 57 | }, 58 | limit: 2, 59 | expectedOutput: types.BlockHistory{ 60 | {Number: 1}, 61 | {Number: 2}, 62 | }, 63 | }, 64 | } 65 | 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | // Prepare mock MetadataStore 69 | mockMetadataStore := &mocks.MockMetadataStore{} 70 | mockMetadataStore.On("GetBlockHistory").Return(tt.blockHistory) 71 | 72 | // Prepare logger 73 | var logBuf bytes.Buffer 74 | logger := log.New(&logBuf, "", 0) 75 | 76 | // Create the hook with mock MetadataStore and logger 77 | addBlockHistoryHook := NewAddBlockHistoryHook(mockMetadataStore, logger) 78 | 79 | // Prepare automation observation 80 | obs := &ocr2keepersv3.AutomationObservation{ 81 | BlockHistory: tt.existingBlocks, 82 | } 83 | 84 | // Run the hook 85 | addBlockHistoryHook.RunHook(obs, tt.limit) 86 | 87 | // Assert that the observation's BlockHistory matches the expected output 88 | assert.Equal(t, tt.expectedOutput, obs.BlockHistory) 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/add_conditional_proposals.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | 8 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/random" 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 12 | ) 13 | 14 | type AddConditionalProposalsHook struct { 15 | metadata types.MetadataStore 16 | logger *log.Logger 17 | coord types.Coordinator 18 | } 19 | 20 | func NewAddConditionalProposalsHook(ms types.MetadataStore, coord types.Coordinator, logger *log.Logger) AddConditionalProposalsHook { 21 | return AddConditionalProposalsHook{ 22 | metadata: ms, 23 | coord: coord, 24 | logger: log.New(logger.Writer(), fmt.Sprintf("[%s | build hook:add-conditional-samples]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 25 | } 26 | } 27 | 28 | func (h *AddConditionalProposalsHook) RunHook(obs *ocr2keepersv3.AutomationObservation, limit int, rSrc [16]byte) error { 29 | conditionals := h.metadata.ViewProposals(types.ConditionTrigger) 30 | 31 | var err error 32 | conditionals, err = h.coord.FilterProposals(conditionals) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | // Do random shuffling. Sorting isn't done here as we don't require multiple nodes 38 | // to agree on the same proposal, hence each node just sends a random subset of its proposals 39 | rand.New(random.NewKeyedCryptoRandSource(rSrc)).Shuffle(len(conditionals), func(i, j int) { 40 | conditionals[i], conditionals[j] = conditionals[j], conditionals[i] 41 | }) 42 | 43 | // take first limit 44 | if len(conditionals) > limit { 45 | conditionals = conditionals[:limit] 46 | } 47 | 48 | h.logger.Printf("adding %d conditional proposals to observation", len(conditionals)) 49 | obs.UpkeepProposals = append(obs.UpkeepProposals, conditionals...) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/add_log_proposals.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | 8 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/random" 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 12 | ) 13 | 14 | type AddLogProposalsHook struct { 15 | metadata types.MetadataStore 16 | coordinator types.Coordinator 17 | logger *log.Logger 18 | } 19 | 20 | func NewAddLogProposalsHook(metadataStore types.MetadataStore, coordinator types.Coordinator, logger *log.Logger) AddLogProposalsHook { 21 | return AddLogProposalsHook{ 22 | metadata: metadataStore, 23 | coordinator: coordinator, 24 | logger: log.New(logger.Writer(), fmt.Sprintf("[%s | build hook:add-log-recovery-proposals]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 25 | } 26 | } 27 | 28 | func (h *AddLogProposalsHook) RunHook(obs *ocr2keepersv3.AutomationObservation, limit int, rSrc [16]byte) error { 29 | proposals := h.metadata.ViewProposals(types.LogTrigger) 30 | 31 | var err error 32 | proposals, err = h.coordinator.FilterProposals(proposals) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | // Do random shuffling. Sorting isn't done here as we don't require multiple nodes 38 | // to agree on the same proposal, hence each node just sends a random subset of its proposals 39 | rand.New(random.NewKeyedCryptoRandSource(rSrc)).Shuffle(len(proposals), func(i, j int) { 40 | proposals[i], proposals[j] = proposals[j], proposals[i] 41 | }) 42 | 43 | // take first limit 44 | if len(proposals) > limit { 45 | proposals = proposals[:limit] 46 | } 47 | 48 | h.logger.Printf("adding %d log recovery proposals to observation", len(proposals)) 49 | obs.UpkeepProposals = append(obs.UpkeepProposals, proposals...) 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/add_to_proposalq.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 8 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 10 | ) 11 | 12 | func NewAddToProposalQHook(proposalQ types.ProposalQueue, logger *log.Logger) AddToProposalQHook { 13 | return AddToProposalQHook{ 14 | proposalQ: proposalQ, 15 | logger: log.New(logger.Writer(), fmt.Sprintf("[%s | pre-build hook:add-to-proposalq]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 16 | } 17 | } 18 | 19 | type AddToProposalQHook struct { 20 | proposalQ types.ProposalQueue 21 | logger *log.Logger 22 | } 23 | 24 | func (hook *AddToProposalQHook) RunHook(outcome ocr2keepersv3.AutomationOutcome) { 25 | addedProposals := 0 26 | for _, roundProposals := range outcome.SurfacedProposals { 27 | err := hook.proposalQ.Enqueue(roundProposals...) 28 | if err != nil { 29 | // Do not return error, just log and skip this round's proposals 30 | hook.logger.Printf("Error adding proposals to queue: %v", err) 31 | continue 32 | } 33 | addedProposals += len(roundProposals) 34 | } 35 | hook.logger.Printf("Added %d proposals from outcome", addedProposals) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/add_to_proposalq_test.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/stores" 12 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 13 | commontypes "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 14 | ) 15 | 16 | func TestAddToProposalQHook_RunHook(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | automationOutcome ocr2keepersv3.AutomationOutcome 20 | expectedQueueSize int 21 | expectedLog string 22 | }{ 23 | { 24 | name: "Happy path add proposals to queue", 25 | automationOutcome: ocr2keepersv3.AutomationOutcome{ 26 | SurfacedProposals: [][]commontypes.CoordinatedBlockProposal{ 27 | {{WorkID: "1"}, {WorkID: "2"}}, 28 | {{WorkID: "3"}}, 29 | }, 30 | }, 31 | expectedQueueSize: 3, 32 | expectedLog: "Added 3 proposals from outcome", 33 | }, 34 | { 35 | name: "Empty automation outcome", 36 | automationOutcome: ocr2keepersv3.AutomationOutcome{ 37 | SurfacedProposals: [][]commontypes.CoordinatedBlockProposal{}, 38 | }, 39 | expectedQueueSize: 0, 40 | expectedLog: "Added 0 proposals from outcome", 41 | }, 42 | { 43 | name: "Multiple rounds with proposals", 44 | automationOutcome: ocr2keepersv3.AutomationOutcome{ 45 | SurfacedProposals: [][]commontypes.CoordinatedBlockProposal{ 46 | {{WorkID: "1"}, {WorkID: "2"}}, 47 | {{WorkID: "3"}}, 48 | {{WorkID: "4"}, {WorkID: "5"}, {WorkID: "6"}}, 49 | }, 50 | }, 51 | expectedQueueSize: 6, 52 | expectedLog: "Added 6 proposals from outcome", 53 | }, 54 | } 55 | 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | upkeepTypeGetter := func(uid commontypes.UpkeepIdentifier) types.UpkeepType { 59 | return types.UpkeepType(uid[15]) 60 | } 61 | proposalQ := stores.NewProposalQueue(upkeepTypeGetter) 62 | 63 | // Prepare mock logger 64 | var logBuf bytes.Buffer 65 | logger := log.New(&logBuf, "", 0) 66 | 67 | // Create the hook with the proposal queue and logger 68 | addToProposalQHook := NewAddToProposalQHook(proposalQ, logger) 69 | 70 | // Run the hook 71 | addToProposalQHook.RunHook(tt.automationOutcome) 72 | 73 | // Assert that the correct number of proposals were added to the queue 74 | assert.Equal(t, tt.expectedQueueSize, proposalQ.Size()) 75 | 76 | // Assert log messages if needed 77 | assert.Contains(t, logBuf.String(), tt.expectedLog) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/remove_from_metadata.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 8 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 10 | ) 11 | 12 | func NewRemoveFromMetadataHook(ms types.MetadataStore, logger *log.Logger) RemoveFromMetadataHook { 13 | return RemoveFromMetadataHook{ 14 | ms: ms, 15 | logger: log.New(logger.Writer(), fmt.Sprintf("[%s | pre-build hook:remove-from-metadata]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 16 | } 17 | } 18 | 19 | type RemoveFromMetadataHook struct { 20 | ms types.MetadataStore 21 | logger *log.Logger 22 | } 23 | 24 | func (hook *RemoveFromMetadataHook) RunHook(outcome ocr2keepersv3.AutomationOutcome) { 25 | removed := 0 26 | for _, round := range outcome.SurfacedProposals { 27 | for _, proposal := range round { 28 | hook.ms.RemoveProposals(proposal) 29 | removed++ 30 | } 31 | } 32 | hook.logger.Printf("%d proposals found in outcome for removal", removed) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/remove_from_metadata_test.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/mock" 9 | 10 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 12 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types/mocks" 13 | commontypes "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 14 | ) 15 | 16 | func TestRemoveFromMetadataHook_RunHook(t *testing.T) { 17 | var uid1 commontypes.UpkeepIdentifier = [32]byte{1} 18 | var uid2 commontypes.UpkeepIdentifier = [32]byte{2} 19 | var uid3 commontypes.UpkeepIdentifier = [32]byte{3} 20 | tests := []struct { 21 | name string 22 | surfacedProposals [][]commontypes.CoordinatedBlockProposal 23 | upkeepTypeGetter map[commontypes.UpkeepIdentifier]types.UpkeepType 24 | expectedRemovals int 25 | }{ 26 | { 27 | name: "Remove proposals from metadata store", 28 | surfacedProposals: [][]commontypes.CoordinatedBlockProposal{ 29 | { 30 | {UpkeepID: uid1, WorkID: "1"}, 31 | {UpkeepID: uid2, WorkID: "2"}, 32 | }, 33 | { 34 | {UpkeepID: uid3, WorkID: "3"}, 35 | }, 36 | }, 37 | upkeepTypeGetter: map[commontypes.UpkeepIdentifier]types.UpkeepType{ 38 | uid1: types.ConditionTrigger, 39 | uid2: types.LogTrigger, 40 | uid3: types.ConditionTrigger, 41 | }, 42 | expectedRemovals: 3, 43 | }, 44 | { 45 | name: "No proposals to remove", 46 | surfacedProposals: [][]commontypes.CoordinatedBlockProposal{ 47 | {}, 48 | {}, 49 | }, 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | // Prepare mock MetadataStore 56 | mockMetadataStore := &mocks.MockMetadataStore{} 57 | if tt.expectedRemovals > 0 { 58 | mockMetadataStore.On("RemoveProposals", mock.Anything).Times(tt.expectedRemovals) 59 | } 60 | 61 | // Prepare logger 62 | var logBuf bytes.Buffer 63 | logger := log.New(&logBuf, "", 0) 64 | 65 | // Create the hook with mock MetadataStore, mock UpkeepTypeGetter, and logger 66 | removeFromMetadataHook := NewRemoveFromMetadataHook(mockMetadataStore, logger) 67 | 68 | // Prepare automation outcome with agreed proposals 69 | automationOutcome := ocr2keepersv3.AutomationOutcome{ 70 | SurfacedProposals: tt.surfacedProposals, 71 | } 72 | // Run the hook 73 | removeFromMetadataHook.RunHook(automationOutcome) 74 | 75 | mockMetadataStore.AssertExpectations(t) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/remove_from_staging.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 8 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 10 | ) 11 | 12 | func NewRemoveFromStagingHook(store types.ResultStore, logger *log.Logger) RemoveFromStagingHook { 13 | return RemoveFromStagingHook{ 14 | store: store, 15 | logger: log.New(logger.Writer(), fmt.Sprintf("[%s | pre-build hook:remove-from-staging]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 16 | } 17 | } 18 | 19 | type RemoveFromStagingHook struct { 20 | store types.ResultStore 21 | logger *log.Logger 22 | } 23 | 24 | func (hook *RemoveFromStagingHook) RunHook(outcome ocr2keepersv3.AutomationOutcome) { 25 | toRemove := make([]string, 0, len(outcome.AgreedPerformables)) 26 | for _, result := range outcome.AgreedPerformables { 27 | toRemove = append(toRemove, result.WorkID) 28 | } 29 | 30 | hook.logger.Printf("%d results found in outcome for removal", len(toRemove)) 31 | hook.store.Remove(toRemove...) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/v3/plugin/hooks/remove_from_staging_test.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 11 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 12 | types "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 13 | ) 14 | 15 | func TestRemoveFromStagingHook(t *testing.T) { 16 | tests := []struct { 17 | Name string 18 | Input []ocr2keepers.CheckResult 19 | }{ 20 | { 21 | Name: "No Results", 22 | Input: []ocr2keepers.CheckResult{}, 23 | }, 24 | { 25 | Name: "One Result", 26 | Input: []ocr2keepers.CheckResult{ 27 | { 28 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), 29 | WorkID: "1", 30 | }, 31 | }, 32 | }, 33 | { 34 | Name: "Five Results", 35 | Input: []ocr2keepers.CheckResult{ 36 | { 37 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), 38 | WorkID: "2", 39 | }, 40 | { 41 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{2}), 42 | WorkID: "3", 43 | }, 44 | { 45 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{3}), 46 | WorkID: "4", 47 | }, 48 | { 49 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{4}), 50 | WorkID: "5", 51 | }, 52 | { 53 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{5}), 54 | WorkID: "6", 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | for _, test := range tests { 61 | t.Run(test.Name, func(t *testing.T) { 62 | ob := ocr2keepersv3.AutomationOutcome{ 63 | AgreedPerformables: test.Input, 64 | } 65 | 66 | mr := &mockResultStore{} 67 | 68 | r := NewRemoveFromStagingHook(mr, log.New(io.Discard, "", 0)) 69 | 70 | r.RunHook(ob) 71 | assert.Equal(t, len(ob.AgreedPerformables), len(mr.removedIDs)) 72 | }) 73 | } 74 | } 75 | 76 | // MockResultStore is a mock implementation of types.ResultStore for testing 77 | type mockResultStore struct { 78 | removedIDs []string 79 | } 80 | 81 | func (m *mockResultStore) Remove(ids ...string) { 82 | m.removedIDs = append(m.removedIDs, ids...) 83 | } 84 | 85 | func (m *mockResultStore) Add(...types.CheckResult) { 86 | } 87 | 88 | func (m *mockResultStore) View() ([]types.CheckResult, error) { 89 | return nil, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/combine.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 8 | ) 9 | 10 | // CombinedPostprocessor ... 11 | type CombinedPostprocessor struct { 12 | source []PostProcessor 13 | } 14 | 15 | // NewCombinedPostprocessor ... 16 | func NewCombinedPostprocessor(src ...PostProcessor) *CombinedPostprocessor { 17 | return &CombinedPostprocessor{source: src} 18 | } 19 | 20 | // PostProcess implements the PostProcessor interface and runs all source 21 | // processors in the sequence in which they were provided. All processors are 22 | // run and errors are joined. 23 | func (cpp *CombinedPostprocessor) PostProcess(ctx context.Context, results []ocr2keepers.CheckResult, payloads []ocr2keepers.UpkeepPayload) error { 24 | var err error 25 | 26 | for _, pp := range cpp.source { 27 | err = errors.Join(err, pp.PostProcess(ctx, results, payloads)) 28 | } 29 | 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/combine_test.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | func TestCombinedPostprocessor(t *testing.T) { 15 | t.Run("no errors", func(t *testing.T) { 16 | ctx, cancel := context.WithCancel(context.Background()) 17 | defer cancel() 18 | 19 | one := new(MockPostProcessor) 20 | two := new(MockPostProcessor) 21 | tre := new(MockPostProcessor) 22 | 23 | cmb := NewCombinedPostprocessor(one, two, tre) 24 | rst := []ocr2keepers.CheckResult{{UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), WorkID: "0x1", Retryable: true}} 25 | p := []ocr2keepers.UpkeepPayload{{UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), WorkID: "0x1"}} 26 | 27 | one.On("PostProcess", ctx, rst, p).Return(nil) 28 | two.On("PostProcess", ctx, rst, p).Return(nil) 29 | tre.On("PostProcess", ctx, rst, p).Return(nil) 30 | 31 | assert.NoError(t, cmb.PostProcess(ctx, rst, p), "no error expected from combined post processing") 32 | }) 33 | 34 | t.Run("with errors", func(t *testing.T) { 35 | ctx, cancel := context.WithCancel(context.Background()) 36 | defer cancel() 37 | 38 | one := new(MockPostProcessor) 39 | two := new(MockPostProcessor) 40 | tre := new(MockPostProcessor) 41 | 42 | cmb := NewCombinedPostprocessor(one, two, tre) 43 | rst := []ocr2keepers.CheckResult{{UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), WorkID: "0x1", Retryable: true}} 44 | p := []ocr2keepers.UpkeepPayload{{UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), WorkID: "0x1"}} 45 | 46 | one.On("PostProcess", ctx, rst, p).Return(nil) 47 | two.On("PostProcess", ctx, rst, p).Return(fmt.Errorf("error")) 48 | tre.On("PostProcess", ctx, rst, p).Return(fmt.Errorf("error")) 49 | 50 | assert.Error(t, cmb.PostProcess(ctx, rst, p), "error expected from combined post processing") 51 | }) 52 | } 53 | 54 | type MockPostProcessor struct { 55 | mock.Mock 56 | } 57 | 58 | func (_m *MockPostProcessor) PostProcess(ctx context.Context, r []ocr2keepers.CheckResult, p []ocr2keepers.UpkeepPayload) error { 59 | ret := _m.Called(ctx, r, p) 60 | return ret.Error(0) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/eligible.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 11 | ) 12 | 13 | // checkResultAdder is a general interface for a result store that accepts check results 14 | type checkResultAdder interface { 15 | // Add inserts the provided check result in the store 16 | Add(...ocr2keepers.CheckResult) 17 | } 18 | 19 | // PostProcessor is the general interface for a processing function after checking eligibility 20 | // status 21 | type PostProcessor interface { 22 | // PostProcess takes a slice of results where eligibility status is known 23 | PostProcess(context.Context, []ocr2keepers.CheckResult, []ocr2keepers.UpkeepPayload) error 24 | } 25 | 26 | type eligiblePostProcessor struct { 27 | lggr *log.Logger 28 | resultsAdder checkResultAdder 29 | } 30 | 31 | func NewEligiblePostProcessor(resultsAdder checkResultAdder, logger *log.Logger) *eligiblePostProcessor { 32 | return &eligiblePostProcessor{ 33 | lggr: log.New(logger.Writer(), fmt.Sprintf("[%s | eligible-post-processor]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 34 | resultsAdder: resultsAdder, 35 | } 36 | } 37 | 38 | func (p *eligiblePostProcessor) PostProcess(_ context.Context, results []ocr2keepers.CheckResult, _ []ocr2keepers.UpkeepPayload) error { 39 | eligible := 0 40 | for _, res := range results { 41 | if res.PipelineExecutionState == 0 && res.Eligible { 42 | eligible++ 43 | p.resultsAdder.Add(res) 44 | } 45 | } 46 | p.lggr.Printf("post-processing %d results, %d eligible\n", len(results), eligible) 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/eligible_test.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 13 | 14 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/stores" 15 | ) 16 | 17 | func TestNewEligiblePostProcessor(t *testing.T) { 18 | resultsStore := stores.New(log.New(io.Discard, "", 0)) 19 | processor := NewEligiblePostProcessor(resultsStore, log.New(io.Discard, "", 0)) 20 | 21 | t.Run("process eligible results", func(t *testing.T) { 22 | result1 := ocr2keepers.CheckResult{Eligible: false} 23 | result2 := ocr2keepers.CheckResult{Eligible: true} 24 | result3 := ocr2keepers.CheckResult{Eligible: false} 25 | 26 | err := processor.PostProcess(context.Background(), []ocr2keepers.CheckResult{ 27 | result1, 28 | result2, 29 | result3, 30 | }, []ocr2keepers.UpkeepPayload{ 31 | {WorkID: "1"}, 32 | {WorkID: "2"}, 33 | {WorkID: "3"}, 34 | }) 35 | 36 | assert.Nil(t, err) 37 | 38 | storedResults, err := resultsStore.View() 39 | assert.Nil(t, err) 40 | 41 | assert.Len(t, storedResults, 1) 42 | assert.True(t, reflect.DeepEqual(storedResults[0], result2)) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/ineligible.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | 9 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 10 | 11 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 12 | ) 13 | 14 | type ineligiblePostProcessor struct { 15 | lggr *log.Logger 16 | stateUpdater ocr2keepers.UpkeepStateUpdater 17 | } 18 | 19 | func NewIneligiblePostProcessor(stateUpdater ocr2keepers.UpkeepStateUpdater, logger *log.Logger) *ineligiblePostProcessor { 20 | return &ineligiblePostProcessor{ 21 | lggr: log.New(logger.Writer(), fmt.Sprintf("[%s | ineligible-post-processor]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 22 | stateUpdater: stateUpdater, 23 | } 24 | } 25 | 26 | func (p *ineligiblePostProcessor) PostProcess(ctx context.Context, results []ocr2keepers.CheckResult, _ []ocr2keepers.UpkeepPayload) error { 27 | var merr error 28 | ineligible := 0 29 | for _, res := range results { 30 | if res.PipelineExecutionState == 0 && !res.Eligible { 31 | err := p.stateUpdater.SetUpkeepState(ctx, res, ocr2keepers.Ineligible) 32 | if err != nil { 33 | merr = errors.Join(merr, err) 34 | continue 35 | } 36 | ineligible++ 37 | } 38 | } 39 | p.lggr.Printf("post-processing %d results, %d ineligible\n", len(results), ineligible) 40 | return merr 41 | } 42 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/metadata.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 7 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 8 | ) 9 | 10 | type addProposalToMetadataStore struct { 11 | metadataStore types.MetadataStore 12 | } 13 | 14 | func NewAddProposalToMetadataStorePostprocessor(store types.MetadataStore) *addProposalToMetadataStore { 15 | return &addProposalToMetadataStore{metadataStore: store} 16 | } 17 | 18 | func (a *addProposalToMetadataStore) PostProcess(_ context.Context, results []ocr2keepers.CheckResult, _ []ocr2keepers.UpkeepPayload) error { 19 | // should only add values and not remove them 20 | for _, r := range results { 21 | if r.PipelineExecutionState == 0 && r.Eligible { 22 | proposal := ocr2keepers.CoordinatedBlockProposal{ 23 | UpkeepID: r.UpkeepID, 24 | Trigger: r.Trigger, 25 | WorkID: r.WorkID, 26 | } 27 | a.metadataStore.AddProposals(proposal) 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/metadata_test.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/stores" 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 11 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 12 | ) 13 | 14 | func TestMetadataAddSamples(t *testing.T) { 15 | ch := make(chan ocr2keepers.BlockHistory) 16 | ms, err := stores.NewMetadataStore(&mockBlockSubscriber{ch: ch}, func(uid ocr2keepers.UpkeepIdentifier) types.UpkeepType { 17 | return types.ConditionTrigger 18 | }) 19 | assert.NoError(t, err) 20 | 21 | values := []ocr2keepers.CheckResult{ 22 | { 23 | Eligible: true, 24 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), 25 | WorkID: "workID1", 26 | }, 27 | { 28 | Eligible: true, 29 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{2}), 30 | WorkID: "workID2", 31 | }, 32 | { 33 | Eligible: false, 34 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{3}), 35 | WorkID: "workID3", 36 | }, 37 | } 38 | 39 | pp := NewAddProposalToMetadataStorePostprocessor(ms) 40 | err = pp.PostProcess(context.Background(), values, []ocr2keepers.UpkeepPayload{ 41 | { 42 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{1}), 43 | WorkID: "workID1", 44 | }, 45 | { 46 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{2}), 47 | WorkID: "workID2", 48 | }, 49 | { 50 | UpkeepID: ocr2keepers.UpkeepIdentifier([32]byte{3}), 51 | WorkID: "workID3", 52 | }, 53 | }) 54 | 55 | assert.NoError(t, err, "no error expected from post processor") 56 | 57 | assert.Equal(t, 2, len(ms.ViewProposals(types.ConditionTrigger))) 58 | } 59 | 60 | type mockBlockSubscriber struct { 61 | ch chan ocr2keepers.BlockHistory 62 | StartFn func(ctx context.Context) error 63 | CloseFn func() error 64 | } 65 | 66 | func (_m *mockBlockSubscriber) Subscribe() (int, chan ocr2keepers.BlockHistory, error) { 67 | return 0, _m.ch, nil 68 | } 69 | 70 | func (_m *mockBlockSubscriber) Unsubscribe(int) error { 71 | return nil 72 | } 73 | 74 | func (_m *mockBlockSubscriber) Start(ctx context.Context) error { 75 | return _m.StartFn(ctx) 76 | } 77 | 78 | func (_m *mockBlockSubscriber) Close() error { 79 | return _m.CloseFn() 80 | } 81 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/retry.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/telemetry" 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 11 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 12 | ) 13 | 14 | func NewRetryablePostProcessor(q types.RetryQueue, logger *log.Logger) *retryablePostProcessor { 15 | return &retryablePostProcessor{ 16 | logger: log.New(logger.Writer(), fmt.Sprintf("[%s | retryable-post-processor]", telemetry.ServiceName), telemetry.LogPkgStdFlags), 17 | q: q, 18 | } 19 | } 20 | 21 | type retryablePostProcessor struct { 22 | logger *log.Logger 23 | q types.RetryQueue 24 | } 25 | 26 | var _ PostProcessor = (*retryablePostProcessor)(nil) 27 | 28 | func (p *retryablePostProcessor) PostProcess(_ context.Context, results []ocr2keepers.CheckResult, payloads []ocr2keepers.UpkeepPayload) error { 29 | var err error 30 | retryable := 0 31 | for i, res := range results { 32 | if res.PipelineExecutionState != 0 && res.Retryable { 33 | e := p.q.Enqueue(types.RetryRecord{ 34 | Payload: payloads[i], 35 | Interval: res.RetryInterval, 36 | }) 37 | if e == nil { 38 | retryable++ 39 | } 40 | err = errors.Join(err, e) 41 | } 42 | } 43 | p.logger.Printf("post-processing %d results, %d retryable\n", len(results), retryable) 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /pkg/v3/postprocessors/retry_test.go: -------------------------------------------------------------------------------- 1 | package postprocessors 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "testing" 7 | "time" 8 | 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/stores" 10 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestRetryPostProcessor_PostProcess(t *testing.T) { 15 | lggr := log.Default() 16 | q := stores.NewRetryQueue(lggr) 17 | processor := NewRetryablePostProcessor(q, lggr) 18 | 19 | results := []ocr2keepers.CheckResult{ 20 | {Retryable: true, PipelineExecutionState: 1}, 21 | {Retryable: false, PipelineExecutionState: 3}, 22 | {Retryable: true, RetryInterval: time.Second, PipelineExecutionState: 2}, 23 | } 24 | 25 | // Call the PostProcess method 26 | err := processor.PostProcess(context.Background(), results, []ocr2keepers.UpkeepPayload{ 27 | {WorkID: "1"}, {WorkID: "2"}, {WorkID: "3"}, 28 | }) 29 | assert.Nil(t, err, "PostProcess returned an error: %v", err) 30 | 31 | assert.Equal(t, 2, q.Size()) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/v3/preprocessors/proposal_filterer.go: -------------------------------------------------------------------------------- 1 | package preprocessors 2 | 3 | import ( 4 | "context" 5 | 6 | ocr2keepersv3 "github.com/smartcontractkit/chainlink-automation/pkg/v3" 7 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 8 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | ) 10 | 11 | func NewProposalFilterer(metadata types.MetadataStore, upkeepType types.UpkeepType) ocr2keepersv3.PreProcessor[ocr2keepers.UpkeepPayload] { 12 | return &proposalFilterer{ 13 | upkeepType: upkeepType, 14 | metadata: metadata, 15 | } 16 | } 17 | 18 | type proposalFilterer struct { 19 | metadata types.MetadataStore 20 | upkeepType types.UpkeepType 21 | } 22 | 23 | var _ ocr2keepersv3.PreProcessor[ocr2keepers.UpkeepPayload] = (*proposalFilterer)(nil) 24 | 25 | func (p *proposalFilterer) PreProcess(ctx context.Context, payloads []ocr2keepers.UpkeepPayload) ([]ocr2keepers.UpkeepPayload, error) { 26 | all := p.metadata.ViewProposals(p.upkeepType) 27 | flatten := map[string]bool{} 28 | for _, proposal := range all { 29 | flatten[proposal.WorkID] = true 30 | } 31 | filtered := make([]ocr2keepers.UpkeepPayload, 0) 32 | for _, payload := range payloads { 33 | if _, ok := flatten[payload.WorkID]; !ok { 34 | filtered = append(filtered, payload) 35 | } 36 | } 37 | 38 | return filtered, nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/v3/preprocessors/proposal_filterer_test.go: -------------------------------------------------------------------------------- 1 | package preprocessors 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 10 | commontypes "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 11 | ) 12 | 13 | func TestProposalFilterer_PreProcess(t *testing.T) { 14 | metadata := &mockMetadataStore{ 15 | ViewProposalsFn: func(utype types.UpkeepType) []commontypes.CoordinatedBlockProposal { 16 | return []commontypes.CoordinatedBlockProposal{ 17 | { 18 | WorkID: "workID2", 19 | }, 20 | } 21 | }, 22 | } 23 | filterer := &proposalFilterer{ 24 | metadata: metadata, 25 | upkeepType: types.LogTrigger, 26 | } 27 | payloads, err := filterer.PreProcess(context.Background(), []commontypes.UpkeepPayload{ 28 | { 29 | WorkID: "workID1", 30 | }, 31 | { 32 | WorkID: "workID2", 33 | }, 34 | { 35 | WorkID: "workID3", 36 | }, 37 | }) 38 | assert.Nil(t, err) 39 | assert.Equal(t, []commontypes.UpkeepPayload{{WorkID: "workID1"}, {WorkID: "workID3"}}, payloads) 40 | } 41 | 42 | type mockMetadataStore struct { 43 | types.MetadataStore 44 | ViewProposalsFn func(utype types.UpkeepType) []commontypes.CoordinatedBlockProposal 45 | } 46 | 47 | func (s *mockMetadataStore) ViewProposals(utype types.UpkeepType) []commontypes.CoordinatedBlockProposal { 48 | return s.ViewProposalsFn(utype) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/v3/prommetrics/metrics.go: -------------------------------------------------------------------------------- 1 | package prommetrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | // NamespaceAutomation is the namespace for all Automation related metrics 9 | const NamespaceAutomation = "automation" 10 | 11 | // Plugin error types 12 | const ( 13 | PluginErrorTypeInvalidOracleObservation = "invalid_oracle_observation" 14 | PluginErrorTypeDecodeOutcome = "decode_outcome" 15 | PluginErrorTypeEncodeReport = "encode_report" 16 | ) 17 | 18 | // Plugin steps 19 | const ( 20 | PluginStepResultStore = "result_store" 21 | PluginStepObservation = "observation" 22 | PluginStepOutcome = "outcome" 23 | PluginStepReports = "reports" 24 | ) 25 | 26 | // Automation metrics 27 | var ( 28 | AutomationPluginPerformables = promauto.NewGaugeVec(prometheus.GaugeOpts{ 29 | Namespace: NamespaceAutomation, 30 | Name: "plugin_performables", 31 | Help: "How many performables were present at a given step in the plugin flow", 32 | }, []string{ 33 | "step", 34 | }) 35 | AutomationPluginError = promauto.NewCounterVec(prometheus.CounterOpts{ 36 | Namespace: NamespaceAutomation, 37 | Name: "plugin_error", 38 | Help: "Count of how many errors were encountered in the plugin by label", 39 | }, []string{ 40 | "step", 41 | "error", 42 | }) 43 | ) 44 | -------------------------------------------------------------------------------- /pkg/v3/random/shuffler.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import "math/rand" 4 | 5 | type Shuffler[T any] struct { 6 | Source rand.Source 7 | } 8 | 9 | func (s Shuffler[T]) Shuffle(a []T) []T { 10 | r := rand.New(s.Source) 11 | r.Shuffle(len(a), func(i, j int) { 12 | a[i], a[j] = a[j], a[i] 13 | }) 14 | return a 15 | } 16 | 17 | func ShuffleString(s string, rSrc [16]byte) string { 18 | shuffled := []rune(s) 19 | rand.New(NewKeyedCryptoRandSource(rSrc)).Shuffle(len(shuffled), func(i, j int) { 20 | shuffled[i], shuffled[j] = shuffled[j], shuffled[i] 21 | }) 22 | return string(shuffled) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/v3/random/shuffler_test.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestShuffler_Shuffle(t *testing.T) { 11 | shuffler := Shuffler[int]{Source: rand.NewSource(0)} 12 | arr := []int{1, 2, 3, 4, 5} 13 | arr = shuffler.Shuffle(arr) 14 | assert.Equal(t, arr, []int{3, 4, 2, 1, 5}) 15 | 16 | // Sorting again using a used shuffler should yield a different result 17 | arr = []int{1, 2, 3, 4, 5} 18 | arr = shuffler.Shuffle(arr) 19 | assert.Equal(t, arr, []int{3, 4, 1, 5, 2}) 20 | 21 | // Sorting again using a new shuffler with the same pseudo-random source should yield the same result 22 | shuffler2 := Shuffler[int]{Source: rand.NewSource(0)} 23 | arr2 := []int{1, 2, 3, 4, 5} 24 | arr2 = shuffler2.Shuffle(arr2) 25 | assert.Equal(t, arr2, []int{3, 4, 2, 1, 5}) 26 | } 27 | 28 | func TestShuffler_ShuffleString(t *testing.T) { 29 | assert.Equal(t, ShuffleString("12345", [16]byte{0}), "14523") 30 | // ShuffleString should be deterministic based on rSrc 31 | assert.Equal(t, ShuffleString("12345", [16]byte{0}), "14523") 32 | assert.Equal(t, ShuffleString("12345", [16]byte{1}), "51243") 33 | assert.Equal(t, ShuffleString("123456", [16]byte{0}), "516423") 34 | assert.Equal(t, ShuffleString("", [16]byte{0}), "") 35 | assert.Equal(t, ShuffleString("dsv$\u271387csdv0-`", [16]byte{0}), "d0v`$-8vsscd\u27137") 36 | } 37 | -------------------------------------------------------------------------------- /pkg/v3/random/src.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | crand "crypto/rand" 7 | "encoding/binary" 8 | "math/rand" 9 | 10 | "golang.org/x/crypto/sha3" 11 | ) 12 | 13 | var ( 14 | newCipherFn = aes.NewCipher 15 | randReadFn = crand.Read 16 | ) 17 | 18 | // Generates a randomness source derived from the prefix and seq # so 19 | // that it's the same across the network for the same input. 20 | func GetRandomKeySource(prefix []byte, seq uint64) [16]byte { 21 | // similar key building as libocr transmit selector 22 | hash := sha3.NewLegacyKeccak256() 23 | hash.Write(prefix[:]) 24 | temp := make([]byte, 8) 25 | binary.LittleEndian.PutUint64(temp, seq) 26 | hash.Write(temp) 27 | 28 | var keyRandSource [16]byte 29 | copy(keyRandSource[:], hash.Sum(nil)) 30 | return keyRandSource 31 | } 32 | 33 | type keyedCryptoRandSource struct { 34 | stream cipher.Stream 35 | } 36 | 37 | func NewKeyedCryptoRandSource(key [16]byte) rand.Source { 38 | var iv [16]byte // zero IV is fine here 39 | block, err := newCipherFn(key[:]) 40 | if err != nil { 41 | // assertion 42 | panic(err) 43 | } 44 | return &keyedCryptoRandSource{cipher.NewCTR(block, iv[:])} 45 | } 46 | 47 | const int63Mask = 1<<63 - 1 48 | 49 | func (crs *keyedCryptoRandSource) Int63() int64 { 50 | var buf [8]byte 51 | crs.stream.XORKeyStream(buf[:], buf[:]) 52 | return int64(binary.LittleEndian.Uint64(buf[:]) & int63Mask) 53 | } 54 | 55 | func (crs *keyedCryptoRandSource) Seed(seed int64) { 56 | panic("keyedCryptoRandSource.Seed: Not supported") 57 | } 58 | 59 | type cryptoRandSource struct{} 60 | 61 | func NewCryptoRandSource() rand.Source { 62 | return cryptoRandSource{} 63 | } 64 | 65 | func (_ cryptoRandSource) Int63() int64 { 66 | var b [8]byte 67 | _, err := randReadFn(b[:]) 68 | if err != nil { 69 | panic(err) 70 | } 71 | return int64(binary.LittleEndian.Uint64(b[:]) & (1<<63 - 1)) 72 | } 73 | 74 | func (_ cryptoRandSource) Seed(_ int64) { 75 | panic("cryptoRandSource.Seed: Not supported") 76 | } 77 | -------------------------------------------------------------------------------- /pkg/v3/random/src_test.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "crypto/cipher" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_GetRandomKeySource(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | prefix []byte 15 | seq uint64 16 | want [16]byte 17 | }{ 18 | { 19 | name: "happy path", 20 | prefix: []byte{1, 2, 3, 4}, 21 | seq: 1234, 22 | want: [16]byte{0x3e, 0xb0, 0x60, 0xd8, 0x59, 0x17, 0x19, 0xd, 0x80, 0x60, 0x41, 0xd4, 0x61, 0xaa, 0x5f, 0x12}, 23 | }, 24 | { 25 | name: "nil prefix", 26 | prefix: nil, 27 | seq: 1234, 28 | want: [16]byte{0x44, 0x36, 0xac, 0xd0, 0x86, 0xda, 0xf2, 0xaf, 0xf2, 0xd9, 0x40, 0xaf, 0x64, 0xf6, 0xb8, 0x84}, 29 | }, 30 | } 31 | 32 | for _, tc := range tests { 33 | t.Run(tc.name, func(t *testing.T) { 34 | assert.Equal(t, tc.want, GetRandomKeySource(tc.prefix, tc.seq)) 35 | }) 36 | } 37 | } 38 | 39 | func TestCryptoRandSource(t *testing.T) { 40 | t.Run("creates a new CryptoRandSource", func(t *testing.T) { 41 | s := NewCryptoRandSource() 42 | i := s.Int63() 43 | assert.NotEqual(t, 0, i) 44 | }) 45 | 46 | t.Run("panics on Seed", func(t *testing.T) { 47 | defer func() { 48 | if r := recover(); r == nil { 49 | t.Fatalf("expected a panic, did not panic") 50 | } 51 | }() 52 | 53 | s := NewCryptoRandSource() 54 | s.Seed(int64(123)) 55 | }) 56 | 57 | t.Run("an error on crand.Read causes a panic", func(t *testing.T) { 58 | oldRandReadFn := randReadFn 59 | randReadFn = func(b []byte) (n int, err error) { 60 | return 0, errors.New("read error") 61 | } 62 | defer func() { 63 | randReadFn = oldRandReadFn 64 | }() 65 | defer func() { 66 | if r := recover(); r == nil { 67 | t.Fatalf("expected a panic, did not panic") 68 | } 69 | }() 70 | 71 | s := NewCryptoRandSource() 72 | s.Int63() 73 | }) 74 | } 75 | 76 | func TestNewKeyedCryptoRandSource(t *testing.T) { 77 | t.Run("creates a new KeyedCryptoRandSource", func(t *testing.T) { 78 | src := NewKeyedCryptoRandSource([16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) 79 | i := src.Int63() 80 | assert.Equal(t, i, int64(1590255750952055259)) 81 | 82 | defer func() { 83 | if r := recover(); r == nil { 84 | t.Fatalf("expected a panic, did not panic") 85 | } 86 | }() 87 | src.Seed(99) 88 | }) 89 | 90 | t.Run("fails to create a new KeyedCryptoRandSource due to invalid key size", func(t *testing.T) { 91 | oldNewCipherFn := newCipherFn 92 | newCipherFn = func(key []byte) (cipher.Block, error) { 93 | return nil, errors.New("invalid key") 94 | } 95 | defer func() { 96 | newCipherFn = oldNewCipherFn 97 | }() 98 | 99 | defer func() { 100 | if r := recover(); r == nil { 101 | t.Fatalf("expected a panic, did not panic") 102 | } 103 | }() 104 | 105 | NewKeyedCryptoRandSource([16]byte{1}) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/v3/runner/result.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type result[T any] struct { 8 | // this struct type isn't expressly defined to run in a single thread or 9 | // multiple threads so internally a mutex provides the thread safety 10 | // guarantees in the case it is used in a multi-threaded way 11 | mu sync.RWMutex 12 | successes int 13 | failures int 14 | err error 15 | values []T 16 | } 17 | 18 | func newResult[T any]() *result[T] { 19 | return &result[T]{ 20 | values: make([]T, 0), 21 | } 22 | } 23 | 24 | func (r *result[T]) Successes() int { 25 | r.mu.RLock() 26 | defer r.mu.RUnlock() 27 | 28 | return r.successes 29 | } 30 | 31 | func (r *result[T]) AddSuccesses(v int) { 32 | r.mu.Lock() 33 | defer r.mu.Unlock() 34 | 35 | r.successes += v 36 | } 37 | 38 | func (r *result[T]) Failures() int { 39 | r.mu.RLock() 40 | defer r.mu.RUnlock() 41 | 42 | return r.failures 43 | } 44 | 45 | func (r *result[T]) AddFailures(v int) { 46 | r.mu.Lock() 47 | defer r.mu.Unlock() 48 | 49 | r.failures += v 50 | } 51 | 52 | func (r *result[T]) Err() error { 53 | r.mu.RLock() 54 | defer r.mu.RUnlock() 55 | 56 | return r.err 57 | } 58 | 59 | func (r *result[T]) SetErr(err error) { 60 | r.mu.Lock() 61 | defer r.mu.Unlock() 62 | 63 | r.err = err 64 | } 65 | 66 | func (r *result[T]) Total() int { 67 | r.mu.RLock() 68 | defer r.mu.RUnlock() 69 | 70 | return r.successes + r.failures 71 | } 72 | 73 | func (r *result[T]) unsafeTotal() int { 74 | return r.successes + r.failures 75 | } 76 | 77 | func (r *result[T]) SuccessRate() float64 { 78 | r.mu.RLock() 79 | defer r.mu.RUnlock() 80 | 81 | if r.unsafeTotal() == 0 { 82 | return 0 83 | } 84 | 85 | return float64(r.successes) / float64(r.unsafeTotal()) 86 | } 87 | 88 | func (r *result[T]) FailureRate() float64 { 89 | r.mu.RLock() 90 | defer r.mu.RUnlock() 91 | 92 | if r.unsafeTotal() == 0 { 93 | return 0 94 | } 95 | 96 | return float64(r.failures) / float64(r.unsafeTotal()) 97 | } 98 | 99 | func (r *result[T]) Add(res T) { 100 | r.mu.Lock() 101 | defer r.mu.Unlock() 102 | 103 | r.values = append(r.values, res) 104 | } 105 | 106 | func (r *result[T]) Values() []T { 107 | r.mu.RLock() 108 | defer r.mu.RUnlock() 109 | 110 | return r.values 111 | } 112 | -------------------------------------------------------------------------------- /pkg/v3/telemetry/log.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | const ( 9 | ServiceName = "automation-ocr3" 10 | LogPkgStdFlags = log.Lshortfile 11 | ) 12 | 13 | func WrapLogger(logger *log.Logger, ns string) *log.Logger { 14 | return log.New(logger.Writer(), fmt.Sprintf("[%s | %s]", ServiceName, ns), LogPkgStdFlags) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/v3/tickers/tick.go: -------------------------------------------------------------------------------- 1 | package tickers 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Tick is the container for the individual tick 8 | type Tick[T any] interface { 9 | // Value provides data scoped to the tick 10 | Value(ctx context.Context) (T, error) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/v3/tickers/time.go: -------------------------------------------------------------------------------- 1 | package tickers 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/smartcontractkit/chainlink-common/pkg/services" 9 | ) 10 | 11 | type observer[T any] interface { 12 | Process(context.Context, Tick[T]) error 13 | } 14 | 15 | type getterFunc[T any] func(context.Context, time.Time) (Tick[T], error) 16 | 17 | type timeTicker[T any] struct { 18 | services.StateMachine 19 | interval time.Duration 20 | observer observer[T] 21 | getterFn getterFunc[T] 22 | logger *log.Logger 23 | done chan struct{} 24 | stopCh services.StopChan 25 | } 26 | 27 | func NewTimeTicker[T any](interval time.Duration, observer observer[T], getterFn getterFunc[T], logger *log.Logger) *timeTicker[T] { 28 | t := &timeTicker[T]{ 29 | interval: interval, 30 | observer: observer, 31 | getterFn: getterFn, 32 | logger: logger, 33 | done: make(chan struct{}), 34 | stopCh: make(chan struct{}), 35 | } 36 | 37 | return t 38 | } 39 | 40 | // Start uses the provided context for each call to the getter function with the 41 | // configured interval as a timeout. This function blocks until Close is called 42 | // or the parent context is cancelled. 43 | func (t *timeTicker[T]) Start(ctx context.Context) error { 44 | if err := t.StartOnce("timeTicker", func() error { return nil }); err != nil { 45 | return err 46 | } 47 | defer close(t.done) 48 | ctx, cancel := t.stopCh.Ctx(ctx) 49 | defer cancel() 50 | 51 | t.logger.Printf("starting ticker service") 52 | defer t.logger.Printf("ticker service stopped") 53 | 54 | ticker := time.NewTicker(t.interval) 55 | defer ticker.Stop() 56 | 57 | for { 58 | select { 59 | case <-ctx.Done(): 60 | return nil 61 | case tm := <-ticker.C: 62 | if t.getterFn == nil { 63 | continue 64 | } 65 | tick, err := t.getterFn(ctx, tm) 66 | if err != nil { 67 | t.logger.Printf("error fetching tick: %s", err.Error()) 68 | continue 69 | } 70 | // observer.Process can be a heavy call taking upto ObservationProcessLimit seconds 71 | // so it is run in a separate goroutine to not block further ticks 72 | // Exploratory: Add some control to limit the number of goroutines spawned 73 | go func(c context.Context, t Tick[T], o observer[T], l *log.Logger) { 74 | if err := o.Process(c, t); err != nil { 75 | l.Printf("error processing observer: %s", err.Error()) 76 | } 77 | }(ctx, tick, t.observer, t.logger) 78 | } 79 | } 80 | } 81 | 82 | func (t *timeTicker[T]) Close() error { 83 | return t.StopOnce("timeTicker", func() error { 84 | close(t.stopCh) 85 | <-t.done 86 | return nil 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/v3/types/basetypes.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 7 | ) 8 | 9 | type UpkeepType uint8 10 | 11 | const ( 12 | // Exploratory AUTO 4335: add type for unknown 13 | ConditionTrigger UpkeepType = iota 14 | LogTrigger 15 | ) 16 | 17 | // RetryRecord is a record of a payload that can be retried after a certain interval. 18 | type RetryRecord struct { 19 | // payload is the desired unit of work to be retried 20 | Payload automation.UpkeepPayload 21 | // Interval is the time interval after which the same payload can be retried. 22 | Interval time.Duration 23 | } 24 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/block_subscriber.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // MockBlockSubscriber is an autogenerated mock type for the BlockSubscriber type 14 | type MockBlockSubscriber struct { 15 | mock.Mock 16 | } 17 | 18 | // Close provides a mock function with given fields: 19 | func (_m *MockBlockSubscriber) Close() error { 20 | ret := _m.Called() 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for Close") 24 | } 25 | 26 | var r0 error 27 | if rf, ok := ret.Get(0).(func() error); ok { 28 | r0 = rf() 29 | } else { 30 | r0 = ret.Error(0) 31 | } 32 | 33 | return r0 34 | } 35 | 36 | // Start provides a mock function with given fields: _a0 37 | func (_m *MockBlockSubscriber) Start(_a0 context.Context) error { 38 | ret := _m.Called(_a0) 39 | 40 | if len(ret) == 0 { 41 | panic("no return value specified for Start") 42 | } 43 | 44 | var r0 error 45 | if rf, ok := ret.Get(0).(func(context.Context) error); ok { 46 | r0 = rf(_a0) 47 | } else { 48 | r0 = ret.Error(0) 49 | } 50 | 51 | return r0 52 | } 53 | 54 | // Subscribe provides a mock function with given fields: 55 | func (_m *MockBlockSubscriber) Subscribe() (int, chan automation.BlockHistory, error) { 56 | ret := _m.Called() 57 | 58 | if len(ret) == 0 { 59 | panic("no return value specified for Subscribe") 60 | } 61 | 62 | var r0 int 63 | var r1 chan automation.BlockHistory 64 | var r2 error 65 | if rf, ok := ret.Get(0).(func() (int, chan automation.BlockHistory, error)); ok { 66 | return rf() 67 | } 68 | if rf, ok := ret.Get(0).(func() int); ok { 69 | r0 = rf() 70 | } else { 71 | r0 = ret.Get(0).(int) 72 | } 73 | 74 | if rf, ok := ret.Get(1).(func() chan automation.BlockHistory); ok { 75 | r1 = rf() 76 | } else { 77 | if ret.Get(1) != nil { 78 | r1 = ret.Get(1).(chan automation.BlockHistory) 79 | } 80 | } 81 | 82 | if rf, ok := ret.Get(2).(func() error); ok { 83 | r2 = rf() 84 | } else { 85 | r2 = ret.Error(2) 86 | } 87 | 88 | return r0, r1, r2 89 | } 90 | 91 | // Unsubscribe provides a mock function with given fields: _a0 92 | func (_m *MockBlockSubscriber) Unsubscribe(_a0 int) error { 93 | ret := _m.Called(_a0) 94 | 95 | if len(ret) == 0 { 96 | panic("no return value specified for Unsubscribe") 97 | } 98 | 99 | var r0 error 100 | if rf, ok := ret.Get(0).(func(int) error); ok { 101 | r0 = rf(_a0) 102 | } else { 103 | r0 = ret.Error(0) 104 | } 105 | 106 | return r0 107 | } 108 | 109 | // NewMockBlockSubscriber creates a new instance of MockBlockSubscriber. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 110 | // The first argument is typically a *testing.T value. 111 | func NewMockBlockSubscriber(t interface { 112 | mock.TestingT 113 | Cleanup(func()) 114 | }) *MockBlockSubscriber { 115 | mock := &MockBlockSubscriber{} 116 | mock.Mock.Test(t) 117 | 118 | t.Cleanup(func() { mock.AssertExpectations(t) }) 119 | 120 | return mock 121 | } 122 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/conditionalupkeepprovider.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // MockConditionalUpkeepProvider is an autogenerated mock type for the ConditionalUpkeepProvider type 14 | type MockConditionalUpkeepProvider struct { 15 | mock.Mock 16 | } 17 | 18 | // GetActiveUpkeeps provides a mock function with given fields: _a0 19 | func (_m *MockConditionalUpkeepProvider) GetActiveUpkeeps(_a0 context.Context) ([]automation.UpkeepPayload, error) { 20 | ret := _m.Called(_a0) 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for GetActiveUpkeeps") 24 | } 25 | 26 | var r0 []automation.UpkeepPayload 27 | var r1 error 28 | if rf, ok := ret.Get(0).(func(context.Context) ([]automation.UpkeepPayload, error)); ok { 29 | return rf(_a0) 30 | } 31 | if rf, ok := ret.Get(0).(func(context.Context) []automation.UpkeepPayload); ok { 32 | r0 = rf(_a0) 33 | } else { 34 | if ret.Get(0) != nil { 35 | r0 = ret.Get(0).([]automation.UpkeepPayload) 36 | } 37 | } 38 | 39 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 40 | r1 = rf(_a0) 41 | } else { 42 | r1 = ret.Error(1) 43 | } 44 | 45 | return r0, r1 46 | } 47 | 48 | // NewMockConditionalUpkeepProvider creates a new instance of MockConditionalUpkeepProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 49 | // The first argument is typically a *testing.T value. 50 | func NewMockConditionalUpkeepProvider(t interface { 51 | mock.TestingT 52 | Cleanup(func()) 53 | }) *MockConditionalUpkeepProvider { 54 | mock := &MockConditionalUpkeepProvider{} 55 | mock.Mock.Test(t) 56 | 57 | t.Cleanup(func() { mock.AssertExpectations(t) }) 58 | 59 | return mock 60 | } 61 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/encoder.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockEncoder is an autogenerated mock type for the Encoder type 11 | type MockEncoder struct { 12 | mock.Mock 13 | } 14 | 15 | // Encode provides a mock function with given fields: _a0 16 | func (_m *MockEncoder) Encode(_a0 ...automation.CheckResult) ([]byte, error) { 17 | _va := make([]interface{}, len(_a0)) 18 | for _i := range _a0 { 19 | _va[_i] = _a0[_i] 20 | } 21 | var _ca []interface{} 22 | _ca = append(_ca, _va...) 23 | ret := _m.Called(_ca...) 24 | 25 | if len(ret) == 0 { 26 | panic("no return value specified for Encode") 27 | } 28 | 29 | var r0 []byte 30 | var r1 error 31 | if rf, ok := ret.Get(0).(func(...automation.CheckResult) ([]byte, error)); ok { 32 | return rf(_a0...) 33 | } 34 | if rf, ok := ret.Get(0).(func(...automation.CheckResult) []byte); ok { 35 | r0 = rf(_a0...) 36 | } else { 37 | if ret.Get(0) != nil { 38 | r0 = ret.Get(0).([]byte) 39 | } 40 | } 41 | 42 | if rf, ok := ret.Get(1).(func(...automation.CheckResult) error); ok { 43 | r1 = rf(_a0...) 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | // Extract provides a mock function with given fields: _a0 52 | func (_m *MockEncoder) Extract(_a0 []byte) ([]automation.ReportedUpkeep, error) { 53 | ret := _m.Called(_a0) 54 | 55 | if len(ret) == 0 { 56 | panic("no return value specified for Extract") 57 | } 58 | 59 | var r0 []automation.ReportedUpkeep 60 | var r1 error 61 | if rf, ok := ret.Get(0).(func([]byte) ([]automation.ReportedUpkeep, error)); ok { 62 | return rf(_a0) 63 | } 64 | if rf, ok := ret.Get(0).(func([]byte) []automation.ReportedUpkeep); ok { 65 | r0 = rf(_a0) 66 | } else { 67 | if ret.Get(0) != nil { 68 | r0 = ret.Get(0).([]automation.ReportedUpkeep) 69 | } 70 | } 71 | 72 | if rf, ok := ret.Get(1).(func([]byte) error); ok { 73 | r1 = rf(_a0) 74 | } else { 75 | r1 = ret.Error(1) 76 | } 77 | 78 | return r0, r1 79 | } 80 | 81 | // NewMockEncoder creates a new instance of MockEncoder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 82 | // The first argument is typically a *testing.T value. 83 | func NewMockEncoder(t interface { 84 | mock.TestingT 85 | Cleanup(func()) 86 | }) *MockEncoder { 87 | mock := &MockEncoder{} 88 | mock.Mock.Test(t) 89 | 90 | t.Cleanup(func() { mock.AssertExpectations(t) }) 91 | 92 | return mock 93 | } 94 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/logeventprovider.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // MockLogEventProvider is an autogenerated mock type for the LogEventProvider type 14 | type MockLogEventProvider struct { 15 | mock.Mock 16 | } 17 | 18 | // Close provides a mock function with given fields: 19 | func (_m *MockLogEventProvider) Close() error { 20 | ret := _m.Called() 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for Close") 24 | } 25 | 26 | var r0 error 27 | if rf, ok := ret.Get(0).(func() error); ok { 28 | r0 = rf() 29 | } else { 30 | r0 = ret.Error(0) 31 | } 32 | 33 | return r0 34 | } 35 | 36 | // GetLatestPayloads provides a mock function with given fields: _a0 37 | func (_m *MockLogEventProvider) GetLatestPayloads(_a0 context.Context) ([]automation.UpkeepPayload, error) { 38 | ret := _m.Called(_a0) 39 | 40 | if len(ret) == 0 { 41 | panic("no return value specified for GetLatestPayloads") 42 | } 43 | 44 | var r0 []automation.UpkeepPayload 45 | var r1 error 46 | if rf, ok := ret.Get(0).(func(context.Context) ([]automation.UpkeepPayload, error)); ok { 47 | return rf(_a0) 48 | } 49 | if rf, ok := ret.Get(0).(func(context.Context) []automation.UpkeepPayload); ok { 50 | r0 = rf(_a0) 51 | } else { 52 | if ret.Get(0) != nil { 53 | r0 = ret.Get(0).([]automation.UpkeepPayload) 54 | } 55 | } 56 | 57 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 58 | r1 = rf(_a0) 59 | } else { 60 | r1 = ret.Error(1) 61 | } 62 | 63 | return r0, r1 64 | } 65 | 66 | // SetConfig provides a mock function with given fields: _a0 67 | func (_m *MockLogEventProvider) SetConfig(_a0 automation.LogEventProviderConfig) { 68 | _m.Called(_a0) 69 | } 70 | 71 | // Start provides a mock function with given fields: _a0 72 | func (_m *MockLogEventProvider) Start(_a0 context.Context) error { 73 | ret := _m.Called(_a0) 74 | 75 | if len(ret) == 0 { 76 | panic("no return value specified for Start") 77 | } 78 | 79 | var r0 error 80 | if rf, ok := ret.Get(0).(func(context.Context) error); ok { 81 | r0 = rf(_a0) 82 | } else { 83 | r0 = ret.Error(0) 84 | } 85 | 86 | return r0 87 | } 88 | 89 | // NewMockLogEventProvider creates a new instance of MockLogEventProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 90 | // The first argument is typically a *testing.T value. 91 | func NewMockLogEventProvider(t interface { 92 | mock.TestingT 93 | Cleanup(func()) 94 | }) *MockLogEventProvider { 95 | mock := &MockLogEventProvider{} 96 | mock.Mock.Test(t) 97 | 98 | t.Cleanup(func() { mock.AssertExpectations(t) }) 99 | 100 | return mock 101 | } 102 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/payloadbuilder.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // MockPayloadBuilder is an autogenerated mock type for the PayloadBuilder type 14 | type MockPayloadBuilder struct { 15 | mock.Mock 16 | } 17 | 18 | // BuildPayloads provides a mock function with given fields: _a0, _a1 19 | func (_m *MockPayloadBuilder) BuildPayloads(_a0 context.Context, _a1 ...automation.CoordinatedBlockProposal) ([]automation.UpkeepPayload, error) { 20 | _va := make([]interface{}, len(_a1)) 21 | for _i := range _a1 { 22 | _va[_i] = _a1[_i] 23 | } 24 | var _ca []interface{} 25 | _ca = append(_ca, _a0) 26 | _ca = append(_ca, _va...) 27 | ret := _m.Called(_ca...) 28 | 29 | if len(ret) == 0 { 30 | panic("no return value specified for BuildPayloads") 31 | } 32 | 33 | var r0 []automation.UpkeepPayload 34 | var r1 error 35 | if rf, ok := ret.Get(0).(func(context.Context, ...automation.CoordinatedBlockProposal) ([]automation.UpkeepPayload, error)); ok { 36 | return rf(_a0, _a1...) 37 | } 38 | if rf, ok := ret.Get(0).(func(context.Context, ...automation.CoordinatedBlockProposal) []automation.UpkeepPayload); ok { 39 | r0 = rf(_a0, _a1...) 40 | } else { 41 | if ret.Get(0) != nil { 42 | r0 = ret.Get(0).([]automation.UpkeepPayload) 43 | } 44 | } 45 | 46 | if rf, ok := ret.Get(1).(func(context.Context, ...automation.CoordinatedBlockProposal) error); ok { 47 | r1 = rf(_a0, _a1...) 48 | } else { 49 | r1 = ret.Error(1) 50 | } 51 | 52 | return r0, r1 53 | } 54 | 55 | // NewMockPayloadBuilder creates a new instance of MockPayloadBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 56 | // The first argument is typically a *testing.T value. 57 | func NewMockPayloadBuilder(t interface { 58 | mock.TestingT 59 | Cleanup(func()) 60 | }) *MockPayloadBuilder { 61 | mock := &MockPayloadBuilder{} 62 | mock.Mock.Test(t) 63 | 64 | t.Cleanup(func() { mock.AssertExpectations(t) }) 65 | 66 | return mock 67 | } 68 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/ratio.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // MockRatio is an autogenerated mock type for the Ratio type 8 | type MockRatio struct { 9 | mock.Mock 10 | } 11 | 12 | // OfInt provides a mock function with given fields: _a0 13 | func (_m *MockRatio) OfInt(_a0 int) int { 14 | ret := _m.Called(_a0) 15 | 16 | if len(ret) == 0 { 17 | panic("no return value specified for OfInt") 18 | } 19 | 20 | var r0 int 21 | if rf, ok := ret.Get(0).(func(int) int); ok { 22 | r0 = rf(_a0) 23 | } else { 24 | r0 = ret.Get(0).(int) 25 | } 26 | 27 | return r0 28 | } 29 | 30 | // NewMockRatio creates a new instance of MockRatio. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 31 | // The first argument is typically a *testing.T value. 32 | func NewMockRatio(t interface { 33 | mock.TestingT 34 | Cleanup(func()) 35 | }) *MockRatio { 36 | mock := &MockRatio{} 37 | mock.Mock.Test(t) 38 | 39 | t.Cleanup(func() { mock.AssertExpectations(t) }) 40 | 41 | return mock 42 | } 43 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/recoverableprovider.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // MockRecoverableProvider is an autogenerated mock type for the RecoverableProvider type 14 | type MockRecoverableProvider struct { 15 | mock.Mock 16 | } 17 | 18 | // GetRecoveryProposals provides a mock function with given fields: _a0 19 | func (_m *MockRecoverableProvider) GetRecoveryProposals(_a0 context.Context) ([]automation.UpkeepPayload, error) { 20 | ret := _m.Called(_a0) 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for GetRecoveryProposals") 24 | } 25 | 26 | var r0 []automation.UpkeepPayload 27 | var r1 error 28 | if rf, ok := ret.Get(0).(func(context.Context) ([]automation.UpkeepPayload, error)); ok { 29 | return rf(_a0) 30 | } 31 | if rf, ok := ret.Get(0).(func(context.Context) []automation.UpkeepPayload); ok { 32 | r0 = rf(_a0) 33 | } else { 34 | if ret.Get(0) != nil { 35 | r0 = ret.Get(0).([]automation.UpkeepPayload) 36 | } 37 | } 38 | 39 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 40 | r1 = rf(_a0) 41 | } else { 42 | r1 = ret.Error(1) 43 | } 44 | 45 | return r0, r1 46 | } 47 | 48 | // NewMockRecoverableProvider creates a new instance of MockRecoverableProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 49 | // The first argument is typically a *testing.T value. 50 | func NewMockRecoverableProvider(t interface { 51 | mock.TestingT 52 | Cleanup(func()) 53 | }) *MockRecoverableProvider { 54 | mock := &MockRecoverableProvider{} 55 | mock.Mock.Test(t) 56 | 57 | t.Cleanup(func() { mock.AssertExpectations(t) }) 58 | 59 | return mock 60 | } 61 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/result_store.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockResultStore is an autogenerated mock type for the ResultStore type 11 | type MockResultStore struct { 12 | mock.Mock 13 | } 14 | 15 | // Add provides a mock function with given fields: _a0 16 | func (_m *MockResultStore) Add(_a0 ...automation.CheckResult) { 17 | _va := make([]interface{}, len(_a0)) 18 | for _i := range _a0 { 19 | _va[_i] = _a0[_i] 20 | } 21 | var _ca []interface{} 22 | _ca = append(_ca, _va...) 23 | _m.Called(_ca...) 24 | } 25 | 26 | // Remove provides a mock function with given fields: _a0 27 | func (_m *MockResultStore) Remove(_a0 ...string) { 28 | _va := make([]interface{}, len(_a0)) 29 | for _i := range _a0 { 30 | _va[_i] = _a0[_i] 31 | } 32 | var _ca []interface{} 33 | _ca = append(_ca, _va...) 34 | _m.Called(_ca...) 35 | } 36 | 37 | // View provides a mock function with given fields: 38 | func (_m *MockResultStore) View() ([]automation.CheckResult, error) { 39 | ret := _m.Called() 40 | 41 | if len(ret) == 0 { 42 | panic("no return value specified for View") 43 | } 44 | 45 | var r0 []automation.CheckResult 46 | var r1 error 47 | if rf, ok := ret.Get(0).(func() ([]automation.CheckResult, error)); ok { 48 | return rf() 49 | } 50 | if rf, ok := ret.Get(0).(func() []automation.CheckResult); ok { 51 | r0 = rf() 52 | } else { 53 | if ret.Get(0) != nil { 54 | r0 = ret.Get(0).([]automation.CheckResult) 55 | } 56 | } 57 | 58 | if rf, ok := ret.Get(1).(func() error); ok { 59 | r1 = rf() 60 | } else { 61 | r1 = ret.Error(1) 62 | } 63 | 64 | return r0, r1 65 | } 66 | 67 | // NewMockResultStore creates a new instance of MockResultStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 68 | // The first argument is typically a *testing.T value. 69 | func NewMockResultStore(t interface { 70 | mock.TestingT 71 | Cleanup(func()) 72 | }) *MockResultStore { 73 | mock := &MockResultStore{} 74 | mock.Mock.Test(t) 75 | 76 | t.Cleanup(func() { mock.AssertExpectations(t) }) 77 | 78 | return mock 79 | } 80 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/runnable.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // MockRunnable is an autogenerated mock type for the Runnable type 14 | type MockRunnable struct { 15 | mock.Mock 16 | } 17 | 18 | // CheckUpkeeps provides a mock function with given fields: _a0, _a1 19 | func (_m *MockRunnable) CheckUpkeeps(_a0 context.Context, _a1 ...automation.UpkeepPayload) ([]automation.CheckResult, error) { 20 | _va := make([]interface{}, len(_a1)) 21 | for _i := range _a1 { 22 | _va[_i] = _a1[_i] 23 | } 24 | var _ca []interface{} 25 | _ca = append(_ca, _a0) 26 | _ca = append(_ca, _va...) 27 | ret := _m.Called(_ca...) 28 | 29 | if len(ret) == 0 { 30 | panic("no return value specified for CheckUpkeeps") 31 | } 32 | 33 | var r0 []automation.CheckResult 34 | var r1 error 35 | if rf, ok := ret.Get(0).(func(context.Context, ...automation.UpkeepPayload) ([]automation.CheckResult, error)); ok { 36 | return rf(_a0, _a1...) 37 | } 38 | if rf, ok := ret.Get(0).(func(context.Context, ...automation.UpkeepPayload) []automation.CheckResult); ok { 39 | r0 = rf(_a0, _a1...) 40 | } else { 41 | if ret.Get(0) != nil { 42 | r0 = ret.Get(0).([]automation.CheckResult) 43 | } 44 | } 45 | 46 | if rf, ok := ret.Get(1).(func(context.Context, ...automation.UpkeepPayload) error); ok { 47 | r1 = rf(_a0, _a1...) 48 | } else { 49 | r1 = ret.Error(1) 50 | } 51 | 52 | return r0, r1 53 | } 54 | 55 | // NewMockRunnable creates a new instance of MockRunnable. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 56 | // The first argument is typically a *testing.T value. 57 | func NewMockRunnable(t interface { 58 | mock.TestingT 59 | Cleanup(func()) 60 | }) *MockRunnable { 61 | mock := &MockRunnable{} 62 | mock.Mock.Test(t) 63 | 64 | t.Cleanup(func() { mock.AssertExpectations(t) }) 65 | 66 | return mock 67 | } 68 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/transmit_event_provider.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // TransmitEventProvider is an autogenerated mock type for the TransmitEventProvider type 14 | type TransmitEventProvider struct { 15 | mock.Mock 16 | } 17 | 18 | // GetLatestEvents provides a mock function with given fields: _a0 19 | func (_m *TransmitEventProvider) GetLatestEvents(_a0 context.Context) ([]automation.TransmitEvent, error) { 20 | ret := _m.Called(_a0) 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for GetLatestEvents") 24 | } 25 | 26 | var r0 []automation.TransmitEvent 27 | var r1 error 28 | if rf, ok := ret.Get(0).(func(context.Context) ([]automation.TransmitEvent, error)); ok { 29 | return rf(_a0) 30 | } 31 | if rf, ok := ret.Get(0).(func(context.Context) []automation.TransmitEvent); ok { 32 | r0 = rf(_a0) 33 | } else { 34 | if ret.Get(0) != nil { 35 | r0 = ret.Get(0).([]automation.TransmitEvent) 36 | } 37 | } 38 | 39 | if rf, ok := ret.Get(1).(func(context.Context) error); ok { 40 | r1 = rf(_a0) 41 | } else { 42 | r1 = ret.Error(1) 43 | } 44 | 45 | return r0, r1 46 | } 47 | 48 | // NewTransmitEventProvider creates a new instance of TransmitEventProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 49 | // The first argument is typically a *testing.T value. 50 | func NewTransmitEventProvider(t interface { 51 | mock.TestingT 52 | Cleanup(func()) 53 | }) *TransmitEventProvider { 54 | mock := &TransmitEventProvider{} 55 | mock.Mock.Test(t) 56 | 57 | t.Cleanup(func() { mock.AssertExpectations(t) }) 58 | 59 | return mock 60 | } 61 | -------------------------------------------------------------------------------- /pkg/v3/types/mocks/upkeep_state_updater.generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.43.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | automation "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // MockUpkeepStateUpdater is an autogenerated mock type for the UpkeepStateUpdater type 14 | type MockUpkeepStateUpdater struct { 15 | mock.Mock 16 | } 17 | 18 | // SetUpkeepState provides a mock function with given fields: _a0, _a1, _a2 19 | func (_m *MockUpkeepStateUpdater) SetUpkeepState(_a0 context.Context, _a1 automation.CheckResult, _a2 automation.UpkeepState) error { 20 | ret := _m.Called(_a0, _a1, _a2) 21 | 22 | if len(ret) == 0 { 23 | panic("no return value specified for SetUpkeepState") 24 | } 25 | 26 | var r0 error 27 | if rf, ok := ret.Get(0).(func(context.Context, automation.CheckResult, automation.UpkeepState) error); ok { 28 | r0 = rf(_a0, _a1, _a2) 29 | } else { 30 | r0 = ret.Error(0) 31 | } 32 | 33 | return r0 34 | } 35 | 36 | // NewMockUpkeepStateUpdater creates a new instance of MockUpkeepStateUpdater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 37 | // The first argument is typically a *testing.T value. 38 | func NewMockUpkeepStateUpdater(t interface { 39 | mock.TestingT 40 | Cleanup(func()) 41 | }) *MockUpkeepStateUpdater { 42 | mock := &MockUpkeepStateUpdater{} 43 | mock.Mock.Test(t) 44 | 45 | t.Cleanup(func() { mock.AssertExpectations(t) }) 46 | 47 | return mock 48 | } 49 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectName=chainlink-automation 2 | # projectKey is required (may be found under "Project Information" in Sonar or project url) 3 | sonar.projectKey=smartcontractkit_chainlink-automation 4 | sonar.sources=. 5 | 6 | # Full exclusions from the static analysis 7 | sonar.exclusions=**/node_modules/**/*, **/mocks/**/*, **/testdata/**/*, **/contracts/typechain/**/*, **/contracts/artifacts/**/*, **/contracts/cache/**/*, **/contracts/scripts/**/*, **/generated/**/*, **/fixtures/**/*, **/docs/**/*, **/tools/**/*, **/*.pb.go, **/*report.xml, **/*.config.ts, **/*.txt, **/*.abi, **/*.bin 8 | # Coverage exclusions 9 | sonar.coverage.exclusions=**/*.test.ts, **/*_test.go, **/contracts/test/**/*, **/contracts/**/tests/**/*, **/core/**/testutils/**/*, **/core/**/cltest/**/*, **/integration-tests/**/* 10 | 11 | # Tests' root folder, inclusions (tests to check and count) and exclusions 12 | sonar.tests=. 13 | sonar.test.inclusions=**/*_test.go -------------------------------------------------------------------------------- /tools/simulator/config/duration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | type Duration time.Duration 10 | 11 | func (d *Duration) UnmarshalJSON(b []byte) error { 12 | var raw string 13 | if err := json.Unmarshal(b, &raw); err != nil { 14 | return err 15 | } 16 | 17 | p, err := time.ParseDuration(raw) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | *d = Duration(p) 23 | return nil 24 | } 25 | 26 | func (d Duration) MarshalJSON() ([]byte, error) { 27 | str := fmt.Sprintf(`"%s"`, time.Duration(d).String()) 28 | 29 | return []byte(str), nil 30 | } 31 | 32 | func (d Duration) Value() time.Duration { 33 | return time.Duration(d) 34 | } 35 | -------------------------------------------------------------------------------- /tools/simulator/config/duration_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDurationEncoding(t *testing.T) { 12 | rawValue := `"300ms"` 13 | 14 | var tm Duration 15 | err := json.Unmarshal([]byte(rawValue), &tm) 16 | 17 | require.NoError(t, err, "no error expected from unmarshalling") 18 | 19 | value, err := tm.MarshalJSON() 20 | 21 | require.NoError(t, err, "no error expected from marshalling") 22 | 23 | assert.Equal(t, rawValue, string(value)) 24 | } 25 | -------------------------------------------------------------------------------- /tools/simulator/config/keyring_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "testing" 8 | 9 | "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "golang.org/x/crypto/curve25519" 13 | 14 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/plugin" 15 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 16 | ) 17 | 18 | func TestOffchainKeyring_OffchainSign(t *testing.T) { 19 | keyring, err := config.NewOffchainKeyring(rand.Reader, rand.Reader) 20 | 21 | require.NoError(t, err) 22 | 23 | signature, err := keyring.OffchainSign([]byte("message")) 24 | 25 | require.NoError(t, err) 26 | assert.Greater(t, len(signature), 0, "signature must have a length greater than zero") 27 | 28 | signature2, _ := keyring.OffchainSign([]byte("message")) 29 | 30 | assert.Equal(t, signature, signature2, "signing the same message twice should return equal results") 31 | } 32 | 33 | func TestOffchainKeyring_OffchainPublicKey(t *testing.T) { 34 | keyring, err := config.NewOffchainKeyring(rand.Reader, rand.Reader) 35 | 36 | require.NoError(t, err) 37 | 38 | pubkey := keyring.OffchainPublicKey() 39 | 40 | var compare [ed25519.PublicKeySize]byte 41 | 42 | assert.NotEqual(t, compare, pubkey, "public key should not be empty bytes") 43 | } 44 | 45 | func TestOffchainKeyring_ConfigEncryptionPublicKey(t *testing.T) { 46 | keyring, err := config.NewOffchainKeyring(rand.Reader, rand.Reader) 47 | 48 | require.NoError(t, err) 49 | 50 | pubkey := keyring.ConfigEncryptionPublicKey() 51 | 52 | var compare [curve25519.PointSize]byte 53 | 54 | assert.NotEqual(t, compare, pubkey, "public key should not be empty bytes") 55 | } 56 | 57 | func TestEvmKeyring(t *testing.T) { 58 | keyring, err := config.NewEVMKeyring(rand.Reader) 59 | keyring2, _ := config.NewEVMKeyring(rand.Reader) 60 | 61 | require.NoError(t, err) 62 | 63 | assert.NotEmpty(t, keyring.PublicKey(), "public key bytes should not be empty") 64 | assert.NotEmpty(t, keyring.PKString(), "public key string should not be empty") 65 | 66 | digest := sha256.Sum256([]byte("message")) 67 | round := uint64(10_000) 68 | report := ocr3types.ReportWithInfo[plugin.AutomationReportInfo]{ 69 | Report: []byte("report"), 70 | Info: plugin.AutomationReportInfo{}, 71 | } 72 | 73 | signature, err := keyring.Sign(digest, round, report) 74 | 75 | require.NoError(t, err) 76 | require.NotEmpty(t, signature, "signature bytes should not be empty") 77 | 78 | verified := keyring2.Verify(keyring.PublicKey(), digest, round, report, signature) 79 | 80 | assert.True(t, verified, "keyring 2 must be able to verify signature of keyring 1") 81 | } 82 | 83 | func TestEvmKeyring_Encode(t *testing.T) { 84 | keyring, err := config.NewEVMKeyring(rand.Reader) 85 | keyring2, _ := config.NewEVMKeyring(rand.Reader) 86 | 87 | require.NoError(t, err) 88 | 89 | encoded, err := keyring.Marshal() 90 | 91 | require.NoError(t, err) 92 | require.NoError(t, keyring2.Unmarshal(encoded)) 93 | 94 | assert.Equal(t, keyring.PKString(), keyring2.PKString()) 95 | } 96 | -------------------------------------------------------------------------------- /tools/simulator/config/simulation_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestSimulationPlan_EncodeDecode(t *testing.T) { 13 | plan := SimulationPlan{ 14 | Node: Node{ 15 | Count: 4, 16 | MaxServiceWorkers: 10, 17 | MaxQueueSize: 1000, 18 | }, 19 | Network: Network{ 20 | MaxLatency: Duration(300 * time.Millisecond), 21 | }, 22 | RPC: RPC{}, 23 | Blocks: Blocks{ 24 | Genesis: big.NewInt(3), 25 | Cadence: Duration(1 * time.Second), 26 | Jitter: Duration(200 * time.Millisecond), 27 | Duration: 20, 28 | EndPadding: 20, 29 | }, 30 | ConfigEvents: []OCR3ConfigEvent{}, 31 | GenerateUpkeeps: []GenerateUpkeepEvent{}, 32 | LogEvents: []LogTriggerEvent{}, 33 | } 34 | 35 | encoded, err := plan.Encode() 36 | 37 | require.NoError(t, err, "no error expected from encoding the simulation plan") 38 | 39 | decodedPlan, err := DecodeSimulationPlan(encoded) 40 | 41 | require.NoError(t, err, "no error expected from decoding the simulation plan") 42 | 43 | assert.Equal(t, plan, decodedPlan, "simulation plan should match after encoding and decoding") 44 | } 45 | -------------------------------------------------------------------------------- /tools/simulator/io/monitor.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import "io" 4 | 5 | type MonitorToWriter struct { 6 | w io.Writer 7 | } 8 | 9 | func NewMonitorToWriter(w io.Writer) *MonitorToWriter { 10 | return &MonitorToWriter{ 11 | w: w, 12 | } 13 | } 14 | 15 | func (m *MonitorToWriter) SendLog(log []byte) { 16 | _, _ = m.w.Write(log) 17 | } 18 | -------------------------------------------------------------------------------- /tools/simulator/node/active.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | type Closable interface { 4 | Close() error 5 | } 6 | 7 | type Simulator struct { 8 | Service Closable 9 | } 10 | -------------------------------------------------------------------------------- /tools/simulator/node/group.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "io" 5 | "log" 6 | 7 | "github.com/smartcontractkit/libocr/commontypes" 8 | "github.com/smartcontractkit/libocr/offchainreporting2/types" 9 | 10 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 11 | simio "github.com/smartcontractkit/chainlink-automation/tools/simulator/io" 12 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 13 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/loader" 14 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/net" 15 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/telemetry" 16 | ) 17 | 18 | type GroupConfig struct { 19 | SimulationPlan config.SimulationPlan 20 | Digester types.OffchainConfigDigester 21 | Upkeeps []chain.SimulatedUpkeep 22 | Collectors []telemetry.Collector 23 | Logger *log.Logger 24 | } 25 | 26 | type Group struct { 27 | conf config.SimulationPlan 28 | nodes map[string]*Simulator 29 | network *net.SimulatedNetwork 30 | digester types.OffchainConfigDigester 31 | blockSrc *chain.BlockBroadcaster 32 | transmitter *loader.OCR3TransmitLoader 33 | confLoader *loader.OCR3ConfigLoader 34 | upkeeps []chain.SimulatedUpkeep 35 | monitor commontypes.MonitoringEndpoint 36 | collectors []telemetry.Collector 37 | logger *log.Logger 38 | } 39 | 40 | func NewGroup(conf GroupConfig, progress *telemetry.ProgressTelemetry) (*Group, error) { 41 | lTransmit, err := loader.NewOCR3TransmitLoader(conf.SimulationPlan, progress, conf.Logger) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | lOCR3Config := loader.NewOCR3ConfigLoader(conf.SimulationPlan, progress, conf.Digester, conf.Logger) 47 | 48 | lUpkeep, err := loader.NewUpkeepConfigLoader(conf.SimulationPlan, progress) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | lLogTriggers, err := loader.NewLogTriggerLoader(conf.SimulationPlan, progress) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | loaders := []chain.BlockLoaderFunc{ 59 | lTransmit.Load, 60 | lOCR3Config.Load, 61 | lUpkeep.Load, 62 | lLogTriggers.Load, 63 | } 64 | 65 | return &Group{ 66 | conf: conf.SimulationPlan, 67 | nodes: make(map[string]*Simulator), 68 | network: net.NewSimulatedNetwork(conf.SimulationPlan.Network.MaxLatency.Value()), 69 | digester: conf.Digester, 70 | blockSrc: chain.NewBlockBroadcaster(conf.SimulationPlan.Blocks, conf.SimulationPlan.RPC.MaxBlockDelay, conf.Logger, progress, loaders...), 71 | transmitter: lTransmit, 72 | confLoader: lOCR3Config, 73 | upkeeps: conf.Upkeeps, 74 | monitor: simio.NewMonitorToWriter(io.Discard), // monitor data is not text so not sure what to do with this yet 75 | collectors: conf.Collectors, 76 | logger: conf.Logger, 77 | }, nil 78 | } 79 | -------------------------------------------------------------------------------- /tools/simulator/node/statistics.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import "math" 4 | 5 | func findMedianAndSplitData(values []int) (median float64, a []int, b []int) { 6 | if len(values)%2 == 0 { 7 | // median is the average of the two middle integers 8 | idx := len(values) / 2 9 | median = float64(values[idx]+values[idx+1]) / 2 10 | 11 | a = values[:idx] 12 | b = values[idx:] 13 | } else { 14 | // median is the middle value 15 | idx := int(math.Floor(float64(len(values))/2)) + 1 16 | median = float64(values[idx]) 17 | 18 | a = values[:idx] 19 | b = values[idx+1:] 20 | } 21 | 22 | return 23 | } 24 | 25 | func findLowestAndOutliers(lowerFence float64, set []int) (lowest int, outliers int) { 26 | lowest = math.MaxInt 27 | for i := 0; i < len(set); i++ { 28 | if set[i] < int(lowerFence) { 29 | outliers++ 30 | if set[i] < lowest { 31 | lowest = set[i] 32 | } 33 | } 34 | } 35 | if lowest == math.MaxInt { 36 | lowest = -1 37 | } 38 | return 39 | } 40 | 41 | func findHighestAndOutliers(upperFence float64, set []int) (highest int, outliers int) { 42 | highest = -1 43 | for i := 0; i < len(set); i++ { 44 | if set[i] > int(upperFence) { 45 | outliers++ 46 | if set[i] > highest { 47 | highest = set[i] 48 | } 49 | } 50 | } 51 | 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /tools/simulator/plans/simplan_failed_rpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "totalNodeCount": 4, 4 | "maxNodeServiceWorkers": 100, 5 | "maxNodeServiceQueueSize": 1000 6 | }, 7 | "p2pNetwork": { 8 | "maxLatency": "100ms" 9 | }, 10 | "rpc": { 11 | "maxBlockDelay": 600, 12 | "averageLatency": 300, 13 | "errorRate": 1.0, 14 | "rateLimitThreshold": 1000 15 | }, 16 | "blocks": { 17 | "genesisBlock": 128943862, 18 | "blockCadence": "1s", 19 | "durationInBlocks": 30, 20 | "endPadding": 10 21 | }, 22 | "events": [ 23 | { 24 | "type": "ocr3config", 25 | "eventBlockNumber": 128943863, 26 | "comment": "initial ocr config (valid)", 27 | "maxFaultyNodes": 1, 28 | "encodedOffchainConfig": "{\"version\":\"v3\",\"performLockoutWindow\":100000,\"targetProbability\":\"0.999\",\"targetInRounds\":4,\"minConfirmations\":1,\"gasLimitPerReport\":1000000,\"gasOverheadPerUpkeep\":300000,\"maxUpkeepBatchSize\":10}", 29 | "maxRoundsPerEpoch": 7, 30 | "deltaProgress": "10s", 31 | "deltaResend": "10s", 32 | "deltaInitial": "300ms", 33 | "deltaRound": "1100ms", 34 | "deltaGrace": "300ms", 35 | "deltaCertifiedCommitRequest": "200ms", 36 | "deltaStage": "20s", 37 | "maxQueryTime": "50ms", 38 | "maxObservationTime": "100ms", 39 | "maxShouldAcceptTime": "50ms", 40 | "maxShouldTransmitTime": "50ms" 41 | }, 42 | { 43 | "type": "generateUpkeeps", 44 | "eventBlockNumber": 128943862, 45 | "comment": "~3 performs per upkeep", 46 | "count": 10, 47 | "startID": 200, 48 | "eligibilityFunc": "30x - 15", 49 | "offsetFunc": "2x + 1", 50 | "upkeepType": "conditional", 51 | "expected": "none" 52 | }, 53 | { 54 | "type": "generateUpkeeps", 55 | "eventBlockNumber": 128943862, 56 | "comment": "single log triggered upkeep", 57 | "count": 1, 58 | "startID": 300, 59 | "eligibilityFunc": "always", 60 | "upkeepType": "logTrigger", 61 | "logTriggeredBy": "test_trigger_event", 62 | "expected": "none" 63 | }, 64 | { 65 | "type": "generateUpkeeps", 66 | "eventBlockNumber": 128943882, 67 | "comment": "single log triggered upkeep", 68 | "count": 1, 69 | "startID": 400, 70 | "eligibilityFunc": "never", 71 | "upkeepType": "logTrigger", 72 | "logTriggeredBy": "test_trigger_event" 73 | }, 74 | { 75 | "type": "logTrigger", 76 | "eventBlockNumber": 128943872, 77 | "comment": "trigger 10 blocks after trigger upkeep created", 78 | "triggerValue": "test_trigger_event" 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /tools/simulator/plans/simplan_fast_check.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": { 3 | "totalNodeCount": 4, 4 | "maxNodeServiceWorkers": 100, 5 | "maxNodeServiceQueueSize": 1000 6 | }, 7 | "p2pNetwork": { 8 | "maxLatency": "100ms" 9 | }, 10 | "rpc": { 11 | "maxBlockDelay": 600, 12 | "averageLatency": 300, 13 | "errorRate": 0.02, 14 | "rateLimitThreshold": 1000 15 | }, 16 | "blocks": { 17 | "genesisBlock": 128943862, 18 | "blockCadence": "1s", 19 | "durationInBlocks": 60, 20 | "endPadding": 20 21 | }, 22 | "events": [ 23 | { 24 | "type": "ocr3config", 25 | "eventBlockNumber": 128943863, 26 | "comment": "initial ocr config (valid)", 27 | "maxFaultyNodes": 1, 28 | "encodedOffchainConfig": "{\"version\":\"v3\",\"performLockoutWindow\":100000,\"targetProbability\":\"0.999\",\"targetInRounds\":4,\"minConfirmations\":1,\"gasLimitPerReport\":1000000,\"gasOverheadPerUpkeep\":300000,\"maxUpkeepBatchSize\":10}", 29 | "maxRoundsPerEpoch": 7, 30 | "deltaProgress": "10s", 31 | "deltaResend": "10s", 32 | "deltaInitial": "300ms", 33 | "deltaRound": "1100ms", 34 | "deltaGrace": "300ms", 35 | "deltaCertifiedCommitRequest": "200ms", 36 | "deltaStage": "20s", 37 | "maxQueryTime": "50ms", 38 | "maxObservationTime": "100ms", 39 | "maxShouldAcceptTime": "50ms", 40 | "maxShouldTransmitTime": "50ms" 41 | }, 42 | { 43 | "type": "generateUpkeeps", 44 | "eventBlockNumber": 128943862, 45 | "comment": "~3 performs per upkeep", 46 | "count": 10, 47 | "startID": 200, 48 | "eligibilityFunc": "30x - 15", 49 | "offsetFunc": "2x + 1", 50 | "upkeepType": "conditional" 51 | }, 52 | { 53 | "type": "generateUpkeeps", 54 | "eventBlockNumber": 128943862, 55 | "comment": "single log triggered upkeep", 56 | "count": 1, 57 | "startID": 300, 58 | "eligibilityFunc": "always", 59 | "upkeepType": "logTrigger", 60 | "logTriggeredBy": "test_trigger_event" 61 | }, 62 | { 63 | "type": "generateUpkeeps", 64 | "eventBlockNumber": 128943882, 65 | "comment": "single log triggered upkeep", 66 | "count": 1, 67 | "startID": 400, 68 | "eligibilityFunc": "never", 69 | "upkeepType": "logTrigger", 70 | "logTriggeredBy": "test_trigger_event" 71 | }, 72 | { 73 | "type": "logTrigger", 74 | "eventBlockNumber": 128943872, 75 | "comment": "trigger 10 blocks after trigger upkeep created", 76 | "triggerValue": "test_trigger_event" 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /tools/simulator/run/profile.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type ProfilerConfig struct { 11 | // Enabled true starts the profiler 12 | Enabled bool 13 | // PprofPort is the port to listen for pprof input 14 | PprofPort int 15 | // Wait is the time to wait for the profiler to start before moving on 16 | Wait time.Duration 17 | } 18 | 19 | func Profiler(config ProfilerConfig, logger *log.Logger) { 20 | if config.Enabled { 21 | if logger != nil { 22 | logger.Println("starting profiler; waiting 5 seconds to start simulation") 23 | } 24 | 25 | go func() { 26 | err := http.ListenAndServe(fmt.Sprintf("localhost:%d", config.PprofPort), nil) 27 | if logger != nil && err != nil { 28 | logger.Printf("pprof listener returned error on exit: %s", err) 29 | } 30 | }() 31 | 32 | if config.Wait > 0 { 33 | time.Sleep(config.Wait) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tools/simulator/run/runbook.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 7 | ) 8 | 9 | func LoadSimulationPlan(path string) (config.SimulationPlan, error) { 10 | data, err := os.ReadFile(path) 11 | if err != nil { 12 | return config.SimulationPlan{}, err 13 | } 14 | 15 | return config.DecodeSimulationPlan(data) 16 | } 17 | -------------------------------------------------------------------------------- /tools/simulator/simulate/chain/block.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/smartcontractkit/libocr/offchainreporting2plus/types" 7 | ) 8 | 9 | type Block struct { 10 | Hash [32]byte 11 | Number *big.Int 12 | Transactions []interface{} 13 | } 14 | 15 | type Log struct { 16 | TxHash [32]byte 17 | BlockNumber *big.Int 18 | BlockHash [32]byte 19 | Idx uint32 20 | TriggerValue string 21 | } 22 | 23 | type OCR3ConfigTransaction struct { 24 | Config types.ContractConfig 25 | } 26 | 27 | type PerformUpkeepTransaction struct { 28 | Transmits []TransmitEvent 29 | } 30 | 31 | type UpkeepCreatedTransaction struct { 32 | Upkeep SimulatedUpkeep 33 | } 34 | 35 | // below this line should not be in this package 36 | type UpkeepType int 37 | 38 | const ( 39 | ConditionalType UpkeepType = iota 40 | LogTriggerType 41 | ) 42 | 43 | type SimulatedUpkeep struct { 44 | ID *big.Int 45 | CreateInBlock *big.Int 46 | UpkeepID [32]byte 47 | Type UpkeepType 48 | AlwaysEligible bool 49 | EligibleAt []*big.Int 50 | TriggeredBy string 51 | CheckData []byte 52 | Expected bool 53 | } 54 | 55 | type SimulatedLog struct { 56 | TriggerAt *big.Int 57 | TriggerValue string 58 | } 59 | 60 | type TransmitEvent struct { 61 | SendingAddress string 62 | Report []byte 63 | Hash [32]byte 64 | Round uint64 65 | BlockNumber *big.Int 66 | BlockHash [32]byte 67 | } 68 | -------------------------------------------------------------------------------- /tools/simulator/simulate/chain/broadcaster_test.go: -------------------------------------------------------------------------------- 1 | package chain_test 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "math/big" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 13 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 14 | ) 15 | 16 | func TestBlockBroadcaster(t *testing.T) { 17 | t.Parallel() 18 | 19 | conf := config.Blocks{ 20 | Genesis: big.NewInt(10), 21 | Cadence: config.Duration(50 * time.Millisecond), 22 | Jitter: config.Duration(10 * time.Millisecond), 23 | Duration: 5, 24 | EndPadding: 0, 25 | } 26 | maxDelay := 10 27 | logger := log.New(io.Discard, "", 0) 28 | loader := new(mockBlockLoader) 29 | broadcaster := chain.NewBlockBroadcaster(conf, maxDelay, logger, nil, loader.Load) 30 | 31 | sub1ID, chBlocks1 := broadcaster.Subscribe(true) 32 | sub2ID, chBlocks2 := broadcaster.Subscribe(true) 33 | _ = broadcaster.Start() 34 | 35 | <-chBlocks1 36 | <-chBlocks2 37 | 38 | broadcaster.Unsubscribe(sub1ID) 39 | broadcaster.Unsubscribe(sub2ID) 40 | broadcaster.Stop() 41 | 42 | assert.True(t, loader.called) 43 | } 44 | 45 | func TestBlockBroadcaster_Close_After_Limit(t *testing.T) { 46 | t.Parallel() 47 | 48 | conf := config.Blocks{ 49 | Genesis: big.NewInt(10), 50 | Cadence: config.Duration(50 * time.Millisecond), 51 | Jitter: config.Duration(10 * time.Millisecond), 52 | Duration: 5, 53 | EndPadding: 1, 54 | } 55 | maxDelay := 10 56 | logger := log.New(io.Discard, "", 0) 57 | broadcaster := chain.NewBlockBroadcaster(conf, maxDelay, logger, nil) 58 | 59 | closed := broadcaster.Start() 60 | 61 | <-closed 62 | } 63 | 64 | type mockBlockLoader struct { 65 | called bool 66 | } 67 | 68 | func (_m *mockBlockLoader) Load(_ *chain.Block) { 69 | _m.called = true 70 | } 71 | -------------------------------------------------------------------------------- /tools/simulator/simulate/chain/generate_test.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | 7 | "github.com/shopspring/decimal" 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 11 | ) 12 | 13 | func TestGenerateAllUpkeeps(t *testing.T) { 14 | plan := config.SimulationPlan{ 15 | Blocks: config.Blocks{ 16 | Genesis: big.NewInt(128_943_862), 17 | Duration: 10, 18 | }, 19 | GenerateUpkeeps: []config.GenerateUpkeepEvent{ 20 | { 21 | Count: 15, 22 | StartID: big.NewInt(200), 23 | EligibilityFunc: "24x - 3", 24 | OffsetFunc: "3x - 4", 25 | UpkeepType: config.ConditionalUpkeepType, 26 | }, 27 | { 28 | Count: 4, 29 | StartID: big.NewInt(200), 30 | EligibilityFunc: "always", 31 | UpkeepType: config.LogTriggerUpkeepType, 32 | }, 33 | }, 34 | } 35 | 36 | generated, err := GenerateAllUpkeeps(plan) 37 | 38 | assert.NoError(t, err) 39 | assert.Len(t, generated, 19) 40 | } 41 | 42 | func TestGenerateEligibles(t *testing.T) { 43 | up := SimulatedUpkeep{} 44 | err := generateEligibles(&up, big.NewInt(9), big.NewInt(50), "4x + 5") 45 | expected := []int64{14, 18, 22, 26, 30, 34, 38, 42, 46} 46 | 47 | s := []int64{} 48 | for _, v := range up.EligibleAt { 49 | s = append(s, v.Int64()) 50 | } 51 | 52 | assert.NoError(t, err) 53 | assert.Equal(t, expected, s) 54 | } 55 | 56 | func TestOperate(t *testing.T) { 57 | tests := []struct { 58 | Name string 59 | A int64 60 | B int64 61 | Op string 62 | ExpZ int64 63 | }{ 64 | {Name: "Addition", A: 1, B: 4, Op: "+", ExpZ: 5}, 65 | {Name: "Multiplication", A: 3, B: 4, Op: "*", ExpZ: 12}, 66 | {Name: "Subtraction", A: 4, B: 2, Op: "-", ExpZ: 2}, 67 | } 68 | 69 | for _, test := range tests { 70 | a := decimal.NewFromInt(test.A) 71 | b := decimal.NewFromInt(test.B) 72 | 73 | z := operate(a, b, test.Op) 74 | 75 | assert.Equal(t, decimal.NewFromInt(test.ExpZ), z) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tools/simulator/simulate/chain/history.go: -------------------------------------------------------------------------------- 1 | package chain 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "runtime" 7 | "sync" 8 | 9 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 10 | 11 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/util" 12 | ) 13 | 14 | const ( 15 | defaultBlockHistoryChannelDepth = 100 16 | defaultHistoryDepth = 256 17 | ) 18 | 19 | type BlockHistoryTracker struct { 20 | // provided dependencies 21 | listener *Listener 22 | logger *log.Logger 23 | 24 | // internal state values 25 | mu sync.RWMutex 26 | channels map[int]chan ocr2keepers.BlockHistory 27 | history *util.SortedKeyMap[Block] 28 | count int 29 | 30 | // service values 31 | chDone chan struct{} 32 | } 33 | 34 | func NewBlockHistoryTracker(listener *Listener, logger *log.Logger) *BlockHistoryTracker { 35 | tracker := &BlockHistoryTracker{ 36 | listener: listener, 37 | logger: logger, 38 | channels: make(map[int]chan ocr2keepers.BlockHistory), 39 | history: util.NewSortedKeyMap[Block](), 40 | chDone: make(chan struct{}), 41 | } 42 | 43 | go tracker.run() 44 | 45 | runtime.SetFinalizer(tracker, func(srv *BlockHistoryTracker) { srv.stop() }) 46 | 47 | return tracker 48 | } 49 | 50 | func (src *BlockHistoryTracker) Close() error { 51 | return nil 52 | } 53 | 54 | func (src *BlockHistoryTracker) Start(_ context.Context) error { 55 | return nil 56 | } 57 | 58 | // Subscribe provides an identifier integer, a new channel, and potentially an error 59 | func (ht *BlockHistoryTracker) Subscribe() (int, chan ocr2keepers.BlockHistory, error) { 60 | ht.mu.Lock() 61 | defer ht.mu.Unlock() 62 | 63 | chHistory := make(chan ocr2keepers.BlockHistory, defaultBlockHistoryChannelDepth) 64 | ht.count++ 65 | 66 | ht.channels[ht.count] = chHistory 67 | 68 | return ht.count, chHistory, nil 69 | } 70 | 71 | // Unsubscribe requires an identifier integer and indicates the provided channel should be closed 72 | func (ht *BlockHistoryTracker) Unsubscribe(channelID int) error { 73 | ht.mu.Lock() 74 | defer ht.mu.Unlock() 75 | 76 | if chOpen, ok := ht.channels[channelID]; ok { 77 | close(chOpen) 78 | delete(ht.channels, channelID) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (ht *BlockHistoryTracker) run() { 85 | chEvents := ht.listener.Subscribe(BlockChannel) 86 | 87 | for { 88 | select { 89 | case event := <-chEvents: 90 | switch evt := event.Event.(type) { 91 | case Block: 92 | ht.history.Set(evt.Number.String(), evt) 93 | ht.broadcast() 94 | } 95 | case <-ht.chDone: 96 | return 97 | } 98 | } 99 | } 100 | 101 | func (ht *BlockHistoryTracker) stop() { 102 | close(ht.chDone) 103 | } 104 | 105 | func (ht *BlockHistoryTracker) broadcast() { 106 | ht.mu.RLock() 107 | defer ht.mu.RUnlock() 108 | 109 | history := []ocr2keepers.BlockKey{} 110 | 111 | keys := ht.history.Keys(defaultHistoryDepth) 112 | for _, key := range keys { 113 | block, _ := ht.history.Get(key) 114 | 115 | history = append(history, ocr2keepers.BlockKey{ 116 | Number: ocr2keepers.BlockNumber(block.Number.Uint64()), 117 | Hash: block.Hash, 118 | }) 119 | } 120 | 121 | for _, chOpen := range ht.channels { 122 | chOpen <- history 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tools/simulator/simulate/chain/history_test.go: -------------------------------------------------------------------------------- 1 | package chain_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "math/big" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 15 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 16 | ) 17 | 18 | func TestBlockHistoryTracker(t *testing.T) { 19 | t.Parallel() 20 | 21 | logger := log.New(io.Discard, "", 0) 22 | conf := config.Blocks{ 23 | Genesis: new(big.Int).SetInt64(1), 24 | Cadence: config.Duration(100 * time.Millisecond), 25 | Jitter: config.Duration(0), 26 | Duration: 10, 27 | } 28 | 29 | broadcaster := chain.NewBlockBroadcaster(conf, 1, logger, nil, loadTestUpkeep) 30 | listener := chain.NewListener(broadcaster, logger) 31 | 32 | deadline, ok := t.Deadline() 33 | if !ok { 34 | deadline = time.Now().Add(5 * time.Second) 35 | } 36 | 37 | tracker := chain.NewBlockHistoryTracker(listener, logger) 38 | 39 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 40 | idx, chHistory, err := tracker.Subscribe() 41 | 42 | require.NoError(t, err, "no error expected from subscribe to tracker") 43 | 44 | broadcaster.Start() 45 | select { 46 | case <-ctx.Done(): 47 | t.Log("context deadline was passed before upkeep was broadcast") 48 | t.Fail() 49 | case <-chHistory: 50 | } 51 | 52 | err = tracker.Unsubscribe(idx) 53 | 54 | assert.NoError(t, err, "no error expected when unsubscribing from tracker") 55 | 56 | cancel() 57 | 58 | broadcaster.Stop() 59 | } 60 | -------------------------------------------------------------------------------- /tools/simulator/simulate/chain/listener_test.go: -------------------------------------------------------------------------------- 1 | package chain_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "math/big" 8 | "testing" 9 | "time" 10 | 11 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 12 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 13 | ) 14 | 15 | func TestListener(t *testing.T) { 16 | t.Parallel() 17 | 18 | logger := log.New(io.Discard, "", 0) 19 | conf := config.Blocks{ 20 | Genesis: new(big.Int).SetInt64(1), 21 | Cadence: config.Duration(100 * time.Millisecond), 22 | Jitter: config.Duration(0), 23 | Duration: 10, 24 | } 25 | 26 | broadcaster := chain.NewBlockBroadcaster(conf, 1, logger, nil, loadTestUpkeep) 27 | listener := chain.NewListener(broadcaster, logger) 28 | 29 | deadline, ok := t.Deadline() 30 | if !ok { 31 | deadline = time.Now().Add(5 * time.Second) 32 | } 33 | 34 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 35 | 36 | broadcaster.Start() 37 | 38 | select { 39 | case <-ctx.Done(): 40 | t.Log("context deadline was passed before upkeep was broadcast") 41 | t.Fail() 42 | case <-listener.Subscribe(chain.CreateUpkeepChannel): 43 | } 44 | 45 | cancel() 46 | 47 | broadcaster.Stop() 48 | } 49 | 50 | func loadTestUpkeep(block *chain.Block) { 51 | if block.Number.Cmp(new(big.Int).SetInt64(5)) >= 1 { 52 | block.Transactions = append(block.Transactions, chain.UpkeepCreatedTransaction{}) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tools/simulator/simulate/db/ocr3.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/smartcontractkit/libocr/offchainreporting2plus/types" 9 | ) 10 | 11 | type SimulatedOCR3Database struct { 12 | mu sync.RWMutex 13 | states map[[32]byte]types.PersistentState 14 | transmitDigestLookup map[[32]byte][]types.ReportTimestamp 15 | pendingTransmit map[types.ReportTimestamp]types.PendingTransmission 16 | 17 | // used values 18 | protoState map[string][]byte 19 | config *types.ContractConfig 20 | } 21 | 22 | func NewSimulatedOCR3Database() *SimulatedOCR3Database { 23 | return &SimulatedOCR3Database{ 24 | states: make(map[[32]byte]types.PersistentState), 25 | transmitDigestLookup: make(map[[32]byte][]types.ReportTimestamp), 26 | pendingTransmit: make(map[types.ReportTimestamp]types.PendingTransmission), 27 | // used values 28 | protoState: make(map[string][]byte), 29 | } 30 | } 31 | 32 | func (d *SimulatedOCR3Database) ReadConfig(_ context.Context) (*types.ContractConfig, error) { 33 | d.mu.RLock() 34 | defer d.mu.RUnlock() 35 | 36 | if d.config == nil { 37 | return nil, fmt.Errorf("not found") 38 | } 39 | 40 | return d.config, nil 41 | } 42 | 43 | func (d *SimulatedOCR3Database) WriteConfig(_ context.Context, config types.ContractConfig) error { 44 | d.mu.Lock() 45 | defer d.mu.Unlock() 46 | 47 | d.config = &config 48 | return nil 49 | } 50 | 51 | // In case the key is not found, nil should be returned. 52 | func (d *SimulatedOCR3Database) ReadProtocolState(ctx context.Context, configDigest types.ConfigDigest, key string) ([]byte, error) { 53 | // might need to check against latest config digest or scope to digest 54 | val, ok := d.protoState[key] 55 | if !ok { 56 | return nil, nil 57 | } 58 | 59 | return val, nil 60 | } 61 | 62 | // Writing with a nil value is the same as deleting. 63 | func (d *SimulatedOCR3Database) WriteProtocolState(ctx context.Context, configDigest types.ConfigDigest, key string, value []byte) error { 64 | d.protoState[key] = value 65 | 66 | // might need to check against latest config digest or scope to digest 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /tools/simulator/simulate/db/upkeep.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | 6 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 7 | ) 8 | 9 | type UpkeepStateDatabase struct { 10 | state map[string]ocr2keepers.UpkeepState 11 | } 12 | 13 | func NewUpkeepStateDatabase() *UpkeepStateDatabase { 14 | return &UpkeepStateDatabase{ 15 | state: make(map[string]ocr2keepers.UpkeepState), 16 | } 17 | } 18 | 19 | func (usd *UpkeepStateDatabase) SetUpkeepState(_ context.Context, result ocr2keepers.CheckResult, state ocr2keepers.UpkeepState) error { 20 | usd.state[result.WorkID] = state 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /tools/simulator/simulate/hydrator.go: -------------------------------------------------------------------------------- 1 | package simulate 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/plugin" 7 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 8 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 9 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/db" 10 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/loader" 11 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/net" 12 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/ocr" 13 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/upkeep" 14 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/telemetry" 15 | ) 16 | 17 | const ( 18 | DefaultLookbackBlocks = 100 19 | ) 20 | 21 | func HydrateConfig( 22 | name string, 23 | config *plugin.DelegateConfig, 24 | blocks *chain.BlockBroadcaster, 25 | transmitter *loader.OCR3TransmitLoader, 26 | conf config.SimulationPlan, 27 | netTelemetry net.NetTelemetry, 28 | conTelemetry *telemetry.WrappedContractCollector, 29 | logger *log.Logger, 30 | ) error { 31 | listener := chain.NewListener(blocks, logger) 32 | active := upkeep.NewActiveTracker(listener, logger) 33 | performs := upkeep.NewPerformTracker(listener, logger) 34 | 35 | triggered := upkeep.NewLogTriggerTracker(listener, active, performs, logger) 36 | source := upkeep.NewSource(active, triggered, DefaultLookbackBlocks, logger) 37 | 38 | config.ContractConfigTracker = ocr.NewOCR3ConfigTracker(listener, logger) 39 | config.ContractTransmitter = ocr.NewOCR3Transmitter(name, transmitter) 40 | config.KeepersDatabase = db.NewSimulatedOCR3Database() 41 | 42 | config.LogProvider = source 43 | config.EventProvider = ocr.NewReportTracker(listener, logger) 44 | config.Runnable = upkeep.NewCheckPipeline(conf, active, performs, netTelemetry, conTelemetry, logger) 45 | 46 | config.Encoder = source.Util 47 | config.BlockSubscriber = chain.NewBlockHistoryTracker(listener, logger) 48 | config.RecoverableProvider = source 49 | 50 | config.PayloadBuilder = source 51 | config.UpkeepProvider = source 52 | config.UpkeepStateUpdater = db.NewUpkeepStateDatabase() 53 | 54 | config.UpkeepTypeGetter = source.Util.GetType 55 | config.WorkIDGenerator = source.Util.GenerateWorkID 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /tools/simulator/simulate/loader/logtrigger.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "crypto/sha256" 5 | "sync" 6 | "time" 7 | 8 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 9 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 10 | ) 11 | 12 | // LogTriggerLoader ... 13 | type LogTriggerLoader struct { 14 | // provided dependencies 15 | progress ProgressTelemetry 16 | 17 | // internal state values 18 | mu sync.RWMutex 19 | triggers map[string][]chain.Log 20 | } 21 | 22 | // NewLogTriggerLoader ... 23 | func NewLogTriggerLoader(plan config.SimulationPlan, progress ProgressTelemetry) (*LogTriggerLoader, error) { 24 | logs, err := chain.GenerateLogTriggers(plan) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | events := make(map[string][]chain.Log) 30 | for _, logEvt := range logs { 31 | trigger := logEvt.TriggerAt 32 | 33 | existing, ok := events[trigger.String()] 34 | if !ok { 35 | existing = []chain.Log{} 36 | } 37 | 38 | events[trigger.String()] = append(existing, chain.Log{ 39 | TriggerValue: logEvt.TriggerValue, 40 | }) 41 | } 42 | 43 | if progress != nil { 44 | if err := progress.Register(emitLogNamespace, int64(len(logs))); err != nil { 45 | return nil, err 46 | } 47 | } 48 | 49 | return &LogTriggerLoader{ 50 | progress: progress, 51 | triggers: events, 52 | }, nil 53 | } 54 | 55 | // Load implements the chain.BlockLoaderFunc type and loads log trigger events 56 | // into blocks 57 | func (ltl *LogTriggerLoader) Load(block *chain.Block) { 58 | ltl.mu.RLock() 59 | defer ltl.mu.RUnlock() 60 | 61 | if events, ok := ltl.triggers[block.Number.String()]; ok { 62 | for _, event := range events { 63 | event.BlockNumber = block.Number 64 | event.BlockHash = block.Hash 65 | event.Idx = uint32(len(block.Transactions)) 66 | event.TxHash = sha256.Sum256([]byte(time.Now().Format(time.RFC3339Nano))) 67 | 68 | block.Transactions = append(block.Transactions, event) 69 | } 70 | 71 | if ltl.progress != nil { 72 | ltl.progress.Increment(emitLogNamespace, int64(len(events))) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tools/simulator/simulate/loader/logtrigger_test.go: -------------------------------------------------------------------------------- 1 | package loader_test 2 | -------------------------------------------------------------------------------- /tools/simulator/simulate/loader/ocr3config_test.go: -------------------------------------------------------------------------------- 1 | package loader_test 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "fmt" 8 | "io" 9 | "log" 10 | "math/big" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/mock" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/smartcontractkit/libocr/offchainreporting2plus/types" 17 | 18 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 19 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 20 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/loader" 21 | ) 22 | 23 | func TestOCR3ConfigLoader(t *testing.T) { 24 | t.Parallel() 25 | 26 | logger := log.New(io.Discard, "", 0) 27 | 28 | digester := new(mockDigester) 29 | plan := config.SimulationPlan{ 30 | ConfigEvents: []config.OCR3ConfigEvent{ 31 | { 32 | Event: config.Event{ 33 | TriggerBlock: big.NewInt(2), 34 | }, 35 | }, 36 | }, 37 | } 38 | 39 | loader := loader.NewOCR3ConfigLoader(plan, nil, digester, logger) 40 | block := chain.Block{ 41 | Number: big.NewInt(1), 42 | } 43 | 44 | digester.On("ConfigDigest", mock.Anything).Return(nil, nil) 45 | 46 | loader.Load(&block) 47 | require.Len(t, block.Transactions, 0, "no transactions at block 1") 48 | 49 | onKey, _ := config.NewEVMKeyring(rand.Reader) 50 | offKey, _ := config.NewOffchainKeyring(rand.Reader, rand.Reader) 51 | loader.AddSigner("signer", onKey, offKey) 52 | 53 | block.Number = big.NewInt(2) 54 | 55 | loader.Load(&block) 56 | require.Len(t, block.Transactions, 1, "1 transaction at block 2") 57 | } 58 | 59 | type mockDigester struct { 60 | mock.Mock 61 | } 62 | 63 | func (_m *mockDigester) ConfigDigest(ctx context.Context, config types.ContractConfig) (types.ConfigDigest, error) { 64 | req := _m.Called(config) 65 | 66 | hash := sha256.Sum256([]byte(fmt.Sprintf("%+v", config))) 67 | 68 | return hash, req.Error(1) 69 | } 70 | -------------------------------------------------------------------------------- /tools/simulator/simulate/loader/ocr3transmit_test.go: -------------------------------------------------------------------------------- 1 | package loader_test 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "math/big" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 13 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 14 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/loader" 15 | ) 16 | 17 | func TestOCR3TransmitLoader(t *testing.T) { 18 | t.Parallel() 19 | 20 | logger := log.New(io.Discard, "", 0) 21 | loader, err := loader.NewOCR3TransmitLoader(config.SimulationPlan{}, nil, logger) 22 | 23 | require.NoError(t, err) 24 | 25 | block := chain.Block{ 26 | Number: big.NewInt(1), 27 | Transactions: []interface{}{}, 28 | } 29 | 30 | // nothing to load 31 | loader.Load(&block) 32 | 33 | require.Len(t, block.Transactions, 0, "nothing should be added to the block on first load") 34 | require.NoError(t, loader.Transmit("test", []byte("message1"), 10_000)) 35 | require.NoError(t, loader.Transmit("test", []byte("message2"), 10_000)) 36 | require.NotNil(t, loader.Transmit("test", []byte("message2"), 10_000), "cannot transmit the same report") 37 | 38 | loader.Load(&block) 39 | 40 | require.Len(t, block.Transactions, 1, "both transmitted transactions should be included") 41 | 42 | trx, ok := block.Transactions[0].(chain.PerformUpkeepTransaction) 43 | require.True(t, ok, "transaction should be perform type") 44 | assert.Len(t, trx.Transmits, 2, "transaction should contain expected number of transmits") 45 | 46 | require.NoError(t, loader.Transmit("test", []byte("message3"), 10_000)) 47 | require.Len(t, loader.Results(), 3, "return all transmitted results") 48 | } 49 | -------------------------------------------------------------------------------- /tools/simulator/simulate/loader/upkeep.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 7 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 8 | ) 9 | 10 | const ( 11 | CreateUpkeepNamespace = "Emitting create upkeep transactions" 12 | emitLogNamespace = "Emitting log events" 13 | ) 14 | 15 | // UpkeepConfigLoader provides upkeep configurations to a block broadcaster. Use 16 | // this loader to introduce upkeeps or change upkeep configs at specific block 17 | // numbers. 18 | type UpkeepConfigLoader struct { 19 | // provided dependencies 20 | progress ProgressTelemetry 21 | 22 | // internal state values 23 | mu sync.RWMutex 24 | create map[string][]chain.UpkeepCreatedTransaction 25 | } 26 | 27 | // NewUpkeepConfigLoader ... 28 | func NewUpkeepConfigLoader(plan config.SimulationPlan, progress ProgressTelemetry) (*UpkeepConfigLoader, error) { 29 | // combine all upkeeps together for transmit 30 | allUpkeeps, err := chain.GenerateAllUpkeeps(plan) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | create := make(map[string][]chain.UpkeepCreatedTransaction) 36 | for _, upkeep := range allUpkeeps { 37 | evts, ok := create[upkeep.CreateInBlock.String()] 38 | if !ok { 39 | evts = []chain.UpkeepCreatedTransaction{} 40 | } 41 | 42 | create[upkeep.CreateInBlock.String()] = append(evts, chain.UpkeepCreatedTransaction{ 43 | Upkeep: upkeep, 44 | }) 45 | } 46 | 47 | if progress != nil { 48 | if err := progress.Register(CreateUpkeepNamespace, int64(len(allUpkeeps))); err != nil { 49 | return nil, err 50 | } 51 | } 52 | 53 | return &UpkeepConfigLoader{ 54 | create: create, 55 | progress: progress, 56 | }, nil 57 | } 58 | 59 | // Load implements the chain.BlockLoaderFunc type and loads configured upkeep 60 | // events into blocks. 61 | func (ucl *UpkeepConfigLoader) Load(block *chain.Block) { 62 | ucl.mu.RLock() 63 | defer ucl.mu.RUnlock() 64 | 65 | if events, ok := ucl.create[block.Number.String()]; ok { 66 | for _, event := range events { 67 | block.Transactions = append(block.Transactions, event) 68 | } 69 | 70 | if ucl.progress != nil { 71 | ucl.progress.Increment(CreateUpkeepNamespace, int64(len(events))) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tools/simulator/simulate/loader/upkeep_test.go: -------------------------------------------------------------------------------- 1 | package loader_test 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 13 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 14 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/loader" 15 | ) 16 | 17 | func TestUpkeepConfigLoader(t *testing.T) { 18 | plan := config.SimulationPlan{ 19 | Blocks: config.Blocks{ 20 | Genesis: big.NewInt(1), 21 | Cadence: config.Duration(time.Second), 22 | Duration: 10, 23 | }, 24 | GenerateUpkeeps: []config.GenerateUpkeepEvent{ 25 | { 26 | Event: config.Event{ 27 | TriggerBlock: big.NewInt(2), 28 | }, 29 | Count: 10, 30 | StartID: big.NewInt(1), 31 | EligibilityFunc: "2x", 32 | OffsetFunc: "x", 33 | UpkeepType: config.ConditionalUpkeepType, 34 | }, 35 | }, 36 | } 37 | telemetry := new(mockProgressTelemetry) 38 | 39 | telemetry.On("Register", loader.CreateUpkeepNamespace, int64(10)).Return(nil) 40 | telemetry.On("Increment", loader.CreateUpkeepNamespace, int64(10)) 41 | 42 | loader, err := loader.NewUpkeepConfigLoader(plan, telemetry) 43 | 44 | require.NoError(t, err) 45 | 46 | block := chain.Block{ 47 | Number: big.NewInt(2), 48 | Transactions: []interface{}{}, 49 | } 50 | 51 | loader.Load(&block) 52 | 53 | assert.Len(t, block.Transactions, 10) 54 | } 55 | 56 | type mockProgressTelemetry struct { 57 | mock.Mock 58 | } 59 | 60 | func (_m *mockProgressTelemetry) Register(namespace string, total int64) error { 61 | res := _m.Called(namespace, total) 62 | 63 | return res.Error(0) 64 | } 65 | 66 | func (_m *mockProgressTelemetry) Increment(namespace string, count int64) { 67 | _m.Called(namespace, count) 68 | } 69 | -------------------------------------------------------------------------------- /tools/simulator/simulate/net/service_test.go: -------------------------------------------------------------------------------- 1 | package net_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | 11 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/net" 12 | ) 13 | 14 | func TestSimulatedNetworkService(t *testing.T) { 15 | load := 0.999 16 | rate := 1 17 | latency := 100 18 | netTelemetry := new(mockNetTelemetry) 19 | 20 | service := net.NewSimulatedNetworkService(load, rate, latency, netTelemetry) 21 | 22 | netTelemetry.On("Register", "test", mock.Anything, mock.Anything) 23 | netTelemetry.On("AddRateDataPoint", mock.Anything).Maybe() 24 | 25 | err := <-service.Call(context.Background(), "test") 26 | 27 | assert.NotNil(t, err) 28 | } 29 | 30 | type mockNetTelemetry struct { 31 | mock.Mock 32 | } 33 | 34 | func (_m *mockNetTelemetry) Register(name string, duration time.Duration, err error) { 35 | _m.Called(name, duration, err) 36 | } 37 | 38 | func (_m *mockNetTelemetry) AddRateDataPoint(quantity int) { 39 | _m.Called(quantity) 40 | } 41 | -------------------------------------------------------------------------------- /tools/simulator/simulate/ocr/config_test.go: -------------------------------------------------------------------------------- 1 | package ocr_test 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "io" 7 | "log" 8 | "math/big" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | 15 | "github.com/smartcontractkit/libocr/offchainreporting2plus/types" 16 | 17 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 18 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 19 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/ocr" 20 | ) 21 | 22 | func TestOCR3ConfigTracker(t *testing.T) { 23 | t.Parallel() 24 | 25 | logger := log.New(io.Discard, "", 0) 26 | conf := config.Blocks{ 27 | Genesis: new(big.Int).SetInt64(1), 28 | Cadence: config.Duration(100 * time.Millisecond), 29 | Jitter: config.Duration(0), 30 | Duration: 5, 31 | } 32 | 33 | ocrConfig := types.ContractConfig{ 34 | ConfigDigest: sha256.Sum256([]byte("some config data")), 35 | } 36 | 37 | broadcaster := chain.NewBlockBroadcaster(conf, 1, logger, nil, loadConfigAt(ocrConfig, 2)) 38 | listener := chain.NewListener(broadcaster, logger) 39 | tracker := ocr.NewOCR3ConfigTracker(listener, logger) 40 | 41 | broadcaster.Start() 42 | 43 | <-tracker.Notify() 44 | 45 | broadcaster.Stop() 46 | 47 | changedInBlock, digest, err := tracker.LatestConfigDetails(context.Background()) 48 | 49 | require.NoError(t, err) 50 | 51 | assert.Equal(t, uint64(2), changedInBlock, "changed in block should be equal to the block loaded at") 52 | assert.Equal(t, ocrConfig.ConfigDigest, digest, "config digest should match") 53 | 54 | latest, err := tracker.LatestConfig(context.Background(), 0) 55 | 56 | require.NoError(t, err) 57 | 58 | assert.Equal(t, ocrConfig, latest, "configs should match") 59 | 60 | blockHeight, err := tracker.LatestBlockHeight(context.Background()) 61 | 62 | require.NoError(t, err) 63 | 64 | assert.Greater(t, blockHeight, uint64(1), "should advance at least higher than 2") 65 | } 66 | 67 | func loadConfigAt(ocrConfig types.ContractConfig, atBlock int64) func(*chain.Block) { 68 | return func(block *chain.Block) { 69 | if block.Number.Cmp(new(big.Int).SetInt64(atBlock)) == 0 { 70 | block.Transactions = append(block.Transactions, chain.OCR3ConfigTransaction{ 71 | Config: ocrConfig, 72 | }) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tools/simulator/simulate/ocr/report_test.go: -------------------------------------------------------------------------------- 1 | package ocr_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "math/big" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 15 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 16 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 17 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/ocr" 18 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/util" 19 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 20 | ) 21 | 22 | func TestReportTracker(t *testing.T) { 23 | t.Parallel() 24 | 25 | logger := log.New(io.Discard, "", 0) 26 | conf := config.Blocks{ 27 | Genesis: new(big.Int).SetInt64(1), 28 | Cadence: config.Duration(100 * time.Millisecond), 29 | Jitter: config.Duration(0), 30 | Duration: 10, 31 | } 32 | 33 | upkeepID := util.NewUpkeepID(big.NewInt(8).Bytes(), uint8(types.ConditionTrigger)) 34 | workID := util.UpkeepWorkID( 35 | upkeepID, 36 | ocr2keepers.NewLogTrigger( 37 | ocr2keepers.BlockNumber(5), 38 | [32]byte{}, 39 | nil)) 40 | 41 | report1, err := util.EncodeCheckResultsToReportBytes([]ocr2keepers.CheckResult{ 42 | { 43 | UpkeepID: upkeepID, 44 | WorkID: workID, 45 | }, 46 | }) 47 | 48 | require.NoError(t, err) 49 | 50 | transmits := []chain.TransmitEvent{ 51 | { 52 | SendingAddress: "test", 53 | BlockNumber: big.NewInt(1), 54 | Report: report1, 55 | }, 56 | } 57 | 58 | broadcaster := chain.NewBlockBroadcaster(conf, 1, logger, nil, loadTransmitsAt(transmits, 2)) 59 | listener := chain.NewListener(broadcaster, logger) 60 | 61 | tracker := ocr.NewReportTracker(listener, logger) 62 | 63 | <-broadcaster.Start() 64 | broadcaster.Stop() 65 | 66 | evts, err := tracker.GetLatestEvents(context.Background()) 67 | 68 | require.NoError(t, err) 69 | assert.Len(t, evts, 1) 70 | } 71 | 72 | func loadTransmitsAt(transmits []chain.TransmitEvent, atBlock int64) func(*chain.Block) { 73 | return func(block *chain.Block) { 74 | if block.Number.Cmp(new(big.Int).SetInt64(atBlock)) == 0 { 75 | block.Transactions = append(block.Transactions, chain.PerformUpkeepTransaction{ 76 | Transmits: transmits, 77 | }) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tools/simulator/simulate/ocr/transmit.go: -------------------------------------------------------------------------------- 1 | package ocr 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/smartcontractkit/libocr/offchainreporting2/types" 8 | "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" 9 | 10 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/plugin" 11 | ) 12 | 13 | type Transmitter interface { 14 | Transmit(string, []byte, uint64) error 15 | } 16 | 17 | type OCR3Transmitter struct { 18 | // configured values 19 | transmitterID string 20 | loader Transmitter 21 | 22 | // internal state values 23 | mu sync.RWMutex 24 | } 25 | 26 | func NewOCR3Transmitter(id string, loader Transmitter) *OCR3Transmitter { 27 | return &OCR3Transmitter{ 28 | transmitterID: id, 29 | loader: loader, 30 | } 31 | } 32 | 33 | func (tr *OCR3Transmitter) Transmit( 34 | ctx context.Context, 35 | digest types.ConfigDigest, 36 | v uint64, 37 | r ocr3types.ReportWithInfo[plugin.AutomationReportInfo], 38 | s []types.AttributedOnchainSignature, 39 | ) error { 40 | return tr.loader.Transmit(tr.transmitterID, []byte(r.Report), v) 41 | } 42 | 43 | // Account from which the transmitter invokes the contract 44 | func (tr *OCR3Transmitter) FromAccount(ctx context.Context) (types.Account, error) { 45 | tr.mu.RLock() 46 | defer tr.mu.RUnlock() 47 | 48 | return types.Account(tr.transmitterID), nil 49 | } 50 | -------------------------------------------------------------------------------- /tools/simulator/simulate/ocr/transmit_test.go: -------------------------------------------------------------------------------- 1 | package ocr_test 2 | -------------------------------------------------------------------------------- /tools/simulator/simulate/upkeep/active_test.go: -------------------------------------------------------------------------------- 1 | package upkeep_test 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "math/big" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 14 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/config" 15 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 16 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/upkeep" 17 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/util" 18 | ) 19 | 20 | func TestActiveTracker(t *testing.T) { 21 | t.Parallel() 22 | 23 | logger := log.New(io.Discard, "", 0) 24 | conf := config.Blocks{ 25 | Genesis: new(big.Int).SetInt64(1), 26 | Cadence: config.Duration(100 * time.Millisecond), 27 | Jitter: config.Duration(0), 28 | Duration: 10, 29 | } 30 | 31 | upkeep1 := chain.SimulatedUpkeep{ 32 | ID: big.NewInt(8), 33 | UpkeepID: util.NewUpkeepID(big.NewInt(8).Bytes(), uint8(types.ConditionTrigger)), 34 | Type: chain.ConditionalType, 35 | } 36 | 37 | upkeep2 := chain.SimulatedUpkeep{ 38 | ID: big.NewInt(10), 39 | UpkeepID: util.NewUpkeepID(big.NewInt(10).Bytes(), uint8(types.LogTrigger)), 40 | Type: chain.LogTriggerType, 41 | } 42 | 43 | broadcaster := chain.NewBlockBroadcaster(conf, 1, logger, nil, loadUpkeepAt(upkeep1, 2), loadUpkeepAt(upkeep2, 2)) 44 | listener := chain.NewListener(broadcaster, logger) 45 | 46 | tracker := upkeep.NewActiveTracker(listener, logger) 47 | 48 | <-broadcaster.Start() 49 | broadcaster.Stop() 50 | 51 | assert.Len(t, tracker.GetAllByType(chain.ConditionalType), 1, "should have 1 conditional upkeep") 52 | assert.Len(t, tracker.GetAllByType(chain.LogTriggerType), 1, "should have 1 log upkeeps") 53 | 54 | trackedUpkeep, ok := tracker.GetByUpkeepID(upkeep1.UpkeepID) 55 | 56 | require.True(t, ok) 57 | assert.Equal(t, upkeep1, trackedUpkeep) 58 | 59 | var otherID [32]byte 60 | otherID[5] = 1 61 | 62 | _, ok = tracker.GetByUpkeepID(otherID) 63 | 64 | assert.False(t, ok) 65 | 66 | bl := tracker.GetLatestBlock() 67 | 68 | assert.GreaterOrEqual(t, 1, bl.Number.Cmp(conf.Genesis)) 69 | } 70 | 71 | func loadUpkeepAt(upkeep chain.SimulatedUpkeep, atBlock int64) func(*chain.Block) { 72 | return func(block *chain.Block) { 73 | if block.Number.Cmp(new(big.Int).SetInt64(atBlock)) == 0 { 74 | block.Transactions = append(block.Transactions, chain.UpkeepCreatedTransaction{ 75 | Upkeep: upkeep, 76 | }) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tools/simulator/simulate/upkeep/perform.go: -------------------------------------------------------------------------------- 1 | package upkeep 2 | 3 | import ( 4 | "log" 5 | "math/big" 6 | "runtime" 7 | "sync" 8 | 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 10 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/chain" 11 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/util" 12 | ) 13 | 14 | type PerformTracker struct { 15 | // provided dependencies 16 | listener *chain.Listener 17 | logger *log.Logger 18 | 19 | // internal state props 20 | mu sync.RWMutex 21 | conditionals map[string][]*big.Int // maps UpkeepID to performed in block for conditionals 22 | performed map[string]bool // maps WorkIDs to performed state 23 | 24 | // service values 25 | chDone chan struct{} 26 | } 27 | 28 | // NewPerformTracker ... 29 | func NewPerformTracker(listener *chain.Listener, logger *log.Logger) *PerformTracker { 30 | src := &PerformTracker{ 31 | listener: listener, 32 | logger: log.New(logger.Writer(), "[perform-tracker] ", log.Ldate|log.Ltime|log.Lshortfile), 33 | conditionals: make(map[string][]*big.Int), 34 | performed: make(map[string]bool), 35 | chDone: make(chan struct{}), 36 | } 37 | 38 | go src.run() 39 | 40 | runtime.SetFinalizer(src, func(srv *PerformTracker) { srv.stop() }) 41 | 42 | return src 43 | } 44 | 45 | func (pt *PerformTracker) PerformsForUpkeepID(upkeepID string) []*big.Int { 46 | pt.mu.RLock() 47 | defer pt.mu.RUnlock() 48 | 49 | if performs, ok := pt.conditionals[upkeepID]; ok { 50 | return performs 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (pt *PerformTracker) IsWorkIDPerformed(workID string) bool { 57 | pt.mu.RLock() 58 | defer pt.mu.RUnlock() 59 | 60 | if ok, exists := pt.performed[workID]; exists && ok { 61 | return true 62 | } 63 | 64 | return false 65 | } 66 | 67 | func (pt *PerformTracker) run() { 68 | chEvents := pt.listener.Subscribe(chain.PerformUpkeepChannel) 69 | 70 | for { 71 | select { 72 | case event := <-chEvents: 73 | switch evt := event.Event.(type) { 74 | case chain.PerformUpkeepTransaction: 75 | pt.registerTransmitted(evt.Transmits...) 76 | } 77 | case <-pt.chDone: 78 | return 79 | } 80 | } 81 | } 82 | 83 | func (pt *PerformTracker) stop() { 84 | close(pt.chDone) 85 | } 86 | 87 | func (pt *PerformTracker) registerTransmitted(transmits ...chain.TransmitEvent) { 88 | pt.mu.Lock() 89 | defer pt.mu.Unlock() 90 | 91 | for _, transmit := range transmits { 92 | trResults, err := util.DecodeCheckResultsFromReportBytes(transmit.Report) 93 | if err != nil { 94 | pt.logger.Println(err) 95 | 96 | continue 97 | } 98 | 99 | for _, result := range trResults { 100 | key := result.UpkeepID.String() 101 | 102 | pt.performed[result.WorkID] = true 103 | 104 | if util.GetUpkeepType(result.UpkeepID) == types.ConditionTrigger { 105 | performs, ok := pt.conditionals[key] 106 | if !ok { 107 | performs = []*big.Int{} 108 | } 109 | 110 | pt.conditionals[key] = append(performs, transmit.BlockNumber) 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tools/simulator/simulate/upkeep/util.go: -------------------------------------------------------------------------------- 1 | package upkeep 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 7 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/util" 8 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 9 | ) 10 | 11 | var ( 12 | ErrUnexpectedResult = fmt.Errorf("unexpected result struct") 13 | ) 14 | 15 | // Util contains basic utilities for upkeeps. 16 | type Util struct{} 17 | 18 | func (u Util) Encode(results ...ocr2keepers.CheckResult) ([]byte, error) { 19 | return util.EncodeCheckResultsToReportBytes(results) 20 | } 21 | 22 | func (u Util) Extract(b []byte) ([]ocr2keepers.ReportedUpkeep, error) { 23 | results, err := util.DecodeCheckResultsFromReportBytes(b) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | reported := make([]ocr2keepers.ReportedUpkeep, len(results)) 29 | 30 | for i, result := range results { 31 | reported[i] = ocr2keepers.ReportedUpkeep{ 32 | UpkeepID: result.UpkeepID, 33 | Trigger: result.Trigger, 34 | WorkID: result.WorkID, 35 | } 36 | } 37 | 38 | return reported, nil 39 | } 40 | 41 | // GetType returns the upkeep type from an identifier. 42 | func (u Util) GetType(id ocr2keepers.UpkeepIdentifier) types.UpkeepType { 43 | return util.GetUpkeepType(id) 44 | } 45 | 46 | // GenerateWorkID creates a unique work id from an identifier and trigger. 47 | func (u Util) GenerateWorkID(id ocr2keepers.UpkeepIdentifier, trigger ocr2keepers.Trigger) string { 48 | return util.UpkeepWorkID(id, trigger) 49 | } 50 | -------------------------------------------------------------------------------- /tools/simulator/simulate/upkeep/util_test.go: -------------------------------------------------------------------------------- 1 | package upkeep_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | 9 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 10 | 11 | "github.com/smartcontractkit/chainlink-automation/tools/simulator/simulate/upkeep" 12 | ) 13 | 14 | func TestUtil_EncodeDecode(t *testing.T) { 15 | utilities := upkeep.Util{} 16 | 17 | encoded, err := utilities.Encode(ocr2keepers.CheckResult{}) 18 | 19 | require.NoError(t, err) 20 | 21 | reported, err := utilities.Extract(encoded) 22 | 23 | require.NoError(t, err) 24 | assert.Len(t, reported, 1) 25 | } 26 | -------------------------------------------------------------------------------- /tools/simulator/telemetry/base.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Collector interface { 8 | Type() CollectorType 9 | Close() error 10 | } 11 | 12 | type CollectorType int 13 | 14 | const ( 15 | RPCType CollectorType = iota 16 | NodeLogType 17 | ) 18 | 19 | type baseCollector struct { 20 | t CollectorType 21 | io []io.WriteCloser 22 | ioLookup map[string]int 23 | } 24 | 25 | func (c *baseCollector) Type() CollectorType { 26 | return c.t 27 | } 28 | 29 | func (c *baseCollector) Close() error { 30 | for _, w := range c.io { 31 | if err := w.Close(); err != nil { 32 | return err 33 | } 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /tools/simulator/telemetry/log.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "os" 8 | ) 9 | 10 | type NodeLogCollector struct { 11 | baseCollector 12 | filePath string 13 | verbose bool 14 | } 15 | 16 | func NewNodeLogCollector(path string, verbose bool) *NodeLogCollector { 17 | if verbose { 18 | if err := os.MkdirAll(path, 0750); err != nil && !os.IsExist(err) { 19 | panic(err) 20 | } 21 | } 22 | 23 | return &NodeLogCollector{ 24 | baseCollector: baseCollector{ 25 | t: NodeLogType, 26 | io: []io.WriteCloser{}, 27 | ioLookup: make(map[string]int), 28 | }, 29 | filePath: path, 30 | verbose: verbose, 31 | } 32 | } 33 | 34 | func (c *NodeLogCollector) ContractLog(node string) io.Writer { 35 | key := fmt.Sprintf("contract/%s", node) 36 | 37 | idx, ok := c.baseCollector.ioLookup[key] 38 | if !ok { 39 | panic(fmt.Errorf("missing contract log for %s", node)) 40 | } 41 | 42 | return c.baseCollector.io[idx] 43 | } 44 | 45 | func (c *NodeLogCollector) GeneralLog(node string) io.Writer { 46 | key := fmt.Sprintf("general/%s", node) 47 | 48 | idx, ok := c.baseCollector.ioLookup[key] 49 | if !ok { 50 | panic(fmt.Errorf("missing general log for %s", node)) 51 | } 52 | 53 | return c.baseCollector.io[idx] 54 | } 55 | 56 | func (c *NodeLogCollector) AddNode(node string) error { 57 | path := fmt.Sprintf("%s/%s", c.filePath, node) 58 | 59 | if c.verbose { 60 | if err := os.MkdirAll(path, 0750); err != nil && !os.IsExist(err) { 61 | panic(err) 62 | } 63 | } 64 | 65 | if err := c.addWriterForKey(fmt.Sprintf("%s/general.log", path), fmt.Sprintf("general/%s", node)); err != nil { 66 | return err 67 | } 68 | 69 | if err := c.addWriterForKey(fmt.Sprintf("%s/contract.log", path), fmt.Sprintf("contract/%s", node)); err != nil { 70 | return err 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func (c *NodeLogCollector) addWriterForKey(path, key string) error { 77 | if !c.verbose { 78 | c.ioLookup[key] = len(c.io) 79 | c.io = append(c.io, writeCloseDiscard{}) 80 | 81 | return nil 82 | } 83 | 84 | var perms fs.FileMode = 0666 85 | 86 | flag := os.O_RDWR | os.O_CREATE | os.O_TRUNC 87 | 88 | file, err := os.OpenFile(path, flag, perms) 89 | if err != nil { 90 | file.Close() 91 | 92 | return err 93 | } 94 | 95 | c.ioLookup[key] = len(c.io) 96 | c.io = append(c.io, file) 97 | 98 | return nil 99 | } 100 | 101 | type writeCloseDiscard struct{} 102 | 103 | func (writeCloseDiscard) Write(bts []byte) (int, error) { 104 | return len(bts), nil 105 | } 106 | 107 | func (writeCloseDiscard) Close() error { 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /tools/simulator/util/encode.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/smartcontractkit/chainlink-automation/pkg/v3/types" 10 | 11 | "github.com/ethereum/go-ethereum/crypto" 12 | 13 | ocr2keepers "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 14 | ) 15 | 16 | const ( 17 | // upkeepTypeStartIndex is the index where the upkeep type bytes start. 18 | // for 2.1 we use 11 zeros (reserved bytes for future use) 19 | // and 1 byte to represent the type, with index equal upkeepTypeByteIndex 20 | upkeepTypeStartIndex = 4 21 | // upkeepTypeByteIndex is the index of the byte that holds the upkeep type. 22 | upkeepTypeByteIndex = 15 23 | ) 24 | 25 | var ( 26 | ErrInvalidUpkeepID = fmt.Errorf("invalid upkeepID") 27 | ) 28 | 29 | func EncodeCheckResultsToReportBytes(results []ocr2keepers.CheckResult) ([]byte, error) { 30 | if len(results) == 0 { 31 | return []byte{}, nil 32 | } 33 | 34 | bts, err := json.Marshal(results) 35 | if err != nil { 36 | return nil, fmt.Errorf("failed to marshal check results: %w", err) 37 | } 38 | 39 | return bts, nil 40 | } 41 | 42 | func DecodeCheckResultsFromReportBytes(bts []byte) ([]ocr2keepers.CheckResult, error) { 43 | if len(bts) == 0 { 44 | return []ocr2keepers.CheckResult{}, nil 45 | } 46 | 47 | var results []ocr2keepers.CheckResult 48 | 49 | if err := json.Unmarshal(bts, &results); err != nil { 50 | return nil, fmt.Errorf("failed to unmarshal check results from bytes: %w", err) 51 | } 52 | 53 | return results, nil 54 | } 55 | 56 | // GetUpkeepType returns the upkeep type from the given ID. 57 | // it follows the same logic as the contract, but performs it locally. 58 | func GetUpkeepType(id ocr2keepers.UpkeepIdentifier) types.UpkeepType { 59 | for i := upkeepTypeStartIndex; i < upkeepTypeByteIndex; i++ { 60 | if id[i] != 0 { // old id 61 | return types.ConditionTrigger 62 | } 63 | } 64 | 65 | typeByte := id[upkeepTypeByteIndex] 66 | 67 | return types.UpkeepType(typeByte) 68 | } 69 | 70 | func UpkeepWorkID(uid ocr2keepers.UpkeepIdentifier, trigger ocr2keepers.Trigger) string { 71 | var triggerExtBytes []byte 72 | 73 | if trigger.LogTriggerExtension != nil { 74 | triggerExtBytes = trigger.LogTriggerExtension.LogIdentifier() 75 | } 76 | 77 | hash := crypto.Keccak256(append(uid[:], triggerExtBytes...)) 78 | 79 | return hex.EncodeToString(hash[:]) 80 | } 81 | 82 | func NewUpkeepID(entropy []byte, uType uint8) [32]byte { 83 | /* 84 | Following the contract convention, an identifier is composed of 32 bytes: 85 | 86 | - 4 bytes of entropy 87 | - 11 bytes of zeros 88 | - 1 identifying byte for the trigger type 89 | - 16 bytes of entropy 90 | */ 91 | hashedValue := sha256.Sum256(entropy) 92 | 93 | for x := 4; x < 15; x++ { 94 | hashedValue[x] = uint8(0) 95 | } 96 | 97 | hashedValue[15] = uType 98 | 99 | return hashedValue 100 | } 101 | -------------------------------------------------------------------------------- /tools/simulator/util/rand.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | ) 7 | 8 | type cryptoRandSource struct{} 9 | 10 | func NewCryptoRandSource() cryptoRandSource { 11 | return cryptoRandSource{} 12 | } 13 | 14 | func (cryptoRandSource) Uint64() uint64 { 15 | var b [8]byte 16 | _, err := rand.Read(b[:]) 17 | if err != nil { 18 | panic(err) 19 | } 20 | return binary.LittleEndian.Uint64(b[:]) 21 | } 22 | 23 | func (cryptoRandSource) Seed(_ uint64) {} 24 | -------------------------------------------------------------------------------- /tools/simulator/util/sort.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | ) 7 | 8 | type SortedKeyMap[T any] struct { 9 | mu sync.RWMutex 10 | values map[string]T 11 | keys []string 12 | } 13 | 14 | func NewSortedKeyMap[T any]() *SortedKeyMap[T] { 15 | return &SortedKeyMap[T]{ 16 | values: make(map[string]T), 17 | keys: make([]string, 0), 18 | } 19 | } 20 | 21 | func (m *SortedKeyMap[T]) Set(key string, value T) { 22 | m.mu.Lock() 23 | defer m.mu.Unlock() 24 | 25 | _, ok := m.values[key] 26 | if !ok { 27 | m.keys = append(m.keys, key) 28 | sort.Strings(m.keys) 29 | } 30 | 31 | m.values[key] = value 32 | } 33 | 34 | func (m *SortedKeyMap[T]) Get(key string) (T, bool) { 35 | m.mu.RLock() 36 | defer m.mu.RUnlock() 37 | 38 | v, ok := m.values[key] 39 | if ok { 40 | return v, ok 41 | } 42 | 43 | return getZero[T](), false 44 | } 45 | 46 | // Keys returns the specified number of keys sorted highest to lowest. 47 | func (m *SortedKeyMap[T]) Keys(count int) []string { 48 | m.mu.RLock() 49 | defer m.mu.RUnlock() 50 | 51 | keysLen := len(m.keys) 52 | 53 | if count > keysLen { 54 | count = keysLen 55 | } 56 | 57 | // only return the last 'count' keys 58 | keys := make([]string, count) 59 | 60 | // keys are sorted internally in ascending order but the return 61 | // should be decending 62 | // loop starting at 1 so the first insert can be l-1, or the last item 63 | for i := 1; i <= count; i++ { 64 | keys[i-1] = m.keys[keysLen-i] 65 | } 66 | 67 | return keys 68 | } 69 | 70 | func getZero[T any]() T { 71 | var result T 72 | return result 73 | } 74 | -------------------------------------------------------------------------------- /tools/testprotocol/modify/byte.go: -------------------------------------------------------------------------------- 1 | package modify 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | type NamedByteModifier func(context.Context, []byte, error) (string, []byte, error) 10 | type MapModifier func(context.Context, map[string]interface{}, error) (map[string]interface{}, error) 11 | type ValueModifierFunc func(string, interface{}) interface{} 12 | 13 | // WithModifyKeyValue recursively operates on all key-value pairs in the provided map and applies the provided modifier 14 | // function if the key matches. The path provided to the modifier function starts with `root` and is appended with every 15 | // key encountered in the tree. ex: `root.someKey.anotherKey`. 16 | func WithModifyKeyValue(key string, fn ValueModifierFunc) MapModifier { 17 | return func(ctx context.Context, values map[string]interface{}, err error) (map[string]interface{}, error) { 18 | return recursiveModify(key, "root", fn, values), err 19 | } 20 | } 21 | 22 | // ModifyBytes deconstructs provided bytes into a map[string]interface{} and passes the decoded map to provided 23 | // modifiers. The final modified map is re-encoded as bytes and returned by the modifier function. 24 | func ModifyBytes(name string, modifiers ...MapModifier) NamedByteModifier { 25 | return func(ctx context.Context, bytes []byte, err error) (string, []byte, error) { 26 | var values map[string]interface{} 27 | 28 | if err := json.Unmarshal(bytes, &values); err != nil { 29 | return name, bytes, err 30 | } 31 | 32 | for _, modifier := range modifiers { 33 | values, err = modifier(ctx, values, err) 34 | } 35 | 36 | bytes, err = json.Marshal(values) 37 | 38 | return name, bytes, err 39 | } 40 | } 41 | 42 | func recursiveModify(key, path string, mod ValueModifierFunc, values map[string]interface{}) map[string]interface{} { 43 | for mapKey, mapValue := range values { 44 | newPath := fmt.Sprintf("%s.%s", path, mapKey) 45 | 46 | switch nextValues := mapValue.(type) { 47 | case map[string]interface{}: 48 | values[key] = recursiveModify(key, newPath, mod, nextValues) 49 | case []interface{}: 50 | for idx, arrayValue := range nextValues { 51 | newPath = fmt.Sprintf("%s[%d]", newPath, idx) 52 | 53 | if mappedArray, ok := arrayValue.(map[string]interface{}); ok { 54 | nextValues[idx] = recursiveModify(key, newPath, mod, mappedArray) 55 | } 56 | } 57 | default: 58 | if mapKey == key { 59 | values[key] = mod(newPath, mapValue) 60 | } 61 | } 62 | } 63 | 64 | return values 65 | } 66 | -------------------------------------------------------------------------------- /tools/testprotocol/modify/byte_test.go: -------------------------------------------------------------------------------- 1 | package modify_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | ocr2keeperstypes "github.com/smartcontractkit/chainlink-common/pkg/types/automation" 11 | 12 | ocr2keepers "github.com/smartcontractkit/chainlink-automation/pkg/v3" 13 | "github.com/smartcontractkit/chainlink-automation/tools/testprotocol/modify" 14 | ) 15 | 16 | func TestModifyBytes(t *testing.T) { 17 | originalName := "test modifier" 18 | modifier := modify.ModifyBytes( 19 | originalName, 20 | modify.WithModifyKeyValue( 21 | "BlockNumber", 22 | func(path string, values interface{}) interface{} { 23 | return -1 24 | })) 25 | 26 | observation := ocr2keepers.AutomationObservation{ 27 | Performable: []ocr2keeperstypes.CheckResult{ 28 | { 29 | Trigger: ocr2keeperstypes.NewLogTrigger( 30 | ocr2keeperstypes.BlockNumber(10), 31 | [32]byte{}, 32 | &ocr2keeperstypes.LogTriggerExtension{ 33 | TxHash: [32]byte{}, 34 | Index: 1, 35 | BlockHash: [32]byte{}, 36 | BlockNumber: ocr2keeperstypes.BlockNumber(10), 37 | }, 38 | ), 39 | }, 40 | }, 41 | UpkeepProposals: []ocr2keeperstypes.CoordinatedBlockProposal{}, 42 | BlockHistory: []ocr2keeperstypes.BlockKey{}, 43 | } 44 | 45 | original, err := json.Marshal(observation) 46 | name, modified, err := modifier(context.Background(), original, err) 47 | 48 | assert.NoError(t, err) 49 | assert.NotEqual(t, original, modified) 50 | assert.Equal(t, originalName, name) 51 | } 52 | --------------------------------------------------------------------------------