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
323 changes: 323 additions & 0 deletions benchmarks/wormhole-vaa-latency.yml

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions harnesses/wormhole-vaa-latency/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM golang:1.24-alpine AS builder

WORKDIR /app
RUN apk add --no-cache git

COPY go.mod ./
RUN go mod download || true

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o /app/monitor ./cmd/script

FROM debian:bookworm-slim

WORKDIR /app
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/monitor /app/monitor

EXPOSE 2112

CMD ["/app/monitor"]
37 changes: 37 additions & 0 deletions harnesses/wormhole-vaa-latency/cmd/script/chains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

// emitterChainSlug maps Wormhole's numeric emitter-chain id to the site's
// chain slug. Only chains with non-trivial VAA volume are enumerated —
// unrecognised chains fall through to a synthetic "chain<id>" slug in
// main.go so the harness never drops a sample silently.
//
// Source: https://docs.wormhole.com/wormhole/reference/blockchain-ids
// (kept manually short — Wormhole periodically adds new chain ids and
// we only want to surface chains that (a) have public infrastructure
// and (b) are already tracked elsewhere on the site.)
var emitterChainSlug = map[int]string{
1: "solana",
2: "ethereum",
4: "bnb",
5: "polygon",
6: "avalanche",
10: "fantom",
14: "celo",
15: "near",
16: "moonbeam",
19: "injective",
21: "sui",
22: "aptos",
23: "arbitrum",
24: "optimism",
30: "base",
32: "sei",
34: "scroll",
35: "mantle",
38: "linea",
39: "berachain",
44: "unichain",
45: "world-chain",
48: "monad",
50: "ink",
}
198 changes: 198 additions & 0 deletions harnesses/wormhole-vaa-latency/cmd/script/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// wormhole-vaa-latency: single-provider observation-only bench.
//
// Every 60s, polls https://api.wormholescan.io/api/v1/vaas?pageSize=100 for
// the most recent VAAs. For each VAA the monitor has not seen before,
// computes `updatedAt − timestamp` as the finalization latency (source-tx
// observation → Guardian quorum reached + indexer wrote the row), and
// records it in a Prometheus histogram bucketed by source chain.
//
// The harness never sends canary transactions — Wormhole handles enough
// organic volume (~thousands of VAAs/day) that passive observation gives
// statistically robust per-chain distributions inside a 24-hour window.
//
// Dedupe: last N=10000 VAA ids kept in memory (rolling LRU on insert
// order). Wormholescan returns ~100 VAAs/min sustained; a 10k cache
// holds ~100 min of history, comfortably wider than the poll window.
package main

import (
"container/list"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"

"github.com/prometheus/client_golang/prometheus/promhttp"
)

const (
wormholescanURL = "https://api.wormholescan.io/api/v1/vaas?pageSize=100"
pollInterval = 60 * time.Second
requestTimeout = 15 * time.Second
dedupeCacheMaxLen = 10_000
metricsListenAddr = ":2112"
)

type vaaEntry struct {
ID string `json:"id"`
EmitterChain int `json:"emitterChain"`
Timestamp string `json:"timestamp"`
UpdatedAt string `json:"updatedAt"`
}

type vaaResponse struct {
Data []vaaEntry `json:"data"`
}

// lruSet keeps the last N inserted keys, evicting FIFO. Not thread-safe —
// caller (single-goroutine poller) synchronises.
type lruSet struct {
max int
order *list.List
set map[string]*list.Element
}

func newLRUSet(max int) *lruSet {
return &lruSet{max: max, order: list.New(), set: make(map[string]*list.Element, max)}
}

func (l *lruSet) contains(k string) bool { _, ok := l.set[k]; return ok }

func (l *lruSet) add(k string) {
if _, ok := l.set[k]; ok {
return
}
el := l.order.PushBack(k)
l.set[k] = el
for l.order.Len() > l.max {
front := l.order.Front()
if front == nil {
return
}
l.order.Remove(front)
delete(l.set, front.Value.(string))
}
}

func (l *lruSet) size() int { return l.order.Len() }

func chainSlug(id int) string {
if s, ok := emitterChainSlug[id]; ok {
return s
}
return "chain" + strconv.Itoa(id)
}

func poll(ctx context.Context, client *http.Client, seen *lruSet) error {
req, err := http.NewRequestWithContext(ctx, "GET", wormholescanURL, nil)
if err != nil {
return fmt.Errorf("request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "OpenChainBench/wormhole-vaa-latency")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("http: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
return fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
}
var parsed vaaResponse
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return fmt.Errorf("decode: %w", err)
}
fresh := 0
for _, v := range parsed.Data {
if v.ID == "" || seen.contains(v.ID) {
continue
}
ts, err1 := time.Parse(time.RFC3339, v.Timestamp)
ua, err2 := time.Parse(time.RFC3339Nano, v.UpdatedAt)
if err1 != nil || err2 != nil {
// Skip malformed rows silently — wormholescan occasionally
// backfills with non-RFC3339 nano timestamps; not worth
// polluting the error counter with parse noise.
seen.add(v.ID)
continue
}
delta := ua.Sub(ts).Seconds()
if delta < 0 || delta > 3600 {
// Guard against clock skew / backfilled VAAs whose
// updatedAt refers to a much later re-indexing event.
seen.add(v.ID)
continue
}
slug := chainSlug(v.EmitterChain)
vaaLatencySeconds.WithLabelValues(slug).Observe(delta)
vaaSeenTotal.WithLabelValues(slug).Inc()
seen.add(v.ID)
fresh++
}
dedupeCacheSize.Set(float64(seen.size()))
log.Printf("poll: %d rows, %d fresh, dedupe cache=%d", len(parsed.Data), fresh, seen.size())
return nil
}

func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("wormhole-vaa-latency: starting")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

client := &http.Client{Timeout: requestTimeout}
seen := newLRUSet(dedupeCacheMaxLen)
var mu sync.Mutex

go func() {
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(200)
_, _ = w.Write([]byte("ok"))
})
log.Printf("metrics: listening on %s", metricsListenAddr)
if err := http.ListenAndServe(metricsListenAddr, nil); err != nil {
log.Fatalf("metrics server: %v", err)
}
}()

// Prime once immediately so scrapes get non-empty series without
// waiting the first interval.
mu.Lock()
if err := poll(ctx, client, seen); err != nil {
log.Printf("poll error: %v", err)
pollErrors.Inc()
}
mu.Unlock()

ticker := time.NewTicker(pollInterval)
defer ticker.Stop()

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

for {
select {
case <-ticker.C:
mu.Lock()
if err := poll(ctx, client, seen); err != nil {
log.Printf("poll error: %v", err)
pollErrors.Inc()
}
mu.Unlock()
case <-sig:
log.Println("shutdown")
return
}
}
}
53 changes: 53 additions & 0 deletions harnesses/wormhole-vaa-latency/cmd/script/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"github.com/prometheus/client_golang/prometheus"
)

// wormhole_vaa_latency_seconds is a histogram of finalization latency,
// bucketed by source chain (Wormhole emitterChain). Latency is defined
// as `updatedAt − timestamp` from the wormholescan `/api/v1/vaas`
// payload: source-chain observation → Guardian quorum + indexer.
//
// Buckets are geometric across the observed distribution (BSC ~5-11s,
// Ethereum ~15-25s, Solana ~17-26s, Moonbeam ~40s, worst case >5min on
// slow chains during backfills).
var (
vaaLatencySeconds = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "wormhole_vaa_latency_seconds",
Help: "Time from source-chain observation to Guardian quorum for a Wormhole VAA, by source chain.",
Buckets: []float64{2, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 300, 600},
},
[]string{"source_chain"},
)

vaaSeenTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "wormhole_vaa_seen_total",
Help: "Total number of unique VAAs observed by this monitor, by source chain.",
},
[]string{"source_chain"},
)

pollErrors = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "wormhole_vaa_poll_errors_total",
Help: "Total number of failed pollings of the wormholescan VAA feed.",
},
)

dedupeCacheSize = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "wormhole_vaa_dedupe_cache_size",
Help: "Number of recent VAA ids the monitor keeps for deduplication.",
},
)
)

func init() {
prometheus.MustRegister(vaaLatencySeconds)
prometheus.MustRegister(vaaSeenTotal)
prometheus.MustRegister(pollErrors)
prometheus.MustRegister(dedupeCacheSize)
}
18 changes: 18 additions & 0 deletions harnesses/wormhole-vaa-latency/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module wormhole-vaa-latency

go 1.24.0

require github.com/prometheus/client_golang v1.23.2

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/sys v0.36.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
46 changes: 46 additions & 0 deletions harnesses/wormhole-vaa-latency/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Binary file added public/logos/wormhole.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/data/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,12 @@ export const PROVIDER_REGISTRY: Record<string, ProviderRegistryEntry> = {
"Gelato Network runs a rollup-as-a-service platform and operates RPC infrastructure for OP Stack + Arbitrum Orbit chains it hosts. On the RPC latency cluster it appears as the Kraken Ink chain's Gelato-backed official endpoint (`rpc-gel.inkonchain.com`), running active-active alongside the QuickNode-backed sibling.",
twitter: "@gelatonetwork",
},
wormhole: {
url: "https://wormhole.com",
description:
"Wormhole is a cross-chain messaging network. A network of 19 Guardian nodes observes events on source chains and signs VAAs (Verified Action Approvals) once source-chain finality is reached; 13-of-19 Guardian signatures constitute quorum. Any signed VAA can then be relayed to any destination chain Wormhole supports (30+ EVM, Solana, Sui, Aptos, Near, Cosmos SDK chains) to unlock the corresponding action.",
twitter: "@wormhole",
},
infura: {
url: "https://www.infura.io",
description:
Expand Down
Loading
Loading