Skip to content

Commit

Permalink
feat: headwatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
dputko committed Dec 25, 2024
1 parent 137e7b7 commit 0aa38ba
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = true
end_of_line = lf
insert_final_newline = true

[*.{js,json,yml}]
[*.{ts,js,json,yml}]
charset = utf-8
indent_style = space
indent_size = 2
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@
path = ofchain/scripts
url = [email protected]:lidofinance/scripts.git
branch = feat/pectra-devnet
[submodule "ofchain/headwatcher"]
path = ofchain/headwatcher
url = [email protected]:lidofinance/ethereum-head-watcher.git
branch = feature/val-1404-eip-7251-head-watcher-alerts-for-new-el-requests
27 changes: 27 additions & 0 deletions cli/commands/headwatcher/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {baseConfig, jsonDb} from "../../config/index.js";
import {getLidoLocatorAddress} from "../../lib/lido/index.js";
import fs from "node:fs";

export async function getEnv() {
const state = await jsonDb.read();
const {network} = baseConfig;

const el = state.network?.binding?.elNodesPrivate?.[0] ?? network.el.url;
const cl = state.network?.binding?.clNodesPrivate?.[0] ?? network.cl.url;
const name = state.network?.binding?.name ?? network.name;
const locator = await getLidoLocatorAddress();

if (!fs.existsSync(baseConfig.headwatcher.alertsOutputPath)) {
fs.mkdirSync(baseConfig.headwatcher.alertsOutputPath, {recursive: true});
}

return {
DOCKER_FILE_PATH: baseConfig.headwatcher.root,
ALERTS_OUTPUT_DIR: baseConfig.headwatcher.alertsOutputPath,
CONSENSUS_CLIENT_URI: cl,
EXECUTION_CLIENT_URI: el,
LIDO_LOCATOR_ADDRESS: locator,
DOCKER_NETWORK_NAME: 'kt-' + name,
KEYS_API_URI: 'http://localhost:9030',
}
}
38 changes: 38 additions & 0 deletions cli/commands/headwatcher/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
networks:
devnet:
name: ${DOCKER_NETWORK_NAME}
external: true

services:
stub_alertmanager:
container_name: ethereum-head-watcher-stub-alertmanager
build: ./stub_alertmanager
networks:
- devnet
volumes:
- ${ALERTS_OUTPUT_DIR:-../../../artifacts/headwatcher}:/opt/alerts:rw
environment:
- ALERTS_DIR=/opt/alerts
expose:
- 41288

app:
container_name: ethereum-head-watcher
build: ${DOCKER_FILE_PATH}
restart: unless-stopped
networks:
- devnet
deploy:
resources:
limits:
memory: 2g
depends_on:
- stub_alertmanager
environment:
- CONSENSUS_CLIENT_URI=${CONSENSUS_CLIENT_URI}
- EXECUTION_CLIENT_URI=${EXECUTION_CLIENT_URI}
- LIDO_LOCATOR_ADDRESS=${LIDO_LOCATOR_ADDRESS}
- KEYS_SOURCE=keys_api
- KEYS_API_URI=${KEYS_API_URI}
- ALERTMANAGER_URI="http://stub_alertmanager:41288"
- LOG_LEVEL=INFO
26 changes: 26 additions & 0 deletions cli/commands/headwatcher/down.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Command} from "@oclif/core";
import {execa} from "execa";
import {getEnv} from "./common.js";

export default class HeadwatcherDown extends Command {
static description = "Shutdown Ethereum Head Watcher";

async run() {
this.log("Stopping Ethereum Head Watcher...");

try {
await execa(
"docker",
["compose", "-f", "docker-compose.yml", "down", "-v"],
{
stdio: "inherit",
cwd: import.meta.dirname,
env: await getEnv()
}
);
this.log("Ethereum Head Watcher stopped successfully.");
} catch (error: any) {
this.error(`Failed to stop Ethereum Head Watcher: ${error.message}`);
}
}
}
19 changes: 19 additions & 0 deletions cli/commands/headwatcher/logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Command} from "@oclif/core";
import {execa} from "execa";
import {getEnv} from "./common.js";

export default class HeadwatcherLogs extends Command {
static description = "Output logs of Ethereum Head Watcher";

async run() {
await execa(
"docker",
["compose", "-f", "docker-compose.yml", "logs", "-f"],
{
stdio: "inherit",
cwd: import.meta.dirname,
env: await getEnv()
}
);
}
}
5 changes: 5 additions & 0 deletions cli/commands/headwatcher/stub_alertmanager/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM golang:1.23.4
WORKDIR /app
COPY stub.go ./
EXPOSE 41288
CMD ["go", "run", "stub.go"]
50 changes: 50 additions & 0 deletions cli/commands/headwatcher/stub_alertmanager/stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
)

func main() {
dir := os.Getenv("ALERTS_DIR")
if dir == "" {
fmt.Println("Environment variable ALERTS_DIR is not set")
os.Exit(1)
}

http.HandleFunc("/api/v1/alerts", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}

body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()

filename := fmt.Sprintf("%s.json", time.Now().Format("20060102_150405"))
filepath := filepath.Join(dir, filename)

if err := os.WriteFile(filepath, body, 0644); err != nil {
message := fmt.Sprintf("Failed to write file: %s", err.Error())
http.Error(w, message, http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Alert saved to %s\n", filepath)
})

port := ":41288"
fmt.Printf("Starting server on port %s\n", port)
if err := http.ListenAndServe(port, nil); err != nil {
fmt.Printf("Server failed: %s\n", err)
}
}
26 changes: 26 additions & 0 deletions cli/commands/headwatcher/up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Command} from "@oclif/core";
import {execa} from "execa";
import {getEnv} from "./common.js";

export default class HeadwatcherUp extends Command {
static description = "Start Ethereum Head Watcher";

async run() {
this.log("Starting Ethereum Head Watcher...");

try {
await execa(
"docker",
["compose", "-f", "docker-compose.yml", "up", "--build", "-d"],
{
stdio: "inherit",
cwd: import.meta.dirname,
env: await getEnv()
}
);
this.log("Ethereum Head Watcher started successfully.");
} catch (error: any) {
this.error(`Failed to start Ethereum Head Watcher: ${error.message}`);
}
}
}
10 changes: 7 additions & 3 deletions cli/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { readFileSync } from "fs";
import {readFileSync} from "fs";
import path from "path";
import YAML from "yaml";
import { JsonDb } from "../lib/state/index.js";
import { sharedWallet } from "./shared-wallet.js";
import {JsonDb} from "../lib/state/index.js";
import {sharedWallet} from "./shared-wallet.js";
import assert from "assert";

const CHAIN_ID = "32382";
Expand Down Expand Up @@ -127,6 +127,10 @@ export const baseConfig = {
root: BLOCKSCOUT_ROOT,
},
},
headwatcher: {
root: path.join(OFCHAIN_ROOT, "headwatcher"),
alertsOutputPath: path.join(ARTIFACTS_PATH, "headwatcher"),
},
onchain: {
lido: {
core: {
Expand Down

0 comments on commit 0aa38ba

Please sign in to comment.