Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.github
.gitignore
*.md
ev-metrics
*.test
*.out
Dockerfile
.dockerignore
13 changes: 5 additions & 8 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,21 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Log in to Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
Expand All @@ -49,7 +46,7 @@ jobs:
type=sha

- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: govulncheck

on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '0 6 * * 1'

env:
# reachable vulnerability with no upstream fix yet; delete once x/crypto ships a fix
ALLOWED_VULN: GO-2026-5932

jobs:
govulncheck:
name: govulncheck
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v7
with:
# scan with the latest Go release, not the go.mod minimum (1.26.0): the minimum has
# many already-fixed standard library vulnerabilities that are not about this code.
# The Docker image builds on golang:1.26-alpine, which tracks the latest patch.
go-version: 'stable'
check-latest: true

- name: Run govulncheck
run: |
go run golang.org/x/vuln/cmd/govulncheck@latest -format json ./... \
| jq -e -s --arg allowed "$ALLOWED_VULN" \
'[.[] | select(.finding != null and .finding.trace[0].function != null) | .finding.osv] | unique - [$allowed] | length == 0'
8 changes: 4 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v9
with:
version: latest
version: v2.15.0
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'

Expand Down
20 changes: 9 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# build stage
FROM golang:1.26-alpine AS builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder

WORKDIR /build
ARG TARGETOS TARGETARCH

# install build dependencies
RUN apk add --no-cache git gcc musl-dev
WORKDIR /build

# copy go mod files
COPY go.mod go.sum ./
Expand All @@ -13,17 +12,16 @@ RUN go mod download
# copy source code
COPY . .

# build binary
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ev-metrics .
# build static binary for the target platform
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o ev-metrics .

# final stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates
# final stage: includes CA certificates and a nonroot user (uid 65532)
FROM gcr.io/distroless/static:nonroot@sha256:516757106de35205efb70b91ba03f6f1298424a0678c603255cf2852a7000087

WORKDIR /app

# copy binary from builder
COPY --from=builder /build/ev-metrics .

USER 65532:65532

ENTRYPOINT ["/app/ev-metrics"]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ lint-fix:
# run all tests
test:
@echo "Running tests..."
go test -v ./...
go test -race -v ./...

# build the binary
build:
Expand Down
20 changes: 3 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,12 @@ Blocks without transactions have only their header blob verified (`type="header"
### `ev_metrics_submission_attempts_total`
- **Type**: Counter
- **Labels**: `chain_id`, `type`
- **Description**: Total number of DA submission attempts (both successful and failed)
- **Description**: Total number of DA submission attempts (both successful and failed). Counted once per blob type per verification round, not once per RPC attempt. A block whose retries run out and which is re-verified after the cooldown starts a new round.

### `ev_metrics_submission_failures_total`
- **Type**: Counter
- **Labels**: `chain_id`, `type`
- **Description**: Total number of failed DA submission attempts
- **Description**: Total number of failed DA submission attempts. Counted once per blob type per verification round, not once per RPC attempt. A block whose retries run out and which is re-verified after the cooldown starts a new round.

### `ev_metrics_last_submission_attempt_time`
- **Type**: Gauge
Expand Down Expand Up @@ -328,18 +328,4 @@ When `--balance.addresses` and `--balance.consensus-rpc-urls` are provided:

## Debug Logging

There are two independent debug logging mechanisms:

- `--verbose` flag: sets the application log level to debug. Per-block and per-poll success messages (block verification, drift polls, balance checks, JSON-RPC health checks) are logged at debug level, so use `--verbose` to see them. Warnings and errors are always logged.
- `EVMETRICS_DEBUG=true` environment variable: logs DA submission details from the metrics package.

**Use cases**: Troubleshoot submission failures, verify submission flow.

**`EVMETRICS_DEBUG` logs include**: Successful/failed submission attempts and DA height updates.

**Example**:
```bash
# Enable debug logging
export EVMETRICS_DEBUG=true
./ev-metrics monitor --header-namespace testnet_header --data-namespace testnet_data
```
Use the `--verbose` flag to set the application log level to debug. It is the single way to get debug output: per-block, per-poll (drift, balance, JSON-RPC health checks) and per-verification success messages. Warnings and errors are always logged.
18 changes: 15 additions & 3 deletions internal/clients/evm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,29 @@ func (c *Client) HealthCheckRequest(ctx context.Context) (time.Duration, error)
}
}()

duration := time.Since(startTime)

// Read the response to ensure full RTT
_, err = io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("failed to read response: %w", err)
}
duration := time.Since(startTime)

if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

var rpcResp struct {
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal(body, &rpcResp); err != nil {
return 0, fmt.Errorf("failed to decode response: %w", err)
}
if rpcResp.Error != nil {
return 0, fmt.Errorf("JSON-RPC error: code %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
}

return duration, nil
}
53 changes: 53 additions & 0 deletions internal/clients/evm/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package evm

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)

func newTestClient(srv *httptest.Server) *Client {
return &Client{rpcURL: srv.URL, httpClient: srv.Client(), logger: zerolog.Nop()}
}

func TestHealthCheckRequest_SuccessMeasuresFullBody(t *testing.T) {
const delay = 100 * time.Millisecond
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,`))
w.(http.Flusher).Flush()
time.Sleep(delay)
_, _ = w.Write([]byte(`"result":"0x10"}`))
}))
defer srv.Close()

d, err := newTestClient(srv).HealthCheckRequest(context.Background())
require.NoError(t, err)
require.GreaterOrEqual(t, d, delay)
}

func TestHealthCheckRequest_UnexpectedStatus(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()

_, err := newTestClient(srv).HealthCheckRequest(context.Background())
require.ErrorContains(t, err, "unexpected status code: 500")
}

func TestHealthCheckRequest_JSONRPCError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}`))
}))
defer srv.Close()

_, err := newTestClient(srv).HealthCheckRequest(context.Background())
require.ErrorContains(t, err, "-32601")
require.ErrorContains(t, err, "method not found")
}
25 changes: 0 additions & 25 deletions internal/clients/evnode/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/evstack/ev-node/types/pb/evnode/v1/v1connect"
"github.com/rs/zerolog"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)

type Client struct {
Expand All @@ -28,18 +27,6 @@ func NewClient(addr string, logger zerolog.Logger) *Client {
}
}

// GetState queries the current state including latest block height
func (c *Client) GetState(ctx context.Context) (*evnode.GetStateResponse, error) {
req := connect.NewRequest(&emptypb.Empty{})

resp, err := c.storeClient.GetState(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to get state: %w", err)
}

return resp.Msg, nil
}

type GetBlockResult struct {
HeaderDaHeight uint64
DataDaHeight uint64
Expand Down Expand Up @@ -96,15 +83,3 @@ func (c *Client) GetBlockWithBlobs(ctx context.Context, height uint64) (*GetBloc
DataBlob: dataBlob,
}, nil
}

// GetGenesisDaHeight retrieves the DA height at which the first block was included
func (c *Client) GetGenesisDaHeight(ctx context.Context) (uint64, error) {
req := connect.NewRequest(&emptypb.Empty{})

resp, err := c.storeClient.GetGenesisDaHeight(ctx, req)
if err != nil {
return 0, fmt.Errorf("failed to get genesis DA height: %w", err)
}

return resp.Msg.Height, nil
}
Loading
Loading