Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update readme for fault detector app #28

Merged
merged 11 commits into from
Feb 12, 2024
47 changes: 47 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# .github/workflows/release.yml
name: goreleaser

on:
push:
# run only against tags
tags: ["v*"]


permissions:
contents: write

jobs:
goreleaser:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
# More assembly might be required: Docker logins, GPG, etc.
# It all depends on your needs.
- name: build
run: make build
- name: golangci-lint
run: make lint
- name: format
run: |
make format
if [ -z "$(git status --untracked-files=no --porcelain)" ]; then
echo "All files formatted"
else
echo "Running format is required"
exit 1
fi
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
shuse2 marked this conversation as resolved.
Show resolved Hide resolved
args: release -f .goreleaser.yml --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14 changes: 14 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# .goreleaser.yml
project_name: fault_detector
builds:
- env: [CGO_ENABLED=0]
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
id: "fault_detector"
dir: .
main: ./cmd/
38 changes: 25 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@

PKGS=$(shell go list ./... | grep -v "/vendor/")

.PHONY: test

APP_NAME = faultdetector
GREEN = \033[0;32m
BLUE = \033[0;34m
GREEN = \033[1;32m
BLUE = \033[1;34m
COLOR_END = \033[0;39m

build:
build: # Builds the application and create a binary at ./bin/
@echo "$(BLUE)» Building fault detector application binary... $(COLOR_END)"
@CGO_ENABLED=0 go build -a -v -o bin/$(APP_NAME) ./cmd/
@CGO_ENABLED=0 go build -a -o bin/$(APP_NAME) ./cmd/...
@echo "$(GREEN) Binary successfully built$(COLOR_END)"

run-app:
install: # Installs faultdetector cmd and creates executable at $GOPATH/bin/
@echo "$(BLUE)» Installing fault detector command... $(COLOR_END)"
@CGO_ENABLED=0 go install ./cmd/$(APP_NAME)
@echo "$(GREEN) $(APP_NAME) successfully installed$(COLOR_END)"

run-app: # Runs the application, use `make run-app config={PATH_TO_CONFIG_FILE}` to provide custom config
ifdef config
@./bin/${APP_NAME} --config $(config)
else
@./bin/${APP_NAME}
endif

test:
.PHONY: test
test: # Runs tests
@echo "Test packages"
@go test -race -shuffle=on -coverprofile=coverage.out -cover $(PKGS)

Expand All @@ -31,29 +35,37 @@ test.coverage: test
test.coverage.html: test
go tool cover -html=coverage.out

lint:
lint: # Runs golangci-lint on the repo
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run

format:
format: # Runs gofmt on the repo
gofmt -s -w .

godocs:
godocs: # Runs godoc and serves via endpoint
@go install golang.org/x/tools/cmd/godoc@latest
@echo "open http://localhost:6060/pkg/github.com/LiskHQ/op-fault-detector"
godoc -http=:6060

.PHONY: docker-build
docker-build:
docker-build: # Builds docker image
@echo "$(BLUE) Building docker image...$(COLOR_END)"
@docker build -t $(APP_NAME) .

.PHONY: docker-run
docker-run:
docker-run: # Runs docker image, use `make docker-run config={PATH_TO_CONFIG_FILE}` to provide custom config and to provide slack access token use `make docker-run slack_access_token={ACCESS_TOKEN}`
ifdef config
@echo "$(BLUE) Running docker image...$(COLOR_END)"
ifdef slack_access_token
@docker run -p 8080:8080 -v $(config):/home/onchain/faultdetector/config.yaml -t -e SLACK_ACCESS_TOKEN_KEY=$(slack_access_token) $(APP_NAME)
else
@docker run -p 8080:8080 -v $(config):/home/onchain/faultdetector/config.yaml -t $(APP_NAME)
endif
else
@echo "$(BLUE) Running docker image...$(COLOR_END)"
@docker run -p 8080:8080 $(APP_NAME)
endif

.PHONY: help
help: # Show help for each of the Makefile recipes
@grep -E '^[a-zA-Z0-9 -]+:.*#' Makefile | sort | while read -r l; do printf "$(GREEN)$$(echo $$l | cut -f 1 -d':')$(COLOR_END):$$(echo $$l | cut -f 2- -d'#')\n"; done
167 changes: 166 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,168 @@
# op-fault-detector

Fault detector is a service to detect mismatch between a local view of the Optimism network and L2 output proposals published to Ethereum.
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
![GitHub repo size](https://img.shields.io/github/repo-size/liskhq/op-fault-detector)
![GitHub issues](https://img.shields.io/github/issues-raw/liskhq/op-fault-detector)
![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/liskhq/op-fault-detector)
[![PR CI](https://github.com/LiskHQ/op-fault-detector/actions/workflows/pr.yaml/badge.svg?branch=main&event=merge_group)](https://github.com/LiskHQ/op-fault-detector/actions/workflows/pr.yaml)
ishantiw marked this conversation as resolved.
Show resolved Hide resolved

Fault detector is a service that identifies mismatches between a local view of the Optimism or superchain network and L2 output proposals published to Ethereum. Here is the reference to the original implementation of the [fault monitoring](https://github.com/ethereum-optimism/optimism/blob/v1.5.0/packages/chain-mon/src/fault-mon/README.md) service from [Optimism](https://www.optimism.io/).

## How it works

The state root of the block is published to the [L2OutputOracle](https://github.com/ethereum-optimism/optimism/blob/39b7262cc3ffd78cd314341b8512b2683c1d9af7/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol) contract on Ethereum. The `L2OutputOracle` is inferred from the portal contract.

In the application, we take the state root of the given block as reported by an Optimism node, compute `outputRoot` from it and compare it with the `outputRoot` as published to `L2OutputOracle` contract on Ethereum.

## Installation

```
git clone https://github.com/liskhq/op-fault-detector
make install
```

## Running Fault Detector
shuse2 marked this conversation as resolved.
Show resolved Hide resolved

Copy `config.yaml` file at the root path and use any name with `.yaml` extension or edit existing `config.yaml` file to set configuration for the application.

To run with default config,

```sh
faultdetector
```

To run with custom config file,

```sh
faultdetector --config /PATH/TO/YOUR/CUSTOM/CONFIG
```

### To build and run from source code

#### Build

```
make build
```

#### Run

```
make run-app
```
if want to provide custom config file, for example, `my-config.yaml`, run,

```
make run-app config=/path/to/my-config.yaml
```

View all available commands by running `make help` and view the commands with options as below.

```sh
build: Builds the application and create a binary at ./bin/

install: Installs faultdetector cmd and creates executable at $GOPATH/bin/

docker-build: Builds docker image

docker-run: Runs docker image, use `make docker-run config={PATH_TO_CONFIG_FILE}` to provide custom config and to provide slack access token use `make docker-run slack_access_token={ACCESS_TOKEN}`

format: Runs gofmt on the repo

godocs: Runs godoc and serves via endpoint

help: Show help for each of the Makefile recipes

lint: Runs golangci-lint on the repo

run-app: Runs the application, use `make run-app config={PATH_TO_CONFIG_FILE}` to provide custom config

test: Runs tests
```

## Config
shuse2 marked this conversation as resolved.
Show resolved Hide resolved

The configuration file is used to configure the application. Currently, the default configuration is found under `./config.yaml`. To provide custom config, edit the `./config.yaml` or create own and provide it while running the application `make run-app config={PATH_TO_CUSTOM_CONFIG_FILE}`.

```yaml
# General system configurations
system:
log_level: "info"

# API related configurations
api:
server:
host: "127.0.0.1"
port: 8080
base_path: "/api"
register_versions:
- v1

# Faultdetector configurations
fault_detector:
l1_rpc_endpoint: "https://rpc.notadegen.com/eth"
l2_rpc_endpoint: "https://mainnet.optimism.io/"
start_batch_index: -1
l2_output_oracle_contract_address: "0x0000000000000000000000000000000000000000"

```
nagdahimanshu marked this conversation as resolved.
Show resolved Hide resolved
### System Config
- `system.log_level`: Set log level of the application, by default `info` and available options are `warn`, `debug`, `error` and `fatal`

### API Config
- `api.server.host`: Host of application
- `api.server.port`: Port of application
- `api.base_path`: Base path for the API
- `register_versions`: Versions for APIs

### Fault Detector Config

- `fault_detector.l1_rpc_endpoint`: RPC endpoint for L1 chain.
- `fault_detector.l2_rpc_endpoint`: RPC endpoint for L2 chain.
- `fault_detector.start_batch_index`: Provide batch_index to start from. If not provided, it will pick default `-1` and then application will find the first unfinalized batch index that has not yet passed the fault proof window.
- `fault_detector.l2_output_oracle_contract_address`: Deployed `L2OutputOracle` contract address used to retrieve necessary info for output verification. Only provided for the chains other than Optimism and Lisk Superchain.

## API and Metrics

### API
- Status API exposed via `{api.server.host}:{api.server.port}/api/v1/status`
- Metrics is exposed at `{api.server.host}:{api.server.port}/metrics`
- `{api.server.host}` in `config.yaml` defaults to `127.0.0.1`
- `{api.server.port}` in `config.yaml` defaults to `8080`

### Metrics

```sh
- fault_detector_highest_output_index prometheus.Gauge Highest known output index
- fault_detector_is_state_mismatch prometheus.Gauge 0 if state is ok, 1 if state is mismatched
- fault_detector_api_connection_failure prometheus.Gauge Number of API RPC calls failed for L1 and L2 nodes
```

## Notification Service

When the state root for the proposed batch index on `L2OutputOracle` doesn't match the local view, user can also get notifications on [Slack](https://slack.com/).
This is an optional feature that can be enabled by following steps,
- Set configuration as below,

```yaml
notification:
enable: true
slack:
channel_id: "YOUR_CHANNEL_ID"
```

`channel_id` can be found on Slack, reference [Locate your Slack URL or ID](https://slack.com/intl/en-gb/help/articles/221769328-Locate-your-Slack-URL-or-ID).

- Set environment variable `SLACK_ACCESS_TOKEN_KEY`,

```sh
export SLACK_ACCESS_TOKEN_KEY="{SLACK_ACCESS_TOKEN}"
```

[Access token](https://api.slack.com/authentication/token-types) for Slack can be found in Slack application, reference [How to quickly get and use a Slack API token](https://api.slack.com/tutorials/tracks/getting-a-token).

- Run app `faultdetector` or `faultdetector --config /PATH/TO/CUSTOM/CONFIG/FILE`

To run notification service with docker.

- Run `make docker-run config={/PATH/TO/CUSTOM/CONFIG/FILE} slack_access_token={ACCESS_TOKEN}`
File renamed without changes.
2 changes: 1 addition & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ fault_detector:

# Notification service related configurations
notification:
enable: true
enable: false
slack:
channel_id: ""
6 changes: 5 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ func (c *Config) Validate() error {
sysConfigError := c.System.Validate()
apiConfigError := c.Api.Validate()
fdConfigError := c.FaultDetectorConfig.Validate()
notificationConfigError := c.Notification.Validate()
// Validate notification config only when it is enabled
var notificationConfigError error
if c.Notification.Enable {
notificationConfigError = c.Notification.Validate()
}

validationErrors = multierr.Combine(sysConfigError, apiConfigError, fdConfigError, notificationConfigError)

Expand Down
5 changes: 3 additions & 2 deletions pkg/faultdetector/faultdetector.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package faultdetector

import (
"context"
"errors"
"fmt"
"sync"
"time"
Expand Down Expand Up @@ -251,8 +252,8 @@ func (fd *FaultDetector) checkFault() error {

if fd.notification != nil {
notificationMessage := fmt.Sprintf("*Fault detected*, state root does not match:\noutputIndex: %d\nExpectedStateRoot: %s\nCalculatedStateRoot: %s\nFinalizationTime: %s", fd.currentOutputIndex, expectedOutputRoot, calculatedOutputRoot, finalizationTime)
if err := fd.notification.Notify(notificationMessage); err != nil {
fd.logger.Errorf("Error while sending notification, error: %w", err)
if err := fd.notification.Notify(notificationMessage); len(*err) != 0 {
shuse2 marked this conversation as resolved.
Show resolved Hide resolved
fd.logger.Errorf("Error while sending notification, error: %w", errors.Join(*err...).Error())
}
}

Expand Down
14 changes: 12 additions & 2 deletions pkg/utils/notification/channel/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"fmt"
"os"
"strconv"
"strings"
"time"

"github.com/LiskHQ/op-fault-detector/pkg/config"
"github.com/LiskHQ/op-fault-detector/pkg/log"
Expand Down Expand Up @@ -42,12 +45,19 @@ func (s *Slack) Notify(msg string) error {
s.ChannelID,
slack.MsgOptionText(msg, false),
)

if err != nil {
s.logger.Errorf("Failed to send notification to the channel %s, error: %w", s.ChannelID, err)
return err
}

s.logger.Infof("Message successfully sent to the channel %s at %s", s.ChannelID, timestamp)
parts := strings.Split(timestamp, ".")

timeInMS, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return err
}
localTime := time.UnixMilli(timeInMS * int64(time.Microsecond)).Local()

s.logger.Infof("Message successfully sent to the channel %s at %s", s.ChannelID, localTime.String())
return nil
}
Loading