├── tests └── e2e │ ├── .ipfs │ ├── version │ ├── datastore_spec │ └── config │ ├── ethaccounts │ ├── password │ ├── gethkeyfile │ └── ethaccounts.go │ ├── deps-stop.sh │ ├── cmd │ ├── node │ │ ├── main_test.go │ │ ├── run.sh │ │ └── Dockerfile │ └── cli │ │ └── main_test.go │ ├── disco.config.yml │ ├── agents │ └── txdetectoragent │ │ ├── Dockerfile │ │ └── main.go │ ├── .forta │ ├── .keys │ │ └── UTC--2022-03-22T01-22-51.315989103Z--222244861c15a8f2a05fbd15e747ea8f20c2c0c9 │ └── config.yml │ ├── .forta-private │ ├── .keys │ │ └── UTC--2022-03-22T01-22-51.315989103Z--222244861c15a8f2a05fbd15e747ea8f20c2c0c9 │ └── config.yml │ ├── e2e_agent_test.go │ ├── e2e_forta_cmd_test.go │ ├── e2e_forta_run_private_test.go │ ├── genesis.json │ ├── deps-start.sh │ └── build.sh ├── _release ├── .gitignore ├── apt │ ├── forta-x │ │ ├── LICENSE │ │ └── Makefile │ └── debian │ │ ├── source │ │ ├── format │ │ └── local-options │ │ ├── README.Debian │ │ ├── patches │ │ └── series │ │ ├── watch │ │ ├── changelog │ │ ├── control │ │ ├── copyright │ │ └── rules ├── yum │ ├── Forta.repo │ └── forta.spec ├── scripts │ ├── major-minor.sh │ ├── debian-version.sh │ ├── aptly-publish.sh │ └── init-aptly.sh ├── .rpmmacros └── systemd │ └── forta.service ├── scripts ├── set-permissions.sh ├── stop-server.sh ├── after-start.sh ├── ipfs-add.sh ├── publish-release.sh ├── build-for-release.sh ├── build-for-local.sh ├── install.sh ├── build-manifest.sh ├── docker-build-push.sh ├── manifest-template.json ├── build.sh ├── total-coverage.sh ├── docker-push.sh ├── inject-secrets.sh └── start-server.sh ├── .dockerignore ├── cmd ├── node │ ├── main.go │ └── nodecmd │ │ └── nodecmd.go ├── cmdutils │ └── utils.go ├── cmd_images.go ├── cmd_version.go ├── color.go ├── json-rpc │ └── main.go ├── publisher │ └── main.go ├── runner │ └── runner.go ├── cmd_run.go ├── cmd_account.go ├── supervisor │ └── main.go ├── cmd_batch.go ├── cmd_agent.go ├── ens.go ├── updater │ └── updater.go ├── cmd_init.go └── cmd_tx.go ├── main.go ├── services ├── registry │ ├── regtypes │ │ └── regtypes.go │ ├── registry_test.go │ └── registry.go ├── json-rpc │ ├── rate_limiter_test.go │ ├── errors_test.go │ ├── errors.go │ └── rate_limiter.go ├── scanner │ ├── agentpool │ │ ├── poolagent │ │ │ └── error_counter.go │ │ └── agent_pool_test.go │ ├── tx_logger.go │ ├── interfaces.go │ ├── api.go │ └── tx_stream.go ├── service_test.go ├── runner │ └── health.go ├── publisher │ ├── metrics_test.go │ ├── testalerts │ │ └── logger.go │ └── publisher_test.go ├── updater │ └── updater_test.go ├── supervisor │ ├── health.go │ └── agent_logs.go └── service.go ├── Dockerfile.cli ├── .github ├── actions │ ├── go │ │ └── action.yml │ ├── setup │ │ └── action.yml │ ├── propose │ │ ├── package.json │ │ ├── action.yml │ │ └── index.js │ ├── e2e │ │ └── action.yml │ └── build-push │ │ └── action.yml └── workflows │ ├── pull-request.yml │ ├── codedeploy-dev.yml │ ├── codedeploy-prod.yml │ ├── build-codedeploy-prod.yml │ └── codedeploy-staging.yml ├── Dockerfile.node ├── config ├── defaults.go ├── agents_test.go ├── env.go ├── utils.go ├── release.go ├── resources.go ├── containers.go ├── agents.go └── chains.go ├── healthutils └── utils.go ├── Dockerfile.buildkit.node ├── .gitignore ├── appspec.yml ├── store ├── string.go ├── batch_ref.go ├── ens_override.go ├── mocks │ └── mock_registry.go └── container.go ├── clients ├── messaging │ ├── subjects.go │ └── client.go ├── agentgrpc │ ├── encoding.go │ ├── testserver │ │ └── main.go │ ├── agent_client.go │ ├── encoding_test.go │ └── encoding_bench_test.go ├── alertapi │ └── api.go ├── interfaces.go └── alert_sender.go ├── testutils ├── helpers.go └── alertserver │ └── alertserver.go ├── Makefile ├── go.mod ├── README.md ├── LICENSE.md └── metrics └── agent_metrics.go /tests/e2e/.ipfs/version: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /tests/e2e/ethaccounts/password: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /_release/.gitignore: -------------------------------------------------------------------------------- 1 | yum/rpmbuild/**/* 2 | -------------------------------------------------------------------------------- /_release/apt/forta-x/LICENSE: -------------------------------------------------------------------------------- 1 | No license. 2 | -------------------------------------------------------------------------------- /_release/apt/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /_release/apt/debian/README.Debian: -------------------------------------------------------------------------------- 1 | forta for Debian 2 | -------------------------------------------------------------------------------- /scripts/set-permissions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | chmod 755 /usr/local/bin/forta 4 | -------------------------------------------------------------------------------- /_release/apt/debian/source/local-options: -------------------------------------------------------------------------------- 1 | #abort-on-upstream-changes 2 | #unapply-patches 3 | -------------------------------------------------------------------------------- /scripts/stop-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | (killall -s SIGINT forta && sleep 1m) || true 4 | -------------------------------------------------------------------------------- /scripts/after-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker system prune -f 4 | docker image prune -f -a -------------------------------------------------------------------------------- /_release/apt/debian/patches/series: -------------------------------------------------------------------------------- 1 | # You must remove unused comment lines for the released package. 2 | -------------------------------------------------------------------------------- /tests/e2e/ethaccounts/gethkeyfile: -------------------------------------------------------------------------------- 1 | 0da4d32840b0ef3e30c82d9d4772c8e1bbcd1ac6417b46d958fb5c7db0be99c1 2 | -------------------------------------------------------------------------------- /_release/apt/debian/watch: -------------------------------------------------------------------------------- 1 | # You must remove unused comment lines for the released package. 2 | version=3 3 | -------------------------------------------------------------------------------- /tests/e2e/deps-stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo pkill geth 4 | sudo pkill ipfs 5 | sudo pkill disco 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | tests/e2e/* 2 | !tests/e2e/agents/* 3 | !tests/e2e/ethaccounts/* 4 | !tests/e2e/cmd/* 5 | Makefile 6 | -------------------------------------------------------------------------------- /cmd/node/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/forta-network/forta-node/cmd/node/nodecmd" 4 | 5 | func main() { 6 | nodecmd.Run() 7 | } 8 | -------------------------------------------------------------------------------- /scripts/ipfs-add.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | curl -s -X POST -F file=@$1 https://ipfs.forta.network/api/v0/add | sed 's/.*Qm/Qm/g' |sed 's/\".*$//g' 5 | -------------------------------------------------------------------------------- /_release/apt/debian/changelog: -------------------------------------------------------------------------------- 1 | forta (DEBIAN_VERSION) UNRELEASED; urgency=low 2 | 3 | * Latest release 4 | 5 | -- root <> Mon, 08 Nov 2021 19:35:39 +0000 6 | -------------------------------------------------------------------------------- /_release/yum/Forta.repo: -------------------------------------------------------------------------------- 1 | [forta] 2 | name=Forta 3 | metadata_expire=1 4 | enabled=1 5 | gpgcheck=1 6 | baseurl=BASE_URL/repositories/yum 7 | gpgkey=BASE_URL/pgp.public 8 | -------------------------------------------------------------------------------- /_release/scripts/major-minor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SEMVER="$1" 4 | 5 | IFS=. read SEMVER_MAJOR SEMVER_MINOR SEMVER_PATCH < /dev/null 11 | ./scripts/docker-push.sh "$REGISTRY" "$FULL_IMAGE_NAME" 12 | -------------------------------------------------------------------------------- /_release/.rpmmacros: -------------------------------------------------------------------------------- 1 | %_signature gpg 2 | %_gpg_name GPG_NAME 3 | %__gpg_sign_cmd %{__gpg} gpg --no-verbose --no-armor --pinentry-mode loopback --passphrase PASSPHRASE %{?_gpg_digest_algo:--digest-algo %{_gpg_digest_algo}} %{?_gpg_sign_cmd_extra_args:%{_gpg_sign_cmd_extra_args}} -u "%{_gpg_name}" -sbo %{__signature_filename} %{__plaintext_filename} 4 | -------------------------------------------------------------------------------- /scripts/manifest-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "release": { 3 | "timestamp": "%TIMESTAMP%", 4 | "repository": "https://github.com/forta-network/forta-node", 5 | "version": "%VERSION%", 6 | "commit": "%COMMIT_SHA%", 7 | "services": { 8 | "updater": "%NODE_IMAGE%", 9 | "supervisor": "%NODE_IMAGE%" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile.cli: -------------------------------------------------------------------------------- 1 | FROM alpine AS base 2 | 3 | FROM golang:1.16.4 AS go-builder 4 | WORKDIR /go/app 5 | COPY go.mod . 6 | COPY go.sum . 7 | 8 | RUN go mod download 9 | 10 | COPY . /go/app 11 | 12 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/app/main /go/app/main.go 13 | 14 | FROM base 15 | COPY --from=go-builder /go/app/main /main 16 | 17 | CMD ["/main"] 18 | -------------------------------------------------------------------------------- /_release/systemd/forta.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Forta 3 | After=network-online.target 4 | Wants=network-online.target systemd-networkd-wait-online.service 5 | 6 | StartLimitIntervalSec=500 7 | StartLimitBurst=5 8 | 9 | [Service] 10 | Restart=on-failure 11 | RestartSec=15s 12 | 13 | ExecStart=/usr/bin/forta run 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /.github/actions/go/action.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | description: Validate and test Go code 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Set up Go 7 | uses: actions/setup-go@v2 8 | with: 9 | go-version: 1.16 10 | - name: Build 11 | shell: bash 12 | run: go build -v ./... 13 | - name: Run unit tests 14 | shell: bash 15 | run: make test 16 | -------------------------------------------------------------------------------- /_release/apt/forta-x/Makefile: -------------------------------------------------------------------------------- 1 | prefix = /usr/local 2 | 3 | all: 4 | 5 | install: 6 | wget -O forta ARTIFACTS_URL/forta-REVISION 7 | install -D forta \ 8 | $(DESTDIR)$(prefix)/bin/forta 9 | 10 | clean: 11 | -rm -f forta 12 | 13 | distclean: clean 14 | 15 | uninstall: 16 | -rm -f $(DESTDIR)$(prefix)/bin/forta 17 | 18 | .PHONY: all install clean distclean uninstall 19 | -------------------------------------------------------------------------------- /tests/e2e/cmd/node/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | set -o pipefail 5 | 6 | BIN_NAME="$1" 7 | BIN_NAME=${BIN_NAME:1} 8 | CMD_NAME="$2" 9 | 10 | mkdir -p /.forta/coverage 11 | TIMESTAMP=$(date +%s) 12 | COVERAGE_TMP="/.forta/coverage/$CMD_NAME-coverage-$TIMESTAMP.tmp" 13 | touch "$COVERAGE_TMP" 14 | 15 | exec "$BIN_NAME" -test.coverprofile="$COVERAGE_TMP" "$CMD_NAME" 16 | -------------------------------------------------------------------------------- /Dockerfile.node: -------------------------------------------------------------------------------- 1 | FROM alpine AS base 2 | 3 | FROM golang:1.16.4 AS go-builder 4 | WORKDIR /go/app 5 | COPY go.mod . 6 | COPY go.sum . 7 | 8 | RUN go mod download 9 | 10 | COPY . /go/app 11 | 12 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/app/main /go/app/cmd/node/main.go 13 | 14 | FROM base 15 | COPY --from=go-builder /go/app/main /forta-node 16 | EXPOSE 8089 8090 17 | -------------------------------------------------------------------------------- /tests/e2e/disco.config.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | log: 3 | level: info 4 | fields: 5 | service: disco 6 | environment: development 7 | storage: 8 | ipfs: 9 | url: http://localhost:5001 10 | delete: 11 | enabled: true 12 | maintenance: 13 | uploadpurging: 14 | enabled: false 15 | http: 16 | addr: :5000 17 | headers: 18 | X-Content-Type-Options: [nosniff] 19 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | MODULE_NAME=$(grep 'module' go.mod | cut -c8-) # Get the module name from go.mod 7 | IMPORT="$MODULE_NAME/config" 8 | go build -o forta -ldflags="-X '$IMPORT.DockerSupervisorImage=$1' -X '$IMPORT.DockerUpdaterImage=$1' -X '$IMPORT.UseDockerImages=$2' -X '$IMPORT.ReleaseCid=$3' -X '$IMPORT.CommitHash=$4' -X '$IMPORT.Version=$5'" . 9 | -------------------------------------------------------------------------------- /_release/apt/debian/control: -------------------------------------------------------------------------------- 1 | Source: forta 2 | Section: unknown 3 | Priority: optional 4 | Maintainer: root <> 5 | Build-Depends: debhelper-compat (= 12) 6 | Standards-Version: 4.5.0 7 | Homepage: https://github.com/forta-network/forta-node 8 | 9 | Package: forta 10 | Architecture: any 11 | Multi-Arch: foreign 12 | Depends: systemd, ${misc:Depends}, ${shlibs:Depends} 13 | Description: Forta scanner node CLI 14 | -------------------------------------------------------------------------------- /cmd/cmd_images.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/forta-network/forta-node/config" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func handleFortaImages(cmd *cobra.Command, args []string) error { 10 | cmd.Println("Use images:", config.UseDockerImages) 11 | cmd.Println("Supervisor:", config.DockerSupervisorImage) 12 | cmd.Println("Updater:", config.DockerUpdaterImage) 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /config/defaults.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | DefaultLocalAgentsFileName = "local-agents.json" 5 | DefaultKeysDirName = ".keys" 6 | DefaultConfigFileName = "config.yml" 7 | DefaultNatsPort = "4222" 8 | DefaultContainerPort = "8089" 9 | DefaultHealthPort = "8090" 10 | DefaultFortaNodeBinaryPath = "/forta-node" // the path for the common binary in the container image 11 | ) 12 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | go: 8 | name: Go 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Setup 14 | id: setup 15 | uses: ./.github/actions/setup 16 | - name: Validate and test Go code 17 | id: go 18 | uses: ./.github/actions/go 19 | -------------------------------------------------------------------------------- /cmd/cmd_version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/forta-network/forta-node/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func handleFortaVersion(cmd *cobra.Command, args []string) error { 11 | releaseSummary, ok := config.GetBuildReleaseSummary() 12 | if !ok { 13 | return nil 14 | } 15 | b, _ := json.MarshalIndent(releaseSummary, "", " ") 16 | cmd.Println(string(b)) 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /healthutils/utils.go: -------------------------------------------------------------------------------- 1 | package healthutils 2 | 3 | import ( 4 | "strings" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // DefaultHealthServerErrHandler handlers health server error. 10 | func DefaultHealthServerErrHandler(err error) { 11 | if strings.Contains(strings.ToLower(err.Error()), "server closed") { 12 | log.WithError(err).Warn("health server was shut down") 13 | return 14 | } 15 | log.WithError(err).Panic("health server failed") 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile.buildkit.node: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:latest 2 | 3 | FROM alpine AS base 4 | 5 | FROM golang:1.16.4 AS go-builder 6 | WORKDIR /go/app 7 | COPY go.mod . 8 | COPY go.sum . 9 | 10 | RUN --mount=type=cache,target=/root/.cache/go go mod download 11 | 12 | COPY . /go/app 13 | 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/app/main /go/app/cmd/node/main.go 15 | 16 | FROM base 17 | COPY --from=go-builder /go/app/main /forta-node 18 | EXPOSE 8089 8090 19 | -------------------------------------------------------------------------------- /scripts/total-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | set -o pipefail 5 | 6 | COVERAGE_PREFIX="$1" 7 | DEFAULT_NAME="coverage" 8 | 9 | if [ ! -z "$COVERAGE_PREFIX" ]; then 10 | DEFAULT_NAME="$COVERAGE_PREFIX-$DEFAULT_NAME" 11 | fi 12 | 13 | ALL_COVERAGE="$DEFAULT_NAME.txt" 14 | 15 | echo 'mode: atomic' > $ALL_COVERAGE 16 | 17 | for f in coverage/* 18 | do 19 | tail -n +2 "$f" >> $ALL_COVERAGE 20 | done 21 | 22 | go tool cover -func="$ALL_COVERAGE" > "$DEFAULT_NAME.out" 23 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Install Dependencies 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Set up Go 7 | uses: actions/setup-go@v2 8 | with: 9 | go-version: 1.16 10 | - uses: actions/cache@v2 11 | with: 12 | path: | 13 | ~/go/pkg/mod 14 | ~/.cache/go-build 15 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 16 | restore-keys: | 17 | ${{ runner.os }}-go- 18 | -------------------------------------------------------------------------------- /scripts/docker-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | REGISTRY="$1" 6 | FULL_IMAGE_NAME="$2" 7 | 8 | PUSH_OUTPUT=$(docker push "$FULL_IMAGE_NAME") 9 | DIGEST=$(echo "$PUSH_OUTPUT" | grep -oE '([0-9a-z]{64})') 10 | 11 | FOUND_LINE=$(docker pull -a "$REGISTRY/$DIGEST" | grep bafybei) 12 | if [ -z "$FOUND_LINE" ]; then 13 | echo "failed to find the IPFS reference of the disco image - exiting" 14 | exit 1 15 | fi; 16 | IPFS_REF=${FOUND_LINE::59} 17 | echo "$REGISTRY/$IPFS_REF@sha256:$DIGEST" 18 | -------------------------------------------------------------------------------- /_release/apt/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: forta 3 | Upstream-Contact: info@forta.org 4 | Source: https://github.com/forta-network/forta-node 5 | # 6 | # Please double check copyright with the licensecheck(1) command. 7 | 8 | Files: Makefile 9 | Copyright: FIXME 10 | License: FIXME 11 | 12 | #---------------------------------------------------------------------------- 13 | # License file: LICENSE 14 | No license. 15 | -------------------------------------------------------------------------------- /config/agents_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestAgentConfig_ContainerName(t *testing.T) { 9 | cfg := AgentConfig{ 10 | ID: "0x04f65c638f234548104790d7c692c9273d41f82d784b174ff2fdc3e8e5bf1636", 11 | Image: "bafybeibvkqkf7i3c5ouehviwjb2dzbukgqied3cg36axl7gzm23r6ielnu@sha256:de866feeb97cba4cad6343c4137cb48bc798be0136015bec16d97c8ef28852b9", 12 | } 13 | assert.Equal(t, "forta-agent-0x04f65c-de86", cfg.ContainerName()) 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | main 4 | forta-node 5 | forta 6 | debug 7 | build 8 | .vscode 9 | config-local.yml 10 | *.log 11 | 12 | tests/e2e/.ethereum 13 | tests/e2e/.imagerefs 14 | 15 | !tests/e2e/.ipfs 16 | tests/e2e/.ipfs/* 17 | !tests/e2e/.ipfs/config 18 | !tests/e2e/.ipfs/datastore_spec 19 | !tests/e2e/.ipfs/version 20 | 21 | !tests/e2e/.forta* 22 | tests/e2e/.forta*/* 23 | !tests/e2e/.forta*/.keys 24 | !tests/e2e/.forta*/config.yml 25 | 26 | *coverage.* 27 | !*coverage.sh 28 | coverage 29 | 30 | forta-test 31 | node_modules -------------------------------------------------------------------------------- /.github/actions/propose/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proposal-action", 3 | "version": "1.0.0", 4 | "description": "proposes releases for forta node", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prepare": "ncc build index.js -o dist --source-map" 9 | }, 10 | "author": "Steven Landers (steven.landers@gmail.com)", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@actions/core": "^1.8.0", 14 | "defender-admin-client": "^1.21.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/e2e/agents/txdetectoragent/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:latest 2 | 3 | FROM alpine AS base 4 | 5 | FROM golang:1.16.4 AS go-builder 6 | WORKDIR /go/app 7 | COPY go.mod . 8 | COPY go.sum . 9 | 10 | RUN --mount=type=cache,target=/root/.cache/go go mod download 11 | 12 | COPY . /go/app 13 | 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/app/main \ 15 | /go/app/tests/e2e/agents/txdetectoragent/main.go 16 | 17 | FROM base 18 | COPY --from=go-builder /go/app/main /main 19 | EXPOSE 50051 20 | 21 | ENTRYPOINT [ "/main" ] 22 | -------------------------------------------------------------------------------- /tests/e2e/.forta/.keys/UTC--2022-03-22T01-22-51.315989103Z--222244861c15a8f2a05fbd15e747ea8f20c2c0c9: -------------------------------------------------------------------------------- 1 | {"address":"222244861c15a8f2a05fbd15e747ea8f20c2c0c9","crypto":{"cipher":"aes-128-ctr","ciphertext":"aeab19b9560f1fc67bde0dee2450893d40e8d72b62d0a450f9dd094063d605f3","cipherparams":{"iv":"4cc80af184bcefd9bc618f31ad1e8bcf"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"4cb5bb4ffe27ec89045cc724c89b8bba46f13fb8788730784288269e5e60b4e0"},"mac":"7cb9259c79feec6d1ea8c296c8ab225293a658042f778bbbfba05d6db265e969"},"id":"7a9499da-e3ca-43e0-b94a-715859e56c7a","version":3} -------------------------------------------------------------------------------- /tests/e2e/.forta-private/.keys/UTC--2022-03-22T01-22-51.315989103Z--222244861c15a8f2a05fbd15e747ea8f20c2c0c9: -------------------------------------------------------------------------------- 1 | {"address":"222244861c15a8f2a05fbd15e747ea8f20c2c0c9","crypto":{"cipher":"aes-128-ctr","ciphertext":"aeab19b9560f1fc67bde0dee2450893d40e8d72b62d0a450f9dd094063d605f3","cipherparams":{"iv":"4cc80af184bcefd9bc618f31ad1e8bcf"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"4cb5bb4ffe27ec89045cc724c89b8bba46f13fb8788730784288269e5e60b4e0"},"mac":"7cb9259c79feec6d1ea8c296c8ab225293a658042f778bbbfba05d6db265e969"},"id":"7a9499da-e3ca-43e0-b94a-715859e56c7a","version":3} -------------------------------------------------------------------------------- /_release/yum/forta.spec: -------------------------------------------------------------------------------- 1 | Summary: Forta scanner node CLI 2 | Name: forta 3 | Version: SEMVER 4 | Release: 1 5 | License: FIXME 6 | Group: System 7 | Packager: Forta Protocol 8 | 9 | BuildRoot: %{buildroot} 10 | 11 | Provides: forta 12 | Requires: systemd 13 | 14 | %description 15 | Forta scanner node CLI 16 | 17 | %install 18 | 19 | %files 20 | /usr/lib/systemd/system/forta.service 21 | 22 | %post 23 | curl ARTIFACTS_URL/forta-REVISION -o forta -s 24 | install -m 0755 forta %{_bindir}/forta 25 | 26 | %postun 27 | rm -f %{_bindir}/forta 28 | rm -f /etc/systemd/user/forta.service 29 | -------------------------------------------------------------------------------- /tests/e2e/e2e_agent_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | func (s *Suite) TestLinkUnlink() { 4 | s.startForta(true) 5 | s.expectIn(smallTimeout, func() bool { 6 | return s.fortaProcess.HasOutput("container started") 7 | }) 8 | 9 | tx, err := s.dispatchContract.Link(s.admin, agentIDBigInt, scannerIDBigInt) 10 | s.r.NoError(err) 11 | s.ensureTx("Dispatch.link() agent", tx) 12 | 13 | s.expectUpIn(largeTimeout, agentContainerID) 14 | 15 | tx, err = s.dispatchContract.Unlink(s.admin, agentIDBigInt, scannerIDBigInt) 16 | s.r.NoError(err) 17 | s.ensureTx("Dispatch.unlink() agent", tx) 18 | 19 | s.stopForta() 20 | } 21 | -------------------------------------------------------------------------------- /tests/e2e/.forta-private/config.yml: -------------------------------------------------------------------------------- 1 | chainId: 137 2 | 3 | registry: 4 | checkIntervalSeconds: 1 5 | jsonRpc: 6 | url: http://localhost:8545 7 | 8 | publisher: 9 | batch: 10 | intervalSeconds: 1 11 | 12 | scan: 13 | jsonRpc: 14 | url: http://localhost:8545 15 | 16 | privateMode: 17 | enable: true 18 | webhookUrl: http://localhost:9090/batch/webhook 19 | agentImages: 20 | - forta-e2e-test-agent 21 | 22 | autoUpdate: 23 | checkIntervalSeconds: 1 24 | 25 | trace: 26 | enabled: false 27 | 28 | ens: 29 | override: true 30 | 31 | telemetry: 32 | disable: true 33 | 34 | agentLogs: 35 | disable: true 36 | 37 | log: 38 | level: trace 39 | -------------------------------------------------------------------------------- /cmd/color.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | func toStderr(str string) { 11 | fmt.Fprintf(os.Stderr, str) 12 | } 13 | 14 | func yellowBold(str string, args ...interface{}) { 15 | toStderr(color.New(color.Bold, color.FgYellow).Sprintf(str, args...)) 16 | } 17 | 18 | func greenBold(str string, args ...interface{}) { 19 | color.New(color.Bold, color.FgGreen).Printf(str, args...) 20 | } 21 | 22 | func redBold(str string, args ...interface{}) { 23 | toStderr(color.New(color.Bold, color.FgRed).Sprintf(str, args...)) 24 | } 25 | 26 | func whiteBold(str string, args ...interface{}) { 27 | color.New(color.Bold, color.FgWhite).Printf(str, args...) 28 | } 29 | -------------------------------------------------------------------------------- /_release/apt/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # You must remove unused comment lines for the released package. 3 | #export DH_VERBOSE = 1 4 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 5 | #export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 6 | #export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 7 | 8 | # "nostrip" is required to avoid problematic dh_dwz step: https://manpages.debian.org/bullseye/debhelper/dh_dwz.1.en.html#NOTES 9 | export DEB_BUILD_OPTIONS = nostrip 10 | 11 | %: 12 | dh $@ 13 | 14 | override_dh_installsystemd: 15 | dh_installsystemd --no-enable forta.service 16 | 17 | override_dh_auto_install: 18 | dh_auto_install -- prefix=/usr 19 | 20 | #override_dh_install: 21 | # dh_install --list-missing -X.pyc -X.pyo 22 | -------------------------------------------------------------------------------- /services/json-rpc/rate_limiter_test.go: -------------------------------------------------------------------------------- 1 | package json_rpc_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | json_rpc "github.com/forta-network/forta-node/services/json-rpc" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | const testClientID = "1" 12 | 13 | func TestRateLimiting(t *testing.T) { 14 | r := require.New(t) 15 | rateLimiter := json_rpc.NewRateLimiter(0.5, 1) // replenish every 2s (1/0.5) 16 | reachedLimit := rateLimiter.ExceedsLimit(testClientID) 17 | r.False(reachedLimit) 18 | reachedLimit = rateLimiter.ExceedsLimit(testClientID) 19 | r.True(reachedLimit) 20 | 21 | time.Sleep(time.Second * 5) // way larger than 2s 22 | reachedLimit = rateLimiter.ExceedsLimit(testClientID) 23 | r.False(reachedLimit) 24 | } 25 | -------------------------------------------------------------------------------- /tests/e2e/.forta/config.yml: -------------------------------------------------------------------------------- 1 | chainId: 137 2 | 3 | registry: 4 | ipfs: 5 | gatewayUrl: http://localhost:8080 6 | apiUrl: http://localhost:5001 7 | jsonRpc: 8 | url: http://localhost:8545 9 | containerRegistry: localhost:1970 10 | checkIntervalSeconds: 1 11 | 12 | scan: 13 | jsonRpc: 14 | url: http://localhost:8545 15 | 16 | publish: 17 | apiUrl: http://localhost:9090 18 | ipfs: 19 | gatewayUrl: http://localhost:8080 20 | apiUrl: http://localhost:5001 21 | batch: 22 | skipEmpty: true 23 | 24 | autoUpdate: 25 | updateDelay: 1 26 | 27 | trace: 28 | enabled: false 29 | 30 | ens: 31 | override: true 32 | 33 | telemetry: 34 | disable: true 35 | 36 | agentLogs: 37 | disable: true 38 | 39 | log: 40 | level: trace 41 | -------------------------------------------------------------------------------- /_release/scripts/aptly-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | REVISION="$1" 6 | SEMVER="$2" 7 | GPG_NAME="$3" 8 | GPG_PASSPHRASE="$4" 9 | 10 | # remove old package so we don't get an error if we try to add the same 11 | DEBIAN_VERSION=$(./scripts/debian-version.sh "$SEMVER") 12 | aptly repo show -with-packages forta 13 | aptly repo remove forta 'forta_'"$DEBIAN_VERSION"'_amd64' 14 | 15 | set -e 16 | 17 | aptly repo add forta apt/forta_*_amd64.deb 18 | aptly snapshot create "$REVISION" from repo forta 19 | # "-batch=true" flag here makes passphrase work: https://github.com/aptly-dev/aptly/issues/642 20 | aptly publish snapshot -distribution=stable -batch=true -force-overwrite -gpg-key="$GPG_NAME" \ 21 | -passphrase="$GPG_PASSPHRASE" "$REVISION" s3:releaseBucket:repositories/apt/ 22 | -------------------------------------------------------------------------------- /appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: forta 5 | destination: /usr/local/bin/ 6 | file_exists_behavior: OVERWRITE 7 | hooks: 8 | BeforeInstall: 9 | - location: scripts/stop-server.sh 10 | timeout: 300 11 | runas: forta 12 | AfterInstall: 13 | - location: scripts/install.sh 14 | timeout: 500 15 | runas: root 16 | - location: scripts/set-permissions.sh 17 | timeout: 300 18 | runas: root 19 | - location: scripts/inject-secrets.sh 20 | timeout: 300 21 | runas: root 22 | ApplicationStart: 23 | - location: scripts/start-server.sh 24 | timeout: 300 25 | runas: forta 26 | AfterAllowTraffic: 27 | - location: scripts/after-start.sh 28 | timeout: 300 29 | runas: forta 30 | -------------------------------------------------------------------------------- /config/env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | EnvHostFortaDir = "HOST_FORTA_DIR" // for retrieving forta dir path on the host os 5 | EnvDevelopment = "FORTA_DEVELOPMENT" 6 | EnvReleaseInfo = "FORTA_RELEASE_INFO" 7 | 8 | // Agent env vars 9 | EnvJsonRpcHost = "JSON_RPC_HOST" 10 | EnvJsonRpcPort = "JSON_RPC_PORT" 11 | EnvAgentGrpcPort = "AGENT_GRPC_PORT" 12 | ) 13 | 14 | // EnvDefaults contain default values for one env. 15 | type EnvDefaults struct { 16 | DiscoSubdomain string 17 | } 18 | 19 | // GetEnvDefaults returns the default values for an env. 20 | func GetEnvDefaults(development bool) EnvDefaults { 21 | if development { 22 | return EnvDefaults{ 23 | DiscoSubdomain: "disco-dev", 24 | } 25 | } 26 | return EnvDefaults{ 27 | DiscoSubdomain: "disco", 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /store/string.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "io/ioutil" 6 | "strings" 7 | ) 8 | 9 | type StringStore interface { 10 | Get() (string, error) 11 | Put(string) error 12 | } 13 | 14 | type fileStringStore struct { 15 | path string 16 | } 17 | 18 | func NewFileStringStore(path string) *fileStringStore { 19 | return &fileStringStore{path: path} 20 | } 21 | 22 | func (fss *fileStringStore) Put(body string) error { 23 | return ioutil.WriteFile(fss.path, []byte(body), 0644) 24 | } 25 | 26 | func (fss *fileStringStore) Get() (string, error) { 27 | b, err := ioutil.ReadFile(fss.path) 28 | if err != nil { 29 | log.WithError(err).Warn("failed to read the last batch file") 30 | return "", nil 31 | } 32 | return strings.TrimSpace(string(b)), nil 33 | } 34 | -------------------------------------------------------------------------------- /services/scanner/agentpool/poolagent/error_counter.go: -------------------------------------------------------------------------------- 1 | package poolagent 2 | 3 | import "sync" 4 | 5 | // errorCounter checks incoming errors and tells if we are over 6 | // the max amount of consecutive errors. 7 | type errorCounter struct { 8 | max uint 9 | errCheck func(error) bool 10 | count uint 11 | sync.Mutex 12 | } 13 | 14 | // NewErrorCounter creates a new error counter. 15 | func NewErrorCounter(max uint, errCheck func(error) bool) *errorCounter { 16 | return &errorCounter{ 17 | max: max, 18 | errCheck: errCheck, 19 | } 20 | } 21 | 22 | func (ec *errorCounter) TooManyErrs(err error) bool { 23 | ec.Lock() 24 | defer ec.Unlock() 25 | if err == nil || !ec.errCheck(err) { 26 | ec.count = 0 // reset if other errors or no errors 27 | return false 28 | } 29 | ec.count++ 30 | return ec.count >= ec.max 31 | } 32 | -------------------------------------------------------------------------------- /_release/scripts/init-aptly.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | AWS_REGION="$1" 6 | AWS_BUCKET_NAME="$2" 7 | AWS_ACCESS_KEY="$3" 8 | AWS_SECRET_KEY="$4" 9 | MIRROR_URL="$5" 10 | 11 | cd "$HOME" || exit 1 12 | 13 | aptly mirror create fortamirror "$MIRROR_URL" stable # we refer to where we publish as a mirror here, to load packages 14 | aptly mirror update fortamirror 15 | aptly repo create forta 16 | aptly repo import fortamirror forta forta 17 | 18 | set -e 19 | 20 | S3_CONFIG="{ 21 | \"releaseBucket\": { 22 | \"region\": \"$AWS_REGION\", 23 | \"bucket\": \"$AWS_BUCKET_NAME\", 24 | \"awsAccessKeyID\": \"$AWS_ACCESS_KEY\", 25 | \"awsSecretAccessKey\": \"$AWS_SECRET_KEY\" 26 | } 27 | }" 28 | 29 | jq '.S3PublishEndpoints = $config' --argjson config "$S3_CONFIG" < .aptly.conf > .aptly.new.conf 30 | rm .aptly.conf 31 | mv .aptly.new.conf .aptly.conf 32 | -------------------------------------------------------------------------------- /.github/actions/propose/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Propose Release' 2 | description: 'Propose Release' 3 | inputs: 4 | version: 5 | description: 'Semver tag' 6 | required: true 7 | release-cid: 8 | description: 'CID of Release' 9 | required: true 10 | multisig: 11 | description: 'Address of multisig account' 12 | required: true 13 | network: 14 | description: 'Network for version contract' 15 | required: true 16 | scanner-version-contract: 17 | description: 'Address of scanner version contract' 18 | required: true 19 | api-key: 20 | description: 'API Key for Defender' 21 | required: true 22 | api-secret: 23 | description: 'API Secret for Defender' 24 | required: true 25 | outputs: 26 | proposal-url: 27 | description: "URL of proposal" 28 | 29 | runs: 30 | using: 'node16' 31 | main: 'dist/index.js' 32 | -------------------------------------------------------------------------------- /services/scanner/tx_logger.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // TxLogger logs a tick log every 10 minutes in order to distinguish between a frozen process or stuck log 11 | type TxLogger struct { 12 | ctx context.Context 13 | } 14 | 15 | func (t *TxLogger) Start() error { 16 | ticker := time.NewTicker(10 * time.Minute) 17 | 18 | go func() { 19 | for range ticker.C { 20 | if t.ctx.Err() != nil { 21 | return 22 | } 23 | log.Info("tx-logger tick") 24 | } 25 | }() 26 | return nil 27 | } 28 | 29 | func (t *TxLogger) Stop() error { 30 | log.Infof("Stopping %s", t.Name()) 31 | return nil 32 | } 33 | 34 | func (t *TxLogger) Name() string { 35 | return "TxLogger" 36 | } 37 | 38 | func NewTxLogger(ctx context.Context) *TxLogger { 39 | return &TxLogger{ 40 | ctx: ctx, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/actions/e2e/action.yml: -------------------------------------------------------------------------------- 1 | name: E2E Test 2 | description: Validate and test Go code 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Prepare E2E test dependencies 7 | shell: bash 8 | run: | 9 | set -e 10 | 11 | wget --quiet https://dist-dev.forta.network/dev-dependencies/geth 12 | chmod +x geth 13 | sudo cp geth /usr/bin/geth 14 | 15 | wget --quiet https://dist-dev.forta.network/dev-dependencies/ipfs 16 | chmod +x ipfs 17 | sudo cp ipfs /usr/bin/ipfs 18 | 19 | wget --quiet https://dist-dev.forta.network/dev-dependencies/disco 20 | chmod +x disco 21 | sudo cp disco /usr/bin/disco 22 | - name: Run E2E tests 23 | shell: bash 24 | run: | 25 | RUNNER_TRACKING_ID="" && ./tests/e2e/deps-start.sh & 26 | RUNNER_TRACKING_ID="" && make e2e-test 27 | ./tests/e2e/deps-stop.sh 28 | -------------------------------------------------------------------------------- /config/utils.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "math/big" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | func ParseBigInt(num int) *big.Int { 12 | var val *big.Int 13 | if num != 0 { 14 | val = big.NewInt(int64(num)) 15 | } 16 | return val 17 | } 18 | 19 | func InitLogLevel(cfg Config) error { 20 | if cfg.Log.Level != "" { 21 | lvl, err := log.ParseLevel(cfg.Log.Level) 22 | if err != nil { 23 | return err 24 | } 25 | log.SetLevel(lvl) 26 | } else { 27 | log.SetLevel(log.InfoLevel) 28 | } 29 | log.SetFormatter(&log.TextFormatter{ 30 | FullTimestamp: true, 31 | }) 32 | return nil 33 | } 34 | 35 | func readFile(filename string, cfg *Config) error { 36 | f, err := os.Open(filename) 37 | if f != nil { 38 | defer f.Close() 39 | } 40 | if err != nil { 41 | return err 42 | } 43 | 44 | decoder := yaml.NewDecoder(f) 45 | return decoder.Decode(cfg) 46 | } 47 | -------------------------------------------------------------------------------- /services/json-rpc/errors_test.go: -------------------------------------------------------------------------------- 1 | package json_rpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const testRequestID = 123 15 | 16 | func TestTooManyReqsError(t *testing.T) { 17 | r := require.New(t) 18 | 19 | buf := bytes.NewBuffer([]byte(fmt.Sprintf(`{"id":%d}`, testRequestID))) 20 | req, err := http.NewRequest("POST", "http://asdf.asdf", buf) 21 | r.NoError(err) 22 | recorder := httptest.NewRecorder() 23 | 24 | writeTooManyReqsErr(recorder, req) 25 | 26 | resp := recorder.Result() 27 | r.Equal(http.StatusTooManyRequests, resp.StatusCode) 28 | var errResp errorResponse 29 | r.NoError(json.NewDecoder(resp.Body).Decode(&errResp)) 30 | r.Equal("2.0", errResp.JSONRPC) 31 | r.Equal(testRequestID, errResp.ID) 32 | r.Equal(-32000, errResp.Error.Code) 33 | r.Contains(errResp.Error.Message, "exceeds") 34 | } 35 | -------------------------------------------------------------------------------- /config/release.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/forta-network/forta-core-go/release" 5 | ) 6 | 7 | // Release vars - injected by the compiler 8 | var ( 9 | CommitHash = "" 10 | ReleaseCid = "" 11 | Version = "" 12 | ) 13 | 14 | // GetBuildReleaseSummary returns the build summary from build vars. 15 | func GetBuildReleaseSummary() (*release.ReleaseSummary, bool) { 16 | if len(CommitHash) == 0 { 17 | return nil, false 18 | } 19 | 20 | return &release.ReleaseSummary{ 21 | Commit: CommitHash, 22 | IPFS: ReleaseCid, 23 | Version: Version, 24 | }, true 25 | } 26 | 27 | // GetBuildReleaseInfo collects and returns the release info from build vars. 28 | func GetBuildReleaseInfo() *release.ReleaseInfo { 29 | return &release.ReleaseInfo{ 30 | FromBuild: true, 31 | IPFS: ReleaseCid, 32 | Manifest: release.ReleaseManifest{ 33 | Release: release.Release{ 34 | Version: Version, 35 | Commit: CommitHash, 36 | }, 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/e2e/cmd/node/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:latest 2 | 3 | FROM alpine AS base 4 | 5 | FROM golang:1.16.4-alpine AS go-builder 6 | 7 | RUN apk add --no-cache bash gcc g++ 8 | 9 | WORKDIR /go/app 10 | COPY go.mod . 11 | COPY go.sum . 12 | 13 | RUN --mount=type=cache,target=/root/.cache/go go mod download 14 | 15 | COPY . /go/app 16 | RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go test -c -o main -race -covermode=atomic -coverpkg \ 17 | $(go list ./... | grep -v tests | tr "\n" ",") github.com/forta-network/forta-node/tests/e2e/cmd/node 18 | 19 | FROM base 20 | 21 | RUN apk add --no-cache bash gcc g++ tini 22 | 23 | COPY --from=go-builder /go/app/main /usr/bin/forta-node 24 | COPY --from=go-builder /go/app/tests/e2e/cmd/node/run.sh /run.sh 25 | EXPOSE 8089 8090 26 | 27 | RUN mkdir -p /.forta/coverage 28 | 29 | # receives '/forta-node ' from runner and supervisor 30 | # https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact 31 | ENTRYPOINT [ "tini", "-v", "--", "/run.sh" ] 32 | -------------------------------------------------------------------------------- /clients/messaging/subjects.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "github.com/forta-network/forta-core-go/protocol" 5 | "github.com/forta-network/forta-node/config" 6 | ) 7 | 8 | // Message types 9 | const ( 10 | SubjectAgentsVersionsLatest = "agents.versions.latest" 11 | SubjectAgentsActionRun = "agents.action.run" 12 | SubjectAgentsActionStop = "agents.action.stop" 13 | SubjectAgentsStatusRunning = "agents.status.running" 14 | SubjectAgentsStatusAttached = "agents.status.attached" 15 | SubjectAgentsStatusStopped = "agents.status.stopped" 16 | SubjectMetricAgent = "metric.agent" 17 | SubjectScannerBlock = "scanner.block" 18 | ) 19 | 20 | // AgentPayload is the message payload. 21 | type AgentPayload []config.AgentConfig 22 | 23 | // AgentMetricPayload is the message payload for metrics. 24 | type AgentMetricPayload *protocol.AgentMetricList 25 | 26 | // ScannerPayload is the message payload for general scanner info. 27 | type ScannerPayload struct { 28 | LatestBlockInput uint64 `json:"latestBlockInput"` 29 | } 30 | -------------------------------------------------------------------------------- /tests/e2e/e2e_forta_cmd_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "github.com/forta-network/forta-node/cmd" 5 | "github.com/forta-network/forta-node/tests/e2e/ethaccounts" 6 | ) 7 | 8 | // TestRegister tests what happens when registering with or without registration. 9 | func (s *Suite) TestRegister() { 10 | s.forta("", "run") 11 | s.fortaProcess.Wait() 12 | s.True(s.fortaProcess.HasOutput(cmd.ErrCannotRunScanner.Error())) 13 | s.T().Log("as expected: could not run scan node without registration") 14 | 15 | s.T().Log("trying to run with --no-check") 16 | s.forta("", "run", "--no-check") 17 | s.expectUpIn(largeTimeout, runnerSupervisedContainers...) 18 | s.T().Log("--no-check works") 19 | s.stopForta() 20 | 21 | s.forta("", "register", "--owner-address", ethaccounts.ScannerOwnerAddress.Hex()) 22 | s.fortaProcess.Wait() 23 | s.fortaProcess.HasOutput("polygonscan") 24 | 25 | // should work without pre-registration (false) now 26 | s.startForta(false) 27 | s.expectIn(smallTimeout, func() bool { 28 | return s.fortaProcess.HasOutput("container started") 29 | }) 30 | s.stopForta() 31 | } 32 | -------------------------------------------------------------------------------- /services/scanner/interfaces.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/forta-network/forta-core-go/domain" 5 | "github.com/forta-network/forta-core-go/protocol" 6 | "github.com/forta-network/forta-node/config" 7 | ) 8 | 9 | // TxResult contains request and response data. 10 | type TxResult struct { 11 | AgentConfig config.AgentConfig 12 | Request *protocol.EvaluateTxRequest 13 | Response *protocol.EvaluateTxResponse 14 | Timestamps *domain.TrackingTimestamps 15 | } 16 | 17 | // BlockResult contains request and response data. 18 | type BlockResult struct { 19 | AgentConfig config.AgentConfig 20 | Request *protocol.EvaluateBlockRequest 21 | Response *protocol.EvaluateBlockResponse 22 | Timestamps *domain.TrackingTimestamps 23 | } 24 | 25 | // AgentPool contains all of the agents which we can forward the block and tx requests 26 | // to and receive the results from. 27 | type AgentPool interface { 28 | SendEvaluateTxRequest(req *protocol.EvaluateTxRequest) 29 | TxResults() <-chan *TxResult 30 | SendEvaluateBlockRequest(req *protocol.EvaluateBlockRequest) 31 | BlockResults() <-chan *BlockResult 32 | } 33 | -------------------------------------------------------------------------------- /services/json-rpc/errors.go: -------------------------------------------------------------------------------- 1 | package json_rpc 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type requestPayload struct { 11 | ID int `json:"id"` 12 | } 13 | 14 | type errorResponse struct { 15 | JSONRPC string `json:"jsonrpc"` 16 | ID int `json:"id"` 17 | Error jsonRpcError `json:"error"` 18 | } 19 | 20 | type jsonRpcError struct { 21 | Code int `json:"code"` 22 | Message string `json:"message"` 23 | } 24 | 25 | func writeTooManyReqsErr(w http.ResponseWriter, req *http.Request) { 26 | w.WriteHeader(http.StatusTooManyRequests) 27 | 28 | var reqPayload requestPayload 29 | if err := json.NewDecoder(req.Body).Decode(&reqPayload); err != nil { 30 | log.WithError(err).Error("failed to decode jsonrpc request body") 31 | return 32 | } 33 | 34 | if err := json.NewEncoder(w).Encode(&errorResponse{ 35 | JSONRPC: "2.0", 36 | ID: reqPayload.ID, 37 | Error: jsonRpcError{ 38 | Code: -32000, 39 | Message: "agent exceeds scan node request limit", 40 | }, 41 | }); err != nil { 42 | log.WithError(err).Error("failed to write jsonrpc error response body") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /services/service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "syscall" 7 | "testing" 8 | "time" 9 | 10 | "github.com/sirupsen/logrus" 11 | "github.com/stretchr/testify/assert" 12 | "golang.org/x/sync/errgroup" 13 | ) 14 | 15 | type TestService struct { 16 | cancelled bool 17 | ctx context.Context 18 | } 19 | 20 | func (t *TestService) Start() error { 21 | grp, ctx := errgroup.WithContext(t.ctx) 22 | grp.Go(func() error { 23 | select { 24 | case <-ctx.Done(): 25 | t.cancelled = true 26 | return ctx.Err() 27 | } 28 | }) 29 | return grp.Wait() 30 | } 31 | 32 | func (t *TestService) Stop() error { 33 | return nil 34 | } 35 | 36 | func (t *TestService) Name() string { 37 | return "test" 38 | } 39 | 40 | func TestSigIntSignalCancelsService(t *testing.T) { 41 | sigc = make(chan os.Signal, 1) 42 | ctx, cancel := InitMainContext() 43 | 44 | go func() { 45 | time.Sleep(1 * time.Second) 46 | sigc <- syscall.SIGINT 47 | }() 48 | 49 | svc := &TestService{ctx: ctx} 50 | err := StartServices(ctx, cancel, logrus.NewEntry(logrus.StandardLogger()), []Service{svc}) 51 | assert.Error(t, err, context.Canceled) 52 | assert.True(t, svc.cancelled) 53 | } 54 | -------------------------------------------------------------------------------- /testutils/helpers.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ethereum/go-ethereum/common" 7 | "github.com/ethereum/go-ethereum/core/types" 8 | "github.com/forta-network/forta-core-go/domain" 9 | ) 10 | 11 | // mocking code borrowed from go-ethereum/core/types tests 12 | 13 | // TestTxs get a list of mock transactions with the following nonces 14 | func TestTxs(nonces ...int) []domain.Transaction { 15 | var result []domain.Transaction 16 | for _, nonce := range nonces { 17 | result = append(result, domain.Transaction{ 18 | Hash: fmt.Sprintf("%x", nonce), 19 | Nonce: fmt.Sprintf("%x", nonce), 20 | }) 21 | } 22 | return result 23 | } 24 | 25 | // TestBlock gets a block with a list of transactions with the following nonces 26 | func TestBlock(nonces ...int) *domain.Block { 27 | return &domain.Block{ 28 | Number: "0x0", 29 | Hash: "0x1", 30 | Transactions: TestTxs(nonces...), 31 | } 32 | } 33 | 34 | func TestLogs(indexes ...int) []types.Log { 35 | var result []types.Log 36 | for _, index := range indexes { 37 | result = append(result, types.Log{ 38 | TxHash: common.HexToHash(fmt.Sprintf("%x", index)), 39 | TxIndex: uint(index), 40 | }) 41 | } 42 | return result 43 | } 44 | -------------------------------------------------------------------------------- /tests/e2e/e2e_forta_run_private_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/forta-network/forta-core-go/clients/webhook/client/models" 8 | "github.com/forta-network/forta-node/cmd" 9 | ) 10 | 11 | func (s *Suite) TestPrivateMode() { 12 | const privateModeDir = ".forta-private" 13 | 14 | // make sure that non-registered private nodes also cannot start 15 | s.forta(privateModeDir, "run") 16 | s.fortaProcess.Wait() 17 | s.True(s.fortaProcess.HasOutput(cmd.ErrCannotRunScanner.Error())) 18 | s.T().Log("as expected: could not run scan node without registration") 19 | 20 | s.registerNode() 21 | s.forta(privateModeDir, "run") 22 | defer s.stopForta() 23 | // the bot in private mode list should run 24 | s.expectUpIn(smallTimeout, "forta-agent") 25 | 26 | // an alert should be detected and sent to the webhook url 27 | var b []byte 28 | s.expectIn(smallTimeout, func() (ok bool) { 29 | // trigger an alert 30 | s.sendExploiterTx() 31 | time.Sleep(time.Second * 2) 32 | // try to receive the alert 33 | b, ok = s.alertServer.GetAlert("webhook") 34 | return ok 35 | }) 36 | var webhookAlerts models.AlertList 37 | s.r.NoError(json.Unmarshal(b, &webhookAlerts)) 38 | s.T().Log(string(b)) 39 | } 40 | -------------------------------------------------------------------------------- /config/resources.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // AgentResourceLimits contain the agent resource limits data. 4 | type AgentResourceLimits struct { 5 | CPUQuota int64 // in microseconds 6 | Memory int64 // in bytes 7 | } 8 | 9 | // GetAgentResourceLimits calculates and returns the resource limits by 10 | // taking the configuration into account. Zero values mean no limits. 11 | func GetAgentResourceLimits(resourcesCfg ResourcesConfig) *AgentResourceLimits { 12 | var limits AgentResourceLimits 13 | 14 | if resourcesCfg.DisableAgentLimits { 15 | return &limits 16 | } 17 | 18 | limits.CPUQuota = getDefaultCPUQuotaPerAgent() 19 | if resourcesCfg.AgentMaxCPUs > 0 { 20 | limits.CPUQuota = int64(resourcesCfg.AgentMaxCPUs * float64(100000)) 21 | } 22 | 23 | limits.Memory = getDefaultMemoryPerAgent() 24 | if resourcesCfg.AgentMaxMemoryMiB > 0 { 25 | limits.Memory = int64(resourcesCfg.AgentMaxMemoryMiB * 104858) 26 | } 27 | 28 | return &limits 29 | } 30 | 31 | // getDefaultCPUQuotaPerAgent returns the default CFS microseconds value allowed per agent 32 | func getDefaultCPUQuotaPerAgent() int64 { 33 | return 20000 // just 20% 34 | } 35 | 36 | // getDefaultMemoryPerAgent returns the constant default memory allowed per agent. 37 | func getDefaultMemoryPerAgent() int64 { 38 | return 1048580000 // 1000 MiB in bytes 39 | } 40 | -------------------------------------------------------------------------------- /clients/agentgrpc/encoding.go: -------------------------------------------------------------------------------- 1 | package agentgrpc 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "reflect" 7 | "unsafe" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/encoding" 11 | "google.golang.org/grpc/encoding/proto" 12 | ) 13 | 14 | var ( 15 | defaultCodec = encoding.GetCodec(proto.Name) 16 | destPrepMsgType = reflect.TypeOf(&grpc.PreparedMsg{}) 17 | ) 18 | 19 | type preparedMsg struct { 20 | encodedData []byte 21 | hdr []byte 22 | payload []byte 23 | } 24 | 25 | // EncodeMessage encodes request as a PreparedMsg so the client stream can use it 26 | // directly instead of allocating a new encoded message. 27 | // 28 | // See https://github.com/grpc/grpc-go/blob/1ffd63de37de4571028efedb6422e29d08716d0c/stream.go#L1623 29 | func EncodeMessage(msg interface{}) (*grpc.PreparedMsg, error) { 30 | msgB, err := defaultCodec.Marshal(msg) 31 | if err != nil { 32 | return nil, fmt.Errorf("agentgrpc: failed to encode message: %v", err) 33 | } 34 | hdr := make([]byte, 5) 35 | // write length of payload into header buffer 36 | binary.BigEndian.PutUint32(hdr[1:], uint32(len(msgB))) 37 | // hacky conversion to avoid compiler error 38 | return (*grpc.PreparedMsg)((unsafe.Pointer)(&preparedMsg{ 39 | encodedData: msgB, 40 | payload: msgB, 41 | hdr: hdr, 42 | })), nil 43 | } 44 | -------------------------------------------------------------------------------- /config/containers.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | ) 7 | 8 | const ContainerNamePrefix = "forta" 9 | 10 | // Docker container names 11 | var ( 12 | DockerSupervisorImage = "forta-network/forta-node:latest" 13 | DockerUpdaterImage = "forta-network/forta-node:latest" 14 | UseDockerImages = "local" 15 | 16 | DockerSupervisorManagedContainers = 4 17 | DockerUpdaterContainerName = fmt.Sprintf("%s-updater", ContainerNamePrefix) 18 | DockerSupervisorContainerName = fmt.Sprintf("%s-supervisor", ContainerNamePrefix) 19 | DockerNatsContainerName = fmt.Sprintf("%s-nats", ContainerNamePrefix) 20 | DockerIpfsContainerName = fmt.Sprintf("%s-ipfs", ContainerNamePrefix) 21 | DockerScannerContainerName = fmt.Sprintf("%s-scanner", ContainerNamePrefix) 22 | DockerJSONRPCProxyContainerName = fmt.Sprintf("%s-json-rpc", ContainerNamePrefix) 23 | 24 | DockerNetworkName = DockerScannerContainerName 25 | 26 | DefaultContainerFortaDirPath = "/.forta" 27 | DefaultContainerConfigPath = path.Join(DefaultContainerFortaDirPath, DefaultConfigFileName) 28 | DefaultContainerKeyDirPath = path.Join(DefaultContainerFortaDirPath, DefaultKeysDirName) 29 | DefaultContainerLocalAgentsFilePath = path.Join(DefaultContainerFortaDirPath, DefaultLocalAgentsFileName) 30 | ) 31 | -------------------------------------------------------------------------------- /store/batch_ref.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path" 7 | "strings" 8 | 9 | "github.com/ipfs/go-cid" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const lastBatchFileName = ".last-batch" 14 | 15 | // BatchRefStore writes to and reads from somewhere the last batch reference. 16 | type BatchRefStore interface { 17 | GetLast() (string, error) 18 | Put(string) error 19 | } 20 | 21 | type batchRefStore struct { 22 | filePath string 23 | } 24 | 25 | // NewBatchRefStore creates a new ref store. 26 | func NewBatchRefStore(dir string) *batchRefStore { 27 | return &batchRefStore{ 28 | filePath: path.Join(dir, lastBatchFileName), 29 | } 30 | } 31 | 32 | func (store *batchRefStore) GetLast() (string, error) { 33 | b, err := ioutil.ReadFile(store.filePath) 34 | if err != nil { 35 | log.WithError(err).Warn("failed to read the last batch file") 36 | return "", nil 37 | } 38 | if _, err = cid.Parse(string(b)); err != nil { 39 | return "", fmt.Errorf("invalid batch ref found: %v", err) 40 | } 41 | return strings.TrimSpace(string(b)), nil 42 | } 43 | 44 | func (store *batchRefStore) Put(ref string) error { 45 | if _, err := cid.Parse(ref); err != nil { 46 | return fmt.Errorf("invalid batch ref provided: %v", err) 47 | } 48 | return ioutil.WriteFile(store.filePath, []byte(ref), 0644) 49 | } 50 | -------------------------------------------------------------------------------- /clients/agentgrpc/testserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/forta-network/forta-core-go/protocol" 9 | "github.com/forta-network/forta-node/config" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | type AgentServer struct { 14 | protocol.UnimplementedAgentServer 15 | } 16 | 17 | func (as *AgentServer) Initialize(context.Context, *protocol.InitializeRequest) (*protocol.InitializeResponse, error) { 18 | return &protocol.InitializeResponse{ 19 | Status: protocol.ResponseStatus_SUCCESS, 20 | }, nil 21 | } 22 | 23 | func (as *AgentServer) EvaluateTx(ctx context.Context, txRequest *protocol.EvaluateTxRequest) (*protocol.EvaluateTxResponse, error) { 24 | return &protocol.EvaluateTxResponse{ 25 | Status: protocol.ResponseStatus_SUCCESS, 26 | }, nil 27 | } 28 | 29 | func (as *AgentServer) EvaluateBlock(context.Context, *protocol.EvaluateBlockRequest) (*protocol.EvaluateBlockResponse, error) { 30 | return &protocol.EvaluateBlockResponse{ 31 | Status: protocol.ResponseStatus_SUCCESS, 32 | }, nil 33 | } 34 | 35 | func main() { 36 | lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", config.AgentGrpcPort)) 37 | if err != nil { 38 | panic(err) 39 | } 40 | defer lis.Close() 41 | 42 | server := grpc.NewServer() 43 | as := &AgentServer{} 44 | protocol.RegisterAgentServer(server, as) 45 | server.Serve(lis) 46 | } 47 | -------------------------------------------------------------------------------- /config/agents.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/forta-network/forta-core-go/protocol" 7 | "github.com/forta-network/forta-core-go/utils" 8 | ) 9 | 10 | const ( 11 | AgentGrpcPort = "50051" 12 | ) 13 | 14 | type AgentConfig struct { 15 | ID string `yaml:"id" json:"id"` 16 | Image string `yaml:"image" json:"image"` 17 | Manifest string `yaml:"manifest" json:"manifest"` 18 | IsLocal bool `yaml:"isLocal" json:"isLocal"` 19 | StartBlock *uint64 `yaml:"startBlock" json:"startBlock,omitempty"` 20 | StopBlock *uint64 `yaml:"stopBlock" json:"stopBlock,omitempty"` 21 | } 22 | 23 | // ToAgentInfo transforms the agent config to the agent info. 24 | func (ac AgentConfig) ToAgentInfo() *protocol.AgentInfo { 25 | return &protocol.AgentInfo{ 26 | Id: ac.ID, 27 | Image: ac.Image, 28 | ImageHash: ac.ImageHash(), 29 | Manifest: ac.Manifest, 30 | } 31 | } 32 | 33 | func (ac AgentConfig) ImageHash() string { 34 | _, digest := utils.SplitImageRef(ac.Image) 35 | return digest 36 | } 37 | 38 | func (ac AgentConfig) ContainerName() string { 39 | _, digest := utils.SplitImageRef(ac.Image) 40 | if ac.IsLocal { 41 | return fmt.Sprintf("%s-agent-%s", ContainerNamePrefix, utils.ShortenString(ac.ID, 8)) 42 | } 43 | return fmt.Sprintf("%s-agent-%s-%s", ContainerNamePrefix, utils.ShortenString(ac.ID, 8), utils.ShortenString(digest, 4)) 44 | } 45 | 46 | func (ac AgentConfig) GrpcPort() string { 47 | return AgentGrpcPort 48 | } 49 | -------------------------------------------------------------------------------- /.github/actions/build-push/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build and push images' 2 | description: 'Build and push container images' 3 | inputs: 4 | registry: 5 | description: 'Disco host' 6 | required: true 7 | name: 8 | description: 'Name of container to build' 9 | required: true 10 | version: 11 | description: 'Forta scan node version' 12 | required: true 13 | outputs: 14 | release-cid: 15 | description: 'IPFS CID of the release manifest' 16 | value: ${{ steps.build-and-push.outputs.release-cid }} 17 | image-reference: 18 | description: 'Reference of the built and pushed image' 19 | value: ${{ steps.build-and-push.outputs.image-reference }} 20 | runs: 21 | using: "composite" 22 | steps: 23 | - name: Login to Forta Disco 24 | uses: docker/login-action@v1 25 | with: 26 | registry: ${{ inputs.registry }} 27 | username: discouser 28 | password: discopass 29 | - name: Build and push container 30 | id: build-and-push 31 | shell: bash 32 | env: 33 | REGISTRY: ${{ inputs.registry }} 34 | IMAGE_NAME: ${{ inputs.name }} 35 | VERSION: ${{ inputs.version }} 36 | COMMIT_SHA: ${{ github.sha }} 37 | run: | 38 | IMAGE_REF=$(./scripts/docker-build-push.sh "$REGISTRY" "$IMAGE_NAME" "$COMMIT_SHA") 39 | echo "::set-output name=image-reference::$IMAGE_REF" 40 | ./scripts/build-manifest.sh ./scripts/manifest-template.json manifest.json "$GITHUB_SHA" "$IMAGE_REF" "$VERSION" 41 | MANIFEST_REF=$(./scripts/ipfs-add.sh "./manifest.json") 42 | echo "::set-output name=release-cid::$MANIFEST_REF" 43 | -------------------------------------------------------------------------------- /store/ens_override.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "io/ioutil" 5 | "path" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/forta-network/forta-core-go/domain/registry" 9 | "github.com/forta-network/forta-core-go/ens" 10 | "github.com/forta-network/forta-node/config" 11 | "github.com/goccy/go-json" 12 | ) 13 | 14 | type ensOverrideStore struct { 15 | contracts registry.RegistryContracts 16 | contractsMap map[string]string 17 | } 18 | 19 | func NewENSOverrideStore(cfg config.Config) (*ensOverrideStore, error) { 20 | var store ensOverrideStore 21 | b, err := ioutil.ReadFile(path.Join(cfg.FortaDir, "ens-override.json")) 22 | if err != nil { 23 | return nil, err 24 | } 25 | if err := json.Unmarshal(b, &store.contractsMap); err != nil { 26 | return nil, err 27 | } 28 | store.contracts.Dispatch = common.HexToAddress(store.contractsMap[ens.DispatchContract]) 29 | store.contracts.AgentRegistry = common.HexToAddress(store.contractsMap[ens.AgentRegistryContract]) 30 | store.contracts.ScannerRegistry = common.HexToAddress(store.contractsMap[ens.ScannerRegistryContract]) 31 | store.contracts.ScannerNodeVersion = common.HexToAddress(store.contractsMap[ens.ScannerNodeVersionContract]) 32 | store.contracts.FortaStaking = common.HexToAddress(store.contractsMap[ens.StakingContract]) 33 | return &store, nil 34 | } 35 | 36 | func (store *ensOverrideStore) Resolve(input string) (common.Address, error) { 37 | return common.HexToAddress(store.contractsMap[input]), nil 38 | } 39 | 40 | func (store *ensOverrideStore) ResolveRegistryContracts() (*registry.RegistryContracts, error) { 41 | return &store.contracts, nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/json-rpc/main.go: -------------------------------------------------------------------------------- 1 | package json_rpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/forta-network/forta-core-go/clients/health" 7 | "github.com/forta-network/forta-core-go/utils" 8 | "github.com/forta-network/forta-node/config" 9 | "github.com/forta-network/forta-node/healthutils" 10 | "github.com/forta-network/forta-node/services" 11 | jrp "github.com/forta-network/forta-node/services/json-rpc" 12 | ) 13 | 14 | func initJsonRpcProxy(ctx context.Context, cfg config.Config) (*jrp.JsonRpcProxy, error) { 15 | return jrp.NewJsonRpcProxy(ctx, cfg) 16 | } 17 | 18 | func initServices(ctx context.Context, cfg config.Config) ([]services.Service, error) { 19 | // can't dial localhost - need to dial host gateway from container 20 | cfg.Scan.JsonRpc.Url = utils.ConvertToDockerHostURL(cfg.Scan.JsonRpc.Url) 21 | cfg.JsonRpcProxy.JsonRpc.Url = utils.ConvertToDockerHostURL(cfg.JsonRpcProxy.JsonRpc.Url) 22 | 23 | proxy, err := initJsonRpcProxy(ctx, cfg) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return []services.Service{ 29 | health.NewService( 30 | ctx, "", healthutils.DefaultHealthServerErrHandler, 31 | health.CheckerFrom(summarizeReports, proxy), 32 | ), 33 | proxy, 34 | }, nil 35 | } 36 | 37 | func summarizeReports(reports health.Reports) *health.Report { 38 | summary := health.NewSummary() 39 | 40 | apiErr, ok := reports.NameContains("service.json-rpc-proxy.api") 41 | if ok && len(apiErr.Details) > 0 { 42 | summary.Addf("last time the api failed with error '%s'.", apiErr.Details) 43 | } 44 | 45 | return summary.Finish() 46 | } 47 | 48 | func Run() { 49 | services.ContainerMain("json-rpc", initServices) 50 | } 51 | -------------------------------------------------------------------------------- /tests/e2e/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 137, 4 | "homesteadBlock": 0, 5 | "eip150Block": 0, 6 | "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", 7 | "eip155Block": 0, 8 | "eip158Block": 0, 9 | "byzantiumBlock": 0, 10 | "constantinopleBlock": 0, 11 | "petersburgBlock": 0, 12 | "istanbulBlock": 0, 13 | "clique": { 14 | "period": 0, 15 | "epoch": 30000 16 | } 17 | }, 18 | "nonce": "0x0", 19 | 20 | "extraData": "0x00000000000000000000000000000000000000000000000000000000000000001111e291778ae830cfe4e34185e4e560e94047c70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 21 | "gasLimit": "100000000", 22 | "difficulty": "0x1", 23 | "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 24 | "coinbase": "0x0000000000000000000000000000000000000000", 25 | "alloc": { 26 | "1111e291778AE830cfE4e34185e4e560E94047c7": { "balance": "10000000000000000000" }, 27 | "222244861C15A8F2A05fbD15E747Ea8F20c2C0c9": { "balance": "10000000000000000000" }, 28 | "3333C25Cb71F00F113425c60E0CbF551c00cEf49": { "balance": "10000000000000000000" }, 29 | "44443b6c4899e3c11Ff666fD98B1cc9bF283174F": { "balance": "10000000000000000000" }, 30 | "55557b2a04394aBf4bb216f85628686E496C5aaF": { "balance": "10000000000000000000" }, 31 | "66664f69BCFE12A7bA2857fAbC42a02729e5c160": { "balance": "10000000000000000000" } 32 | }, 33 | "number": "0x0", 34 | "gasUsed": "0x0", 35 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 36 | "baseFeePerGas": null 37 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | containers: 2 | docker build -t forta-network/forta-node -f Dockerfile.node . 3 | docker pull nats:2.3.2 4 | 5 | containers-dev: 6 | DOCKER_BUILDKIT=1 docker build -t forta-network/forta-node -f Dockerfile.buildkit.node . 7 | docker pull nats:2.3.2 8 | 9 | main: 10 | docker build -t build-forta -f Dockerfile.cli . 11 | docker create --name build-forta build-forta 12 | docker cp build-forta:/main forta 13 | docker rm -f build-forta 14 | chmod 755 forta 15 | 16 | mocks: 17 | mockgen -source clients/interfaces.go -destination clients/mocks/mock_clients.go 18 | mockgen -source services/registry/registry.go -destination services/registry/mocks/mock_registry.go 19 | mockgen -source store/registry.go -destination store/mocks/mock_registry.go 20 | 21 | test: 22 | go test -v -count=1 ./... 23 | 24 | perf-test: 25 | go test ./... -tags=perf_test 26 | 27 | e2e-test-deps: 28 | ./tests/e2e/deps-start.sh 29 | 30 | e2e-test: 31 | rm -rf tests/e2e/.forta/coverage 32 | mkdir -p tests/e2e/.forta/coverage 33 | 34 | ./tests/e2e/build.sh 35 | 36 | cd tests/e2e && E2E_TEST=1 go test -v -count=1 . 37 | 38 | rm -rf coverage 39 | cp -r tests/e2e/.forta/coverage . 40 | cp -r tests/e2e/.forta-private/coverage/* coverage/ 41 | ./scripts/total-coverage.sh e2e 42 | 43 | run: 44 | go build -o forta . && ./forta --passphrase 123 45 | 46 | build-local: ## Build for local installation from source 47 | ./scripts/build-for-local.sh 48 | 49 | build-remote: ## Try the "remote" containers option for build 50 | ./scripts/build-for-release.sh disco-dev.forta.network 51 | 52 | .PHONY: install 53 | install: build-local ## Single install target for local installation 54 | cp forta /usr/local/bin/forta 55 | -------------------------------------------------------------------------------- /tests/e2e/deps-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./tests/e2e/deps-stop.sh 4 | 5 | export RUNNER_TRACKING_ID="" 6 | 7 | TEST_DIR=$(dirname "${BASH_SOURCE[0]}") 8 | export IPFS_PATH="$TEST_DIR/.ipfs" 9 | export REGISTRY_CONFIGURATION_PATH="$TEST_DIR/disco.config.yml" 10 | export IPFS_URL="http://localhost:5001" 11 | export DISCO_PORT="1970" 12 | 13 | ETHEREUM_DIR="$TEST_DIR/.ethereum" 14 | ETHEREUM_PASSWORD_FILE="$TEST_DIR/ethaccounts/password" 15 | ETHEREUM_KEY_FILE="$TEST_DIR/ethaccounts/gethkeyfile" 16 | ETHEREUM_GENESIS_FILE="$TEST_DIR/genesis.json" 17 | ETHEREUM_NODE_ADDRESS="0x1111e291778AE830cfE4e34185e4e560E94047c7" 18 | 19 | # ensure that the binaries are installed 20 | set -e 21 | which geth docker ipfs disco 22 | set +e 23 | 24 | 25 | # ignore error from 'ipfs init' here since it might be failing due to reusing ipfs dir from previous run. 26 | # this is useful for making container-related steps faster in local development. 27 | ipfs init 28 | ipfs daemon --routing none > /dev/null 2>&1 & 29 | 30 | disco > /dev/null 2>&1 & 31 | 32 | rm -rf "$ETHEREUM_DIR" 33 | geth account import --datadir "$ETHEREUM_DIR" --password "$ETHEREUM_PASSWORD_FILE" "$ETHEREUM_KEY_FILE" 34 | geth init --datadir "$ETHEREUM_DIR" "$ETHEREUM_GENESIS_FILE" 35 | # rpc.gascap=0 means infinite 36 | geth \ 37 | --nodiscover \ 38 | --rpc.allow-unprotected-txs \ 39 | --rpc.gascap 0 \ 40 | --networkid 137 \ 41 | --datadir "$ETHEREUM_DIR" \ 42 | --allow-insecure-unlock \ 43 | --unlock "$ETHEREUM_NODE_ADDRESS" \ 44 | --password "$ETHEREUM_PASSWORD_FILE" \ 45 | --mine \ 46 | --http \ 47 | --http.vhosts '*' \ 48 | --http.port 8545 \ 49 | --http.addr '0.0.0.0' \ 50 | --http.corsdomain '*' \ 51 | --http.api personal,eth,net,web3,txpool,miner \ 52 | > /dev/null 2>&1 & 53 | -------------------------------------------------------------------------------- /services/json-rpc/rate_limiter.go: -------------------------------------------------------------------------------- 1 | package json_rpc 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "golang.org/x/time/rate" 9 | ) 10 | 11 | // RateLimiter rate limits requests. 12 | type RateLimiter struct { 13 | rate float64 14 | burst int 15 | clientLimiters map[string]*clientLimiter 16 | mu sync.Mutex 17 | } 18 | 19 | type clientLimiter struct { 20 | lastReservation time.Time 21 | *rate.Limiter 22 | } 23 | 24 | // NewRateLimiter creates a new rate limiter. 25 | func NewRateLimiter(rateN float64, burst int) *RateLimiter { 26 | if rateN <= 0 { 27 | log.Panic("non-positive rate limiter arg") 28 | } 29 | rl := &RateLimiter{ 30 | rate: rateN, 31 | burst: burst, 32 | clientLimiters: make(map[string]*clientLimiter), 33 | } 34 | go rl.autoCleanup() 35 | return rl 36 | } 37 | 38 | // ExceedsLimit tries adding a request to the limiting channel and returns boolean to signal 39 | // if we hit the rate limit. 40 | func (rl *RateLimiter) ExceedsLimit(clientID string) bool { 41 | rl.mu.Lock() 42 | defer rl.mu.Unlock() 43 | limiter := rl.clientLimiters[clientID] 44 | if limiter == nil { 45 | limiter = &clientLimiter{Limiter: rate.NewLimiter(rate.Limit(rl.rate), rl.burst)} 46 | rl.clientLimiters[clientID] = limiter 47 | } 48 | limiter.lastReservation = time.Now() 49 | return !limiter.Allow() 50 | } 51 | 52 | // deallocate inactive limiters 53 | func (rl *RateLimiter) autoCleanup() { 54 | ticker := time.NewTicker(time.Hour) 55 | for range ticker.C { 56 | rl.mu.Lock() 57 | for clientID, limiter := range rl.clientLimiters { 58 | if time.Since(limiter.lastReservation) > time.Minute*10 { 59 | delete(rl.clientLimiters, clientID) 60 | } 61 | } 62 | rl.mu.Unlock() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/forta-network/forta-node 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 7 | github.com/Microsoft/go-winio v0.5.0 // indirect 8 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 9 | github.com/creasty/defaults v1.5.2 10 | github.com/docker/distribution v2.7.1+incompatible // indirect 11 | github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf 12 | github.com/docker/go-connections v0.4.0 13 | github.com/ethereum/go-ethereum v1.10.16 14 | github.com/fatih/color v1.12.0 15 | github.com/forta-network/forta-core-go v0.0.0-20220510203742-37192760de6a 16 | github.com/go-playground/validator/v10 v10.9.0 17 | github.com/goccy/go-json v0.9.4 18 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 19 | github.com/golang/mock v1.6.0 20 | github.com/golang/protobuf v1.5.2 21 | github.com/google/uuid v1.3.0 22 | github.com/gorilla/mux v1.8.0 23 | github.com/ipfs/go-cid v0.1.0 24 | github.com/ipfs/go-ipfs-api v0.3.0 25 | github.com/multiformats/go-multiaddr v0.3.2 // indirect 26 | github.com/nats-io/nats-server/v2 v2.3.2 // indirect 27 | github.com/nats-io/nats.go v1.11.1-0.20210623165838-4b75fc59ae30 28 | github.com/opencontainers/go-digest v1.0.0 // indirect 29 | github.com/opencontainers/image-spec v1.0.1 // indirect 30 | github.com/rs/cors v1.7.0 31 | github.com/shopspring/decimal v1.2.0 32 | github.com/sirupsen/logrus v1.8.1 33 | github.com/spf13/cobra v1.2.1 34 | github.com/spf13/viper v1.8.1 35 | github.com/stretchr/testify v1.7.0 36 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 37 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 38 | google.golang.org/grpc v1.44.0 39 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b 40 | ) 41 | -------------------------------------------------------------------------------- /cmd/publisher/main.go: -------------------------------------------------------------------------------- 1 | package publisher 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/forta-network/forta-core-go/clients/health" 7 | "github.com/forta-network/forta-core-go/utils" 8 | "github.com/forta-network/forta-node/healthutils" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/forta-network/forta-node/config" 13 | "github.com/forta-network/forta-node/services" 14 | "github.com/forta-network/forta-node/services/publisher" 15 | ) 16 | 17 | func initServices(ctx context.Context, cfg config.Config) ([]services.Service, error) { 18 | cfg.Publish.APIURL = utils.ConvertToDockerHostURL(cfg.Publish.APIURL) 19 | cfg.Publish.IPFS.APIURL = utils.ConvertToDockerHostURL(cfg.Publish.IPFS.APIURL) 20 | cfg.Publish.IPFS.GatewayURL = utils.ConvertToDockerHostURL(cfg.Publish.IPFS.GatewayURL) 21 | cfg.PrivateModeConfig.WebhookURL = utils.ConvertToDockerHostURL(cfg.PrivateModeConfig.WebhookURL) 22 | 23 | p, err := publisher.NewPublisher(ctx, cfg) 24 | if err != nil { 25 | log.Errorf("Error while initializing Listener: %s", err.Error()) 26 | return nil, err 27 | } 28 | 29 | return []services.Service{ 30 | health.NewService( 31 | ctx, "", healthutils.DefaultHealthServerErrHandler, 32 | health.CheckerFrom(summarizeReports, p), 33 | ), 34 | p, 35 | }, nil 36 | } 37 | 38 | func summarizeReports(reports health.Reports) *health.Report { 39 | summary := health.NewSummary() 40 | 41 | batchPublishErr, ok := reports.NameContains("publisher.event.batch-publish.error") 42 | if ok && len(batchPublishErr.Details) > 0 { 43 | summary.Addf("failed to publish the last batch with error '%s'", batchPublishErr.Details) 44 | summary.Status(health.StatusFailing) 45 | } 46 | 47 | return summary.Finish() 48 | } 49 | 50 | func Run() { 51 | services.ContainerMain("publisher", initServices) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/node/nodecmd/nodecmd.go: -------------------------------------------------------------------------------- 1 | package nodecmd 2 | 3 | import ( 4 | json_rpc "github.com/forta-network/forta-node/cmd/json-rpc" 5 | "github.com/forta-network/forta-node/cmd/publisher" 6 | "github.com/forta-network/forta-node/cmd/scanner" 7 | "github.com/forta-network/forta-node/cmd/supervisor" 8 | "github.com/forta-network/forta-node/cmd/updater" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | cmdFortaNode = &cobra.Command{ 14 | Use: "forta-node", 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | return cmd.Help() 17 | }, 18 | SilenceUsage: true, 19 | } 20 | 21 | cmdUpdater = &cobra.Command{ 22 | Use: "updater", 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | updater.Run() 25 | return nil 26 | }, 27 | } 28 | 29 | cmdSupervisor = &cobra.Command{ 30 | Use: "supervisor", 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | supervisor.Run() 33 | return nil 34 | }, 35 | } 36 | 37 | cmdScanner = &cobra.Command{ 38 | Use: "scanner", 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | scanner.Run() 41 | return nil 42 | }, 43 | } 44 | 45 | cmdPublisher = &cobra.Command{ 46 | Use: "publisher", 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | publisher.Run() 49 | return nil 50 | }, 51 | } 52 | 53 | cmdJsonRpc = &cobra.Command{ 54 | Use: "json-rpc", 55 | RunE: func(cmd *cobra.Command, args []string) error { 56 | json_rpc.Run() 57 | return nil 58 | }, 59 | } 60 | ) 61 | 62 | func init() { 63 | cmdFortaNode.AddCommand(cmdUpdater) 64 | cmdFortaNode.AddCommand(cmdSupervisor) 65 | cmdFortaNode.AddCommand(cmdScanner) 66 | cmdFortaNode.AddCommand(cmdPublisher) 67 | cmdFortaNode.AddCommand(cmdJsonRpc) 68 | } 69 | 70 | func Run() error { 71 | return cmdFortaNode.Execute() 72 | } 73 | -------------------------------------------------------------------------------- /tests/e2e/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | set -o pipefail 5 | 6 | # SKIP_CONTAINER_BUILD="$1" 7 | # if [ "$SKIP_CONTAINER_BUILD" == "1" ]; then 8 | # exit 0 9 | # fi 10 | 11 | TEST_DIR=$(dirname "${BASH_SOURCE[0]}") 12 | SCRIPTS_DIR="$TEST_DIR/../../scripts" 13 | 14 | REGISTRY="localhost:1970" 15 | NODE_IMAGE_FULL_NAME="$REGISTRY/forta-node" 16 | AGENT_IMAGE_SHORT_NAME="forta-e2e-test-agent" 17 | AGENT_IMAGE_FULL_NAME="$REGISTRY/$AGENT_IMAGE_SHORT_NAME" 18 | 19 | # build a node image that creates coverage output 20 | DOCKER_BUILDKIT=1 docker build -t "$NODE_IMAGE_FULL_NAME" -f "$TEST_DIR/cmd/node/Dockerfile" . 21 | # build test agent image 22 | DOCKER_BUILDKIT=1 docker build -t "$AGENT_IMAGE_FULL_NAME" -f "$TEST_DIR/agents/txdetectoragent/Dockerfile" . 23 | docker tag "$AGENT_IMAGE_FULL_NAME" "$AGENT_IMAGE_SHORT_NAME" 24 | 25 | NODE_IMAGE_REF=$("$SCRIPTS_DIR/docker-push.sh" "$REGISTRY" "$NODE_IMAGE_FULL_NAME") 26 | AGENT_IMAGE_REF=$("$SCRIPTS_DIR/docker-push.sh" "$REGISTRY" "$AGENT_IMAGE_FULL_NAME") 27 | 28 | IMAGE_REFS_DIR="$TEST_DIR/.imagerefs" 29 | mkdir -p "$IMAGE_REFS_DIR" 30 | echo "$NODE_IMAGE_REF" > "$IMAGE_REFS_DIR/node" 31 | echo "$AGENT_IMAGE_REF" > "$IMAGE_REFS_DIR/agent" 32 | 33 | # build the test cli/runner binary 34 | MODULE_NAME=$(grep 'module' "$TEST_DIR/../../go.mod" | cut -c8-) # Get the module name from go.mod 35 | IMPORT="$MODULE_NAME/config" 36 | GO_PACKAGES=$(go list ./... | grep -v tests | tr "\n" ",") 37 | GO_PACKAGES=${GO_PACKAGES%?} # cut trailing comma 38 | 39 | go test -c -o forta-test -race -covermode=atomic -coverpkg \ 40 | "$GO_PACKAGES" \ 41 | -ldflags="-X '$IMPORT.DockerSupervisorImage=$NODE_IMAGE_REF' -X '$IMPORT.DockerUpdaterImage=$NODE_IMAGE_REF' -X '$IMPORT.UseDockerImages=remote' -X '$IMPORT.Version=0.0.1-test'" \ 42 | "$TEST_DIR/cmd/cli" 43 | mv -f forta-test "$TEST_DIR/" 44 | -------------------------------------------------------------------------------- /services/runner/health.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/forta-network/forta-core-go/clients/health" 8 | "github.com/forta-network/forta-node/config" 9 | ) 10 | 11 | func (runner *Runner) checkHealth() (allReports health.Reports) { 12 | containers, err := runner.globalClient.GetFortaServiceContainers(runner.ctx) 13 | if err != nil { 14 | return health.Reports{ 15 | { 16 | Name: "docker", 17 | Status: health.StatusDown, 18 | Details: err.Error(), 19 | }, 20 | } 21 | } 22 | 23 | for _, container := range containers { 24 | name := fmt.Sprintf("forta.container.%s", container.Names[0][1:]) 25 | 26 | if container.State != "running" { 27 | allReports = append(allReports, &health.Report{ 28 | Name: name, 29 | Status: health.StatusDown, 30 | Details: container.State, 31 | }) 32 | continue 33 | } 34 | 35 | allReports = append(allReports, &health.Report{ 36 | Name: name, 37 | Status: health.StatusOK, 38 | Details: container.State, 39 | }) 40 | 41 | // no further checks if nats 42 | if container.Names[0][1:] == config.DockerNatsContainerName { 43 | continue 44 | } 45 | 46 | var gotReports bool 47 | for _, port := range container.Ports { 48 | if strconv.Itoa(int(port.PrivatePort)) == config.DefaultHealthPort { 49 | reports := runner.healthClient.CheckHealth(name, strconv.Itoa(int(port.PublicPort))) 50 | for _, report := range reports { 51 | report.Name = fmt.Sprintf("%s.%s", name, report.Name) 52 | } 53 | reports.ObfuscateDetails() 54 | allReports = append(allReports, reports...) 55 | gotReports = true 56 | break 57 | } 58 | } 59 | if gotReports { 60 | continue 61 | } 62 | allReports = append(allReports, &health.Report{ 63 | Name: name, 64 | Status: health.StatusInfo, 65 | Details: "no source found", 66 | }) 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /clients/alertapi/api.go: -------------------------------------------------------------------------------- 1 | package alertapi 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/forta-network/forta-core-go/domain" 11 | "github.com/goccy/go-json" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type client struct { 16 | apiUrl string 17 | } 18 | 19 | func (c *client) post(path string, body interface{}, headers map[string]string, target interface{}) error { 20 | jsonVal, err := json.Marshal(body) 21 | if err != nil { 22 | return err 23 | } 24 | req, err := http.NewRequest("POST", fmt.Sprintf("%s%s", c.apiUrl, path), bytes.NewBuffer(jsonVal)) 25 | if err != nil { 26 | return err 27 | } 28 | for n, v := range headers { 29 | req.Header[n] = []string{v} 30 | } 31 | hClient := &http.Client{ 32 | Timeout: 30 * time.Second, 33 | } 34 | resp, err := hClient.Do(req) 35 | if err != nil { 36 | return err 37 | } 38 | b, _ := io.ReadAll(resp.Body) 39 | defer resp.Body.Close() 40 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 41 | log.WithFields(log.Fields{ 42 | "apiUrl": c.apiUrl, 43 | "path": path, 44 | "body": string(jsonVal), 45 | "response": string(b), 46 | "status": resp.StatusCode, 47 | }).Error("alert api error") 48 | return fmt.Errorf("%d error: %s", resp.StatusCode, string(b)) 49 | } 50 | return json.Unmarshal(b, target) 51 | } 52 | 53 | func (c *client) PostBatch(batch *domain.AlertBatchRequest, token string) (*domain.AlertBatchResponse, error) { 54 | path := fmt.Sprintf("/batch/%s", batch.Ref) 55 | headers := map[string]string{ 56 | "content-type": "application/json", 57 | "Authorization": fmt.Sprintf("Bearer %s", token), 58 | } 59 | var resp domain.AlertBatchResponse 60 | if err := c.post(path, batch, headers, &resp); err != nil { 61 | return nil, err 62 | } 63 | return &resp, nil 64 | } 65 | 66 | func NewClient(apiUrl string) *client { 67 | return &client{apiUrl: apiUrl} 68 | } 69 | -------------------------------------------------------------------------------- /.github/actions/propose/index.js: -------------------------------------------------------------------------------- 1 | const { AdminClient } = require('defender-admin-client'); 2 | const core = require('@actions/core'); 3 | 4 | async function proposeUpgrade(apiKey, apiSecret, versionContract, network, multisig, version, cid) { 5 | const client = new AdminClient({apiKey, apiSecret}); 6 | 7 | const params = { 8 | contract: { address: versionContract, network: network }, 9 | title: `Forta Node Release ${version}`, 10 | description: `Release forta-node ${version} (${cid})`, 11 | type: 'custom', 12 | functionInterface: { 13 | "inputs": [ 14 | { 15 | "internalType": "string", 16 | "name": "version", 17 | "type": "string" 18 | } 19 | ], 20 | "name": "setScannerNodeVersion", 21 | "outputs": [], 22 | "stateMutability": "nonpayable", 23 | "type": "function" 24 | }, 25 | functionInputs: [cid], 26 | via: `${multisig}`, 27 | viaType: 'Gnosis Safe', 28 | } 29 | 30 | const result = await client.createProposal(params); 31 | return result.url 32 | } 33 | 34 | async function main(){ 35 | try { 36 | const proposalUrl = await proposeUpgrade( 37 | core.getInput('api-key'), 38 | core.getInput('api-secret'), 39 | core.getInput('scanner-version-contract'), 40 | core.getInput('network'), 41 | core.getInput('multisig'), 42 | core.getInput('version'), 43 | core.getInput('release-cid')) 44 | 45 | console.log(`proposal created: ${proposalUrl}`); 46 | core.setOutput("proposal-url", proposalUrl); 47 | } catch (error) { 48 | core.setFailed(error.message); 49 | } 50 | } 51 | 52 | main().then((url) => { 53 | console.log(url) 54 | }).catch((e)=>{ 55 | console.log(e) 56 | }) -------------------------------------------------------------------------------- /cmd/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/forta-network/forta-node/clients" 8 | "github.com/forta-network/forta-node/config" 9 | "github.com/forta-network/forta-node/services" 10 | "github.com/forta-network/forta-node/services/runner" 11 | "github.com/forta-network/forta-node/store" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func initServices(ctx context.Context, cfg config.Config) ([]services.Service, error) { 16 | shouldDisableAutoUpdate := cfg.AutoUpdate.Disable || cfg.PrivateModeConfig.Enable 17 | imgStore, err := store.NewFortaImageStore(ctx, config.DefaultContainerPort, !shouldDisableAutoUpdate) 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to create the image store: %v", err) 20 | } 21 | dockerClient, err := clients.NewDockerClient("runner") 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to create the docker client: %v", err) 24 | } 25 | globalDockerClient, err := clients.NewDockerClient("") 26 | if err != nil { 27 | return nil, fmt.Errorf("failed to create the docker client: %v", err) 28 | } 29 | 30 | if cfg.Development { 31 | log.Warn("running in development mode") 32 | } 33 | 34 | return []services.Service{ 35 | runner.NewRunner(ctx, cfg, imgStore, dockerClient, globalDockerClient), 36 | }, nil 37 | } 38 | 39 | // Run runs the runner. 40 | func Run(cfg config.Config) { 41 | ctx, cancel := services.InitMainContext() 42 | defer cancel() 43 | 44 | logger := log.WithField("process", "runner") 45 | logger.Info("starting") 46 | defer logger.Info("exiting") 47 | 48 | serviceList, err := initServices(ctx, cfg) 49 | if err != nil { 50 | logger.WithError(err).Error("could not initialize services") 51 | return 52 | } 53 | 54 | if err := services.StartServices(ctx, cancel, log.NewEntry(log.StandardLogger()), serviceList); err != nil { 55 | logger.WithError(err).Error("error running services") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /testutils/alertserver/alertserver.go: -------------------------------------------------------------------------------- 1 | package alertserver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "github.com/forta-network/forta-core-go/utils/apiutils" 12 | "github.com/gorilla/mux" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // AlertServer is a fake alert server. 17 | type AlertServer struct { 18 | ctx context.Context 19 | cancel func() 20 | port int 21 | router *mux.Router 22 | 23 | knownBatches map[string][]byte 24 | mu sync.RWMutex 25 | } 26 | 27 | // New creates a new alert server. 28 | func New(ctx context.Context, port int) *AlertServer { 29 | alertServer := &AlertServer{ 30 | port: port, 31 | knownBatches: make(map[string][]byte), 32 | } 33 | alertServer.ctx, alertServer.cancel = context.WithCancel(ctx) 34 | 35 | r := mux.NewRouter() 36 | r.HandleFunc("/batch/{ref}", alertServer.AddAlert).Methods("POST") 37 | alertServer.router = r 38 | 39 | return alertServer 40 | } 41 | 42 | // Start starts the server. 43 | func (as *AlertServer) Start() { 44 | apiutils.ListenAndServe(as.ctx, &http.Server{ 45 | Handler: as.router, 46 | Addr: fmt.Sprintf("0.0.0.0:%d", as.port), 47 | WriteTimeout: 15 * time.Second, 48 | ReadTimeout: 15 * time.Second, 49 | }, "started alert server") 50 | } 51 | 52 | // Close closes the server. 53 | func (as *AlertServer) Close() error { 54 | as.cancel() 55 | return nil 56 | } 57 | 58 | func (as *AlertServer) GetAlert(ref string) ([]byte, bool) { 59 | as.mu.RLock() 60 | defer as.mu.RUnlock() 61 | b, ok := as.knownBatches[ref] 62 | return b, ok 63 | } 64 | 65 | func (as *AlertServer) AddAlert(w http.ResponseWriter, r *http.Request) { 66 | as.mu.Lock() 67 | defer as.mu.Unlock() 68 | 69 | vars := mux.Vars(r) 70 | ref := vars["ref"] 71 | b, _ := ioutil.ReadAll(r.Body) 72 | logrus.WithField("ref", ref).Info("received alert: ", string(b)) 73 | as.knownBatches[ref] = b 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /cmd/cmd_run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/forta-network/forta-core-go/registry" 9 | "github.com/forta-network/forta-core-go/security" 10 | "github.com/forta-network/forta-node/cmd/runner" 11 | "github.com/forta-network/forta-node/store" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // errors 16 | var ( 17 | ErrCannotRunScanner = errors.New("cannot run scanner") 18 | ) 19 | 20 | func handleFortaRun(cmd *cobra.Command, args []string) error { 21 | if err := checkScannerState(); err != nil { 22 | return err 23 | } 24 | runner.Run(cfg) 25 | return nil 26 | } 27 | 28 | func checkScannerState() error { 29 | if parsedArgs.NoCheck { 30 | return nil 31 | } 32 | 33 | scannerKey, err := security.LoadKeyWithPassphrase(cfg.KeyDirPath, cfg.Passphrase) 34 | if err != nil { 35 | return fmt.Errorf("failed to load scanner key: %v", err) 36 | } 37 | scannerAddressStr := scannerKey.Address.Hex() 38 | 39 | registry, err := store.GetRegistryClient(context.Background(), cfg, registry.ClientConfig{ 40 | JsonRpcUrl: cfg.Registry.JsonRpc.Url, 41 | ENSAddress: cfg.ENSConfig.ContractAddress, 42 | Name: "registry-client", 43 | }) 44 | if err != nil { 45 | return fmt.Errorf("failed to create registry client: %v", err) 46 | } 47 | scanner, err := registry.GetScanner(scannerAddressStr) 48 | if err != nil { 49 | return fmt.Errorf("failed to check scanner state: %v", err) 50 | } 51 | 52 | // treat reverts the same as non-registered 53 | if scanner == nil { 54 | yellowBold("Scanner not registered - please make sure you register with 'forta register' first.\n") 55 | toStderr("You can disable this behaviour with --no-check flag.\n") 56 | return ErrCannotRunScanner 57 | } 58 | if !scanner.Enabled { 59 | yellowBold("Warning! Your scan node is either disabled or does not meet with the minimum staking requirement. It will not receive any detection bots yet.\n") 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /tests/e2e/ethaccounts/ethaccounts.go: -------------------------------------------------------------------------------- 1 | package ethaccounts 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/ethereum/go-ethereum/crypto" 6 | ) 7 | 8 | var ( 9 | DefaultPassword = "0" 10 | 11 | GethNodeKey, _ = crypto.HexToECDSA("0da4d32840b0ef3e30c82d9d4772c8e1bbcd1ac6417b46d958fb5c7db0be99c1") 12 | GethNodeAddress = common.HexToAddress("0x1111e291778AE830cfE4e34185e4e560E94047c7") 13 | 14 | ScannerKey, _ = crypto.HexToECDSA("412ae1bd0021a1489a8824e11edc4a017b4b0e12c39be936089b350ea55e997d") 15 | ScannerAddress = common.HexToAddress("0x222244861C15A8F2A05fbD15E747Ea8F20c2C0c9") 16 | 17 | DeployerKey, _ = crypto.HexToECDSA("02b432bb5b53daf8edf652c028b2eef9a383d688806b6a0bc2b253b3392195b2") 18 | DeployerAddress = common.HexToAddress("0x3333C25Cb71F00F113425c60E0CbF551c00cEf49") 19 | 20 | ProxyAdminKey, _ = crypto.HexToECDSA("065d74b69b496014c8d23a7eb60edf1da2928f17a868962d1c3f70b6983bde2d") 21 | ProxyAdminAddress = common.HexToAddress("0x44443b6c4899e3c11Ff666fD98B1cc9bF283174F") 22 | 23 | AccessAdminKey, _ = crypto.HexToECDSA("25582762d32c064e5c7d86c62e3cebe612a6f7bfac45b6403d395d99f90037ed") 24 | AccessAdminAddress = common.HexToAddress("0x55557b2a04394aBf4bb216f85628686E496C5aaF") 25 | 26 | ExploiterKey, _ = crypto.HexToECDSA("9983c18517758908acd2fa32909dd1490949eee0fa63501b0adeb02802481773") 27 | ExploiterAddress = common.HexToAddress("0x66664f69BCFE12A7bA2857fAbC42a02729e5c160") 28 | 29 | MiscKey, _ = crypto.HexToECDSA("4c2c30fe62230a6e4550b7f56e4e877c9fcb5aa9f468f3c0e2f94b24785c019a") 30 | MiscAddress = common.HexToAddress("0x1337B4cBAe461949A00854EECd27Bc331CcaD2f1") 31 | 32 | ForwarderKey, _ = crypto.HexToECDSA("ec80d29573324a3ba1b4e9e9f8376282816bfb9876100af955bc098bf6c986c6") 33 | ForwarderAddress = common.HexToAddress("0x2337608875c0D3eFDf5232aFf3343a43C73a900F") 34 | 35 | ScannerOwnerKey, _ = crypto.HexToECDSA("51416b7884fb4ab925f1e259116ef4a7663e44e9b4705d26eed9267cbbbe38c2") 36 | ScannerOwnerAddress = common.HexToAddress("0x3337CCbdcbb3edBcc5f438DEd0f91948e7Df6BE5") 37 | ) 38 | -------------------------------------------------------------------------------- /cmd/cmd_account.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/ethereum/go-ethereum/accounts/keystore" 11 | "github.com/ethereum/go-ethereum/crypto" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func handleFortaAccountAddress(cmd *cobra.Command, args []string) error { 16 | ks := keystore.NewKeyStore(cfg.KeyDirPath, keystore.StandardScryptN, keystore.StandardScryptP) 17 | accounts := ks.Accounts() 18 | if len(accounts) > 1 { 19 | redBold("You have multiple accounts. Please import your scanner account again with 'forta account import'.") 20 | cmd.Println("Your current account addresses:") 21 | for _, account := range accounts { 22 | cmd.Println(account.Address.Hex()) 23 | } 24 | return errors.New("multiple accounts") 25 | } 26 | 27 | if len(accounts) == 0 { 28 | redBold("You have no accounts. Please import your scanner account with 'forta account import'.") 29 | return errors.New("no accounts") 30 | } 31 | 32 | cmd.Println(accounts[0].Address.Hex()) 33 | return nil 34 | } 35 | 36 | func handleFortaAccountImport(cmd *cobra.Command, args []string) error { 37 | path, err := cmd.Flags().GetString("file") 38 | if err != nil { 39 | return err 40 | } 41 | b, err := ioutil.ReadFile(path) 42 | if err != nil { 43 | return fmt.Errorf("failed to read the private key: %v", err) 44 | } 45 | hexKey := strings.TrimSpace(string(b)) 46 | 47 | if len(cfg.Passphrase) == 0 { 48 | redBold("Your passphrase is not set. Please set it with FORTA_PASSPHRASE environment variable or provide it with the --passphrase flag.\n") 49 | return errors.New("empty passhphrase") 50 | } 51 | 52 | os.RemoveAll(cfg.KeyDirPath) 53 | ks := keystore.NewKeyStore(cfg.KeyDirPath, keystore.StandardScryptN, keystore.StandardScryptP) 54 | privateKey, err := crypto.HexToECDSA(hexKey) 55 | if err != nil { 56 | return fmt.Errorf("could not parse the private key hex: %v", err) 57 | } 58 | account, err := ks.ImportECDSA(privateKey, cfg.Passphrase) 59 | if err != nil { 60 | return fmt.Errorf("failed to import: %v", err) 61 | } 62 | cmd.Println(account.Address.Hex()) 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /config/chains.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const defaultBlockOffset = 0 4 | 5 | var defaultRateLimiting = &RateLimitConfig{ 6 | Rate: 50, // 0.347, // 30k/day 7 | Burst: 50, // 100, 8 | } 9 | 10 | // ChainSettings contains chain-specific settings. 11 | type ChainSettings struct { 12 | Name string 13 | ChainID int 14 | Offset int 15 | JsonRpcRateLimiting *RateLimitConfig 16 | } 17 | 18 | var allChainSettings = []ChainSettings{ 19 | { 20 | Name: "Ethereum Mainnet", 21 | ChainID: 1, 22 | Offset: defaultBlockOffset, 23 | JsonRpcRateLimiting: defaultRateLimiting, 24 | }, 25 | { 26 | Name: "BSC", 27 | ChainID: 56, 28 | Offset: defaultBlockOffset, 29 | JsonRpcRateLimiting: defaultRateLimiting, 30 | }, 31 | { 32 | Name: "Polygon", 33 | ChainID: 137, 34 | Offset: defaultBlockOffset, 35 | JsonRpcRateLimiting: defaultRateLimiting, 36 | }, 37 | { 38 | Name: "Avalanche", 39 | ChainID: 43114, 40 | Offset: defaultBlockOffset, 41 | JsonRpcRateLimiting: defaultRateLimiting, 42 | }, 43 | { 44 | Name: "Arbitrum", 45 | ChainID: 42161, 46 | Offset: defaultBlockOffset, 47 | JsonRpcRateLimiting: defaultRateLimiting, 48 | }, 49 | { 50 | Name: "Optimism", 51 | ChainID: 10, 52 | Offset: defaultBlockOffset, 53 | JsonRpcRateLimiting: defaultRateLimiting, 54 | }, 55 | } 56 | 57 | // GetChainSettings returns the settings for the chain. 58 | func GetChainSettings(chainID int) *ChainSettings { 59 | for _, settings := range allChainSettings { 60 | if settings.ChainID == chainID { 61 | return &settings 62 | } 63 | } 64 | return &ChainSettings{ 65 | Name: "Unknown chain", 66 | ChainID: chainID, 67 | Offset: defaultBlockOffset, 68 | JsonRpcRateLimiting: defaultRateLimiting, 69 | } 70 | } 71 | 72 | // GetBlockOffset returns the block offset for a chain. 73 | func GetBlockOffset(chainID int) int { 74 | return GetChainSettings(chainID).Offset 75 | } 76 | -------------------------------------------------------------------------------- /services/publisher/metrics_test.go: -------------------------------------------------------------------------------- 1 | package publisher_test 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/forta-network/forta-core-go/protocol" 7 | "github.com/forta-network/forta-core-go/utils" 8 | "github.com/forta-network/forta-node/services/publisher" 9 | "github.com/stretchr/testify/assert" 10 | 11 | "testing" 12 | ) 13 | 14 | var ( 15 | testNow = time.Now() 16 | ) 17 | 18 | func init() { 19 | publisher.DefaultBucketInterval = time.Millisecond 20 | } 21 | 22 | type MetricsMathTest struct { 23 | metrics []float64 24 | expected *protocol.MetricSummary 25 | } 26 | 27 | func TestAgentMetricsAggregator_math(t *testing.T) { 28 | 29 | tests := []*MetricsMathTest{ 30 | { 31 | metrics: []float64{1, 2, 3, 4, 5}, 32 | expected: &protocol.MetricSummary{ 33 | Name: "test.metric", 34 | Count: 5, 35 | Max: 5, 36 | Average: 3, 37 | Sum: 15, 38 | P95: 4, 39 | }, 40 | }, 41 | { 42 | metrics: []float64{1, 10, 34}, 43 | expected: &protocol.MetricSummary{ 44 | Name: "test.metric", 45 | Count: 3, 46 | Max: 34, 47 | Average: 15, 48 | Sum: 45, 49 | P95: 10, 50 | }, 51 | }, 52 | { 53 | metrics: []float64{45}, 54 | expected: &protocol.MetricSummary{ 55 | Name: "test.metric", 56 | Count: 1, 57 | Max: 45, 58 | Average: 45, 59 | Sum: 45, 60 | P95: 45, 61 | }, 62 | }, 63 | } 64 | 65 | for _, test := range tests { 66 | testTime1 := testNow 67 | 68 | var metrics []*protocol.AgentMetric 69 | for _, val := range test.metrics { 70 | metrics = append(metrics, &protocol.AgentMetric{ 71 | AgentId: "agentID", 72 | Timestamp: utils.FormatTime(testTime1), 73 | Name: "test.metric", 74 | Value: val, 75 | }) 76 | } 77 | 78 | aggregator := publisher.NewMetricsAggregator() 79 | err := aggregator.AddAgentMetrics(&protocol.AgentMetricList{Metrics: metrics}) 80 | assert.NoError(t, err) 81 | time.Sleep(publisher.DefaultBucketInterval * 2) 82 | 83 | res := aggregator.TryFlush() 84 | 85 | assert.Len(t, res, 1) 86 | assert.Len(t, res[0].Metrics, 1) 87 | assert.Equal(t, res[0].Metrics[0], test.expected) 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/forta-network/forta-node/actions/workflows/release-codedeploy-dev.yml/badge.svg) 2 | 3 | # forta-node 4 | 5 | Forta node CLI is a Docker container supervisor that runs and manages multiple services and detection bots (agents) to scan a blockchain network and produce alerts. 6 | 7 | # Running a Node 8 | 9 | For information about running a node, see the [Scan Node Quickstart Documentation](https://docs.forta.network/en/latest/scanner-quickstart/) 10 | 11 | # Scan Node Development 12 | 13 | ## Dependencies 14 | 15 | 1. [Install Docker](https://docs.docker.com/get-docker/) and start Docker service 16 | 2. [Install Go](https://golang.org/doc/install) 17 | 18 | ## Dependencies for local development 19 | 20 | ### Tools 21 | 22 | Install [Protobuf Compiler](https://grpc.io/docs/protoc-installation/). 23 | 24 | ### Go libraries 25 | 26 | ```shell 27 | $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc 28 | ``` 29 | ```shell 30 | $ go install github.com/golang/mock/mockgen@v1.5.0 31 | ``` 32 | 33 | ## Build and install 34 | 35 | ### Full build & install using local version of Go 36 | 37 | ```shell 38 | $ make install 39 | ``` 40 | 41 | For a faster iteration in local development, it is sufficient to build the common service container only if it has changed. The CLI requires `forta-network/forta-node:latest` containers to be available by default and uses the local ones if other Docker image references were not specified at the compile time. 42 | 43 | ### CLI-only build using the local version of Go 44 | 45 | ```shell 46 | $ go build -o forta . 47 | ``` 48 | 49 | ### CLI-only build using a specific version of Go 50 | 51 | Edit Go image version at build stage inside `Dockerfile.cli` and then: 52 | 53 | ```shell 54 | $ make main 55 | ``` 56 | 57 | ## Run the node 58 | 59 | ### Run 60 | 61 | ```shell 62 | $ forta init # if you haven't initialized and configured your Forta directory yet 63 | $ forta run 64 | ``` 65 | 66 | ### View logs 67 | 68 | CLI logs are made available via stdout. Logs for the rest of the node services and agents can be inspected by doing: 69 | 70 | ```shell 71 | $ docker ps # see the running containers from here 72 | $ docker logs -f 73 | ``` 74 | 75 | ### Stop 76 | 77 | ``` 78 | CTRL-C 79 | ``` 80 | -------------------------------------------------------------------------------- /scripts/inject-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /home/forta/.forta/ 4 | chown -R forta.forta /home/forta 5 | configPath=/home/forta/.forta/config.yml 6 | # this looks up the alchemy credentials and injects them into the config yaml 7 | instanceId=$(curl -s http://instance-data/latest/meta-data/instance-id) 8 | region=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region) 9 | envPrefix=$(aws ec2 describe-tags --region $region --filters "Name=resource-id,Values=$instanceId" "Name=key,Values=Environment" |jq -r '.Tags[0].Value') 10 | 11 | secretId="${envPrefix}_alchemy_api_url" 12 | apiUrlUnsafe=$(aws secretsmanager --region $region get-secret-value --secret-id $secretId |jq -r '.SecretString') 13 | apiUrl=$(printf '%s\n' "$apiUrlUnsafe" | sed -e 's/[]\/$*.^[]/\\&/g'); 14 | 15 | registryApiUrlId="${envPrefix}_agent_registry_api_url" 16 | registryApiUrlUnsafe=$(aws secretsmanager --region $region get-secret-value --secret-id $registryApiUrlId |jq -r '.SecretString') 17 | registryApiUrl=$(printf '%s\n' "$registryApiUrlUnsafe" | sed -e 's/[]\/$*.^[]/\\&/g'); 18 | 19 | registryWssUrlId="${envPrefix}_agent_registry_wss_url" 20 | registryWssUrlUnsafe=$(aws secretsmanager --region $region get-secret-value --secret-id $registryWssUrlId |jq -r '.SecretString') 21 | registryWssUrl=$(printf '%s\n' "$registryWssUrlUnsafe" | sed -e 's/[]\/$*.^[]/\\&/g'); 22 | 23 | mainnetApiUrlId="${envPrefix}_mainnet_api_url" 24 | mainnetApiUrlUnsafe=$(aws secretsmanager --region $region get-secret-value --secret-id $mainnetApiUrlId |jq -r '.SecretString') 25 | mainnetApiUrl=$(printf '%s\n' "$mainnetApiUrlUnsafe" | sed -e 's/[]\/$*.^[]/\\&/g'); 26 | 27 | # get config file name and config file 28 | configFileName=$(aws ec2 describe-tags --region $region --filters "Name=resource-id,Values=$instanceId" "Name=key,Values=FortaConfig" | jq -r '.Tags[0].Value') 29 | configBucketName="$envPrefix-forta-codedeploy" 30 | aws s3 cp --region $region "s3://$configBucketName/configs/$configFileName" $configPath 31 | 32 | sed -i "s/ALCHEMY_URL/$apiUrl/g" $configPath 33 | sed -i "s/REGISTRY_API_URL/$registryApiUrl/g" $configPath 34 | sed -i "s/REGISTRY_WSS_URL/$registryWssUrl/g" $configPath 35 | sed -i "s/MAINNET_API_URL/$mainnetApiUrl/g" $configPath 36 | 37 | chown -R forta.forta /home/forta -------------------------------------------------------------------------------- /services/publisher/testalerts/logger.go: -------------------------------------------------------------------------------- 1 | package testalerts 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "github.com/goccy/go-json" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "time" 12 | 13 | "github.com/forta-network/forta-core-go/protocol" 14 | ) 15 | 16 | // Logger logs the test alerts either to a log file or to a webhook. 17 | type Logger struct { 18 | file *os.File 19 | webhookUrl string 20 | } 21 | 22 | // NewLogger creates a new logger. 23 | func NewLogger(dst string) *Logger { 24 | if len(dst) != 0 { 25 | return newWebhookLogger(dst) 26 | } 27 | 28 | if err := os.MkdirAll("/test-alerts", 0666); err != nil { 29 | panic(fmt.Errorf("failed to create the test alerts dir: %v", err)) 30 | } 31 | file, err := os.Create(fmt.Sprintf("/test-alerts/forta-test-alert-log-%d", time.Now().Unix())) 32 | if err != nil { 33 | panic(fmt.Errorf("failed to create the test alert log file: %v", err)) 34 | } 35 | if err != nil { 36 | panic(err) 37 | } 38 | return &Logger{file: file} 39 | } 40 | 41 | func newWebhookLogger(dst string) *Logger { 42 | u, err := url.Parse(dst) 43 | if err != nil { 44 | panic(fmt.Errorf("failed to parse the webhook url: %v", err)) 45 | } 46 | if !(u.Scheme == "http" || u.Scheme == "https") { 47 | panic("non-http webhook url") 48 | } 49 | return &Logger{webhookUrl: dst} 50 | } 51 | 52 | // Close implemenets io.Closer. 53 | func (logger *Logger) Close() error { 54 | if logger.file == nil { 55 | return nil 56 | } 57 | return logger.file.Close() 58 | } 59 | 60 | // LogTestAlert logs the test alert by marshalling to JSON. 61 | func (logger *Logger) LogTestAlert(ctx context.Context, alert *protocol.SignedAlert) error { 62 | b, _ := json.Marshal(alert) 63 | if logger.file != nil { 64 | _, err := fmt.Fprintln(logger.file, string(b)) 65 | if err != nil { 66 | return fmt.Errorf("failed to write to the test alert file: %v", err) 67 | } 68 | } 69 | reqCtx, cancel := context.WithTimeout(ctx, time.Second*5) 70 | defer cancel() 71 | req, err := http.NewRequestWithContext(reqCtx, "POST", logger.webhookUrl, bytes.NewBuffer(b)) 72 | if err != nil { 73 | return fmt.Errorf("failed to send the test alert: %v", err) 74 | } 75 | req.Header.Set("Content-Type", "application/json") 76 | _, err = http.DefaultClient.Do(req) 77 | if err != nil { 78 | return fmt.Errorf("test alert webhook request failed: %v", err) 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /clients/agentgrpc/agent_client.go: -------------------------------------------------------------------------------- 1 | package agentgrpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/forta-network/forta-core-go/protocol" 9 | "github.com/forta-network/forta-node/config" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | const defaultAgentResponseMaxByteCount = 1000000 // 1M 16 | 17 | // Method is gRPC method type. 18 | type Method string 19 | 20 | // Agent gRPC methods 21 | const ( 22 | MethodInitialize Method = "/network.forta.Agent/Initialize" 23 | MethodEvaluateTx Method = "/network.forta.Agent/EvaluateTx" 24 | MethodEvaluateBlock Method = "/network.forta.Agent/EvaluateBlock" 25 | ) 26 | 27 | // Client allows us to communicate with an agent. 28 | type Client struct { 29 | conn *grpc.ClientConn 30 | protocol.AgentClient 31 | } 32 | 33 | // NewClient creates a new client. 34 | func NewClient() *Client { 35 | return &Client{} 36 | } 37 | 38 | // Dial dials an agent using the config. 39 | func (client *Client) Dial(cfg config.AgentConfig) error { 40 | var ( 41 | conn *grpc.ClientConn 42 | err error 43 | ) 44 | for i := 0; i < 10; i++ { 45 | conn, err = grpc.Dial( 46 | fmt.Sprintf("%s:%s", cfg.ContainerName(), cfg.GrpcPort()), 47 | grpc.WithInsecure(), 48 | grpc.WithBlock(), 49 | grpc.WithTimeout(10*time.Second), 50 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaultAgentResponseMaxByteCount)), 51 | ) 52 | if err == nil { 53 | break 54 | } 55 | err = fmt.Errorf("failed to connect to agent '%s': %v", cfg.ContainerName(), err) 56 | log.Debug(err) 57 | time.Sleep(time.Second * 2) 58 | } 59 | if err != nil { 60 | log.Error(err) 61 | return err 62 | } 63 | client.WithConn(conn) 64 | log.Debugf("connected to agent: %s", cfg.ContainerName()) 65 | return nil 66 | } 67 | 68 | // WithConn sets the client conn. 69 | func (client *Client) WithConn(conn *grpc.ClientConn) { 70 | client.conn = conn 71 | client.AgentClient = protocol.NewAgentClient(conn) 72 | } 73 | 74 | // Invoke is a generalization of client methods. 75 | func (client *Client) Invoke(ctx context.Context, method Method, in, out interface{}, opts ...grpc.CallOption) error { 76 | return client.conn.Invoke(ctx, string(method), in, out, opts...) 77 | } 78 | 79 | // Close implements io.Closer. 80 | func (client *Client) Close() error { 81 | if client.conn != nil { 82 | return client.conn.Close() 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /scripts/start-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | # dynamically look up the secret name 6 | instanceId=$(curl -s http://instance-data/latest/meta-data/instance-id) 7 | region=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region) 8 | envPrefix=$(aws ec2 describe-tags --region $region --filters "Name=resource-id,Values=$instanceId" "Name=key,Values=Environment" |jq -r '.Tags[0].Value') 9 | secretId="${envPrefix}_forta_passphrase" 10 | 11 | # get secret from secrets manager 12 | passphrase=$(aws secretsmanager --region $region get-secret-value --secret-id $secretId |jq -r '.SecretString') 13 | 14 | # get private key JSON from DynamoDB 15 | privateKeysTable="$envPrefix-forta-node-private-keys" 16 | nodeName=$(aws ec2 describe-tags --region $region --filters "Name=resource-id,Values=$instanceId" "Name=key,Values=Name" | jq -r '.Tags[0].Value') 17 | networkName=$(aws ec2 describe-tags --region $region --filters "Name=resource-id,Values=$instanceId" "Name=key,Values=Network" | jq -r '.Tags[0].Value') 18 | privateKeyItem=$(aws dynamodb get-item --region $region --table $privateKeysTable --key '{"NodeName": { "S": "'$nodeName'" }, "Network": { "S": "'$networkName'"}}' | jq -r .Item) 19 | privateKeyFileName='' 20 | # create and store new one if it doesn't exist 21 | if [ -z "$privateKeyItem" ]; then 22 | geth account new --password <(echo $passphrase) --keystore "$HOME/.forta/.keys" 23 | privateKeyFileName=$(ls $HOME/.forta/.keys | head -n 1) 24 | privateKeyJson=$(cat "$HOME/.forta/.keys/$privateKeyFileName") 25 | ethereumAddress="0x$(echo $privateKeyJson | jq -r .address)" 26 | dynamoItemTpl='{NodeName:{S:$name},EthereumAddress:{S:$address},PrivateKeyJson:{S:$privKeyJson},FileName:{S:$keyFileName},Network:{S:$networkName}}' 27 | privateKeyItem=$(jq -ncM --arg name "$nodeName" --arg address "$ethereumAddress" --arg privKeyJson "$privateKeyJson" --arg keyFileName "$privateKeyFileName" --arg networkName "$networkName" "$dynamoItemTpl") 28 | aws dynamodb put-item --region $region --table-name $privateKeysTable --item "$privateKeyItem" 29 | fi 30 | privateKeyJson=$(echo $privateKeyItem | jq -r '.PrivateKeyJson.S') 31 | privateKeyFileName=$(echo $privateKeyItem | jq -r '.FileName.S') 32 | # write the private key file to ensure it exists in the right place 33 | mkdir -p "$HOME/.forta/.keys" 34 | echo "$privateKeyJson" > "$HOME/.forta/.keys/$privateKeyFileName" 35 | 36 | nohup \ 37 | forta --passphrase $passphrase run > /dev/null 2> /tmp/forta.log < /dev/null & 38 | -------------------------------------------------------------------------------- /cmd/supervisor/main.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "github.com/forta-network/forta-core-go/clients/health" 8 | "github.com/forta-network/forta-core-go/security" 9 | "github.com/forta-network/forta-core-go/utils" 10 | "github.com/forta-network/forta-node/config" 11 | "github.com/forta-network/forta-node/healthutils" 12 | "github.com/forta-network/forta-node/services" 13 | "github.com/forta-network/forta-node/services/supervisor" 14 | ) 15 | 16 | func initServices(ctx context.Context, cfg config.Config) ([]services.Service, error) { 17 | cfg.Registry.JsonRpc.Url = utils.ConvertToDockerHostURL(cfg.Registry.JsonRpc.Url) 18 | cfg.Registry.IPFS.APIURL = utils.ConvertToDockerHostURL(cfg.Registry.IPFS.APIURL) 19 | cfg.Registry.IPFS.GatewayURL = utils.ConvertToDockerHostURL(cfg.Registry.IPFS.GatewayURL) 20 | 21 | passphrase, err := security.ReadPassphrase() 22 | if err != nil { 23 | return nil, err 24 | } 25 | key, err := security.LoadKey(config.DefaultContainerKeyDirPath) 26 | if err != nil { 27 | return nil, err 28 | } 29 | svc, err := supervisor.NewSupervisorService(ctx, supervisor.SupervisorServiceConfig{ 30 | Config: cfg, 31 | Passphrase: passphrase, 32 | Key: key, 33 | }) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return []services.Service{ 38 | health.NewService( 39 | ctx, "", healthutils.DefaultHealthServerErrHandler, 40 | health.CheckerFrom(summarizeReports, svc), 41 | ), 42 | svc, 43 | }, nil 44 | } 45 | 46 | func summarizeReports(reports health.Reports) *health.Report { 47 | summary := health.NewSummary() 48 | 49 | containersManager, ok := reports.NameContains("containers.managed") 50 | if ok { 51 | count, _ := strconv.Atoi(containersManager.Details) 52 | if count < config.DockerSupervisorManagedContainers { 53 | summary.Addf("missing %d containers.", config.DockerSupervisorManagedContainers-count) 54 | summary.Status(health.StatusFailing) 55 | } else { 56 | summary.Addf("all %d service containers are running.", config.DockerSupervisorManagedContainers) 57 | } 58 | } 59 | 60 | telemetryErr, ok := reports.NameContains("telemetry-sync.error") 61 | if ok && len(telemetryErr.Details) > 0 { 62 | summary.Addf("telemetry sync is failing with error '%s' (non-critical).", telemetryErr.Details) 63 | // do not change status - non critical 64 | } 65 | 66 | return summary.Finish() 67 | } 68 | 69 | func Run() { 70 | services.ContainerMain("supervisor", initServices) 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/codedeploy-dev.yml: -------------------------------------------------------------------------------- 1 | name: CodeDeploy to Dev (All Scanners) 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [ master ] 6 | 7 | jobs: 8 | deploy: 9 | name: Copy latest binary and deploy 10 | runs-on: ubuntu-latest 11 | environment: dev 12 | steps: 13 | - name: Clear artifacts 14 | uses: kolpav/purge-artifacts-action@v1 15 | continue-on-error: true 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | expire-in: 7days 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Configure AWS credentials (S3 -> local build artifact) 23 | uses: aws-actions/configure-aws-credentials@v1 24 | with: 25 | aws-access-key-id: ${{ secrets.DEV_RELEASE_AWS_ACCESS_KEY }} 26 | aws-secret-access-key: ${{ secrets.DEV_RELEASE_AWS_SECRET_KEY }} 27 | aws-region: us-east-1 28 | - name: Copy latest build from release artifacts bucket 29 | env: 30 | BUCKET_NAME: dev-forta-releases 31 | REVISION: ${{ github.sha }} 32 | run: | 33 | aws s3 cp "s3://$BUCKET_NAME/artifacts/forta" forta 34 | chmod 755 forta 35 | 36 | - name: Prepare Distribution 37 | run: | 38 | mkdir dist 39 | cp forta dist/ 40 | cp appspec.yml dist/ 41 | cp -R scripts dist/ 42 | - name: Zip Distribution 43 | uses: vimtor/action-zip@v1 44 | with: 45 | files: dist/ 46 | dest: deploy.zip 47 | - uses: actions/upload-artifact@v1 48 | with: 49 | name: deploy-artifact 50 | path: ${{ github.workspace }}/deploy.zip 51 | 52 | - name: Configure AWS credentials (CodeDeploy) 53 | uses: aws-actions/configure-aws-credentials@v1 54 | with: 55 | aws-access-key-id: ${{ secrets.DEV_DEPLOY_AWS_ACCESS_KEY }} 56 | aws-secret-access-key: ${{ secrets.DEV_DEPLOY_AWS_SECRET_KEY }} 57 | aws-region: us-east-1 58 | - name: AWS CodeDeploy 59 | uses: sourcetoad/aws-codedeploy-action@v1 60 | with: 61 | aws_access_key: ${{ secrets.DEV_DEPLOY_AWS_ACCESS_KEY }} 62 | aws_secret_key: ${{ secrets.DEV_DEPLOY_AWS_SECRET_KEY }} 63 | aws_region: us-east-1 64 | codedeploy_name: dev-forta-node 65 | codedeploy_group: dev-forta-all-scanners-deploy-group 66 | s3_bucket: dev-forta-codedeploy 67 | s3_folder: dev 68 | directory: ./dist/ 69 | -------------------------------------------------------------------------------- /.github/workflows/codedeploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: CodeDeploy to Prod (All Scanners) 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [ master ] 6 | 7 | jobs: 8 | deploy: 9 | name: Copy latest binary and deploy 10 | runs-on: ubuntu-latest 11 | environment: prod 12 | steps: 13 | - name: Clear artifacts 14 | uses: kolpav/purge-artifacts-action@v1 15 | continue-on-error: true 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | expire-in: 7days 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Configure AWS credentials (S3 -> local build artifact) 23 | uses: aws-actions/configure-aws-credentials@v1 24 | with: 25 | aws-access-key-id: ${{ secrets.PROD_RELEASE_AWS_ACCESS_KEY }} 26 | aws-secret-access-key: ${{ secrets.PROD_RELEASE_AWS_SECRET_KEY }} 27 | aws-region: us-east-1 28 | - name: Copy latest build from release artifacts bucket 29 | env: 30 | BUCKET_NAME: prod-forta-releases 31 | REVISION: ${{ github.sha }} 32 | run: | 33 | aws s3 cp "s3://$BUCKET_NAME/artifacts/forta" forta 34 | chmod 755 forta 35 | 36 | - name: Prepare Distribution 37 | run: | 38 | mkdir dist 39 | cp forta dist/ 40 | cp appspec.yml dist/ 41 | cp -R scripts dist/ 42 | - name: Zip Distribution 43 | uses: vimtor/action-zip@v1 44 | with: 45 | files: dist/ 46 | dest: deploy.zip 47 | - uses: actions/upload-artifact@v1 48 | with: 49 | name: deploy-artifact 50 | path: ${{ github.workspace }}/deploy.zip 51 | 52 | - name: Configure AWS credentials (CodeDeploy) 53 | uses: aws-actions/configure-aws-credentials@v1 54 | with: 55 | aws-access-key-id: ${{ secrets.PROD_DEPLOY_AWS_ACCESS_KEY }} 56 | aws-secret-access-key: ${{ secrets.PROD_DEPLOY_AWS_SECRET_KEY }} 57 | aws-region: us-east-1 58 | - name: AWS CodeDeploy 59 | uses: sourcetoad/aws-codedeploy-action@v1 60 | with: 61 | aws_access_key: ${{ secrets.PROD_DEPLOY_AWS_ACCESS_KEY }} 62 | aws_secret_key: ${{ secrets.PROD_DEPLOY_AWS_SECRET_KEY }} 63 | aws_region: us-east-1 64 | codedeploy_name: prod-forta-node 65 | codedeploy_group: prod-forta-deploy-group 66 | s3_bucket: prod-forta-codedeploy 67 | s3_folder: prod 68 | directory: ./dist/ 69 | -------------------------------------------------------------------------------- /services/updater/updater_test.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/forta-network/forta-core-go/release" 8 | 9 | rm "github.com/forta-network/forta-core-go/registry/mocks" 10 | im "github.com/forta-network/forta-core-go/release/mocks" 11 | "github.com/golang/mock/gomock" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | const ( 16 | testDefaultCheckIntervalSeconds = 60 17 | ) 18 | 19 | func TestUpdaterService_UpdateLatestRelease(t *testing.T) { 20 | c := gomock.NewController(t) 21 | 22 | rg := rm.NewMockClient(c) 23 | is := im.NewMockClient(c) 24 | updater := NewUpdaterService(context.Background(), rg, is, "8080", false, testDefaultCheckIntervalSeconds) 25 | 26 | rg.EXPECT().GetScannerNodeVersion().Return("reference", nil).Times(1) 27 | is.EXPECT().GetReleaseManifest(gomock.Any(), "reference").Return(&release.ReleaseManifest{}, nil).Times(1) 28 | err := updater.updateLatestRelease() 29 | assert.NoError(t, err) 30 | } 31 | 32 | func TestUpdaterService_UpdateLatestReleaseCached(t *testing.T) { 33 | c := gomock.NewController(t) 34 | rg := rm.NewMockClient(c) 35 | is := im.NewMockClient(c) 36 | updater := NewUpdaterService(context.Background(), rg, is, "8080", false, testDefaultCheckIntervalSeconds) 37 | 38 | // update twice 39 | rg.EXPECT().GetScannerNodeVersion().Return("reference", nil).Times(2) 40 | 41 | // only call ipfs once (because value is the same) 42 | is.EXPECT().GetReleaseManifest(gomock.Any(), "reference").Return(&release.ReleaseManifest{}, nil).Times(1) 43 | assert.NoError(t, updater.updateLatestRelease()) 44 | assert.NoError(t, updater.updateLatestRelease()) 45 | } 46 | 47 | func TestUpdaterService_UpdateLatestReleaseNotCached(t *testing.T) { 48 | c := gomock.NewController(t) 49 | rg := rm.NewMockClient(c) 50 | is := im.NewMockClient(c) 51 | updater := NewUpdaterService(context.Background(), rg, is, "8080", false, testDefaultCheckIntervalSeconds) 52 | 53 | // update twice 54 | rg.EXPECT().GetScannerNodeVersion().Return("reference1", nil).Times(1) 55 | rg.EXPECT().GetScannerNodeVersion().Return("reference2", nil).Times(1) 56 | 57 | // only call ipfs once (because value is the same) 58 | is.EXPECT().GetReleaseManifest(gomock.Any(), "reference1").Return(&release.ReleaseManifest{}, nil).Times(1) 59 | is.EXPECT().GetReleaseManifest(gomock.Any(), "reference2").Return(&release.ReleaseManifest{}, nil).Times(1) 60 | 61 | assert.NoError(t, updater.updateLatestRelease()) 62 | assert.NoError(t, updater.updateLatestRelease()) 63 | } 64 | -------------------------------------------------------------------------------- /store/mocks/mock_registry.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: store/registry.go 3 | 4 | // Package mock_store is a generated GoMock package. 5 | package mock_store 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | config "github.com/forta-network/forta-node/config" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockRegistryStore is a mock of RegistryStore interface. 15 | type MockRegistryStore struct { 16 | ctrl *gomock.Controller 17 | recorder *MockRegistryStoreMockRecorder 18 | } 19 | 20 | // MockRegistryStoreMockRecorder is the mock recorder for MockRegistryStore. 21 | type MockRegistryStoreMockRecorder struct { 22 | mock *MockRegistryStore 23 | } 24 | 25 | // NewMockRegistryStore creates a new mock instance. 26 | func NewMockRegistryStore(ctrl *gomock.Controller) *MockRegistryStore { 27 | mock := &MockRegistryStore{ctrl: ctrl} 28 | mock.recorder = &MockRegistryStoreMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockRegistryStore) EXPECT() *MockRegistryStoreMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // FindAgentGlobally mocks base method. 38 | func (m *MockRegistryStore) FindAgentGlobally(agentID string) (*config.AgentConfig, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "FindAgentGlobally", agentID) 41 | ret0, _ := ret[0].(*config.AgentConfig) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // FindAgentGlobally indicates an expected call of FindAgentGlobally. 47 | func (mr *MockRegistryStoreMockRecorder) FindAgentGlobally(agentID interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAgentGlobally", reflect.TypeOf((*MockRegistryStore)(nil).FindAgentGlobally), agentID) 50 | } 51 | 52 | // GetAgentsIfChanged mocks base method. 53 | func (m *MockRegistryStore) GetAgentsIfChanged(scanner string) ([]*config.AgentConfig, bool, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "GetAgentsIfChanged", scanner) 56 | ret0, _ := ret[0].([]*config.AgentConfig) 57 | ret1, _ := ret[1].(bool) 58 | ret2, _ := ret[2].(error) 59 | return ret0, ret1, ret2 60 | } 61 | 62 | // GetAgentsIfChanged indicates an expected call of GetAgentsIfChanged. 63 | func (mr *MockRegistryStoreMockRecorder) GetAgentsIfChanged(scanner interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentsIfChanged", reflect.TypeOf((*MockRegistryStore)(nil).GetAgentsIfChanged), scanner) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/cmd_batch.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "path" 9 | 10 | "github.com/forta-network/forta-core-go/encoding" 11 | "github.com/forta-network/forta-core-go/protocol" 12 | "github.com/forta-network/forta-core-go/security" 13 | "github.com/ipfs/go-cid" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func handleFortaBatchDecode(cmd *cobra.Command, args []string) error { 18 | batchCid, err := cmd.Flags().GetString("cid") 19 | if err != nil { 20 | return err 21 | } 22 | _, err = cid.Parse(batchCid) 23 | if err != nil { 24 | return fmt.Errorf("invalid cid") 25 | } 26 | fileName, err := cmd.Flags().GetString("o") 27 | if err != nil { 28 | return err 29 | } 30 | // rest of the info lines go to stderr - useful when piping stdout to e.g. jq 31 | printToStdout, err := cmd.Flags().GetBool("stdout") 32 | if err != nil { 33 | return err 34 | } 35 | 36 | cmd.PrintErrln("Downloading...") 37 | 38 | batchResp, err := http.Get(fmt.Sprintf("%s/ipfs/%s", cfg.Publish.IPFS.GatewayURL, batchCid)) 39 | if err != nil { 40 | return fmt.Errorf("failed to get batch: %v", err) 41 | } 42 | if batchResp.StatusCode != http.StatusOK { 43 | return fmt.Errorf("request to get batch failed with status %d", batchResp.StatusCode) 44 | } 45 | defer batchResp.Body.Close() 46 | 47 | cmd.PrintErrln("Successfully downloaded the batch.") 48 | 49 | var signedBatch protocol.SignedPayload 50 | if err := json.NewDecoder(batchResp.Body).Decode(&signedBatch); err != nil { 51 | return fmt.Errorf("failed to decode batch json: %v", err) 52 | } 53 | 54 | if err := security.VerifySignedPayload(&signedBatch); err != nil { 55 | yellowBold("Invalid batch signature: %v\n", err) 56 | } else { 57 | cmd.PrintErrf("Valid batch signature found - scanner: %s\n", signedBatch.Signature.Signer) 58 | } 59 | // continue decoding in any case 60 | 61 | var alertBatch protocol.AlertBatch 62 | if err := encoding.DecodeGzippedProto(signedBatch.Encoded, &alertBatch); err != nil { 63 | redBold("Invalid batch encoding!\n") 64 | return fmt.Errorf("failed to decode: %v", err) 65 | } 66 | 67 | // indent by two spaces 68 | b, _ := json.MarshalIndent(&alertBatch, "", " ") 69 | 70 | if printToStdout { 71 | fmt.Println(string(b)) 72 | return nil 73 | } 74 | 75 | dir, _ := os.Getwd() 76 | filePath := path.Join(dir, fileName) 77 | if err := os.WriteFile(filePath, b, 0644); err != nil { 78 | return fmt.Errorf("failed to write file %s: %v", filePath, err) 79 | } 80 | greenBold("Successfully wrote the decoded batch to %s\n", filePath) 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /services/publisher/publisher_test.go: -------------------------------------------------------------------------------- 1 | package publisher 2 | 3 | import ( 4 | "github.com/forta-network/forta-core-go/protocol" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestBatchData_AppendPrivateAlert_PerFinding(t *testing.T) { 10 | bd := BatchData{} 11 | alert := &protocol.SignedAlert{ 12 | Alert: &protocol.Alert{Id: "alertId", Finding: &protocol.Finding{ 13 | Private: true, 14 | }}, 15 | } 16 | nr := &protocol.NotifyRequest{ 17 | SignedAlert: alert, 18 | EvalTxRequest: &protocol.EvaluateTxRequest{}, 19 | EvalTxResponse: &protocol.EvaluateTxResponse{}, 20 | AgentInfo: &protocol.AgentInfo{ 21 | Manifest: "agentInfo", 22 | }, 23 | } 24 | 25 | assert.Len(t, bd.PrivateAlerts, 0) 26 | bd.AppendAlert(nr) 27 | assert.Len(t, bd.PrivateAlerts, 1) 28 | assert.Equal(t, nr.AgentInfo.Manifest, bd.PrivateAlerts[0].AgentManifest) 29 | assert.Len(t, bd.PrivateAlerts[0].Alerts, 1) 30 | assert.EqualValues(t, alert, bd.PrivateAlerts[0].Alerts[0]) 31 | } 32 | 33 | func TestBatchData_AppendPrivateAlert_Tx(t *testing.T) { 34 | bd := BatchData{} 35 | alert := &protocol.SignedAlert{ 36 | Alert: &protocol.Alert{Id: "alertId", Finding: &protocol.Finding{}}, 37 | } 38 | nr := &protocol.NotifyRequest{ 39 | SignedAlert: alert, 40 | EvalTxRequest: &protocol.EvaluateTxRequest{}, 41 | EvalTxResponse: &protocol.EvaluateTxResponse{ 42 | Private: true, 43 | }, 44 | AgentInfo: &protocol.AgentInfo{ 45 | Manifest: "agentInfo", 46 | }, 47 | } 48 | 49 | assert.Len(t, bd.PrivateAlerts, 0) 50 | bd.AppendAlert(nr) 51 | assert.Len(t, bd.PrivateAlerts, 1) 52 | assert.Equal(t, nr.AgentInfo.Manifest, bd.PrivateAlerts[0].AgentManifest) 53 | assert.Len(t, bd.PrivateAlerts[0].Alerts, 1) 54 | assert.EqualValues(t, alert, bd.PrivateAlerts[0].Alerts[0]) 55 | } 56 | 57 | func TestBatchData_AppendPrivateAlert_Block(t *testing.T) { 58 | bd := BatchData{} 59 | alert := &protocol.SignedAlert{ 60 | Alert: &protocol.Alert{Id: "alertId", Finding: &protocol.Finding{}}, 61 | } 62 | nr := &protocol.NotifyRequest{ 63 | SignedAlert: alert, 64 | EvalBlockRequest: &protocol.EvaluateBlockRequest{}, 65 | EvalBlockResponse: &protocol.EvaluateBlockResponse{ 66 | Private: true, 67 | }, 68 | AgentInfo: &protocol.AgentInfo{ 69 | Manifest: "agentInfo", 70 | }, 71 | } 72 | 73 | assert.Len(t, bd.PrivateAlerts, 0) 74 | bd.AppendAlert(nr) 75 | assert.Len(t, bd.PrivateAlerts, 1) 76 | assert.Equal(t, nr.AgentInfo.Manifest, bd.PrivateAlerts[0].AgentManifest) 77 | assert.Len(t, bd.PrivateAlerts[0].Alerts, 1) 78 | assert.EqualValues(t, alert, bd.PrivateAlerts[0].Alerts[0]) 79 | } 80 | -------------------------------------------------------------------------------- /clients/agentgrpc/encoding_test.go: -------------------------------------------------------------------------------- 1 | package agentgrpc_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "testing" 9 | 10 | "github.com/forta-network/forta-core-go/protocol" 11 | "github.com/forta-network/forta-node/clients/agentgrpc" 12 | "github.com/forta-network/forta-node/config" 13 | "github.com/stretchr/testify/require" 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | var txMsg = &protocol.EvaluateTxRequest{ 18 | RequestId: "123", 19 | Event: &protocol.TransactionEvent{ 20 | Type: protocol.TransactionEvent_BLOCK, 21 | Transaction: &protocol.TransactionEvent_EthTransaction{ 22 | Hash: "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", 23 | }, 24 | }, 25 | } 26 | 27 | type agentServer struct { 28 | r *require.Assertions 29 | doneCh chan struct{} 30 | disableAssertion bool 31 | protocol.UnimplementedAgentServer 32 | } 33 | 34 | func (as *agentServer) Initialize(context.Context, *protocol.InitializeRequest) (*protocol.InitializeResponse, error) { 35 | return &protocol.InitializeResponse{ 36 | Status: protocol.ResponseStatus_SUCCESS, 37 | }, nil 38 | } 39 | 40 | func (as *agentServer) EvaluateTx(ctx context.Context, txRequest *protocol.EvaluateTxRequest) (*protocol.EvaluateTxResponse, error) { 41 | if !as.disableAssertion { 42 | as.r.Equal(txMsg.RequestId, txRequest.RequestId) 43 | as.r.Equal(txMsg.Event.Transaction.Hash, txRequest.Event.Transaction.Hash) 44 | close(as.doneCh) 45 | } 46 | return &protocol.EvaluateTxResponse{ 47 | Status: protocol.ResponseStatus_SUCCESS, 48 | }, nil 49 | } 50 | 51 | func (as *agentServer) EvaluateBlock(context.Context, *protocol.EvaluateBlockRequest) (*protocol.EvaluateBlockResponse, error) { 52 | return &protocol.EvaluateBlockResponse{ 53 | Status: protocol.ResponseStatus_SUCCESS, 54 | }, nil 55 | } 56 | 57 | func TestEncodeMessage(t *testing.T) { 58 | r := require.New(t) 59 | 60 | preparedMsg, err := agentgrpc.EncodeMessage(txMsg) 61 | r.NoError(err) 62 | log.Printf("%+v", preparedMsg) 63 | 64 | lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", config.AgentGrpcPort)) 65 | r.NoError(err) 66 | defer lis.Close() 67 | 68 | server := grpc.NewServer() 69 | as := &agentServer{r: r, doneCh: make(chan struct{})} 70 | protocol.RegisterAgentServer(server, as) 71 | go server.Serve(lis) 72 | 73 | agentClient := agentgrpc.NewClient() 74 | conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", config.AgentGrpcPort), grpc.WithInsecure()) 75 | r.NoError(err) 76 | agentClient.WithConn(conn) 77 | 78 | var resp protocol.EvaluateTxResponse 79 | r.NoError(agentClient.Invoke(context.Background(), agentgrpc.MethodEvaluateTx, preparedMsg, &resp)) 80 | <-as.doneCh 81 | } 82 | -------------------------------------------------------------------------------- /cmd/cmd_agent.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/forta-network/forta-core-go/ethereum" 9 | "github.com/forta-network/forta-node/store" 10 | "github.com/goccy/go-json" 11 | 12 | "github.com/fatih/color" 13 | "github.com/forta-network/forta-node/config" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func handleFortaAgentAdd(cmd *cobra.Command, args []string) error { 18 | ethClient, err := ethereum.NewStreamEthClient(context.Background(), "registry", cfg.Registry.JsonRpc.Url) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | reg, err := store.NewRegistryStore(context.Background(), cfg, ethClient) 24 | if err != nil { 25 | return fmt.Errorf("failed to initialize registry") 26 | } 27 | 28 | agentCfg, err := reg.FindAgentGlobally(args[0]) 29 | if err != nil { 30 | return fmt.Errorf("failed to load the agent: %v", err) 31 | } 32 | 33 | cfg.LocalAgents, err = readLocalAgents() 34 | if err != nil { 35 | return fmt.Errorf("failed to read the local agents: %v", err) 36 | } 37 | 38 | for _, localAgent := range cfg.LocalAgents { 39 | if localAgent.ID != agentCfg.ID { 40 | continue 41 | } 42 | if localAgent.Image == agentCfg.Image { 43 | cmd.Println("Already added to list - ignored") 44 | return nil 45 | } 46 | } 47 | // Two cases to add append an agent: 48 | // 1. Different agent 49 | // 2. Same agent, different image (i.e. different version) 50 | cfg.LocalAgents = append(cfg.LocalAgents, agentCfg) 51 | 52 | if err := writeLocalAgents(cfg.LocalAgents); err != nil { 53 | return fmt.Errorf("failed to write the local agents file: %v", err) 54 | } 55 | 56 | greenBold("Successfully added agent %s locally! You can see the full list in %s.\n", agentCfg.ID, cfg.LocalAgentsPath) 57 | fmt.Printf("Image: %s\n", color.New(color.FgYellow).Sprintf(agentCfg.Image)) 58 | 59 | return nil 60 | } 61 | 62 | // readLocalAgents tries to read the local agents and silently returns an 63 | // empty array if the file is not readable or not found. 64 | func readLocalAgents() ([]*config.AgentConfig, error) { 65 | var agents []*config.AgentConfig 66 | b, err := ioutil.ReadFile(cfg.LocalAgentsPath) 67 | if err == nil { 68 | if err := json.Unmarshal(b, &agents); err != nil { 69 | return nil, fmt.Errorf("failed to unmarshal the local agents file: %v", err) 70 | } 71 | } 72 | for _, agent := range agents { 73 | agent.IsLocal = true 74 | } 75 | return agents, nil 76 | } 77 | 78 | // writeLocalAgents writes the agents to the local list. 79 | func writeLocalAgents(agents []*config.AgentConfig) error { 80 | if len(agents) == 0 { 81 | return nil 82 | } 83 | b, _ := json.MarshalIndent(agents, "", " ") 84 | return ioutil.WriteFile(cfg.LocalAgentsPath, b, 0644) 85 | } 86 | -------------------------------------------------------------------------------- /services/supervisor/health.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/forta-network/forta-core-go/utils" 7 | "github.com/forta-network/forta-node/clients" 8 | 9 | "fmt" 10 | "time" 11 | 12 | "github.com/docker/docker/api/types" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | const defaultHealthCheckInterval = time.Second * 5 17 | const maxAttempts = 10 18 | 19 | func (sup *SupervisorService) healthCheck() { 20 | ticker := time.NewTicker(defaultHealthCheckInterval) 21 | for { 22 | select { 23 | case <-sup.ctx.Done(): 24 | ticker.Stop() 25 | return 26 | 27 | case <-ticker.C: 28 | if err := sup.doHealthCheck(); err != nil { 29 | log.Errorf("failed to do health check: %v", err) 30 | } 31 | } 32 | } 33 | } 34 | 35 | func (sup *SupervisorService) doHealthCheck() error { 36 | sup.mu.RLock() 37 | defer sup.mu.RUnlock() 38 | for _, knownContainer := range sup.containers { 39 | var foundContainer *types.Container 40 | 41 | // this has a threshold so that the healthcheck doesn't fail while a container is starting 42 | err := utils.TryTimes(func(attempt int) error { 43 | var err error 44 | foundContainer, err = sup.client.GetContainerByID(sup.ctx, knownContainer.ID) 45 | currAttempt := attempt + 1 46 | if err != nil && errors.Is(err, clients.ErrContainerNotFound) { 47 | log.Warnf("healthcheck: container '%s' with id '%s' was not found (attempt=%d/%d)", knownContainer.Name, knownContainer.ID, currAttempt, maxAttempts) 48 | return err 49 | } 50 | // If the container is found alive at later attempts, make it obvious. 51 | if currAttempt > 1 { 52 | log.Infof("healthcheck: container '%s' with id '%s' was found alive (attempt=%d/%d)", knownContainer.Name, knownContainer.ID, currAttempt, maxAttempts) 53 | } 54 | return nil 55 | }, maxAttempts, 1*time.Second) 56 | if err != nil { 57 | // If this ever happens, then we have a critical gap in our logic. 58 | log.Error(err.Error()) 59 | continue 60 | } 61 | if err := sup.ensureUp(knownContainer, foundContainer); err != nil { 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func (sup *SupervisorService) ensureUp(knownContainer *Container, foundContainer *types.Container) error { 69 | switch foundContainer.State { 70 | case "created", "running", "restarting", "paused", "dead": 71 | return nil 72 | case "exited": 73 | log.Warnf("starting exited container '%s'", knownContainer.Name) 74 | _, err := sup.client.StartContainer(sup.ctx, knownContainer.Config) 75 | if err != nil { 76 | return fmt.Errorf("failed to start container '%s': %v", knownContainer.Name, err) 77 | } 78 | return nil 79 | default: 80 | log.Panicf("unhandled container state: %s", foundContainer.State) 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /clients/interfaces.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/forta-network/forta-core-go/domain" 8 | "google.golang.org/grpc" 9 | 10 | "github.com/docker/docker/api/types" 11 | "github.com/golang/protobuf/proto" 12 | 13 | "github.com/forta-network/forta-core-go/protocol" 14 | "github.com/forta-network/forta-node/clients/agentgrpc" 15 | "github.com/forta-network/forta-node/config" 16 | ) 17 | 18 | // DockerClient is a client interface for interacting with docker 19 | type DockerClient interface { 20 | PullImage(ctx context.Context, refStr string) error 21 | CreatePublicNetwork(ctx context.Context, name string) (string, error) 22 | CreateInternalNetwork(ctx context.Context, name string) (string, error) 23 | AttachNetwork(ctx context.Context, containerID string, networkID string) error 24 | RemoveNetworkByName(ctx context.Context, networkName string) error 25 | GetContainers(ctx context.Context) (DockerContainerList, error) 26 | GetFortaServiceContainers(ctx context.Context) (fortaContainers DockerContainerList, err error) 27 | GetContainerByName(ctx context.Context, name string) (*types.Container, error) 28 | GetContainerByID(ctx context.Context, id string) (*types.Container, error) 29 | StartContainer(ctx context.Context, config DockerContainerConfig) (*DockerContainer, error) 30 | StopContainer(ctx context.Context, id string) error 31 | InterruptContainer(ctx context.Context, id string) error 32 | TerminateContainer(ctx context.Context, id string) error 33 | RemoveContainer(ctx context.Context, containerID string) error 34 | WaitContainerExit(ctx context.Context, id string) error 35 | WaitContainerStart(ctx context.Context, id string) error 36 | Prune(ctx context.Context) error 37 | WaitContainerPrune(ctx context.Context, id string) error 38 | Nuke(ctx context.Context) error 39 | HasLocalImage(ctx context.Context, ref string) bool 40 | EnsureLocalImage(ctx context.Context, name, ref string) error 41 | GetContainerLogs(ctx context.Context, containerID, tail string, truncate int) (string, error) 42 | } 43 | 44 | // MessageClient receives and publishes messages. 45 | type MessageClient interface { 46 | Subscribe(subject string, handler interface{}) 47 | Publish(subject string, payload interface{}) 48 | PublishProto(subject string, payload proto.Message) 49 | } 50 | 51 | // AgentClient makes the gRPC requests to evaluate block and txs and receive results. 52 | type AgentClient interface { 53 | Dial(config.AgentConfig) error 54 | Invoke(ctx context.Context, method agentgrpc.Method, in, out interface{}, opts ...grpc.CallOption) error 55 | protocol.AgentClient 56 | io.Closer 57 | } 58 | 59 | // AlertAPIClient calls an http api on the analyzer to store alerts 60 | type AlertAPIClient interface { 61 | PostBatch(batch *domain.AlertBatchRequest, token string) (*domain.AlertBatchResponse, error) 62 | } 63 | -------------------------------------------------------------------------------- /tests/e2e/agents/txdetectoragent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "strings" 10 | 11 | "github.com/ethereum/go-ethereum/ethclient" 12 | "github.com/forta-network/forta-core-go/protocol" 13 | "github.com/forta-network/forta-node/config" 14 | "github.com/forta-network/forta-node/tests/e2e/ethaccounts" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | const ( 19 | AlertId = "EXPLOITER_TRANSACTION" 20 | ) 21 | 22 | func main() { 23 | lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", config.AgentGrpcPort)) 24 | if err != nil { 25 | panic(err) 26 | } 27 | server := grpc.NewServer() 28 | ethClient, err := ethclient.Dial( 29 | fmt.Sprintf("http://%s:%s", os.Getenv(config.EnvJsonRpcHost), os.Getenv(config.EnvJsonRpcPort)), 30 | ) 31 | if err != nil { 32 | panic(err) 33 | } 34 | protocol.RegisterAgentServer(server, &agentServer{ 35 | ethClient: ethClient, 36 | }) 37 | 38 | log.Println("Starting agent server...") 39 | log.Println(server.Serve(lis)) 40 | } 41 | 42 | type agentServer struct { 43 | ethClient *ethclient.Client 44 | protocol.UnimplementedAgentServer 45 | } 46 | 47 | func (as *agentServer) Initialize(context.Context, *protocol.InitializeRequest) (*protocol.InitializeResponse, error) { 48 | return &protocol.InitializeResponse{ 49 | Status: protocol.ResponseStatus_SUCCESS, 50 | }, nil 51 | } 52 | 53 | func (as *agentServer) EvaluateTx(ctx context.Context, txRequest *protocol.EvaluateTxRequest) (*protocol.EvaluateTxResponse, error) { 54 | response := &protocol.EvaluateTxResponse{ 55 | Status: protocol.ResponseStatus_SUCCESS, 56 | } 57 | // detect exploiter address transactions 58 | if strings.EqualFold(txRequest.Event.Transaction.From, ethaccounts.ExploiterAddress.Hex()) { 59 | balance, err := as.ethClient.BalanceAt(context.Background(), ethaccounts.ExploiterAddress, nil) 60 | if err != nil { 61 | return &protocol.EvaluateTxResponse{ 62 | Status: protocol.ResponseStatus_ERROR, 63 | Errors: []*protocol.Error{ 64 | { 65 | Message: fmt.Sprintf("failed to get balance: %v", err), 66 | }, 67 | }, 68 | }, nil 69 | } 70 | response.Findings = []*protocol.Finding{ 71 | { 72 | Protocol: "testchain", 73 | Severity: protocol.Finding_CRITICAL, 74 | AlertId: AlertId, 75 | Name: "Exploiter Transaction Detected", 76 | Description: txRequest.Event.Receipt.TransactionHash, 77 | Metadata: map[string]string{ 78 | "exploiter": ethaccounts.ExploiterAddress.Hex(), 79 | "balance": balance.String(), 80 | }, 81 | }, 82 | } 83 | } 84 | return response, nil 85 | } 86 | 87 | func (as *agentServer) EvaluateBlock(context.Context, *protocol.EvaluateBlockRequest) (*protocol.EvaluateBlockResponse, error) { 88 | return &protocol.EvaluateBlockResponse{ 89 | Status: protocol.ResponseStatus_SUCCESS, 90 | }, nil 91 | } 92 | -------------------------------------------------------------------------------- /services/scanner/api.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/forta-network/forta-core-go/feeds" 9 | "github.com/forta-network/forta-core-go/utils" 10 | "github.com/goccy/go-json" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/rs/cors" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // API allows triggering things on scanner 18 | type API struct { 19 | ctx context.Context 20 | started bool 21 | feed feeds.BlockFeed 22 | server *http.Server 23 | } 24 | 25 | type Message struct { 26 | Message string `json:"message"` 27 | } 28 | 29 | func message(str string) []byte { 30 | b, _ := json.Marshal(Message{Message: str}) 31 | return b 32 | } 33 | 34 | func writeError(w http.ResponseWriter, code int, str string) { 35 | w.WriteHeader(code) 36 | if _, err := w.Write(message(str)); err != nil { 37 | log.WithError(err).Errorf("error writing: %s", str) 38 | } 39 | } 40 | 41 | func writeMessage(w http.ResponseWriter, str string) { 42 | w.WriteHeader(200) 43 | if _, err := w.Write(message(str)); err != nil { 44 | log.WithError(err).Errorf("error writing: %s", str) 45 | } 46 | } 47 | 48 | func (a *API) startBlocks(w http.ResponseWriter, r *http.Request) { 49 | if a.feed.IsStarted() { 50 | writeMessage(w, "already started") 51 | } else { 52 | start := r.URL.Query().Get("start") 53 | start64, err := strconv.ParseInt(start, 10, 64) 54 | if err != nil { 55 | writeError(w, 400, "?start is required and must be integer") 56 | return 57 | } 58 | 59 | end := r.URL.Query().Get("end") 60 | end64, err := strconv.ParseInt(end, 10, 64) 61 | if err != nil { 62 | writeError(w, 400, "?end is required and must be integer") 63 | return 64 | } 65 | 66 | rate := r.URL.Query().Get("rate") 67 | rate64, err := strconv.ParseInt(rate, 10, 64) 68 | if err != nil { 69 | writeError(w, 400, "?end is required and must be integer") 70 | return 71 | } 72 | a.feed.StartRange(start64, end64, rate64) 73 | writeMessage(w, "ok") 74 | } 75 | } 76 | 77 | func (t *API) Start() error { 78 | router := mux.NewRouter().StrictSlash(true) 79 | router.HandleFunc("/start", t.startBlocks) 80 | 81 | c := cors.New(cors.Options{ 82 | AllowedOrigins: []string{"*"}, 83 | AllowCredentials: true, 84 | }) 85 | 86 | t.server = &http.Server{ 87 | Addr: ":80", 88 | Handler: c.Handler(router), 89 | } 90 | utils.GoListenAndServe(t.server) 91 | return nil 92 | } 93 | 94 | func (t *API) Stop() error { 95 | log.Infof("Stopping %s", t.Name()) 96 | if t.server != nil { 97 | return t.server.Close() 98 | } 99 | return nil 100 | } 101 | 102 | func (t *API) Name() string { 103 | return "ScannerAPI" 104 | } 105 | 106 | func NewScannerAPI(ctx context.Context, feed feeds.BlockFeed) *API { 107 | return &API{ 108 | ctx: ctx, 109 | feed: feed, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /clients/alert_sender.go: -------------------------------------------------------------------------------- 1 | package clients 2 | 3 | import ( 4 | "context" 5 | "github.com/ethereum/go-ethereum/accounts/keystore" 6 | "github.com/forta-network/forta-core-go/domain" 7 | "github.com/forta-network/forta-core-go/protocol" 8 | "github.com/forta-network/forta-core-go/security" 9 | "github.com/forta-network/forta-node/config" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // AgentRoundTrip contains 14 | type AgentRoundTrip struct { 15 | AgentConfig config.AgentConfig 16 | EvalBlockRequest *protocol.EvaluateBlockRequest 17 | EvalBlockResponse *protocol.EvaluateBlockResponse 18 | EvalTxRequest *protocol.EvaluateTxRequest 19 | EvalTxResponse *protocol.EvaluateTxResponse 20 | } 21 | 22 | type AlertSender interface { 23 | SignAlertAndNotify(rt *AgentRoundTrip, alert *protocol.Alert, chainID, blockNumber string, ts *domain.TrackingTimestamps) error 24 | NotifyWithoutAlert(rt *AgentRoundTrip, ts *domain.TrackingTimestamps) error 25 | } 26 | 27 | // PublishClient implements the interface for a notify 28 | type PublishClient interface { 29 | Notify(ctx context.Context, req *protocol.NotifyRequest) (*protocol.NotifyResponse, error) 30 | } 31 | 32 | type alertSender struct { 33 | ctx context.Context 34 | cfg AlertSenderConfig 35 | pClient PublishClient 36 | } 37 | 38 | type AlertSenderConfig struct { 39 | Key *keystore.Key 40 | } 41 | 42 | func (a *alertSender) SignAlertAndNotify(rt *AgentRoundTrip, alert *protocol.Alert, chainID, blockNumber string, ts *domain.TrackingTimestamps) error { 43 | alert.Scanner = &protocol.ScannerInfo{ 44 | Address: a.cfg.Key.Address.Hex(), 45 | } 46 | signedAlert, err := security.SignAlert(a.cfg.Key, alert) 47 | if err != nil { 48 | log.Errorf("could not sign alert (id=%s), skipping", alert.Id) 49 | return err 50 | } 51 | signedAlert.ChainId = chainID 52 | signedAlert.BlockNumber = blockNumber 53 | _, err = a.pClient.Notify(a.ctx, &protocol.NotifyRequest{ 54 | SignedAlert: signedAlert, 55 | EvalBlockRequest: rt.EvalBlockRequest, 56 | EvalBlockResponse: rt.EvalBlockResponse, 57 | EvalTxRequest: rt.EvalTxRequest, 58 | EvalTxResponse: rt.EvalTxResponse, 59 | AgentInfo: rt.AgentConfig.ToAgentInfo(), 60 | Timestamps: ts.ToMessage(), 61 | }) 62 | return err 63 | } 64 | 65 | func (a *alertSender) NotifyWithoutAlert(rt *AgentRoundTrip, ts *domain.TrackingTimestamps) error { 66 | _, err := a.pClient.Notify(a.ctx, &protocol.NotifyRequest{ 67 | EvalBlockRequest: rt.EvalBlockRequest, 68 | EvalBlockResponse: rt.EvalBlockResponse, 69 | EvalTxRequest: rt.EvalTxRequest, 70 | EvalTxResponse: rt.EvalTxResponse, 71 | AgentInfo: rt.AgentConfig.ToAgentInfo(), 72 | Timestamps: ts.ToMessage(), 73 | }) 74 | return err 75 | } 76 | 77 | func NewAlertSender(ctx context.Context, publisher PublishClient, cfg AlertSenderConfig) (*alertSender, error) { 78 | return &alertSender{ 79 | ctx: ctx, 80 | cfg: cfg, 81 | pClient: publisher, 82 | }, nil 83 | } 84 | -------------------------------------------------------------------------------- /services/supervisor/agent_logs.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/forta-network/forta-core-go/clients/agentlogs" 9 | "github.com/forta-network/forta-core-go/security" 10 | "github.com/forta-network/forta-node/clients" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // adjust these better with auto-upgrade later 15 | const ( 16 | defaultAgentLogSendInterval = time.Minute 17 | defaultAgentLogTailLines = 50 18 | defaultAgentLogAvgMaxCharsPerLine = 200 19 | ) 20 | 21 | func (sup *SupervisorService) syncAgentLogs() { 22 | ticker := time.NewTicker(defaultAgentLogSendInterval) 23 | for range ticker.C { 24 | err := sup.doSyncAgentLogs() 25 | sup.lastAgentLogsRequest.Set() 26 | sup.lastAgentLogsRequestError.Set(err) 27 | if err != nil { 28 | log.WithError(err).Warn("failed to sync agent logs") 29 | } 30 | } 31 | } 32 | 33 | func (sup *SupervisorService) doSyncAgentLogs() error { 34 | sup.mu.RLock() 35 | defer sup.mu.RUnlock() 36 | 37 | var ( 38 | sendLogs agentlogs.Agents 39 | keepLogs agentlogs.Agents 40 | ) 41 | for _, container := range sup.containers { 42 | if !container.IsAgent { 43 | continue 44 | } 45 | dockerContainer, err := sup.client.GetContainerByID(sup.ctx, container.ID) 46 | if err != nil { 47 | log.WithError(err).Warn("failed to get agent container") 48 | continue 49 | } 50 | if dockerContainer.Labels[clients.DockerLabelFortaSettingsAgentLogsEnable] != "true" { 51 | continue 52 | } 53 | logs, err := sup.client.GetContainerLogs( 54 | sup.ctx, container.ID, 55 | strconv.Itoa(defaultAgentLogTailLines), 56 | defaultAgentLogAvgMaxCharsPerLine*defaultAgentLogTailLines, 57 | ) 58 | if err != nil { 59 | log.WithError(err).Warn("failed to get agent container logs") 60 | continue 61 | } 62 | agent := &agentlogs.Agent{ 63 | ID: container.AgentConfig.ID, 64 | Logs: logs, 65 | } 66 | // don't send if it's the same with previous logs but keep it for next time 67 | // so we can check 68 | keepLogs = append(keepLogs, agent) 69 | if !sup.prevAgentLogs.Has(container.AgentConfig.ID, logs) { 70 | log.WithField("agent", agent.ID).Debug("new agent logs found") 71 | sendLogs = append(sendLogs, agent) 72 | } else { 73 | log.WithField("agent", agent.ID).Debug("no new agent logs") 74 | } 75 | } 76 | 77 | if len(sendLogs) > 0 { 78 | scannerJwt, err := security.CreateScannerJWT(sup.config.Key, map[string]interface{}{ 79 | "access": "agent_logs", 80 | }) 81 | if err != nil { 82 | return fmt.Errorf("failed to create scanner token: %v", err) 83 | } 84 | if err := sup.agentLogsClient.SendLogs(sendLogs, scannerJwt); err != nil { 85 | return fmt.Errorf("failed to send agent logs: %v", err) 86 | } 87 | log.WithField("count", len(sendLogs)).Debug("successfully sent new agent logs") 88 | } else { 89 | log.Debug("no new agent logs were found - not sending") 90 | } 91 | 92 | sup.prevAgentLogs = keepLogs 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Forta Network License 2 | ## Definitions 3 | 4 | **Additional Use Grant:** Any uses agreed upon under written contract with the Forta Foundation. 5 | 6 | **Change Date:** The earlier of January 1, 2026 or a date specified by the Forta Foundation. 7 | 8 | **Change License:** GNU General Public License 3.0 or later 9 | 10 | **Licensed Work:** `forta-node` located at https://github.com/forta-network/forta-node. The Licensed Work is Copyright (c) 2022 Forta Foundation 11 | 12 | **Licensor:** Forta Foundation 13 | 14 | ## Notice 15 | For information regarding licensing the Licensed Work under alternative licensing arrangements including Additional Use Grants, please see https://https://forta.org/. 16 | 17 | 18 | This Forta Network License (the “License”) does not meet the Open Source Definition as maintained by the Open Source Initiative, however the source code of the Licensed Work is made available under this License. 19 | 20 | ## Terms 21 | 22 | Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work in accordance with the terms of this License. The Licensor may make an Additional Use Grant, above, permitting limited production use. 23 | 24 | 25 | Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. 26 | 27 | 28 | If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. 29 | 30 | 31 | All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. 32 | 33 | 34 | You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. 35 | 36 | 37 | Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. 38 | 39 | 40 | This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). 41 | 42 | 43 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. -------------------------------------------------------------------------------- /services/scanner/tx_stream.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/forta-network/forta-core-go/clients/health" 8 | "github.com/forta-network/forta-core-go/domain" 9 | "github.com/forta-network/forta-core-go/ethereum" 10 | "github.com/forta-network/forta-core-go/feeds" 11 | "github.com/forta-network/forta-node/config" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // TxStreamService pulls TX info from providers and emits to channel 17 | type TxStreamService struct { 18 | cfg TxStreamServiceConfig 19 | ctx context.Context 20 | blockOutput chan *domain.BlockEvent 21 | txOutput chan *domain.TransactionEvent 22 | txFeed feeds.TransactionFeed 23 | 24 | lastBlockActivity health.TimeTracker 25 | lastTxActivity health.TimeTracker 26 | } 27 | 28 | type TxStreamServiceConfig struct { 29 | JsonRpcConfig config.JsonRpcConfig 30 | TraceJsonRpcConfig config.JsonRpcConfig 31 | SkipBlocksOlderThan *time.Duration 32 | } 33 | 34 | func (t *TxStreamService) ReadOnlyBlockStream() <-chan *domain.BlockEvent { 35 | return t.blockOutput 36 | } 37 | 38 | func (t *TxStreamService) ReadOnlyTxStream() <-chan *domain.TransactionEvent { 39 | return t.txOutput 40 | } 41 | 42 | func (t *TxStreamService) handleBlock(evt *domain.BlockEvent) error { 43 | t.blockOutput <- evt 44 | t.lastBlockActivity.Set() 45 | return nil 46 | } 47 | 48 | func (t *TxStreamService) handleTx(evt *domain.TransactionEvent) error { 49 | t.txOutput <- evt 50 | t.lastTxActivity.Set() 51 | return nil 52 | } 53 | 54 | func (t *TxStreamService) Start() error { 55 | log.Infof("Starting %s", t.Name()) 56 | go func() { 57 | if err := t.txFeed.ForEachTransaction(t.handleBlock, t.handleTx); err != nil { 58 | log.WithError(err).Panic("tx feed error") 59 | } 60 | }() 61 | return nil 62 | } 63 | 64 | func (t *TxStreamService) Stop() error { 65 | log.Infof("Stopping %s", t.Name()) 66 | if t.txOutput != nil { 67 | close(t.txOutput) 68 | } 69 | if t.blockOutput != nil { 70 | close(t.blockOutput) 71 | } 72 | return nil 73 | } 74 | 75 | func (t *TxStreamService) Name() string { 76 | return "tx-stream" 77 | } 78 | 79 | // Health implements health.Reporter interface. 80 | func (t *TxStreamService) Health() health.Reports { 81 | return health.Reports{ 82 | t.lastBlockActivity.GetReport("event.block.time"), 83 | t.lastTxActivity.GetReport("event.transaction.time"), 84 | } 85 | } 86 | 87 | func NewTxStreamService(ctx context.Context, ethClient ethereum.Client, blockFeed feeds.BlockFeed, cfg TxStreamServiceConfig) (*TxStreamService, error) { 88 | txOutput := make(chan *domain.TransactionEvent) 89 | blockOutput := make(chan *domain.BlockEvent) 90 | 91 | txFeed, err := feeds.NewTransactionFeed(ctx, ethClient, blockFeed, cfg.SkipBlocksOlderThan, 10) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return &TxStreamService{ 97 | cfg: cfg, 98 | ctx: ctx, 99 | blockOutput: blockOutput, 100 | txOutput: txOutput, 101 | txFeed: txFeed, 102 | }, nil 103 | } 104 | -------------------------------------------------------------------------------- /cmd/ens.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "path" 6 | "time" 7 | 8 | "github.com/forta-network/forta-core-go/ens" 9 | "github.com/forta-network/forta-node/store" 10 | "github.com/goccy/go-json" 11 | ) 12 | 13 | const ( 14 | contractAddressCacheExpiry = time.Hour 15 | ) 16 | 17 | // useEnsDefaults gets and uses ENS defaults if needed. 18 | func useEnsDefaults() error { 19 | if cfg.Registry.ContractAddress != "" { 20 | return nil 21 | } 22 | 23 | return ensureLatestContractAddresses() 24 | } 25 | 26 | // useEnsAgentReg finds the agent registry from a contract. 27 | func useEnsAgentReg() error { 28 | return ensureLatestContractAddresses() 29 | } 30 | 31 | func ensureLatestContractAddresses() error { 32 | now := time.Now().UTC() 33 | 34 | cache, ok := getContractAddressCache() 35 | if ok && now.Before(cache.ExpiresAt) { 36 | setContractAddressesFromCache(cache) 37 | return nil 38 | } 39 | 40 | whiteBold("Refreshing contract address cache...\n") 41 | 42 | if cfg.ENSConfig.DefaultContract { 43 | cfg.ENSConfig.ContractAddress = "" 44 | } 45 | es, err := ens.DialENSStoreAt(cfg.ENSConfig.JsonRpc.Url, cfg.ENSConfig.ContractAddress) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | contracts, err := es.ResolveRegistryContracts() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | cache.Dispatch = contracts.Dispatch.Hex() 56 | cache.Agents = contracts.AgentRegistry.Hex() 57 | cache.ScannerVersion = contracts.ScannerNodeVersion.Hex() 58 | cache.ExpiresAt = time.Now().UTC().Add(contractAddressCacheExpiry) 59 | 60 | b, err := json.MarshalIndent(&cache, "", " ") // indent by two spaces 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if err := ioutil.WriteFile(path.Join(cfg.FortaDir, "contracts.json"), b, 0644); err != nil { 66 | return err 67 | } 68 | 69 | setContractAddressesFromCache(cache) 70 | return nil 71 | } 72 | 73 | // sets only if not overridden 74 | func setContractAddressesFromCache(cache contractAddressCache) { 75 | if cfg.Registry.ContractAddress == "" { 76 | cfg.Registry.ContractAddress = cache.Dispatch 77 | } 78 | cfg.AgentRegistryContractAddress = cache.Agents 79 | } 80 | 81 | type contractAddressCache struct { 82 | Dispatch string `json:"dispatch"` 83 | Agents string `json:"agents"` 84 | ScannerVersion string `json:"scannerVersion"` 85 | ExpiresAt time.Time `json:"expiresAt"` 86 | } 87 | 88 | func getContractAddressCache() (cache contractAddressCache, ok bool) { 89 | b, err := ioutil.ReadFile(path.Join(cfg.FortaDir, "contracts.json")) 90 | if err != nil { 91 | return 92 | } 93 | 94 | if err := json.Unmarshal(b, &cache); err != nil { 95 | return 96 | } 97 | 98 | ok = true 99 | return 100 | } 101 | 102 | func overrideEns() error { 103 | ensStore, err := store.NewENSOverrideStore(cfg) 104 | if err != nil { 105 | return err 106 | } 107 | contracts, err := ensStore.ResolveRegistryContracts() 108 | if err != nil { 109 | return err 110 | } 111 | cfg.Registry.ContractAddress = contracts.Dispatch.Hex() 112 | cfg.AgentRegistryContractAddress = contracts.AgentRegistry.Hex() 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /clients/agentgrpc/encoding_bench_test.go: -------------------------------------------------------------------------------- 1 | package agentgrpc_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "testing" 10 | "time" 11 | 12 | "github.com/forta-network/forta-core-go/protocol" 13 | "github.com/forta-network/forta-node/clients/agentgrpc" 14 | "github.com/forta-network/forta-node/config" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | var ( 19 | benchBlockMsg = &protocol.EvaluateBlockRequest{} 20 | benchTxMsg = &protocol.EvaluateTxRequest{} 21 | ) 22 | 23 | func init() { 24 | b, err := ioutil.ReadFile("./testdata/bench_block.json") 25 | if err != nil { 26 | panic(err) 27 | } 28 | if err := json.Unmarshal(b, &benchBlockMsg.Event); err != nil { 29 | panic(err) 30 | } 31 | b, err = ioutil.ReadFile("./testdata/bench_tx.json") 32 | if err != nil { 33 | panic(err) 34 | } 35 | if err := json.Unmarshal(b, &benchTxMsg.Event); err != nil { 36 | panic(err) 37 | } 38 | } 39 | 40 | const benchAgentReqCount = 25 41 | 42 | func getBenchClient() *agentgrpc.Client { 43 | agentClient := agentgrpc.NewClient() 44 | for { 45 | conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", config.AgentGrpcPort), grpc.WithInsecure()) 46 | if err == nil { 47 | agentClient.WithConn(conn) 48 | var success bool 49 | _, err1 := agentClient.EvaluateBlock(context.Background(), benchBlockMsg) 50 | _, err2 := agentClient.EvaluateTx(context.Background(), benchTxMsg) 51 | success = (err1 == nil) && (err2 == nil) 52 | if success { 53 | break 54 | } 55 | } 56 | time.Sleep(time.Second * 2) 57 | log.Println("retrying to connect to grpc server") 58 | } 59 | 60 | return agentClient 61 | } 62 | 63 | func BenchmarkEvaluateBlock(b *testing.B) { 64 | agentClient := getBenchClient() 65 | for i := 0; i < b.N; i++ { 66 | for j := 0; j < benchAgentReqCount; j++ { 67 | out, err := agentClient.EvaluateBlock(context.Background(), benchBlockMsg) 68 | if err != nil { 69 | panic(err) 70 | } 71 | _ = out 72 | } 73 | } 74 | } 75 | 76 | func BenchmarkEvaluateBlockWithPreparedMessage(b *testing.B) { 77 | agentClient := getBenchClient() 78 | for i := 0; i < b.N; i++ { 79 | preparedMsg, err := agentgrpc.EncodeMessage(benchBlockMsg) 80 | if err != nil { 81 | panic(err) 82 | } 83 | for j := 0; j < benchAgentReqCount; j++ { 84 | var resp protocol.EvaluateBlockResponse 85 | err := agentClient.Invoke(context.Background(), agentgrpc.MethodEvaluateBlock, preparedMsg, &resp) 86 | if err != nil { 87 | panic(err) 88 | } 89 | } 90 | } 91 | } 92 | 93 | func BenchmarkEvaluateTx(b *testing.B) { 94 | agentClient := getBenchClient() 95 | for i := 0; i < b.N; i++ { 96 | for j := 0; j < benchAgentReqCount; j++ { 97 | out, err := agentClient.EvaluateTx(context.Background(), benchTxMsg) 98 | if err != nil { 99 | panic(err) 100 | } 101 | _ = out 102 | } 103 | } 104 | } 105 | 106 | func BenchmarkEvaluateTxWithPreparedMessage(b *testing.B) { 107 | agentClient := getBenchClient() 108 | for i := 0; i < b.N; i++ { 109 | preparedMsg, err := agentgrpc.EncodeMessage(benchTxMsg) 110 | if err != nil { 111 | panic(err) 112 | } 113 | for j := 0; j < benchAgentReqCount; j++ { 114 | var resp protocol.EvaluateTxResponse 115 | err := agentClient.Invoke(context.Background(), agentgrpc.MethodEvaluateTx, preparedMsg, &resp) 116 | if err != nil { 117 | panic(err) 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /store/container.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/forta-network/forta-core-go/release" 11 | "github.com/goccy/go-json" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/forta-network/forta-node/config" 15 | ) 16 | 17 | const defaultImageCheckInterval = time.Second * 5 18 | 19 | // FortaImageStore keeps track of the latest Forta node image. 20 | type FortaImageStore interface { 21 | Latest() <-chan ImageRefs 22 | EmbeddedImageRefs() ImageRefs 23 | } 24 | 25 | // ImageRefs contains the latest image references. 26 | type ImageRefs struct { 27 | Supervisor string 28 | Updater string 29 | ReleaseInfo *release.ReleaseInfo 30 | } 31 | 32 | type fortaImageStore struct { 33 | updaterPort string 34 | latestCh chan ImageRefs 35 | latestImgs ImageRefs 36 | } 37 | 38 | // NewFortaImageStore creates a new store. 39 | func NewFortaImageStore(ctx context.Context, updaterPort string, autoUpdate bool) (*fortaImageStore, error) { 40 | store := &fortaImageStore{ 41 | updaterPort: updaterPort, 42 | latestCh: make(chan ImageRefs), 43 | } 44 | if autoUpdate { 45 | go store.loop(ctx) 46 | } 47 | return store, nil 48 | } 49 | 50 | func (store *fortaImageStore) loop(ctx context.Context) { 51 | store.check(ctx) 52 | ticker := time.NewTicker(defaultImageCheckInterval) 53 | for { 54 | select { 55 | case <-ctx.Done(): 56 | return 57 | case <-ticker.C: 58 | store.check(ctx) 59 | } 60 | } 61 | } 62 | 63 | func (store *fortaImageStore) EmbeddedImageRefs() ImageRefs { 64 | return ImageRefs{ 65 | Supervisor: config.DockerSupervisorImage, 66 | Updater: config.DockerUpdaterImage, 67 | ReleaseInfo: config.GetBuildReleaseInfo(), 68 | } 69 | } 70 | 71 | func (store *fortaImageStore) check(ctx context.Context) { 72 | latestReleaseInfo, err := store.getFromUpdater(ctx) 73 | if err != nil { 74 | log.WithError(err).Warn("failed to get the latest release from the updater") 75 | } 76 | 77 | if latestReleaseInfo == nil { 78 | return 79 | } 80 | 81 | serviceImgs := latestReleaseInfo.Manifest.Release.Services 82 | if serviceImgs.Supervisor != store.latestImgs.Supervisor || serviceImgs.Updater != store.latestImgs.Updater { 83 | log.WithField("commit", latestReleaseInfo.Manifest.Release.Commit).Info("got newer release from updater") 84 | 85 | store.latestImgs = ImageRefs{ 86 | Supervisor: serviceImgs.Supervisor, 87 | Updater: serviceImgs.Updater, 88 | ReleaseInfo: latestReleaseInfo, 89 | } 90 | store.latestCh <- store.latestImgs 91 | } 92 | } 93 | 94 | func (store *fortaImageStore) getFromUpdater(ctx context.Context) (*release.ReleaseInfo, error) { 95 | resp, err := http.Get(fmt.Sprintf("http://localhost:%s", store.updaterPort)) 96 | if err != nil { 97 | return nil, err 98 | } 99 | respBody, err := io.ReadAll(resp.Body) 100 | if err != nil { 101 | return nil, err 102 | } 103 | if resp.StatusCode == http.StatusNotFound { // 404 == not ready yet 104 | return nil, nil 105 | } 106 | if resp.StatusCode != http.StatusOK { 107 | return nil, fmt.Errorf("unexpected updater response with code %d: %s", resp.StatusCode, string(respBody)) 108 | } 109 | var releaseInfo release.ReleaseInfo 110 | return &releaseInfo, json.Unmarshal(respBody, &releaseInfo) 111 | } 112 | 113 | // Latest returns a channel that provides the latest image reference. 114 | func (store *fortaImageStore) Latest() <-chan ImageRefs { 115 | return store.latestCh 116 | } 117 | -------------------------------------------------------------------------------- /services/registry/registry_test.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "golang.org/x/sync/semaphore" 8 | 9 | "github.com/forta-network/forta-node/clients/messaging" 10 | mock_clients "github.com/forta-network/forta-node/clients/mocks" 11 | "github.com/forta-network/forta-node/config" 12 | mock_store "github.com/forta-network/forta-node/store/mocks" 13 | 14 | "github.com/forta-network/forta-node/services/registry/regtypes" 15 | 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/golang/mock/gomock" 18 | "github.com/stretchr/testify/require" 19 | "github.com/stretchr/testify/suite" 20 | ) 21 | 22 | const ( 23 | testScannerAddressStr = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" 24 | testAgentIDStr = "0x2000000000000000000000000000000000000000000000000000000000000000" 25 | testAgentRef = "QmWacxPov5FVCyvnpXroDJ76urakzN4ckpFhhRzpsAkRek" 26 | testImageRef = "bafybeide7cspdmxqjcpa3qvrayvfpiix2it4v6mjejjc22q72zbq7rm4re@sha256:cdd4ddccf5e9c740eb4144bcc68e3ea3a056789ec7453e94a6416dcfc80937a4" 27 | testContainerRegistry = "some.reg.io" 28 | ) 29 | 30 | var ( 31 | testScannerAddress = common.HexToAddress(testScannerAddressStr) 32 | testAgentFile = ®types.AgentFile{} 33 | ) 34 | 35 | // TestSuite runs the test suite. 36 | func TestSuite(t *testing.T) { 37 | testAgentFile.Manifest.ImageReference = testImageRef 38 | 39 | suite.Run(t, &Suite{}) 40 | } 41 | 42 | // Suite is a test suite to test the tx node runner implementation. 43 | type Suite struct { 44 | r *require.Assertions 45 | 46 | registryStore *mock_store.MockRegistryStore 47 | msgClient *mock_clients.MockMessageClient 48 | 49 | service *RegistryService 50 | 51 | suite.Suite 52 | } 53 | 54 | // SetupTest sets up the test. 55 | func (s *Suite) SetupTest() { 56 | s.r = require.New(s.T()) 57 | s.registryStore = mock_store.NewMockRegistryStore(gomock.NewController(s.T())) 58 | s.msgClient = mock_clients.NewMockMessageClient(gomock.NewController(s.T())) 59 | s.service = &RegistryService{ 60 | scannerAddress: testScannerAddress, 61 | msgClient: s.msgClient, 62 | registryStore: s.registryStore, 63 | done: make(chan struct{}), 64 | sem: semaphore.NewWeighted(1), 65 | } 66 | s.service.cfg.Registry.ContainerRegistry = testContainerRegistry 67 | } 68 | 69 | type agentConfigs []*config.AgentConfig 70 | 71 | func (ac agentConfigs) Matches(x interface{}) bool { 72 | acx, ok := x.([]*config.AgentConfig) 73 | if !ok { 74 | return false 75 | } 76 | 77 | if len(ac) != len(acx) { 78 | return false 79 | } 80 | 81 | for i, agent1 := range ac { 82 | agent2 := acx[i] 83 | if !(agent1.ID == agent2.ID && agent1.Image == agent2.Image) { 84 | return false 85 | } 86 | } 87 | return true 88 | } 89 | 90 | func (ac agentConfigs) String() string { 91 | return fmt.Sprintf("%+v", ([]*config.AgentConfig)(ac)) 92 | } 93 | 94 | func (s *Suite) TestPublishChanges() { 95 | configs := (agentConfigs)([]*config.AgentConfig{ 96 | { 97 | ID: testAgentIDStr, 98 | Image: fmt.Sprintf("%s/%s", testContainerRegistry, testImageRef), 99 | }, 100 | }) 101 | 102 | s.registryStore.EXPECT().GetAgentsIfChanged(s.service.scannerAddress.Hex()).Return(configs, true, nil) 103 | s.msgClient.EXPECT().Publish(messaging.SubjectAgentsVersionsLatest, configs) 104 | 105 | s.NoError(s.service.publishLatestAgents()) 106 | } 107 | 108 | func (s *Suite) TestDoNotPublishChanges() { 109 | s.registryStore.EXPECT().GetAgentsIfChanged(s.service.scannerAddress.Hex()).Return(nil, false, nil) 110 | s.NoError(s.service.publishLatestAgents()) 111 | } 112 | -------------------------------------------------------------------------------- /clients/messaging/client.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/forta-network/forta-core-go/protocol" 8 | "github.com/goccy/go-json" 9 | "github.com/golang/protobuf/proto" 10 | "github.com/nats-io/nats.go" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // Notification and client globals 15 | var ( 16 | BufferSize = 1000 17 | ) 18 | 19 | // Client wraps the NATS client to publish and receive our messages. 20 | type Client struct { 21 | logger *log.Entry 22 | nc *nats.Conn 23 | } 24 | 25 | // NewClient creates and starts a new client. 26 | func NewClient(name, natsURL string) *Client { 27 | logger := log.WithField("name", fmt.Sprintf("%s/messaging", name)).WithField("nats", natsURL) 28 | logger.Infof("connecting to: %s", natsURL) 29 | var ( 30 | nc *nats.Conn 31 | err error 32 | ) 33 | for i := 0; i < 10; i++ { 34 | nc, err = nats.Connect(natsURL) 35 | if err == nil { 36 | break 37 | } 38 | logger.WithError(err).Error("failed to connect to nats server") 39 | time.Sleep(time.Second * 1) // don't retry too quickly - maybe it's not up yet 40 | } 41 | if err != nil { 42 | logger.Panic(err) 43 | } 44 | logger.Info("successfully connected") 45 | client := &Client{ 46 | logger: logger, 47 | nc: nc, 48 | } 49 | return client 50 | } 51 | 52 | // AgentsHandler handles agents.* subjects. 53 | type AgentsHandler func(AgentPayload) error 54 | type AgentMetricHandler func(*protocol.AgentMetricList) error 55 | type ScannerHandler func(ScannerPayload) error 56 | 57 | // Subscribe subscribes the consumer to this client. 58 | func (client *Client) Subscribe(subject string, handler interface{}) { 59 | // TODO: Configure redelivery options somehow. 60 | logger := client.logger.WithField("subject", subject) 61 | _, err := client.nc.Subscribe(subject, func(m *nats.Msg) { 62 | logger.Debugf("received: %s", string(m.Data)) 63 | 64 | var err error 65 | switch h := handler.(type) { 66 | case AgentsHandler: 67 | var payload AgentPayload 68 | err = json.Unmarshal(m.Data, &payload) 69 | if err != nil { 70 | break 71 | } 72 | err = h(payload) 73 | 74 | case AgentMetricHandler: 75 | var payload protocol.AgentMetricList 76 | err = proto.Unmarshal(m.Data, &payload) 77 | if err != nil { 78 | break 79 | } 80 | err = h(&payload) 81 | 82 | case ScannerHandler: 83 | var payload ScannerPayload 84 | err = json.Unmarshal(m.Data, &payload) 85 | if err != nil { 86 | break 87 | } 88 | err = h(payload) 89 | 90 | default: 91 | logger.Panicf("no handler found") 92 | } 93 | 94 | if err != nil { 95 | if err := m.Nak(); err != nil { 96 | logger.Errorf("failed to send nak: %v", err) 97 | } 98 | logger.Errorf("failed to handle msg: %v", err) 99 | } 100 | }) 101 | if err != nil { 102 | logger.Panicf("failed to subscribe: %v", err) 103 | } 104 | logger.Info("subscribed") 105 | } 106 | 107 | // Publish publishes new messages. 108 | func (client *Client) Publish(subject string, payload interface{}) { 109 | logger := client.logger.WithField("subject", subject) 110 | data, _ := json.Marshal(payload) 111 | if err := client.nc.Publish(subject, data); err != nil { 112 | logger.Errorf("failed to publish msg: %v", err) 113 | } 114 | logger.Debugf("published: %s", string(data)) 115 | } 116 | 117 | // PublishProto publishes new messages. 118 | func (client *Client) PublishProto(subject string, payload proto.Message) { 119 | logger := client.logger.WithField("subject", subject) 120 | data, _ := proto.Marshal(payload) 121 | if err := client.nc.Publish(subject, data); err != nil { 122 | logger.Errorf("failed to publish msg: %v", err) 123 | } 124 | logger.Debugf("published: %s", string(data)) 125 | } 126 | -------------------------------------------------------------------------------- /tests/e2e/.ipfs/config: -------------------------------------------------------------------------------- 1 | { 2 | "Identity": { 3 | "PeerID": "12D3KooWQnSGodX2FKUriXDoBMsnUPeT8VY4JKGcnBHoyZbFr2Et", 4 | "PrivKey": "CAESQPMUlTm0g5Lr56yX8RkPTJFFSpMZxrCsN4jvju8FO2e13mDIgjdS+DpO6sWjMqqe3vhkmx/u8zXKF4DdvFn5KhE=" 5 | }, 6 | "Datastore": { 7 | "StorageMax": "10GB", 8 | "StorageGCWatermark": 90, 9 | "GCPeriod": "1h", 10 | "Spec": { 11 | "mounts": [ 12 | { 13 | "child": { 14 | "path": "blocks", 15 | "shardFunc": "/repo/flatfs/shard/v1/next-to-last/2", 16 | "sync": true, 17 | "type": "flatfs" 18 | }, 19 | "mountpoint": "/blocks", 20 | "prefix": "flatfs.datastore", 21 | "type": "measure" 22 | }, 23 | { 24 | "child": { 25 | "compression": "none", 26 | "path": "datastore", 27 | "type": "levelds" 28 | }, 29 | "mountpoint": "/", 30 | "prefix": "leveldb.datastore", 31 | "type": "measure" 32 | } 33 | ], 34 | "type": "mount" 35 | }, 36 | "HashOnRead": false, 37 | "BloomFilterSize": 0 38 | }, 39 | "Addresses": { 40 | "Swarm": [ 41 | "/ip4/0.0.0.0/tcp/4001", 42 | "/ip6/::/tcp/4001", 43 | "/ip4/0.0.0.0/udp/4001/quic", 44 | "/ip6/::/udp/4001/quic" 45 | ], 46 | "Announce": [], 47 | "NoAnnounce": [], 48 | "API": "/ip4/0.0.0.0/tcp/5001", 49 | "Gateway": "/ip4/0.0.0.0/tcp/8080" 50 | }, 51 | "Mounts": { 52 | "IPFS": "/ipfs", 53 | "IPNS": "/ipns", 54 | "FuseAllowOther": false 55 | }, 56 | "Discovery": { 57 | "MDNS": { 58 | "Enabled": false, 59 | "Interval": 10 60 | } 61 | }, 62 | "Routing": { 63 | "Type": "none" 64 | }, 65 | "Ipns": { 66 | "RepublishPeriod": "", 67 | "RecordLifetime": "", 68 | "ResolveCacheSize": 128 69 | }, 70 | "Bootstrap": [], 71 | "Gateway": { 72 | "HTTPHeaders": { 73 | "Access-Control-Allow-Headers": [ 74 | "X-Requested-With", 75 | "Range", 76 | "User-Agent" 77 | ], 78 | "Access-Control-Allow-Methods": [ 79 | "GET" 80 | ], 81 | "Access-Control-Allow-Origin": [ 82 | "*" 83 | ] 84 | }, 85 | "RootRedirect": "", 86 | "Writable": false, 87 | "PathPrefixes": [], 88 | "APICommands": [], 89 | "NoFetch": true, 90 | "NoDNSLink": true, 91 | "PublicGateways": null 92 | }, 93 | "API": { 94 | "HTTPHeaders": {} 95 | }, 96 | "Swarm": { 97 | "AddrFilters": null, 98 | "DisableBandwidthMetrics": false, 99 | "DisableNatPortMap": false, 100 | "EnableRelayHop": false, 101 | "EnableAutoRelay": false, 102 | "Transports": { 103 | "Network": {}, 104 | "Security": {}, 105 | "Multiplexers": {} 106 | }, 107 | "ConnMgr": { 108 | "Type": "basic", 109 | "LowWater": 600, 110 | "HighWater": 900, 111 | "GracePeriod": "20s" 112 | } 113 | }, 114 | "AutoNAT": {}, 115 | "Pubsub": { 116 | "Router": "", 117 | "DisableSigning": false 118 | }, 119 | "Peering": { 120 | "Peers": null 121 | }, 122 | "DNS": { 123 | "Resolvers": {} 124 | }, 125 | "Migration": { 126 | "DownloadSources": [], 127 | "Keep": "" 128 | }, 129 | "Provider": { 130 | "Strategy": "" 131 | }, 132 | "Reprovider": { 133 | "Interval": "12h", 134 | "Strategy": "all" 135 | }, 136 | "Experimental": { 137 | "FilestoreEnabled": false, 138 | "UrlstoreEnabled": false, 139 | "ShardingEnabled": false, 140 | "GraphsyncEnabled": false, 141 | "Libp2pStreamMounting": false, 142 | "P2pHttpProxy": false, 143 | "StrategicProviding": false, 144 | "AcceleratedDHTClient": false 145 | }, 146 | "Plugins": { 147 | "Plugins": null 148 | }, 149 | "Pinning": { 150 | "RemoteServices": {} 151 | }, 152 | "Internal": {} 153 | } 154 | -------------------------------------------------------------------------------- /cmd/updater/updater.go: -------------------------------------------------------------------------------- 1 | package updater 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "math/big" 10 | "path" 11 | "time" 12 | 13 | "github.com/forta-network/forta-core-go/registry" 14 | "github.com/forta-network/forta-core-go/release" 15 | 16 | "github.com/forta-network/forta-core-go/clients/health" 17 | "github.com/forta-network/forta-core-go/utils" 18 | "github.com/forta-network/forta-node/config" 19 | "github.com/forta-network/forta-node/healthutils" 20 | "github.com/forta-network/forta-node/services" 21 | "github.com/forta-network/forta-node/services/updater" 22 | "github.com/forta-network/forta-node/store" 23 | log "github.com/sirupsen/logrus" 24 | ) 25 | 26 | const minUpdateInterval = 1 * time.Minute 27 | const maxUpdateInterval = 1 * time.Hour 28 | 29 | func generateIntervalMs(addr string) int64 { 30 | interval := big.NewInt(0) 31 | interval.Mod(utils.ScannerIDHexToBigInt(addr), big.NewInt((maxUpdateInterval).Milliseconds())) 32 | return interval.Int64() + minUpdateInterval.Milliseconds() 33 | } 34 | 35 | type keyAddress struct { 36 | Address string `json:"address"` 37 | } 38 | 39 | func loadAddressFromKeyFile() (string, error) { 40 | files, err := ioutil.ReadDir(config.DefaultContainerKeyDirPath) 41 | if err != nil { 42 | return "", err 43 | } 44 | 45 | if len(files) != 1 { 46 | return "", errors.New("there must be only one key in key directory") 47 | } 48 | 49 | b, err := ioutil.ReadFile(path.Join(config.DefaultContainerKeyDirPath, files[0].Name())) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | var addr keyAddress 55 | if err := json.Unmarshal(b, &addr); err != nil { 56 | return "", err 57 | } 58 | 59 | return fmt.Sprintf("0x%s", addr.Address), nil 60 | } 61 | 62 | func initServices(ctx context.Context, cfg config.Config) ([]services.Service, error) { 63 | cfg.Registry.JsonRpc.Url = utils.ConvertToDockerHostURL(cfg.Registry.JsonRpc.Url) 64 | cfg.Registry.IPFS.APIURL = utils.ConvertToDockerHostURL(cfg.Registry.IPFS.APIURL) 65 | cfg.Registry.IPFS.GatewayURL = utils.ConvertToDockerHostURL(cfg.Registry.IPFS.GatewayURL) 66 | 67 | rc, err := release.NewClient(cfg.Registry.IPFS.GatewayURL) 68 | if err != nil { 69 | return nil, err 70 | } 71 | rg, err := store.GetRegistryClient(ctx, cfg, registry.ClientConfig{ 72 | JsonRpcUrl: cfg.Registry.JsonRpc.Url, 73 | ENSAddress: cfg.ENSConfig.ContractAddress, 74 | Name: "updater", 75 | }) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | developmentMode := utils.ParseBoolEnvVar(config.EnvDevelopment) 81 | 82 | log.WithFields(log.Fields{ 83 | "developmentMode": developmentMode, 84 | }).Info("updater modes") 85 | 86 | address, err := loadAddressFromKeyFile() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | intervalMs := generateIntervalMs(address) 92 | updateDelay := int(intervalMs / 1000) 93 | if cfg.AutoUpdate.UpdateDelay != nil { 94 | updateDelay = *cfg.AutoUpdate.UpdateDelay 95 | } 96 | 97 | updaterService := updater.NewUpdaterService( 98 | ctx, rg, rc, config.DefaultContainerPort, 99 | developmentMode, updateDelay, 100 | ) 101 | 102 | return []services.Service{ 103 | health.NewService( 104 | ctx, "", healthutils.DefaultHealthServerErrHandler, 105 | health.CheckerFrom(summarizeReports, updaterService), 106 | ), 107 | updaterService, 108 | }, nil 109 | } 110 | 111 | func summarizeReports(reports health.Reports) *health.Report { 112 | summary := health.NewSummary() 113 | 114 | checkedErr, ok := reports.NameContains("event.checked.error") 115 | if !ok { 116 | summary.Fail() 117 | return summary.Finish() 118 | } 119 | if len(checkedErr.Details) > 0 { 120 | summary.Addf("auto-updater is failing to check new versions with error '%s'", checkedErr.Details) 121 | summary.Status(health.StatusFailing) 122 | } 123 | 124 | checkedTime, ok := reports.NameContains("event.checked.time") 125 | if ok { 126 | t, ok := checkedTime.Time() 127 | if ok { 128 | checkDelay := time.Since(*t) 129 | if checkDelay > time.Minute*10 { 130 | summary.Addf("and late for %d minutes", int64(checkDelay.Minutes())) 131 | summary.Status(health.StatusFailing) 132 | } 133 | } 134 | } 135 | summary.Punc(".") 136 | return summary.Finish() 137 | } 138 | 139 | func Run() { 140 | services.ContainerMain("updater", initServices) 141 | } 142 | -------------------------------------------------------------------------------- /metrics/agent_metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/forta-network/forta-core-go/domain" 5 | "time" 6 | 7 | "github.com/forta-network/forta-core-go/protocol" 8 | "github.com/forta-network/forta-node/clients" 9 | "github.com/forta-network/forta-node/clients/messaging" 10 | "github.com/forta-network/forta-node/config" 11 | ) 12 | 13 | const ( 14 | MetricFinding = "finding" 15 | MetricTxRequest = "tx.request" 16 | MetricTxLatency = "tx.latency" 17 | MetricTxError = "tx.error" 18 | MetricTxSuccess = "tx.success" 19 | MetricTxDrop = "tx.drop" 20 | MetricTxBlockAge = "tx.block.age" 21 | MetricTxEventAge = "tx.event.age" 22 | MetricBlockBlockAge = "block.block.age" 23 | MetricBlockEventAge = "block.event.age" 24 | MetricBlockRequest = "block.request" 25 | MetricBlockLatency = "block.latency" 26 | MetricBlockError = "block.error" 27 | MetricBlockSuccess = "block.success" 28 | MetricBlockDrop = "block.drop" 29 | MetricStop = "agent.stop" 30 | MetricJSONRPCLatency = "jsonrpc.latency" 31 | MetricJSONRPCRequest = "jsonrpc.request" 32 | MetricJSONRPCSuccess = "jsonrpc.success" 33 | MetricJSONRPCThrottled = "jsonrpc.throttled" 34 | MetricFindingsDropped = "findings.dropped" 35 | ) 36 | 37 | func SendAgentMetrics(client clients.MessageClient, ms []*protocol.AgentMetric) { 38 | if len(ms) > 0 { 39 | client.PublishProto(messaging.SubjectMetricAgent, &protocol.AgentMetricList{ 40 | Metrics: ms, 41 | }) 42 | } 43 | } 44 | 45 | func CreateAgentMetric(agentID, metric string, value float64) *protocol.AgentMetric { 46 | return &protocol.AgentMetric{ 47 | AgentId: agentID, 48 | Timestamp: time.Now().Format(time.RFC3339), 49 | Name: metric, 50 | Value: value, 51 | } 52 | } 53 | 54 | func createMetrics(agentID, timestamp string, metricMap map[string]float64) []*protocol.AgentMetric { 55 | var res []*protocol.AgentMetric 56 | for name, value := range metricMap { 57 | res = append(res, &protocol.AgentMetric{ 58 | AgentId: agentID, 59 | Timestamp: timestamp, 60 | Name: name, 61 | Value: value, 62 | }) 63 | } 64 | return res 65 | } 66 | 67 | func durationMs(from time.Time, to time.Time) float64 { 68 | return float64(to.Sub(from).Milliseconds()) 69 | } 70 | 71 | func GetBlockMetrics(agt config.AgentConfig, resp *protocol.EvaluateBlockResponse, times *domain.TrackingTimestamps) []*protocol.AgentMetric { 72 | metrics := make(map[string]float64) 73 | 74 | metrics[MetricBlockRequest] = 1 75 | metrics[MetricFinding] = float64(len(resp.Findings)) 76 | metrics[MetricBlockLatency] = float64(resp.LatencyMs) 77 | metrics[MetricBlockBlockAge] = durationMs(times.Block, times.BotRequest) 78 | metrics[MetricBlockEventAge] = durationMs(times.Feed, times.BotRequest) 79 | 80 | if resp.Status == protocol.ResponseStatus_ERROR { 81 | metrics[MetricBlockError] = 1 82 | } else if resp.Status == protocol.ResponseStatus_SUCCESS { 83 | metrics[MetricBlockSuccess] = 1 84 | } 85 | 86 | return createMetrics(agt.ID, resp.Timestamp, metrics) 87 | } 88 | 89 | func GetTxMetrics(agt config.AgentConfig, resp *protocol.EvaluateTxResponse, times *domain.TrackingTimestamps) []*protocol.AgentMetric { 90 | metrics := make(map[string]float64) 91 | 92 | metrics[MetricTxRequest] = 1 93 | metrics[MetricFinding] = float64(len(resp.Findings)) 94 | metrics[MetricTxLatency] = float64(resp.LatencyMs) 95 | metrics[MetricTxBlockAge] = durationMs(times.Block, times.BotRequest) 96 | metrics[MetricTxEventAge] = durationMs(times.Feed, times.BotRequest) 97 | 98 | if resp.Status == protocol.ResponseStatus_ERROR { 99 | metrics[MetricTxError] = 1 100 | } else if resp.Status == protocol.ResponseStatus_SUCCESS { 101 | metrics[MetricTxSuccess] = 1 102 | } 103 | 104 | return createMetrics(agt.ID, resp.Timestamp, metrics) 105 | } 106 | 107 | func GetJSONRPCMetrics(agt config.AgentConfig, at time.Time, success, throttled int, latencyMs time.Duration) []*protocol.AgentMetric { 108 | values := make(map[string]float64) 109 | if latencyMs > 0 { 110 | values[MetricJSONRPCLatency] = float64(latencyMs.Milliseconds()) 111 | } 112 | if success > 0 { 113 | values[MetricJSONRPCSuccess] = float64(success) 114 | values[MetricJSONRPCRequest] += float64(success) 115 | } 116 | if throttled > 0 { 117 | values[MetricJSONRPCThrottled] = float64(throttled) 118 | values[MetricJSONRPCRequest] += float64(throttled) 119 | } 120 | return createMetrics(agt.ID, at.Format(time.RFC3339), values) 121 | } 122 | -------------------------------------------------------------------------------- /.github/workflows/build-codedeploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Build & CodeDeploy to Prod 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [master] 6 | 7 | env: 8 | DEFAULT_PROD_VERSION: v0.5.0 9 | 10 | jobs: 11 | go: 12 | name: Go 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Setup 18 | id: setup 19 | uses: ./.github/actions/setup 20 | - name: Validate and test Go code 21 | id: go 22 | uses: ./.github/actions/go 23 | 24 | e2e: 25 | name: E2E Test 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v2 30 | - name: Setup 31 | id: setup 32 | uses: ./.github/actions/setup 33 | - name: E2E Test 34 | id: e2e 35 | uses: ./.github/actions/e2e 36 | 37 | containers: 38 | name: Build and push containers 39 | needs: [go] 40 | runs-on: ubuntu-latest 41 | environment: prod 42 | outputs: 43 | node-image-ref: ${{ steps.write-output.outputs.node-image-ref }} 44 | node-release-cid: ${{ steps.write-output.outputs.node-release-cid }} 45 | strategy: 46 | matrix: 47 | image-name: [ 48 | "node" 49 | ] 50 | steps: 51 | - name: Clear artifacts 52 | uses: kolpav/purge-artifacts-action@v1 53 | continue-on-error: true 54 | with: 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | expire-in: 7days 57 | - name: Checkout 58 | uses: actions/checkout@v2 59 | - name: Setup 60 | id: setup 61 | uses: ./.github/actions/setup 62 | - name: Build and push container 63 | id: build-and-push 64 | uses: ./.github/actions/build-push 65 | with: 66 | registry: disco.forta.network 67 | name: ${{ matrix.image-name }} 68 | version: ${{ env.DEFAULT_PROD_VERSION }} 69 | - name: Write output values 70 | id: write-output 71 | run: | 72 | echo "::set-output name=${{ matrix.image-name }}-release-cid::${{ steps.build-and-push.outputs.release-cid }}" 73 | echo "::set-output name=${{ matrix.image-name }}-image-ref::${{ steps.build-and-push.outputs.image-reference }}" 74 | 75 | build-deploy: 76 | name: Build and deploy 77 | needs: containers 78 | runs-on: ubuntu-latest 79 | environment: prod 80 | steps: 81 | - name: Echo Image References 82 | run: | 83 | echo "node=${{ needs.containers.outputs.node-image-ref }}" 84 | - name: Clear artifacts 85 | uses: kolpav/purge-artifacts-action@v1 86 | continue-on-error: true 87 | with: 88 | token: ${{ secrets.GITHUB_TOKEN }} 89 | expire-in: 7days 90 | - name: Checkout 91 | uses: actions/checkout@v2 92 | - name: Setup 93 | id: setup 94 | uses: ./.github/actions/setup 95 | - name: Create build for revision 96 | run: | 97 | ./scripts/build.sh ${{ needs.containers.outputs.node-image-ref }} \ 98 | 'remote' ${{ needs.containers.outputs.node-release-cid }} ${{ github.sha }} ${{ env.DEFAULT_PROD_VERSION }} 99 | chmod 755 forta 100 | - name: Prepare Distribution 101 | run: | 102 | mkdir dist 103 | cp forta dist/ 104 | cp appspec.yml dist/ 105 | cp -R scripts dist/ 106 | - name: Zip Distribution 107 | uses: vimtor/action-zip@v1 108 | with: 109 | files: dist/ 110 | dest: deploy.zip 111 | - uses: actions/upload-artifact@v1 112 | with: 113 | name: deploy-artifact 114 | path: ${{ github.workspace }}/deploy.zip 115 | 116 | - name: Configure AWS credentials (CodeDeploy) 117 | uses: aws-actions/configure-aws-credentials@v1 118 | with: 119 | aws-access-key-id: ${{ secrets.PROD_DEPLOY_AWS_ACCESS_KEY }} 120 | aws-secret-access-key: ${{ secrets.PROD_DEPLOY_AWS_SECRET_KEY }} 121 | aws-region: us-east-1 122 | - name: AWS CodeDeploy 123 | uses: sourcetoad/aws-codedeploy-action@v1 124 | with: 125 | aws_access_key: ${{ secrets.PROD_DEPLOY_AWS_ACCESS_KEY }} 126 | aws_secret_key: ${{ secrets.PROD_DEPLOY_AWS_SECRET_KEY }} 127 | aws_region: us-east-1 128 | codedeploy_name: prod-forta-node 129 | codedeploy_group: prod-forta-deploy-group 130 | s3_bucket: prod-forta-codedeploy 131 | s3_folder: prod 132 | directory: ./dist/ 133 | -------------------------------------------------------------------------------- /.github/workflows/codedeploy-staging.yml: -------------------------------------------------------------------------------- 1 | name: Release & CodeDeploy to Staging 2 | 3 | on: 4 | # push: 5 | # branches: [master] 6 | workflow_dispatch: 7 | branches: [master] 8 | 9 | env: 10 | DEFAULT_STAGING_VERSION: v0.5.0 11 | 12 | jobs: 13 | go: 14 | name: Go 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Setup 20 | id: setup 21 | uses: ./.github/actions/setup 22 | - name: Validate and test Go code 23 | id: go 24 | uses: ./.github/actions/go 25 | 26 | e2e: 27 | name: E2E Test 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v2 32 | - name: Setup 33 | id: setup 34 | uses: ./.github/actions/setup 35 | - name: E2E Test 36 | id: e2e 37 | uses: ./.github/actions/e2e 38 | 39 | containers: 40 | name: Build and push containers 41 | needs: [go] 42 | runs-on: ubuntu-latest 43 | environment: staging 44 | outputs: 45 | node-image-ref: ${{ steps.write-output.outputs.node-image-ref }} 46 | node-release-cid: ${{ steps.write-output.outputs.node-release-cid }} 47 | strategy: 48 | matrix: 49 | image-name: [ 50 | "node" 51 | ] 52 | steps: 53 | - name: Clear artifacts 54 | uses: kolpav/purge-artifacts-action@v1 55 | continue-on-error: true 56 | with: 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | expire-in: 7days 59 | - name: Checkout 60 | uses: actions/checkout@v2 61 | - name: Build and push container 62 | id: build-and-push 63 | uses: ./.github/actions/build-push 64 | with: 65 | registry: disco.forta.network 66 | name: ${{ matrix.image-name }} 67 | version: ${{ env.DEFAULT_STAGING_VERSION }} 68 | - name: Write output values 69 | id: write-output 70 | run: | 71 | echo "::set-output name=${{ matrix.image-name }}-release-cid::${{ steps.build-and-push.outputs.release-cid }}" 72 | echo "::set-output name=${{ matrix.image-name }}-image-ref::${{ steps.build-and-push.outputs.image-reference }}" 73 | 74 | build-deploy: 75 | name: Build and deploy 76 | needs: containers 77 | runs-on: ubuntu-latest 78 | environment: staging 79 | steps: 80 | - name: Echo Image References 81 | run: | 82 | echo "node=${{ needs.containers.outputs.node-image-ref }}" 83 | - name: Clear artifacts 84 | uses: kolpav/purge-artifacts-action@v1 85 | continue-on-error: true 86 | with: 87 | token: ${{ secrets.GITHUB_TOKEN }} 88 | expire-in: 7days 89 | - name: Checkout 90 | uses: actions/checkout@v2 91 | - name: Set up Go 92 | uses: actions/setup-go@v2 93 | with: 94 | go-version: 1.16 95 | - name: Create build for revision 96 | run: | 97 | ./scripts/build.sh ${{ needs.containers.outputs.node-image-ref }} \ 98 | 'remote' ${{ needs.containers.outputs.node-release-cid }} ${{ github.sha }} ${{ env.DEFAULT_STAGING_VERSION }} 99 | chmod 755 forta 100 | - name: Prepare Distribution 101 | run: | 102 | mkdir dist 103 | cp forta dist/ 104 | cp appspec.yml dist/ 105 | cp -R scripts dist/ 106 | - name: Zip Distribution 107 | uses: vimtor/action-zip@v1 108 | with: 109 | files: dist/ 110 | dest: deploy.zip 111 | - uses: actions/upload-artifact@v1 112 | with: 113 | name: deploy-artifact 114 | path: ${{ github.workspace }}/deploy.zip 115 | 116 | - name: Configure AWS credentials (CodeDeploy) 117 | uses: aws-actions/configure-aws-credentials@v1 118 | with: 119 | aws-access-key-id: ${{ secrets.PROD_DEPLOY_AWS_ACCESS_KEY }} 120 | aws-secret-access-key: ${{ secrets.PROD_DEPLOY_AWS_SECRET_KEY }} 121 | aws-region: us-east-1 122 | - name: AWS CodeDeploy 123 | uses: sourcetoad/aws-codedeploy-action@v1 124 | with: 125 | aws_access_key: ${{ secrets.PROD_DEPLOY_AWS_ACCESS_KEY }} 126 | aws_secret_key: ${{ secrets.PROD_DEPLOY_AWS_SECRET_KEY }} 127 | aws_region: us-east-1 128 | codedeploy_name: prod-forta-node 129 | codedeploy_group: prod-forta-staging-scanners-deploy-group 130 | s3_bucket: prod-forta-codedeploy 131 | s3_folder: prod 132 | directory: ./dist/ 133 | -------------------------------------------------------------------------------- /cmd/cmd_init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/ethereum/go-ethereum/accounts/keystore" 11 | "github.com/fatih/color" 12 | "github.com/forta-network/forta-node/config" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func handleFortaInit(cmd *cobra.Command, args []string) error { 17 | if isInitialized() { 18 | greenBold("Already initialized - please ensure that your configuration at %s is correct!\n", cfg.ConfigFilePath()) 19 | return nil 20 | } 21 | 22 | if !isDirInitialized() { 23 | if err := os.Mkdir(cfg.FortaDir, 0755); err != nil { 24 | return err 25 | } 26 | } 27 | 28 | if !isConfigFileInitialized() { 29 | tmpl, err := template.New("config-template").Parse(defaultConfig) 30 | if err != nil { 31 | return err 32 | } 33 | var buf bytes.Buffer 34 | if err := tmpl.Execute(&buf, config.GetEnvDefaults(cfg.Development)); err != nil { 35 | return err 36 | } 37 | if err := os.WriteFile(cfg.ConfigFilePath(), buf.Bytes(), 0644); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | if !isKeyDirInitialized() { 43 | if err := os.Mkdir(cfg.KeyDirPath, 0755); err != nil { 44 | return err 45 | } 46 | } 47 | 48 | if !isKeyInitialized() { 49 | if len(cfg.Passphrase) == 0 { 50 | yellowBold("Please provide a passphrase and do not lose it.\n\n") 51 | return cmd.Help() 52 | } 53 | 54 | ks := keystore.NewKeyStore(cfg.KeyDirPath, keystore.StandardScryptN, keystore.StandardScryptP) 55 | acct, err := ks.NewAccount(cfg.Passphrase) 56 | if err != nil { 57 | return err 58 | } 59 | printScannerAddress(acct.Address.Hex()) 60 | } 61 | 62 | color.Green("\nSuccessfully initialized at %s\n", cfg.FortaDir) 63 | whiteBold("\n%s\n", strings.Join([]string{ 64 | "- Please make sure that all of the values in config.yml are set correctly.", 65 | "- Please fund your scanner address with some MATIC.", 66 | "- Please enable it for the chain ID in your config by doing 'forta register --owner-address '.", 67 | //"- Please also ensure that your scanner address satisifies FORT token staking minimum requirement.", 68 | }, "\n")) 69 | 70 | return nil 71 | } 72 | 73 | func printScannerAddress(address string) { 74 | fmt.Printf("\nScanner address: %s\n", color.New(color.FgYellow).Sprintf(address)) 75 | } 76 | 77 | const defaultConfig = `# Auto generated by 'forta init' - safe to modify 78 | # The chainId is the chainId of the network that is analyzed (1=mainnet) 79 | chainId: 1 80 | 81 | # The scan settings are used to retrieve the transactions that are analyzed 82 | scan: 83 | jsonRpc: 84 | url: 85 | 86 | # The trace endpoint must support trace_block (such as alchemy) 87 | trace: 88 | jsonRpc: 89 | url: 90 | 91 | # The registry settings are used to discover and load agents 92 | # registry: 93 | # jsonRpc: 94 | # url: https://polygon-rpc.com/ 95 | # ipfs: 96 | # gatewayUrl: https://ipfs.forta.network 97 | # username: 98 | # password: 99 | 100 | # The jsonRpcProxy settings are used make query requests (defaults to scan url) 101 | # jsonRpcProxy: 102 | # jsonRpc: 103 | # url: 104 | 105 | # The publish settings drive how alerts are sent 106 | # publish: 107 | # ipfs: 108 | # apiUrl: https://ipfs.forta.network 109 | # username: 110 | # password: 111 | 112 | # The log settings drive the log output of the scan node 113 | # log: 114 | # level: info 115 | # maxLogSize: 50m 116 | # maxLogFiles: 10 117 | ` 118 | 119 | func isDirInitialized() bool { 120 | info, err := os.Stat(cfg.FortaDir) 121 | if err != nil { 122 | return false 123 | } 124 | return info.IsDir() 125 | } 126 | 127 | func isConfigFileInitialized() bool { 128 | info, err := os.Stat(cfg.ConfigFilePath()) 129 | if err != nil { 130 | return false 131 | } 132 | return !info.IsDir() 133 | } 134 | 135 | func isKeyDirInitialized() bool { 136 | info, err := os.Stat(cfg.KeyDirPath) 137 | if err != nil { 138 | return false 139 | } 140 | return info.IsDir() 141 | } 142 | 143 | func isKeyInitialized() bool { 144 | if !isKeyDirInitialized() { 145 | return false 146 | } 147 | entries, err := os.ReadDir(cfg.KeyDirPath) 148 | if err != nil { 149 | return false 150 | } 151 | for i, entry := range entries { 152 | if i > 0 { 153 | return false // There must be one key file 154 | } 155 | return !entry.IsDir() // so it should be a geth key file 156 | } 157 | return false // No keys found in dir 158 | } 159 | 160 | func isInitialized() bool { 161 | return isDirInitialized() && isConfigFileInitialized() && isKeyInitialized() 162 | } 163 | -------------------------------------------------------------------------------- /services/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/forta-network/forta-node/store" 9 | 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/rpc" 12 | "github.com/forta-network/forta-core-go/clients/health" 13 | "github.com/forta-network/forta-core-go/ethereum" 14 | "github.com/forta-network/forta-node/clients" 15 | "github.com/forta-network/forta-node/clients/messaging" 16 | "github.com/forta-network/forta-node/config" 17 | "github.com/forta-network/forta-node/services/registry/regtypes" 18 | log "github.com/sirupsen/logrus" 19 | "golang.org/x/sync/semaphore" 20 | ) 21 | 22 | // RegistryService listens to the agent scanner list changes so the node can stay in sync. 23 | type RegistryService struct { 24 | cfg config.Config 25 | scannerAddress common.Address 26 | msgClient clients.MessageClient 27 | ethClient ethereum.Client 28 | 29 | rpcClient *rpc.Client 30 | registryStore store.RegistryStore 31 | 32 | agentsConfigs []*config.AgentConfig 33 | done chan struct{} 34 | version string 35 | sem *semaphore.Weighted 36 | 37 | lastChecked health.TimeTracker 38 | lastChangeDetected health.TimeTracker 39 | lastErr health.ErrorTracker 40 | } 41 | 42 | // IPFSClient interacts with an IPFS Gateway. 43 | type IPFSClient interface { 44 | GetAgentFile(cid string) (*regtypes.AgentFile, error) 45 | } 46 | 47 | // EthClient interacts with the Ethereum API. 48 | type EthClient interface { 49 | ethereum.Client 50 | } 51 | 52 | // New creates a new service. 53 | func New(cfg config.Config, scannerAddress common.Address, msgClient clients.MessageClient, ethClient ethereum.Client) *RegistryService { 54 | return &RegistryService{ 55 | cfg: cfg, 56 | scannerAddress: scannerAddress, 57 | msgClient: msgClient, 58 | ethClient: ethClient, 59 | done: make(chan struct{}), 60 | } 61 | } 62 | 63 | // Init only initializes the service. 64 | func (rs *RegistryService) Init() error { 65 | var ( 66 | regStr store.RegistryStore 67 | err error 68 | ) 69 | if rs.cfg.PrivateModeConfig.Enable { 70 | regStr, err = store.NewPrivateRegistryStore(context.Background(), rs.cfg) 71 | } else { 72 | regStr, err = store.NewRegistryStore(context.Background(), rs.cfg, rs.ethClient) 73 | } 74 | if err != nil { 75 | return err 76 | } 77 | rs.registryStore = regStr 78 | return nil 79 | } 80 | 81 | // Start initializes and starts the registry service. 82 | func (rs *RegistryService) Start() error { 83 | log.Infof("Starting %s", rs.Name()) 84 | if err := rs.Init(); err != nil { 85 | return err 86 | } 87 | rs.sem = semaphore.NewWeighted(1) 88 | return rs.start() 89 | } 90 | 91 | func (rs *RegistryService) start() error { 92 | go func() { 93 | ticker := time.NewTicker(time.Duration(rs.cfg.Registry.CheckIntervalSeconds) * time.Second) 94 | for { 95 | err := rs.publishLatestAgents() 96 | rs.lastErr.Set(err) 97 | if err != nil { 98 | log.WithError(err).Error("failed to publish the latest agents") 99 | } 100 | <-ticker.C 101 | } 102 | }() 103 | 104 | return nil 105 | } 106 | 107 | func (rs *RegistryService) publishLatestAgents() error { 108 | // only allow one executor at a time, even if slow 109 | if rs.sem.TryAcquire(1) { 110 | defer rs.sem.Release(1) 111 | rs.lastChecked.Set() 112 | agts, changed, err := rs.registryStore.GetAgentsIfChanged(rs.scannerAddress.Hex()) 113 | if err != nil { 114 | return fmt.Errorf("failed to get the scanner list agents version: %v", err) 115 | } 116 | if changed { 117 | rs.lastChangeDetected.Set() 118 | log.WithField("count", len(agts)).Infof("publishing list of agents") 119 | rs.agentsConfigs = agts 120 | rs.msgClient.Publish(messaging.SubjectAgentsVersionsLatest, agts) 121 | } else { 122 | log.Info("registry: no agent changes detected") 123 | } 124 | } 125 | return nil 126 | } 127 | 128 | // Stop stops the registry service. 129 | func (rs *RegistryService) Stop() error { 130 | return nil 131 | } 132 | 133 | // Name returns the name of the service. 134 | func (rs *RegistryService) Name() string { 135 | return "registry" 136 | } 137 | 138 | // Health implements the health.Reporter interface. 139 | func (rs *RegistryService) Health() health.Reports { 140 | return health.Reports{ 141 | rs.lastErr.GetReport("event.checked.error"), 142 | &health.Report{ 143 | Name: "event.checked.time", 144 | Status: health.StatusInfo, 145 | Details: rs.lastChecked.String(), 146 | }, 147 | &health.Report{ 148 | Name: "event.change-detected.time", 149 | Status: health.StatusInfo, 150 | Details: rs.lastChangeDetected.String(), 151 | }, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /services/scanner/agentpool/agent_pool_test.go: -------------------------------------------------------------------------------- 1 | package agentpool 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/forta-network/forta-core-go/protocol" 8 | "github.com/forta-network/forta-node/clients" 9 | "github.com/forta-network/forta-node/clients/agentgrpc" 10 | "github.com/forta-network/forta-node/clients/messaging" 11 | mock_clients "github.com/forta-network/forta-node/clients/mocks" 12 | "github.com/forta-network/forta-node/config" 13 | "github.com/forta-network/forta-node/services/scanner" 14 | "google.golang.org/grpc" 15 | 16 | "github.com/golang/mock/gomock" 17 | "github.com/stretchr/testify/require" 18 | "github.com/stretchr/testify/suite" 19 | ) 20 | 21 | const ( 22 | testAgentID = "test-agent" 23 | testRequestID = "test-request-id" 24 | testResponseID = "test-response-id" 25 | ) 26 | 27 | // TestSuite runs the test suite. 28 | func TestSuite(t *testing.T) { 29 | suite.Run(t, &Suite{}) 30 | } 31 | 32 | // Suite is a test suite to test the agent pool. 33 | type Suite struct { 34 | r *require.Assertions 35 | 36 | msgClient *mock_clients.MockMessageClient 37 | agentClient *mock_clients.MockAgentClient 38 | 39 | ap *AgentPool 40 | 41 | suite.Suite 42 | } 43 | 44 | // SetupTest sets up the test. 45 | func (s *Suite) SetupTest() { 46 | s.r = require.New(s.T()) 47 | s.msgClient = mock_clients.NewMockMessageClient(gomock.NewController(s.T())) 48 | s.agentClient = mock_clients.NewMockAgentClient(gomock.NewController(s.T())) 49 | s.ap = &AgentPool{ 50 | ctx: context.Background(), 51 | txResults: make(chan *scanner.TxResult), 52 | blockResults: make(chan *scanner.BlockResult), 53 | msgClient: s.msgClient, 54 | dialer: func(agentCfg config.AgentConfig) (clients.AgentClient, error) { 55 | return s.agentClient, nil 56 | }, 57 | } 58 | } 59 | 60 | // TestStartProcessStop tests the starting, processing and stopping flow for an agent. 61 | func (s *Suite) TestStartProcessStop() { 62 | agentConfig := config.AgentConfig{ 63 | ID: testAgentID, 64 | } 65 | agentPayload := messaging.AgentPayload{ 66 | agentConfig, 67 | } 68 | emptyPayload := messaging.AgentPayload{} 69 | 70 | // Given that there are no agents running 71 | // When the latest list is received, 72 | // Then a "run" action should be published 73 | s.msgClient.EXPECT().Publish(messaging.SubjectAgentsStatusAttached, gomock.Any()) 74 | s.msgClient.EXPECT().Publish(messaging.SubjectAgentsActionRun, gomock.Any()) 75 | s.r.NoError(s.ap.handleAgentVersionsUpdate(agentPayload)) 76 | 77 | // Given that the agent is known to the pool but it is not ready yet 78 | s.r.Equal(1, len(s.ap.agents)) 79 | s.r.False(s.ap.agents[0].IsReady()) 80 | // When the agent pool receives a message saying that the agent started to run 81 | s.r.NoError(s.ap.handleStatusRunning(agentPayload)) 82 | // Then the agent must be marked ready 83 | s.r.True(s.ap.agents[0].IsReady()) 84 | 85 | // Given that the agent is running 86 | // When an evaluate requests are received 87 | // Then the agent should process them 88 | 89 | txReq := &protocol.EvaluateTxRequest{ 90 | Event: &protocol.TransactionEvent{ 91 | Block: &protocol.TransactionEvent_EthBlock{BlockNumber: "123123"}, 92 | Transaction: &protocol.TransactionEvent_EthTransaction{ 93 | Hash: "0x0", 94 | }, 95 | }, 96 | } 97 | txResp := &protocol.EvaluateTxResponse{Metadata: map[string]string{"imageHash": ""}} 98 | blockReq := &protocol.EvaluateBlockRequest{Event: &protocol.BlockEvent{BlockNumber: "123123"}} 99 | blockResp := &protocol.EvaluateBlockResponse{Metadata: map[string]string{"imageHash": ""}} 100 | 101 | s.agentClient.EXPECT().Invoke( 102 | gomock.Any(), agentgrpc.MethodEvaluateTx, 103 | gomock.AssignableToTypeOf(&grpc.PreparedMsg{}), gomock.AssignableToTypeOf(&protocol.EvaluateTxResponse{}), 104 | ).Return(nil) 105 | s.ap.SendEvaluateTxRequest(txReq) 106 | txResult := <-s.ap.TxResults() 107 | txResp.Timestamp = txResult.Response.Timestamp // bypass - hard to match 108 | 109 | s.agentClient.EXPECT().Invoke( 110 | gomock.Any(), agentgrpc.MethodEvaluateBlock, 111 | gomock.AssignableToTypeOf(&grpc.PreparedMsg{}), gomock.AssignableToTypeOf(&protocol.EvaluateBlockResponse{}), 112 | ).Return(nil) 113 | s.msgClient.EXPECT().Publish(messaging.SubjectScannerBlock, gomock.Any()) 114 | s.ap.SendEvaluateBlockRequest(blockReq) 115 | blockResult := <-s.ap.BlockResults() 116 | blockResp.Timestamp = blockResult.Response.Timestamp // bypass - hard to match 117 | 118 | s.r.Equal(txReq, txResult.Request) 119 | s.r.Equal(txResp, txResult.Response) 120 | s.r.Equal(blockReq, blockResult.Request) 121 | s.r.Equal(blockResp, blockResult.Response) 122 | 123 | // Given that the agent is running 124 | // When an empty agent list is received 125 | // Then a "stop" action should be published 126 | s.msgClient.EXPECT().Publish(messaging.SubjectAgentsActionStop, gomock.Any()) 127 | // And the agent must be closed 128 | s.agentClient.EXPECT().Close() 129 | s.r.NoError(s.ap.handleAgentVersionsUpdate(emptyPayload)) 130 | } 131 | -------------------------------------------------------------------------------- /cmd/cmd_tx.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/fatih/color" 11 | "github.com/forta-network/forta-core-go/registry" 12 | "github.com/forta-network/forta-core-go/security" 13 | "github.com/forta-network/forta-node/store" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func handleFortaRegister(cmd *cobra.Command, args []string) error { 18 | ownerAddressStr, err := cmd.Flags().GetString("owner-address") 19 | if err != nil { 20 | return err 21 | } 22 | if !common.IsHexAddress(ownerAddressStr) { 23 | return errors.New("invalid owner address provided") 24 | } 25 | 26 | scannerKey, err := security.LoadKeyWithPassphrase(cfg.KeyDirPath, cfg.Passphrase) 27 | if err != nil { 28 | return fmt.Errorf("failed to load scanner key: %v", err) 29 | } 30 | scannerPrivateKey := scannerKey.PrivateKey 31 | scannerAddressStr := scannerKey.Address.Hex() 32 | 33 | if strings.EqualFold(scannerAddressStr, ownerAddressStr) { 34 | redBold("Scanner and owner cannot be the same identity! Please provide a different wallet address of your own.\n") 35 | } 36 | 37 | registry, err := store.GetRegistryClient(context.Background(), cfg, registry.ClientConfig{ 38 | JsonRpcUrl: cfg.Registry.JsonRpc.Url, 39 | ENSAddress: cfg.ENSConfig.ContractAddress, 40 | Name: "registry-client", 41 | PrivateKey: scannerPrivateKey, 42 | }) 43 | if err != nil { 44 | return fmt.Errorf("failed to create registry client: %v", err) 45 | } 46 | 47 | color.Yellow(fmt.Sprintf("Sending a transaction to register your scan node to chain %d...\n", cfg.ChainID)) 48 | 49 | txHash, err := registry.RegisterScanner(ownerAddressStr, int64(cfg.ChainID), "") 50 | if err != nil && strings.Contains(err.Error(), "insufficient funds") { 51 | yellowBold("This action requires Polygon (Mainnet) MATIC. Have you funded your address %s yet?\n", scannerAddressStr) 52 | } 53 | if err != nil { 54 | return fmt.Errorf("failed to send the transaction: %v", err) 55 | } 56 | 57 | greenBold("Successfully sent the transaction!\n\n") 58 | whiteBold("Please ensure that https://polygonscan.com/tx/%s succeeds before you do 'forta run'. This can take a while depending on the network load.\n", txHash) 59 | 60 | return nil 61 | } 62 | 63 | func handleFortaEnable(cmd *cobra.Command, args []string) error { 64 | scannerKey, err := security.LoadKeyWithPassphrase(cfg.KeyDirPath, cfg.Passphrase) 65 | if err != nil { 66 | return fmt.Errorf("failed to load scanner key: %v", err) 67 | } 68 | scannerPrivateKey := scannerKey.PrivateKey 69 | scannerAddressStr := scannerKey.Address.Hex() 70 | 71 | reg, err := store.GetRegistryClient(context.Background(), cfg, registry.ClientConfig{ 72 | JsonRpcUrl: cfg.Registry.JsonRpc.Url, 73 | ENSAddress: cfg.ENSConfig.ContractAddress, 74 | Name: "registry-client", 75 | PrivateKey: scannerPrivateKey, 76 | }) 77 | if err != nil { 78 | return fmt.Errorf("failed to create registry client: %v", err) 79 | } 80 | 81 | color.Yellow("Sending a transaction to enable your scan node...\n") 82 | 83 | txHash, err := reg.EnableScanner(registry.ScannerPermissionSelf, scannerAddressStr) 84 | if err != nil && strings.Contains(err.Error(), "insufficient funds") { 85 | yellowBold("This action requires Polygon (Mainnet) MATIC. Have you funded your address %s yet?\n", scannerAddressStr) 86 | } 87 | if err != nil { 88 | return fmt.Errorf("failed to send the transaction: %v", err) 89 | } 90 | 91 | greenBold("Successfully sent the transaction!\n\n") 92 | whiteBold("https://polygonscan.com/tx/%s\n", txHash) 93 | 94 | return nil 95 | } 96 | 97 | func handleFortaDisable(cmd *cobra.Command, args []string) error { 98 | scannerKey, err := security.LoadKeyWithPassphrase(cfg.KeyDirPath, cfg.Passphrase) 99 | if err != nil { 100 | return fmt.Errorf("failed to load scanner key: %v", err) 101 | } 102 | scannerPrivateKey := scannerKey.PrivateKey 103 | scannerAddressStr := scannerKey.Address.Hex() 104 | 105 | reg, err := store.GetRegistryClient(context.Background(), cfg, registry.ClientConfig{ 106 | JsonRpcUrl: cfg.Registry.JsonRpc.Url, 107 | ENSAddress: cfg.ENSConfig.ContractAddress, 108 | Name: "registry-client", 109 | PrivateKey: scannerPrivateKey, 110 | }) 111 | if err != nil { 112 | return fmt.Errorf("failed to create registry client: %v", err) 113 | } 114 | 115 | color.Yellow("Sending a transaction to disable your scan node...\n") 116 | 117 | txHash, err := reg.DisableScanner(registry.ScannerPermissionSelf, scannerAddressStr) 118 | if err != nil && strings.Contains(err.Error(), "insufficient funds") { 119 | yellowBold("This action requires Polygon (Mainnet) MATIC. Have you funded your address %s yet?\n", scannerAddressStr) 120 | } 121 | if err != nil { 122 | return fmt.Errorf("failed to send the transaction: %v", err) 123 | } 124 | 125 | greenBold("Successfully sent the transaction!\n\n") 126 | whiteBold("https://polygonscan.com/tx/%s\n", txHash) 127 | 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /services/service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/forta-network/forta-core-go/ens" 11 | 12 | "github.com/google/uuid" 13 | log "github.com/sirupsen/logrus" 14 | 15 | "github.com/forta-network/forta-node/config" 16 | ) 17 | 18 | const ( 19 | defaultServiceStartDelay = time.Minute * 10 20 | ) 21 | 22 | const ( 23 | GracefulShutdownSignal = syscall.SIGTERM 24 | ) 25 | 26 | // Service is a service abstraction. 27 | type Service interface { 28 | Start() error 29 | Stop() error 30 | Name() string 31 | } 32 | 33 | var sigc = make(chan os.Signal) 34 | 35 | var execIDKey = struct{}{} 36 | 37 | func ExecID(ctx context.Context) string { 38 | execID := ctx.Value(execIDKey) 39 | if execID == nil { 40 | panic("cannot get exec ID") 41 | } 42 | return execID.(string) 43 | } 44 | 45 | func initExecID(ctx context.Context) context.Context { 46 | execID, err := uuid.NewUUID() 47 | if err != nil { 48 | panic(err) 49 | } 50 | return context.WithValue(ctx, execIDKey, execID.String()) 51 | } 52 | 53 | func setContracts(cfg *config.Config) error { 54 | es, err := ens.DialENSStoreAt(cfg.ENSConfig.JsonRpc.Url, cfg.ENSConfig.ContractAddress) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | contracts, err := es.ResolveRegistryContracts() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if cfg.Registry.ContractAddress == "" { 65 | cfg.Registry.ContractAddress = contracts.Dispatch.Hex() 66 | } 67 | cfg.ScannerVersionContractAddress = contracts.ScannerRegistry.Hex() 68 | cfg.AgentRegistryContractAddress = contracts.AgentRegistry.Hex() 69 | return nil 70 | } 71 | 72 | func ContainerMain(name string, getServices func(ctx context.Context, cfg config.Config) ([]Service, error)) { 73 | logger := log.WithField("container", name) 74 | 75 | cfg, err := config.GetConfigForContainer() 76 | if err != nil { 77 | logger.WithError(err).Error("could not get config") 78 | return 79 | } 80 | 81 | if err := setContracts(&cfg); err != nil { 82 | logger.WithError(err).Error("could not initialize contract addresses using config") 83 | return 84 | } 85 | 86 | lvl, err := log.ParseLevel(cfg.Log.Level) 87 | if err != nil { 88 | logger.WithError(err).Error("could not initialize log level") 89 | return 90 | } 91 | log.SetLevel(lvl) 92 | log.SetFormatter(&log.JSONFormatter{}) 93 | logger.Info("starting") 94 | defer logger.Info("exiting") 95 | 96 | ctx, cancel := InitMainContext() 97 | defer cancel() 98 | 99 | serviceList, err := getServices(ctx, cfg) 100 | if err != nil { 101 | logger.WithError(err).Error("could not initialize services") 102 | return 103 | } 104 | 105 | if err := StartServices(ctx, cancel, logger, serviceList); err != nil { 106 | logger.WithError(err).Error("failed to start services") 107 | } 108 | } 109 | 110 | var gracefulShutdown bool 111 | 112 | // IsGracefulShutdown tells if we have reached a graceful shutdown condition. 113 | func IsGracefulShutdown() bool { 114 | return gracefulShutdown 115 | } 116 | 117 | func InitMainContext() (context.Context, context.CancelFunc) { 118 | execIDCtx := initExecID(context.Background()) 119 | ctx, cancel := context.WithCancel(execIDCtx) 120 | signal.Notify(sigc, 121 | syscall.SIGHUP, 122 | syscall.SIGINT, 123 | syscall.SIGTERM, 124 | syscall.SIGQUIT) 125 | go func() { 126 | sig := <-sigc 127 | log.Infof("received signal: %s", sig.String()) 128 | gracefulShutdown = sig == GracefulShutdownSignal 129 | cancel() 130 | }() 131 | return ctx, cancel 132 | } 133 | 134 | // InterruptMainContext interrupts by sending a fake interrup signal from within runtime. 135 | func InterruptMainContext() { 136 | select { 137 | case sigc <- syscall.SIGINT: 138 | default: 139 | } 140 | } 141 | 142 | // StartServices kicks off all services. 143 | func StartServices(ctx context.Context, cancelMainCtx context.CancelFunc, logger *log.Entry, services []Service) error { 144 | // each service should be able to start successfully within reasonable time 145 | for _, service := range services { 146 | serviceStartedCtx, serviceStarted := context.WithCancel(context.Background()) 147 | defer serviceStarted() 148 | 149 | logger := logger.WithField("service", service.Name()) 150 | 151 | go func() { 152 | if err := service.Start(); err != nil { 153 | logger.WithError(err).Error("failed to start service") 154 | cancelMainCtx() 155 | return 156 | } 157 | serviceStarted() 158 | }() 159 | 160 | select { 161 | case <-time.After(defaultServiceStartDelay): 162 | logger.Error("took too long to start service") 163 | cancelMainCtx() 164 | break 165 | case <-serviceStartedCtx.Done(): 166 | // ok - do nothing 167 | case <-ctx.Done(): 168 | return ctx.Err() 169 | } 170 | } 171 | 172 | <-ctx.Done() 173 | logger.WithError(ctx.Err()).Info("context is done") 174 | 175 | // stop all services 176 | for _, service := range services { 177 | err := service.Stop() 178 | logger.WithError(err).WithField("service", service.Name()).Info("stopped") 179 | } 180 | 181 | return nil 182 | } 183 | --------------------------------------------------------------------------------