From 26bd79d13b0a1529d5802088b3b8ee771d59011d Mon Sep 17 00:00:00 2001 From: lbtsm Date: Thu, 3 Sep 2026 16:52:52 +0800 Subject: [PATCH 1/3] Add Docker deployment and GitHub CD --- .dockerignore | 26 ++++++++++++ .env.example | 7 ++++ .github/workflows/cd.yml | 90 ++++++++++++++++++++++++++++++++++++++++ Dockerfile | 32 ++++++++++++++ README.md | 37 +++++++++++++++-- compose.yaml | 17 ++++++++ 6 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .github/workflows/cd.yml create mode 100644 Dockerfile create mode 100644 compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fa5a400 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +.git +.github +.codex +.agents +.DS_Store +.idea +.vscode +.history + +build +bin +site +target +tests/*.txt + +.env +.env.example +.config.example +config.json +config.example*.json +keys +*.key +*.block +gethdata1 +gethdata2 +centrifuge-chain diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..96091ad --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +MONITOR_DATA_DIR=/opt/bridge-monitor +MONITOR_IMAGE=bridge-monitor:local +MONITOR_CONTAINER_NAME=bridge-monitor +MONITOR_VERSION=local +TZ=Asia/Shanghai +compass=Mainnet-monitor +hooks= diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..9a58535 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,90 @@ +name: CD + +on: + push: + branches: + - main + - master + tags: + - "v*" + pull_request: + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: cd-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + name: Go test and build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run unit tests + run: go test ./internal/... ./chains/tron ./pkg/monitor + + - name: Build binary + run: go build -trimpath -ldflags="-s -w -X main.Version=${GITHUB_REF_NAME}" -o build/bridge-monitor ./cmd + + docker: + name: Docker build + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Validate Compose file + run: docker compose config + + - name: Normalize image name + run: echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha- + + - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and publish Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ github.ref_name }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fd23864 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 + +ARG GO_VERSION=1.25 + +FROM golang:${GO_VERSION}-bookworm AS builder + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +ARG VERSION=dev +RUN go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" -o /out/bridge-monitor ./cmd + +FROM debian:bookworm-slim AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates tzdata \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app/runtime + +RUN mkdir -p /app/runtime/keys + +COPY --from=builder /out/bridge-monitor /usr/local/bin/bridge-monitor + +ENV TZ=Asia/Shanghai + +ENTRYPOINT ["bridge-monitor"] +CMD ["monitor", "--config", "/app/runtime/config.json"] diff --git a/README.md b/README.md index 8bb742e..81f542d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ whether the transaction is cross-chain, and the user balance # Configuration -See `config.example` for an example configuration. +Use a valid `config.json` for runtime configuration. Keep production config +files and keystores out of git. ## Options @@ -21,6 +22,34 @@ See `config.example` for an example configuration. ## Env -```shell -export hooks="https://hooks.slack.com/services/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" // Slack alarm hook, Apply See This https://api.slack.com/messaging/webhooks -``` \ No newline at end of file +```shell +export hooks="https://hooks.slack.com/services/xxx/yyy/zzz" +``` + +# Docker Deployment + +The container uses `/app/runtime` as its runtime directory. Map one host +directory to it and keep `config.json`, `keys/`, and generated state files in +that host directory. + +```shell +sudo mkdir -p /opt/bridge-monitor/keys +sudo cp /path/to/your/config.json /opt/bridge-monitor/config.json + +cp .env.example .env +vim .env + +docker compose up -d --build +docker compose logs -f bridge-monitor +``` + +After pulling updates on the server: + +```shell +git pull +docker compose up -d --build +``` + +GitHub Actions builds and tests the project on pull requests, and builds the +Docker image on pushes to `main`, `master`, or version tags. Non-PR builds are +published to GitHub Container Registry as `ghcr.io//:`. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..04dee8c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +services: + bridge-monitor: + build: + context: . + args: + VERSION: ${MONITOR_VERSION:-local} + image: ${MONITOR_IMAGE:-bridge-monitor:local} + container_name: ${MONITOR_CONTAINER_NAME:-bridge-monitor} + restart: unless-stopped + working_dir: /app/runtime + environment: + TZ: ${TZ:-Asia/Shanghai} + compass: ${compass:-} + hooks: ${hooks:-} + volumes: + - ${MONITOR_DATA_DIR:-/opt/bridge-monitor}:/app/runtime + command: ["monitor", "--config", "/app/runtime/config.json"] From c9b5f0bc35ed6910bc6c12e778e2e20133ae4994 Mon Sep 17 00:00:00 2001 From: lbtsm Date: Fri, 4 Sep 2026 18:02:52 +0800 Subject: [PATCH 2/3] Support private Go modules in Docker CD --- .github/workflows/cd.yml | 17 +++++++++++++++++ Dockerfile | 15 ++++++++++++++- README.md | 12 ++++++++++++ compose.yaml | 6 ++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9a58535..0125922 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -21,6 +21,8 @@ concurrency: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} + GOPRIVATE: github.com/lbtsm/* + GONOSUMDB: github.com/lbtsm/* jobs: test: @@ -36,6 +38,17 @@ jobs: go-version-file: go.mod cache: true + - name: Configure private Go module access + env: + LBTSM_REPO_TOKEN: ${{ secrets.LBTSM_REPO_TOKEN }} + run: | + if [ -z "$LBTSM_REPO_TOKEN" ]; then + echo "::warning::LBTSM_REPO_TOKEN is not set; private github.com/lbtsm modules must be public or Go module download will fail." + exit 0 + fi + printf "machine github.com\nlogin x-access-token\npassword %s\n" "$LBTSM_REPO_TOKEN" > ~/.netrc + chmod 0600 ~/.netrc + - name: Run unit tests run: go test ./internal/... ./chains/tron ./pkg/monitor @@ -65,6 +78,7 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | + type=raw,value=latest,enable={{is_default_branch}} type=ref,event=branch type=ref,event=tag type=sha,prefix=sha- @@ -86,5 +100,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | VERSION=${{ github.ref_name }} + GOPRIVATE=${{ env.GOPRIVATE }} + secrets: | + github_token=${{ secrets.LBTSM_REPO_TOKEN }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index fd23864..db1c866 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,21 @@ FROM golang:${GO_VERSION}-bookworm AS builder WORKDIR /src +ARG GOPRIVATE=github.com/lbtsm/* +ENV GOPRIVATE=${GOPRIVATE} +ENV GONOSUMDB=${GOPRIVATE} + COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=secret,id=github_token \ + set -eu; \ + cleanup() { rm -f /root/.netrc; }; \ + trap cleanup EXIT; \ + if [ -s /run/secrets/github_token ]; then \ + token="$(cat /run/secrets/github_token)"; \ + printf "machine github.com\nlogin x-access-token\npassword %s\n" "$token" > /root/.netrc; \ + chmod 0600 /root/.netrc; \ + fi; \ + go mod download COPY . . diff --git a/README.md b/README.md index 81f542d..1e5cb4a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,14 @@ docker compose up -d --build docker compose logs -f bridge-monitor ``` +If the `github.com/lbtsm/*` Go modules are private, set a GitHub token with +read access before building locally: + +```shell +export LBTSM_REPO_TOKEN="github_pat_xxx" +docker compose up -d --build +``` + After pulling updates on the server: ```shell @@ -53,3 +61,7 @@ docker compose up -d --build GitHub Actions builds and tests the project on pull requests, and builds the Docker image on pushes to `main`, `master`, or version tags. Non-PR builds are published to GitHub Container Registry as `ghcr.io//:`. +The default branch also publishes `ghcr.io//:latest`. + +For private `github.com/lbtsm/*` dependencies, add a repository secret named +`LBTSM_REPO_TOKEN` with read access to the private dependency repositories. diff --git a/compose.yaml b/compose.yaml index 04dee8c..a0f19ae 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,6 +4,8 @@ services: context: . args: VERSION: ${MONITOR_VERSION:-local} + secrets: + - github_token image: ${MONITOR_IMAGE:-bridge-monitor:local} container_name: ${MONITOR_CONTAINER_NAME:-bridge-monitor} restart: unless-stopped @@ -15,3 +17,7 @@ services: volumes: - ${MONITOR_DATA_DIR:-/opt/bridge-monitor}:/app/runtime command: ["monitor", "--config", "/app/runtime/config.json"] + +secrets: + github_token: + environment: LBTSM_REPO_TOKEN From 2bc932bed2186c2d81393cc5c371504da828775d Mon Sep 17 00:00:00 2001 From: lbtsm Date: Sat, 5 Sep 2026 15:00:40 +0800 Subject: [PATCH 3/3] Add sync height alarm toggle --- README.md | 1 + internal/config/apply.go | 1 + internal/config/apply_test.go | 61 ++++++++++++---------- internal/config/config.go | 88 ++++++++++++++++++-------------- internal/config/config_test.go | 45 ++++++++++++++++ internal/config/const.go | 1 + internal/config/diff_test.go | 26 ++++++++-- internal/config/reloader_test.go | 18 +++++++ pkg/monitor/monitor.go | 13 +++-- pkg/monitor/monitor_test.go | 37 ++++++++++++++ 10 files changed, 216 insertions(+), 75 deletions(-) create mode 100644 internal/config/config_test.go diff --git a/README.md b/README.md index 1e5cb4a..2ce2fc5 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ files and keystores out of git. "waterLine": "5000000000000000000", // If the user balance is lower than, an alarm will be triggered, unit : wei "changeInterval": "3000", // How long does the lightnode height remain unchanged, triggering the alarm, use for near unit : seconds "checkHeightCount": "20", // How long does the lightnode height not change remain unchanged, triggering the alarm, default 15 + "syncHeightAlarm": "false", // Optional: disable other-chain-to-map sync height alarm, default true } ``` diff --git a/internal/config/apply.go b/internal/config/apply.go index 75e156d..f0d0bb3 100644 --- a/internal/config/apply.go +++ b/internal/config/apply.go @@ -23,6 +23,7 @@ func ApplyHotReloadable(target, source *OptConfig) { target.WaterLine = source.WaterLine target.LightNode = source.LightNode target.ApiUrl = source.ApiUrl + target.SyncHeightAlarm = source.SyncHeightAlarm target.From = source.From target.Users = source.Users target.ContractToken = source.ContractToken diff --git a/internal/config/apply_test.go b/internal/config/apply_test.go index bf8dd81..31fb109 100644 --- a/internal/config/apply_test.go +++ b/internal/config/apply_test.go @@ -15,20 +15,21 @@ func TestApplyHotReloadable_OverwritesAllReloadableFields(t *testing.T) { oldTss := &Tss{Maintainer: "old-maint"} target := &OptConfig{ - Name: "bsc", - Id: 56, - Endpoint: "http://old", // immutable, must NOT be touched - KeystorePath: "/keys/old", // immutable - WaterLine: "100", - From: []string{"0xold"}, - Users: []From{{Group: "g1", From: "0xa"}}, - ContractToken: []ContractToken{{Address: "0xold-ct"}}, - Energies: []Energy{{Address: "old-en"}}, - Tss: oldTss, - Tk: oldTk, - Genni: oldGenni, - LightNode: common.HexToAddress("0xaaaa"), - ApiUrl: "old-api", + Name: "bsc", + Id: 56, + Endpoint: "http://old", // immutable, must NOT be touched + KeystorePath: "/keys/old", // immutable + WaterLine: "100", + From: []string{"0xold"}, + Users: []From{{Group: "g1", From: "0xa"}}, + ContractToken: []ContractToken{{Address: "0xold-ct"}}, + Energies: []Energy{{Address: "old-en"}}, + Tss: oldTss, + Tk: oldTk, + Genni: oldGenni, + LightNode: common.HexToAddress("0xaaaa"), + ApiUrl: "old-api", + SyncHeightAlarm: true, } newTk := &Token{BridgeAddr: "new-bridge"} @@ -36,20 +37,21 @@ func TestApplyHotReloadable_OverwritesAllReloadableFields(t *testing.T) { newTss := &Tss{Maintainer: "new-maint"} source := &OptConfig{ - Name: "bsc", - Id: 56, - Endpoint: "http://new", // ignored - KeystorePath: "/keys/new", // ignored - WaterLine: "200", - From: []string{"0xnew"}, - Users: []From{{Group: "g2", From: "0xb"}}, - ContractToken: []ContractToken{{Address: "0xnew-ct"}}, - Energies: []Energy{{Address: "new-en"}}, - Tss: newTss, - Tk: newTk, - Genni: newGenni, - LightNode: common.HexToAddress("0xbbbb"), - ApiUrl: "new-api", + Name: "bsc", + Id: 56, + Endpoint: "http://new", // ignored + KeystorePath: "/keys/new", // ignored + WaterLine: "200", + From: []string{"0xnew"}, + Users: []From{{Group: "g2", From: "0xb"}}, + ContractToken: []ContractToken{{Address: "0xnew-ct"}}, + Energies: []Energy{{Address: "new-en"}}, + Tss: newTss, + Tk: newTk, + Genni: newGenni, + LightNode: common.HexToAddress("0xbbbb"), + ApiUrl: "new-api", + SyncHeightAlarm: false, } ApplyHotReloadable(target, source) @@ -84,6 +86,9 @@ func TestApplyHotReloadable_OverwritesAllReloadableFields(t *testing.T) { if target.ApiUrl != "new-api" { t.Errorf("ApiUrl = %q, want new-api", target.ApiUrl) } + if target.SyncHeightAlarm { + t.Error("SyncHeightAlarm = true, want false") + } } // TestApplyHotReloadable_PreservesImmutableFields verifies that fields diff --git a/internal/config/config.go b/internal/config/config.go index 7b3187f..5d383df 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -255,50 +255,52 @@ func loadConfig(file string, config *Config) error { } type OptConfig struct { - Name string // Human-readable chain name - Id ChainId // ChainID - Endpoint string // url for rpc endpoint - From []string // address of key to use - KeystorePath string // Location of keyfiles - GasLimit *big.Int - MaxGasPrice *big.Int - GasMultiplier *big.Float - WaterLine string - ChangeInterval string - ApiUrl string - StartBlock *big.Int - MapChainID ChainId - LightNode common.Address // the lightnode to sync header - Tk *Token - Genni *Api - CheckHgtCount int64 - Users []From - ContractToken []ContractToken - Energies []Energy - Tss *Tss + Name string // Human-readable chain name + Id ChainId // ChainID + Endpoint string // url for rpc endpoint + From []string // address of key to use + KeystorePath string // Location of keyfiles + GasLimit *big.Int + MaxGasPrice *big.Int + GasMultiplier *big.Float + WaterLine string + ChangeInterval string + ApiUrl string + StartBlock *big.Int + MapChainID ChainId + LightNode common.Address // the lightnode to sync header + Tk *Token + Genni *Api + CheckHgtCount int64 + SyncHeightAlarm bool + Users []From + ContractToken []ContractToken + Energies []Energy + Tss *Tss } // ParseOptConfig uses a core.ChainConfig to construct a corresponding Config func ParseOptConfig(chainCfg *ChainConfig, tks *Token, genni *Api, users []From) (*OptConfig, error) { config := &OptConfig{ - Id: chainCfg.Id, - From: strings.Split(chainCfg.From, ","), - Name: chainCfg.Name, - Endpoint: chainCfg.Endpoint, - KeystorePath: DefaultKeystorePath, - WaterLine: "", - ChangeInterval: "", - StartBlock: big.NewInt(0), - GasLimit: big.NewInt(DefaultGasLimit), - MaxGasPrice: big.NewInt(DefaultGasPrice), - GasMultiplier: big.NewFloat(DefaultGasMultiplier), - Tk: tks, - Genni: genni, - CheckHgtCount: DefaultCheckHgtCount, - ContractToken: chainCfg.ContractToken, - Energies: chainCfg.Energies, - Users: users, - Tss: chainCfg.Tss, + Id: chainCfg.Id, + From: strings.Split(chainCfg.From, ","), + Name: chainCfg.Name, + Endpoint: chainCfg.Endpoint, + KeystorePath: DefaultKeystorePath, + WaterLine: "", + ChangeInterval: "", + StartBlock: big.NewInt(0), + GasLimit: big.NewInt(DefaultGasLimit), + MaxGasPrice: big.NewInt(DefaultGasPrice), + GasMultiplier: big.NewFloat(DefaultGasMultiplier), + Tk: tks, + Genni: genni, + CheckHgtCount: DefaultCheckHgtCount, + SyncHeightAlarm: true, + ContractToken: chainCfg.ContractToken, + Energies: chainCfg.Energies, + Users: users, + Tss: chainCfg.Tss, } if chainCfg.NearKeystorePath != "" { @@ -338,5 +340,13 @@ func ParseOptConfig(chainCfg *ChainConfig, tks *Token, genni *Api, users []From) config.CheckHgtCount = int64(count) } + if syncHeightAlarm, ok := chainCfg.Opts[SyncHeightAlarm]; ok && syncHeightAlarm != "" { + enabled, err := strconv.ParseBool(syncHeightAlarm) + if err != nil { + return nil, fmt.Errorf("%s must be boolean: %w", SyncHeightAlarm, err) + } + config.SyncHeightAlarm = enabled + } + return config, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..03de946 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,45 @@ +package config + +import "testing" + +func TestParseOptConfig_SyncHeightAlarmDefaultEnabled(t *testing.T) { + cfg, err := ParseOptConfig(&ChainConfig{ + Name: "klaytn", + Id: 8217, + Endpoint: "http://klaytn.local", + Opts: map[string]string{}, + }, nil, nil, nil) + if err != nil { + t.Fatalf("ParseOptConfig returned error: %v", err) + } + if !cfg.SyncHeightAlarm { + t.Fatal("SyncHeightAlarm = false, want true by default") + } +} + +func TestParseOptConfig_SyncHeightAlarmCanBeDisabled(t *testing.T) { + cfg, err := ParseOptConfig(&ChainConfig{ + Name: "klaytn", + Id: 8217, + Endpoint: "http://klaytn.local", + Opts: map[string]string{SyncHeightAlarm: "false"}, + }, nil, nil, nil) + if err != nil { + t.Fatalf("ParseOptConfig returned error: %v", err) + } + if cfg.SyncHeightAlarm { + t.Fatal("SyncHeightAlarm = true, want false when opts.syncHeightAlarm=false") + } +} + +func TestParseOptConfig_InvalidSyncHeightAlarmRejected(t *testing.T) { + _, err := ParseOptConfig(&ChainConfig{ + Name: "klaytn", + Id: 8217, + Endpoint: "http://klaytn.local", + Opts: map[string]string{SyncHeightAlarm: "nope"}, + }, nil, nil, nil) + if err == nil { + t.Fatal("ParseOptConfig returned nil error, want invalid syncHeightAlarm rejected") + } +} diff --git a/internal/config/const.go b/internal/config/const.go index 875a15b..a59d87b 100644 --- a/internal/config/const.go +++ b/internal/config/const.go @@ -44,6 +44,7 @@ var ( ChangeInterval = "changeInterval" CheckHeightCount = "checkHeightCount" ApiUrl = "apiUrl" + SyncHeightAlarm = "syncHeightAlarm" ) const ( diff --git a/internal/config/diff_test.go b/internal/config/diff_test.go index 770db04..477fe33 100644 --- a/internal/config/diff_test.go +++ b/internal/config/diff_test.go @@ -99,6 +99,24 @@ func TestDiffChains_DataOnlyChangeUpdates(t *testing.T) { } } +func TestDiffChains_SyncHeightAlarmChangeUpdates(t *testing.T) { + old := []RawChainConfig{chainMAP(), chainBSC(func(c *RawChainConfig) { + c.Opts = map[string]string{SyncHeightAlarm: "true"} + })} + new := []RawChainConfig{chainMAP(), chainBSC(func(c *RawChainConfig) { + c.Opts = map[string]string{SyncHeightAlarm: "false"} + })} + + d := DiffChains(old, new) + + if got := names(d.Updates); !reflect.DeepEqual(got, []string{"bsc"}) { + t.Errorf("Updates = %v, want [bsc]", got) + } + if len(d.Restarts) != 0 { + t.Errorf("did not expect Restarts for syncHeightAlarm change") + } +} + func TestDiffChains_NoChangeProducesEmptyDiff(t *testing.T) { chains := []RawChainConfig{chainMAP(), chainBSC()} d := DiffChains(chains, chains) @@ -111,15 +129,15 @@ func TestDiffChains_MixedAddRemoveRestartUpdate(t *testing.T) { old := []RawChainConfig{ chainMAP(), chainBSC(), - {Name: "tron", Id: "728126428", Endpoint: "http://tron.old"}, // restart candidate - {Name: "old-chain", Id: "999", Endpoint: "http://x"}, // remove + {Name: "tron", Id: "728126428", Endpoint: "http://tron.old"}, // restart candidate + {Name: "old-chain", Id: "999", Endpoint: "http://x"}, // remove {Name: "eth", Id: "1", Endpoint: "u", Users: []From{{Group: "g"}}}, // update } new := []RawChainConfig{ chainMAP(), chainBSC(), - {Name: "tron", Id: "728126428", Endpoint: "http://tron.NEW"}, // restart - {Name: "new-chain", Id: "100", Endpoint: "http://y"}, // add + {Name: "tron", Id: "728126428", Endpoint: "http://tron.NEW"}, // restart + {Name: "new-chain", Id: "100", Endpoint: "http://y"}, // add {Name: "eth", Id: "1", Endpoint: "u", Users: []From{{Group: "g2"}}}, // update } diff --git a/internal/config/reloader_test.go b/internal/config/reloader_test.go index 4910978..57ab2c1 100644 --- a/internal/config/reloader_test.go +++ b/internal/config/reloader_test.go @@ -159,6 +159,24 @@ func TestReloadFromFile_RejectsChangeIntervalChange(t *testing.T) { } } +func TestReloadFromFile_AllowsSyncHeightAlarmChange(t *testing.T) { + dir := t.TempDir() + old := validRawConfig() + old.Chains[1].Opts = map[string]string{SyncHeightAlarm: "true"} + store := NewStore(&old) + + updated := validRawConfig() + updated.Chains[1].Opts = map[string]string{SyncHeightAlarm: "false"} + path := writeJSON(t, dir, "config.json", updated) + + if err := ReloadFromFile(store, path); err != nil { + t.Fatalf("syncHeightAlarm change should be allowed, got error: %v", err) + } + if got := store.Load().Chains[1].Opts[SyncHeightAlarm]; got != "false" { + t.Fatalf("syncHeightAlarm = %q, want false", got) + } +} + func TestReloadFromFile_RejectsNameRenameWithoutIdChange(t *testing.T) { // pure rename (chain.id stays same, only name flips) is rejected dir := t.TempDir() diff --git a/pkg/monitor/monitor.go b/pkg/monitor/monitor.go index 5bee631..0086df1 100644 --- a/pkg/monitor/monitor.go +++ b/pkg/monitor/monitor.go @@ -515,20 +515,25 @@ func (m *Monitor) nativeCheck(contract string) { } func (m *Monitor) OtherChainCheck() { - if m.Cfg.LightNode == config.ZeroAddress { + snap := m.Snapshot() + if !snap.SyncHeightAlarm { + m.heightCount = 0 + return + } + if snap.LightNode == config.ZeroAddress { return } - height, err := mapprotocol.Get2MapHeight(m.Cfg.Id) + height, err := mapprotocol.Get2MapHeight(snap.Id) m.Log.Info("Check Height", "syncHeight", height, "record", m.syncedHeight, "heightCount", m.heightCount) if err != nil { m.Log.Error("get2MapHeight failed", "err", err) } else { if m.syncedHeight.Uint64() == height.Uint64() { m.heightCount = m.heightCount + 1 - if m.heightCount >= m.Cfg.CheckHgtCount { + if m.heightCount >= snap.CheckHgtCount { util.Alarm(context.Background(), fmt.Sprintf("Sync Height No change within %d minutes chains=%s, height=%d", - m.Cfg.CheckHgtCount, m.Cfg.Name, height.Uint64())) + snap.CheckHgtCount, snap.Name, height.Uint64())) } } else { m.heightCount = 0 diff --git a/pkg/monitor/monitor_test.go b/pkg/monitor/monitor_test.go index 1736bbe..745a9da 100644 --- a/pkg/monitor/monitor_test.go +++ b/pkg/monitor/monitor_test.go @@ -1,10 +1,14 @@ package monitor import ( + "math/big" "testing" + "github.com/ChainSafe/log15" + "github.com/ethereum/go-ethereum/common" "github.com/mapprotocol/monitor/internal/chain" "github.com/mapprotocol/monitor/internal/config" + "github.com/mapprotocol/monitor/internal/mapprotocol" ) // TestPrepareTick_ReadsLatestWaterLine: each call to prepareTick should @@ -57,3 +61,36 @@ func TestPrepareTick_SnapshotIsIndependent(t *testing.T) { t.Fatalf("snapshot mutated retroactively, WaterLine=%q", snap.WaterLine) } } + +func TestOtherChainCheck_SkipsWhenSyncHeightAlarmDisabled(t *testing.T) { + original := mapprotocol.Get2MapHeight + defer func() { + mapprotocol.Get2MapHeight = original + }() + + called := false + mapprotocol.Get2MapHeight = func(chainID config.ChainId) (*big.Int, error) { + called = true + return big.NewInt(0), nil + } + + cfg := &config.OptConfig{ + Name: "klaytn", + Id: 8217, + LightNode: common.HexToAddress("0x0000000000000000000000000000000000000001"), + CheckHgtCount: 1000, + SyncHeightAlarm: false, + } + m := New(chain.NewCommonSync(nil, cfg, log15.New(), nil, nil)) + m.heightCount = 99 + m.syncedHeight = big.NewInt(123) + + m.OtherChainCheck() + + if called { + t.Fatal("Get2MapHeight was called, want skipped when SyncHeightAlarm=false") + } + if m.heightCount != 0 { + t.Fatalf("heightCount = %d, want reset to 0", m.heightCount) + } +}