diff --git a/cache/large_payload_test.go b/cache/large_payload_test.go new file mode 100644 index 000000000..8d8fd27bd --- /dev/null +++ b/cache/large_payload_test.go @@ -0,0 +1,70 @@ +package cache + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/ethpandaops/dora/types" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" + "github.com/sirupsen/logrus" +) + +// A mainnet-scale slot arrival payload runs to hundreds of kilobytes, well past +// the 100KB MaxEntrySize. That setting only sizes the initial allocation; the +// real ceiling is HardMaxCacheSize/Shards, so these must still round trip. If +// they ever stop, every request rebuilds and the page cache silently stops +// absorbing load. +func TestLargeArrivalPayloadRoundTrips(t *testing.T) { + utils.Config = &types.Config{} + + tc, err := NewTieredCache(100, "", "test", logrus.New()) + if err != nil { + t.Fatalf("cache init: %v", err) + } + + for _, nodes := range []int{500, 1000, 2000} { + resp := &models.SlotArrivalResponse{Slot: 15060285, Settled: true} + for i := 0; i < nodes; i++ { + resp.Nodes = append(resp.Nodes, &models.SlotArrivalNode{ + Name: fmt.Sprintf("pub-contributoor/operator-%04d/hashed-abcdef01", i), + FullName: fmt.Sprintf("pub-contributoor/operator-%04d/hashed-abcdef0123456789", i), + Group: "community", + Implementation: "lighthouse", + Continent: "EU", + Country: "Germany", + CountryCode: "de", + MinMs: uint32(700 + i%800), + }) + } + + raw, err := json.Marshal(resp) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + key := fmt.Sprintf("slotarrival:15060285:%d", nodes) + if err := tc.Set(key, resp, time.Hour); err != nil { + t.Errorf("%d nodes (%d KB payload): Set failed: %v", nodes, len(raw)/1024, err) + + continue + } + + out := &models.SlotArrivalResponse{} + if _, err := tc.Get(key, out); err != nil { + t.Errorf("%d nodes (%d KB): Get failed: %v", nodes, len(raw)/1024, err) + + continue + } + + if len(out.Nodes) != nodes { + t.Errorf("%d nodes: round-tripped %d", nodes, len(out.Nodes)) + + continue + } + + t.Logf("%4d nodes, %4d KB payload: cached and retrieved OK", nodes, len(raw)/1024) + } +} diff --git a/cache/nil_fidelity_epoch_test.go b/cache/nil_fidelity_epoch_test.go new file mode 100644 index 000000000..ec9c96c7e --- /dev/null +++ b/cache/nil_fidelity_epoch_test.go @@ -0,0 +1,51 @@ +package cache + +import ( + "testing" + "time" + + "github.com/ethpandaops/dora/types" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" + "github.com/sirupsen/logrus" +) + +func TestEpochPageNilArrivalThroughCache(t *testing.T) { + utils.Config = &types.Config{} + + tc, err := NewTieredCache(100, "", "test", logrus.New()) + if err != nil { + t.Fatal(err) + } + + // no arrival data for this slot: ArrivalNodes is the presence signal + in := &models.EpochPageData{ + Epoch: 7, + Slots: []*models.EpochPageDataSlot{ + {Slot: 224}, + {Slot: 225, ArrivalNodes: 12, ArrivalMinMs: 742, ArrivalP90Ms: 1180}, + }, + } + + if err := tc.Set("epoch:7", in, time.Hour); err != nil { + t.Fatalf("set: %v", err) + } + + out := &models.EpochPageData{} + if _, err := tc.Get("epoch:7", out); err != nil { + t.Fatalf("get: %v", err) + } + + absent, present := out.Slots[0], out.Slots[1] + t.Logf("absent slot: nodes=%d min=%d p90=%d", absent.ArrivalNodes, absent.ArrivalMinMs, absent.ArrivalP90Ms) + t.Logf("present slot: nodes=%d min=%d p90=%d", present.ArrivalNodes, present.ArrivalMinMs, present.ArrivalP90Ms) + + if absent.ArrivalNodes != 0 { + t.Errorf("slot with no data came back with %d nodes", absent.ArrivalNodes) + } + + if present.ArrivalNodes != 12 || present.ArrivalMinMs != 742 || present.ArrivalP90Ms != 1180 { + t.Errorf("real arrival data did not survive: nodes=%d min=%d p90=%d", + present.ArrivalNodes, present.ArrivalMinMs, present.ArrivalP90Ms) + } +} diff --git a/cache/nil_fidelity_test.go b/cache/nil_fidelity_test.go new file mode 100644 index 000000000..c22ac44e4 --- /dev/null +++ b/cache/nil_fidelity_test.go @@ -0,0 +1,68 @@ +package cache + +import ( + "testing" + "time" + + "github.com/ethpandaops/dora/types" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" + "github.com/sirupsen/logrus" +) + +func TestNilPointerFidelityThroughCache(t *testing.T) { + utils.Config = &types.Config{} + + tc, err := NewTieredCache(100, "", "test", logrus.New()) + if err != nil { + t.Fatal(err) + } + + // a node that only reported gossip: every other series must stay nil + p2p := uint32(742) + in := &models.SlotArrivalResponse{ + Slot: 1, Settled: true, + Nodes: []*models.SlotArrivalNode{{Name: "n", P2PMs: &p2p}}, + } + + if err := tc.Set("slotarrival:1:x", in, time.Hour); err != nil { + t.Fatalf("set: %v", err) + } + + out := &models.SlotArrivalResponse{} + if _, err := tc.Get("slotarrival:1:x", out); err != nil { + t.Fatalf("get: %v", err) + } + + n := out.Nodes[0] + t.Logf("after round trip: P2PMs=%v APIMs=%v HeadMs=%v NPMs=%v", + deref(n.P2PMs), deref(n.APIMs), deref(n.HeadMs), deref(n.NPMs)) + + for name, ptr := range map[string]*uint32{"APIMs": n.APIMs, "HeadMs": n.HeadMs, "NPMs": n.NPMs} { + if ptr != nil { + t.Errorf("%s should be nil after round trip, got pointer to %d", name, *ptr) + } + } +} + +func deref(p *uint32) string { + if p == nil { + return "nil" + } + + return "&" + itoa(*p) +} + +func itoa(v uint32) string { + if v == 0 { + return "0" + } + + var b []byte + for v > 0 { + b = append([]byte{byte('0' + v%10)}, b...) + v /= 10 + } + + return string(b) +} diff --git a/cache/nil_fidelity_waves_test.go b/cache/nil_fidelity_waves_test.go new file mode 100644 index 000000000..92bc8a934 --- /dev/null +++ b/cache/nil_fidelity_waves_test.go @@ -0,0 +1,78 @@ +package cache + +import ( + "testing" + "time" + + "github.com/ethpandaops/dora/types" + "github.com/ethpandaops/dora/types/models" + "github.com/ethpandaops/dora/utils" + "github.com/sirupsen/logrus" +) + +// The waves response leans on nil to mean "no data": a nil section hides its +// panel, a nil FirstSeenMs renders "not seen". Like the arrival response, it +// must stay on the cache's JSON path, where nil survives a round trip. +func TestWavesNilFidelityThroughCache(t *testing.T) { + utils.Config = &types.Config{} + + tc, err := NewTieredCache(100, "", "test", logrus.New()) + if err != nil { + t.Fatal(err) + } + + pct := 87.5 + in := &models.SlotWavesResponse{ + Slot: 9, Settled: true, + // Attestations deliberately nil: a network without the cbt attestation + // model must not come back as an empty wave. + Columns: &models.SlotColumnWave{ + BlobCount: 3, + Columns: []*models.SlotColumn{ + // probed but never seen + {Index: 0, AvailabilityPct: &pct, Probes: 4}, + // seen but never probed + {Index: 1, FirstSeenMs: ptr(uint32(1300))}, + }, + }, + } + + if err := tc.Set("slotwaves:9:x", in, time.Hour); err != nil { + t.Fatalf("set: %v", err) + } + + out := &models.SlotWavesResponse{} + if _, err := tc.Get("slotwaves:9:x", out); err != nil { + t.Fatalf("get: %v", err) + } + + if out.Attestations != nil { + t.Errorf("Attestations should stay nil, got %+v", out.Attestations) + } + + if out.Columns == nil { + t.Fatal("Columns section lost") + } + + unseen, unprobed := out.Columns.Columns[0], out.Columns.Columns[1] + + if unseen.FirstSeenMs != nil { + t.Errorf("unseen column got FirstSeenMs %d", *unseen.FirstSeenMs) + } + + if unseen.AvailabilityPct == nil || *unseen.AvailabilityPct != pct { + t.Errorf("availability lost: %v", unseen.AvailabilityPct) + } + + if unprobed.AvailabilityPct != nil { + t.Errorf("unprobed column got AvailabilityPct %v", *unprobed.AvailabilityPct) + } + + if unprobed.FirstSeenMs == nil || *unprobed.FirstSeenMs != 1300 { + t.Errorf("first seen lost: %v", unprobed.FirstSeenMs) + } +} + +func ptr[T any](v T) *T { + return &v +} diff --git a/clients/consensus/chainspec.go b/clients/consensus/chainspec.go index b029e8470..fe8c26ee3 100644 --- a/clients/consensus/chainspec.go +++ b/clients/consensus/chainspec.go @@ -136,6 +136,8 @@ type ChainSpecConfig struct { ContributionDueBpsGloas uint64 `yaml:"CONTRIBUTION_DUE_BPS_GLOAS" check-if-fork:"GloasForkEpoch"` SyncMessageDueBpsGloas uint64 `yaml:"SYNC_MESSAGE_DUE_BPS_GLOAS" check-if-fork:"GloasForkEpoch"` PayloadAttestationDueBps uint64 `yaml:"PAYLOAD_ATTESTATION_DUE_BPS" check-if-fork:"GloasForkEpoch"` + BuilderPaymentThresholdNumerator uint64 `yaml:"BUILDER_PAYMENT_THRESHOLD_NUMERATOR" check-if-fork:"GloasForkEpoch"` + BuilderPaymentThresholdDenominator uint64 `yaml:"BUILDER_PAYMENT_THRESHOLD_DENOMINATOR" check-if-fork:"GloasForkEpoch"` PayloadDueBps uint64 `yaml:"PAYLOAD_DUE_BPS" check-if-fork:"GloasForkEpoch"` MaxRequestPayloads uint64 `yaml:"MAX_REQUEST_PAYLOADS" check-if-fork:"GloasForkEpoch"` diff --git a/clients/xatu/client.go b/clients/xatu/client.go new file mode 100644 index 000000000..cf97a3e5c --- /dev/null +++ b/clients/xatu/client.go @@ -0,0 +1,351 @@ +// Package xatu provides read access to a Xatu ClickHouse instance using the +// typed query builders and row structs generated in the xatu repository. +package xatu + +import ( + "context" + "crypto/tls" + "fmt" + "net/url" + "time" + + "github.com/ClickHouse/clickhouse-go/v2" + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + "github.com/sirupsen/logrus" + + "github.com/ethpandaops/dora/types" +) + +const ( + defaultDatabase = "default" + // maxQueryPageSize is the page_size ceiling the generated builders enforce. + maxQueryPageSize = 10000 + // maxQueryPages bounds a paged read so a bad filter cannot walk a table + // forever. The widest caller reduces one epoch of a single event series, so + // 10 pages allows 100k rows there, about 3000 observing nodes per slot + // across 32 slots. Exceeding it fails loudly rather than truncating, + // because a short read yields plausible-looking wrong percentiles. + maxQueryPages = 10 + defaultSettleDelay = 30 * time.Second + defaultConcurrencyLimit = 2 + // cbtSettleDelay is how long after slot start the cbt transformations are + // assumed to still be rewriting a slot's rows. The attestation chunk window + // extends 12s past slot start, availability probes run within the probed + // slot itself, and the transformation batches lag under a minute, so two + // minutes covers all three with room. + cbtSettleDelay = 2 * time.Minute +) + +// GlobalClient is the process-wide xatu client. It is nil when xatu is not +// configured; all xatu-backed features must treat that as disabled. +var GlobalClient *Client + +// GlobalCbtClient is the process-wide xatu-cbt client. It is nil when no cbt +// source is configured; all cbt-backed features must treat that as disabled. +var GlobalCbtClient *Client + +// Client wraps ClickHouse connections to a Xatu instance. Queries for settled +// slots are routed to the cached endpoint when one is configured, so a +// response-caching proxy can absorb repeated queries across instances. +type Client struct { + logger *logrus.Entry + conn driver.Conn + cachedConn driver.Conn + network string + settleDelay time.Duration + sem chan struct{} +} + +// NewClient connects to the configured raw ClickHouse endpoints. +// defaultNetwork is used as the meta_network_name filter when the config does +// not set one. +func NewClient(cfg *types.XatuConfig, defaultNetwork string, logger logrus.FieldLogger) (*Client, error) { + network := cfg.NetworkName + if network == "" { + network = defaultNetwork + } + + settleDelay := cfg.SettleDelay + if settleDelay <= 0 { + settleDelay = defaultSettleDelay + } + + return newClient(clientParams{ + module: "xatu", + dsn: cfg.Raw.ClickhouseDsn, + cachedDsn: cfg.Raw.ClickhouseCachedDsn, + database: cfg.Raw.Database, + network: network, + settleDelay: settleDelay, + concurrency: cfg.ConcurrencyLimit, + }, logger) +} + +// NewCbtClient connects to the configured xatu-cbt ClickHouse endpoints. The +// cbt models live in one database per network, so the database defaults to +// the network name instead of a fixed schema. The settle delay is fixed: it +// covers the transformation pipeline, not the raw ingest lag the configured +// SettleDelay describes. +func NewCbtClient(cfg *types.XatuConfig, defaultNetwork string, logger logrus.FieldLogger) (*Client, error) { + network := cfg.NetworkName + if network == "" { + network = defaultNetwork + } + + database := cfg.Cbt.Database + if database == "" { + database = network + } + + return newClient(clientParams{ + module: "xatu-cbt", + dsn: cfg.Cbt.ClickhouseDsn, + cachedDsn: cfg.Cbt.ClickhouseCachedDsn, + database: database, + network: network, + settleDelay: cbtSettleDelay, + concurrency: cfg.ConcurrencyLimit, + }, logger) +} + +// clientParams carries one source's resolved connection settings into +// newClient. +type clientParams struct { + module string + dsn string + cachedDsn string + database string + network string + settleDelay time.Duration + concurrency int +} + +func newClient(params clientParams, logger logrus.FieldLogger) (*Client, error) { + if params.dsn == "" { + return nil, fmt.Errorf("%s clickhouse dsn is required", params.module) + } + + conn, err := connect(params.dsn, params.database) + if err != nil { + return nil, fmt.Errorf("%s clickhouse: %w", params.module, err) + } + + client := &Client{ + logger: logger.WithField("module", params.module), + conn: conn, + network: params.network, + settleDelay: params.settleDelay, + } + + if params.cachedDsn != "" { + cachedConn, err := connect(params.cachedDsn, params.database) + if err != nil { + return nil, fmt.Errorf("%s cached clickhouse: %w", params.module, err) + } + + client.cachedConn = cachedConn + } + + concurrency := params.concurrency + if concurrency <= 0 { + concurrency = defaultConcurrencyLimit + } + + client.sem = make(chan struct{}, concurrency) + + go client.logReachability() + + return client, nil +} + +// logReachability pings the configured endpoints once and logs the outcome. +// A failure is logged rather than returned: queries recover on their own when +// ClickHouse comes back, so refusing to boot would turn an outage in an +// optional dependency into downtime for the whole explorer. +func (c *Client) logReachability() { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + endpoints := []struct { + name string + conn driver.Conn + }{ + {"primary", c.conn}, + {"cached", c.cachedConn}, + } + + for _, endpoint := range endpoints { + if endpoint.conn == nil { + continue + } + + if err := endpoint.conn.Ping(ctx); err != nil { + c.logger.WithError(err).Errorf("xatu clickhouse %s endpoint unreachable, propagation data stays unavailable until it recovers", endpoint.name) + + continue + } + + c.logger.Infof("xatu clickhouse %s endpoint reachable", endpoint.name) + } +} + +// Network returns the meta_network_name filter value. +func (c *Client) Network() string { + return c.network +} + +// SettleDelay returns how long after slot start the ingest pipeline is assumed +// to still be receiving events for that slot. +func (c *Client) SettleDelay() time.Duration { + return c.settleDelay +} + +// Query runs a built query. Settled queries use the cached endpoint when one +// is configured; unsettled queries always bypass it so a pre-settle response +// never gets frozen into a shared response cache. +func (c *Client) Query(ctx context.Context, settled bool, query string, args ...any) (driver.Rows, error) { + select { + case c.sem <- struct{}{}: + case <-ctx.Done(): + return nil, ctx.Err() + } + defer func() { <-c.sem }() + + conn := c.conn + if settled && c.cachedConn != nil { + conn = c.cachedConn + } + + return conn.Query(ctx, query, args...) +} + +// QueryPaged runs a query across every result page, calling scan for each row. +// +// The generated builders cap page_size at maxQueryPageSize and cannot express +// aggregates, so a caller reducing a whole epoch has to pull the rows and +// follow the page tokens. Reading only the first page loses rows silently, +// which is worse than being slow. build receives the row offset of the page +// to fetch, zero for the first; it encodes the offset into a page token with +// its own table's generated helper, which keeps this client independent of +// which generated package (xatu or xatu-cbt) produced the query. +func (c *Client) QueryPaged( + ctx context.Context, + settled bool, + build func(pageOffset uint32) (query string, args []any, err error), + scan func(rows driver.Rows) error, +) error { + offset := uint32(0) + + for page := 0; page < maxQueryPages; page++ { + query, args, err := build(offset) + if err != nil { + return err + } + + rows, err := c.Query(ctx, settled, query, args...) + if err != nil { + return err + } + + count := 0 + + for rows.Next() { + count++ + + if err := scan(rows); err != nil { + rows.Close() + + return err + } + } + + // a mid-stream failure would otherwise look like a short final page + if err := rows.Err(); err != nil { + rows.Close() + + return err + } + + rows.Close() + + if count < maxQueryPageSize { + return nil + } + + offset += maxQueryPageSize + } + + return fmt.Errorf("query exceeded %d pages of %d rows", maxQueryPages, maxQueryPageSize) +} + +// MaxQueryPageSize is the page size callers should request so QueryPaged can +// tell a full page from the last one. +func MaxQueryPageSize() int32 { + return maxQueryPageSize +} + +// connect opens a ClickHouse connection from a DSN. https/http DSNs use the +// HTTP protocol (chproxy compatible), clickhouse:// DSNs use the native +// protocol. +func connect(dsn, database string) (driver.Conn, error) { + parsed, err := url.Parse(dsn) + if err != nil { + return nil, fmt.Errorf("invalid dsn: %w", err) + } + + if database == "" { + database = defaultDatabase + } + + options := &clickhouse.Options{ + Auth: clickhouse.Auth{ + Database: database, + Username: parsed.User.Username(), + }, + DialTimeout: 10 * time.Second, + ReadTimeout: 60 * time.Second, + // sized above the per-client concurrency limit: the default pool ran + // dry once a slot view fanned out to several series, and an exhausted + // pool surfaces as an acquire timeout that reads like an outage + MaxOpenConns: 8, + MaxIdleConns: 4, + ConnMaxLifetime: time.Hour, + } + + if password, ok := parsed.User.Password(); ok { + options.Auth.Password = password + } + + host := parsed.Hostname() + port := parsed.Port() + + switch parsed.Scheme { + case "https": + if port == "" { + port = "443" + } + + options.Protocol = clickhouse.HTTP + options.TLS = &tls.Config{MinVersion: tls.VersionTLS12} + case "http": + if port == "" { + port = "8123" + } + + options.Protocol = clickhouse.HTTP + case "clickhouse": + if port == "" { + port = "9000" + } + + options.Protocol = clickhouse.Native + default: + return nil, fmt.Errorf("unsupported dsn scheme %q", parsed.Scheme) + } + + options.Addr = []string{host + ":" + port} + + // Open validates the options without dialing. Reachability is checked in + // the background so an unreachable ClickHouse cannot stop dora booting. + return clickhouse.Open(options) +} diff --git a/cmd/dora-explorer/main.go b/cmd/dora-explorer/main.go index c36dabc38..add2b0bfc 100644 --- a/cmd/dora-explorer/main.go +++ b/cmd/dora-explorer/main.go @@ -18,6 +18,7 @@ import ( "github.com/sirupsen/logrus" "github.com/urfave/negroni" + "github.com/ethpandaops/dora/clients/xatu" "github.com/ethpandaops/dora/db" "github.com/ethpandaops/dora/handlers" "github.com/ethpandaops/dora/handlers/api" @@ -89,6 +90,30 @@ func main() { logger.Fatalf("error starting tx signature service: %v", err) } + if cfg.Xatu.Enabled { + specs := services.GlobalBeaconService.GetChainState().GetSpecs() + + // Only invalid configuration fails here; reachability is checked in the + // background so a ClickHouse outage cannot block startup. + xatuClient, err := xatu.NewClient(&cfg.Xatu, specs.ConfigName, logger) + if err != nil { + logger.Fatalf("invalid xatu configuration: %v", err) + } + + xatu.GlobalClient = xatuClient + logger.WithField("module", "xatu").Infof("xatu clickhouse configured (network: %v)", xatuClient.Network()) + + if cfg.Xatu.Cbt.ClickhouseDsn != "" { + cbtClient, err := xatu.NewCbtClient(&cfg.Xatu, specs.ConfigName, logger) + if err != nil { + logger.Fatalf("invalid xatu cbt configuration: %v", err) + } + + xatu.GlobalCbtClient = cbtClient + logger.WithField("module", "xatu-cbt").Infof("xatu cbt clickhouse configured (network: %v)", cbtClient.Network()) + } + } + if cfg.RateLimit.Enabled { err = services.StartCallRateLimiter(cfg.GetProxyCount(), cfg.RateLimit.Rate, cfg.RateLimit.Burst) if err != nil { @@ -199,6 +224,8 @@ func startFrontend(router *mux.Router) { router.HandleFunc("/slot/{slotOrHash}/tracoor", handlers.SlotTracoor).Methods("GET") router.HandleFunc("/slot/{slotOrHash}/duties", handlers.SlotDuties).Methods("GET") router.HandleFunc("/slot/{slotOrHash}/bidseen", handlers.SlotBidSeen).Methods("GET") + router.HandleFunc("/slot/{slotOrHash}/arrival", handlers.SlotArrival).Methods("GET") + router.HandleFunc("/slot/{slotOrHash}/waves", handlers.SlotWaves).Methods("GET") router.HandleFunc("/slot/{root}/blob/{index}", handlers.SlotBlob).Methods("GET") router.HandleFunc("/blocks", handlers.Blocks).Methods("GET") router.HandleFunc("/blocks/filtered", handlers.BlocksFiltered).Methods("GET") diff --git a/config/default.config.yml b/config/default.config.yml index b055de99a..00fe23eb2 100644 --- a/config/default.config.yml +++ b/config/default.config.yml @@ -211,3 +211,37 @@ ensResolver: queueSize: 50000 # capped in-memory queue of pending addresses cacheSize: 50000 # in-memory LRU name cache size + +# Xatu provides the block propagation view on the slot page and the arrival +# columns on the epoch page, read from a Xatu ClickHouse instance. Disabled by +# default: it needs credentials for a ClickHouse holding xatu data, and the +# explorer works fully without it. +xatu: + enabled: false + # Filters rows by meta_network_name. Defaults to the indexed chain's name. + networkName: "" + # How long after slot start the ingest pipeline is assumed to still be + # receiving events. Responses for younger slots are held for one slot only, so a + # partially ingested slot does not get frozen into the page cache. + settleDelay: 30s + # Maximum concurrent ClickHouse queries across all page loads. + concurrencyLimit: 2 + # Xatu's raw event tables (beacon_api_*, libp2p_*, canonical_beacon_*). + raw: + # "https://user:pass@host" / "http://user:pass@host" use the HTTP protocol + # and work through chproxy. "clickhouse://user:pass@host:9000" uses the + # native protocol. + clickhouseDsn: "" + # Optional response-caching proxy. Queries for settled slots are routed + # here, so identical queries across page loads and instances share one + # cached response. + clickhouseCachedDsn: "" + database: "default" + # xatu-cbt's transformed models (fct_*), which live on a separate cluster + # with one database per network. Optional: features backed by cbt tables + # (attestation wave, data column panels) stay hidden when no DSN is set. + cbt: + clickhouseDsn: "" + clickhouseCachedDsn: "" + # Defaults to the network name (e.g. "mainnet"). + database: "" diff --git a/go.mod b/go.mod index 6c3b7e6a5..9cfd5235c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ethpandaops/dora -go 1.26.1 +go 1.26.2 require ( github.com/Masterminds/sprig/v3 v3.3.0 @@ -11,6 +11,8 @@ require ( github.com/ethpandaops/ethcore v0.0.0-20260807103219-0fb0622e156b github.com/ethpandaops/ethwallclock v0.4.0 github.com/ethpandaops/go-eth2-client v0.1.7-0.20260812120339-e742ab5a2de1 + github.com/ethpandaops/xatu v1.22.1-0.20260824050538-619c572d19c3 + github.com/ethpandaops/xatu-cbt v0.0.0-20260825024339-8eadec716ac8 github.com/go-redis/redis/v8 v8.11.5 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/gorilla/mux v1.8.1 @@ -19,7 +21,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/lib/pq v1.12.3 - github.com/libp2p/go-libp2p v0.43.0 + github.com/libp2p/go-libp2p v0.47.0 github.com/mashingan/smapping v0.1.19 github.com/mattn/go-sqlite3 v1.14.50 github.com/minio/minio-go/v7 v7.3.0 @@ -49,11 +51,13 @@ require ( ) require ( + github.com/ClickHouse/ch-go v0.73.0 // indirect github.com/DataDog/zstd v1.5.7 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/OffchainLabs/go-bitfield v0.0.0-20251031151322-f427d04d8506 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/VictoriaMetrics/fastcache v1.13.0 // indirect + github.com/andybalholm/brotli v1.2.2 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 // indirect github.com/cockroachdb/errors v1.12.0 // indirect @@ -63,22 +67,24 @@ require ( github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/dunglas/httpsfv v1.1.0 // indirect github.com/emicklei/dot v1.9.1 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.8 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/filecoin-project/go-clock v0.1.0 // indirect github.com/fjl/jsonw v0.1.0 // indirect github.com/flynn/noise v1.1.0 // indirect - github.com/francoispqt/gojay v1.2.13 // indirect - github.com/getsentry/sentry-go v0.32.0 // indirect + github.com/getsentry/sentry-go v0.35.3 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gopacket v1.1.19 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/huandu/go-assert v1.1.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/go-cid v0.5.0 // indirect github.com/ipfs/go-log/v2 v2.8.1 // indirect @@ -93,10 +99,10 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-libp2p-mplex v0.11.0 // indirect - github.com/libp2p/go-libp2p-pubsub v0.15.0 // indirect + github.com/libp2p/go-libp2p-pubsub v0.16.1-0.20260611143718-41b11d5cb1a7 // indirect github.com/libp2p/go-mplex v0.7.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-netroute v0.4.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/libp2p/go-yamux/v5 v5.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -105,7 +111,7 @@ require ( github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/crc64nvme v1.1.1 // indirect - github.com/minio/highwayhash v1.0.2 // indirect + github.com/minio/highwayhash v1.0.3 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect @@ -120,8 +126,11 @@ require ( github.com/multiformats/go-varint v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.38.2 // indirect + github.com/paulmach/orb v0.13.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/philhofer/fwd v1.2.0 // indirect + github.com/pierrec/lz4/v4 v4.1.27 // indirect + github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect github.com/pion/dtls/v3 v3.1.2 // indirect @@ -143,13 +152,14 @@ require ( github.com/pion/turn/v4 v4.1.1 // indirect github.com/pion/webrtc/v4 v4.1.4 // indirect github.com/pk910/hashtree-bindings v0.2.5 // indirect - github.com/prysmaticlabs/fastssz v0.0.0-20241008181541-518c4ce73516 // indirect + github.com/prysmaticlabs/fastssz v0.0.0-20251103153600-259302269bfc // indirect github.com/prysmaticlabs/gohashtree v0.0.5-beta // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect - github.com/quic-go/webtransport-go v0.9.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.1 // indirect + github.com/quic-go/webtransport-go v0.10.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/segmentio/asm v1.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.3 // indirect @@ -163,24 +173,25 @@ require ( go.uber.org/dig v1.19.0 // indirect go.uber.org/fx v1.24.0 // indirect go.uber.org/mock v0.6.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/mod v0.38.0 // indirect golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect golang.org/x/tools v0.48.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect gopkg.in/ini.v1 v1.67.3 // indirect lukechampine.com/blake3 v1.4.1 // indirect ) require ( - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/ClickHouse/clickhouse-go/v2 v2.47.0 github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.22.0 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect @@ -195,7 +206,7 @@ require ( github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93 github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/holiman/uint256 v1.3.2 github.com/huandu/go-clone v1.7.3 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -206,7 +217,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.3 // indirect + github.com/jackc/pgtype v1.14.4 // indirect github.com/jackc/puddle v1.3.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/kilic/bls12-381 v0.1.0 // indirect @@ -230,8 +241,8 @@ require ( github.com/supranational/blst v0.3.16 // indirect github.com/tdewolff/parse v2.3.4+incompatible // indirect github.com/tdewolff/test v1.0.9 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect @@ -240,7 +251,7 @@ require ( golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/protobuf v1.36.11 gopkg.in/Knetic/govaluate.v3 v3.0.0 gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index d94be9601..ac2a2ebc4 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,13 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ClickHouse/ch-go v0.73.0 h1:jsHiGRbQ3sz+gekvDFJF29LWDo5dzbJm5s1h8TWVP2M= +github.com/ClickHouse/ch-go v0.73.0/go.mod h1:wkFIxrqlXeRJ9cn3r5Fz5Qen9jl5aTMPuGZeuJpANNY= +github.com/ClickHouse/clickhouse-go/v2 v2.47.0 h1:ZDAzrnKSOPTIsm4tdUNfrii2yc8dk4SVRLC77BR7Z5Q= +github.com/ClickHouse/clickhouse-go/v2 v2.47.0/go.mod h1:sPj7C7UYQ2MWHcfX+4eGN6nwnCqwUKfgO6PcwKpd6K8= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -40,16 +35,14 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/allegro/bigcache/v3 v3.2.0 h1:B45F9x3iaoBlhzIA+0jqxlThTUoyg+mOk7HUKSbJOL8= github.com/allegro/bigcache/v3 v3.2.0/go.mod h1:qvxNn6cSKfWRmfDuPJbZcfxsQXEtoskUqPzT0kuHG5s= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= +github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 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/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= -github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 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/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 h1:xz6Nv3zcwO2Lila35hcb0QloCQsc38Al13RNEzWRpX4= @@ -57,7 +50,6 @@ github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9/go.mod h1:2w github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b h1:SHlYZ/bMx7frnmeqCu+xm0TCxXLzX3jQIVuFbnFGtFU= @@ -82,11 +74,11 @@ github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -111,7 +103,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao= github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/dot v1.9.1 h1:SBySmOPaQ6+fpmnqEaG1bCHj5hW65A0jJpcPpz+TG3w= @@ -130,31 +123,33 @@ github.com/ethpandaops/ethwallclock v0.4.0 h1:+sgnhf4pk6hLPukP076VxkiLloE4L0Yk1y github.com/ethpandaops/ethwallclock v0.4.0/go.mod h1:y0Cu+mhGLlem19vnAV2x0hpFS5KZ7oOi2SWYayv9l24= github.com/ethpandaops/go-eth2-client v0.1.7-0.20260812120339-e742ab5a2de1 h1:Hhupjk3QgnoIvmL2owPLMbcraDsd1sgKnd3pWx2pz5E= github.com/ethpandaops/go-eth2-client v0.1.7-0.20260812120339-e742ab5a2de1/go.mod h1:MXQukU/345puJmB2EikoaeQIFizk3zbekPxwYyxG/t8= +github.com/ethpandaops/xatu v1.22.1-0.20260824050538-619c572d19c3 h1:83LDwFEAijcssPdQ8ojzzbi+2mD3a+zL1b4V4cY1sMo= +github.com/ethpandaops/xatu v1.22.1-0.20260824050538-619c572d19c3/go.mod h1:sDLPtT/bQgK+vExtPq77y2W4yHq9tRdiDu852yptTcU= +github.com/ethpandaops/xatu-cbt v0.0.0-20260825024339-8eadec716ac8 h1:YfAXAt5fQcEnzPccCfByhXY2Vy9FlGrOp+NxGLx3QNY= +github.com/ethpandaops/xatu-cbt v0.0.0-20260825024339-8eadec716ac8/go.mod h1:arOg52bVEiL5DTA2AGrxQzBcIzKWqep6G9vgPAULMl0= github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= github.com/fjl/jsonw v0.1.0 h1:V3MyR79fjLpn/+bMgvegdGUIhoJOzjmqWcKDgcOmY1I= github.com/fjl/jsonw v0.1.0/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= -github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getsentry/sentry-go v0.35.3 h1:u5IJaEqZyPdWqe/hKlBKBBnMTSxB/HenCqF3QLabeds= +github.com/getsentry/sentry-go v0.35.3/go.mod h1:mdL49ixwT2yi57k5eh7mpnDyPybixPzlzEJFu0Z76QA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -191,19 +186,13 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -215,39 +204,26 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93 h1:GpQQr4L8jsBtJSURCDqQboOdgpVMU6vR9REjc8nR4Qc= github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -262,8 +238,9 @@ github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25 github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs= +github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs= github.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ= github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= @@ -317,8 +294,8 @@ github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCM github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus= -github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= +github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= @@ -335,13 +312,10 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= @@ -364,7 +338,6 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -384,33 +357,33 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.43.0 h1:b2bg2cRNmY4HpLK8VHYQXLX2d3iND95OjodLFymvqXU= -github.com/libp2p/go-libp2p v0.43.0/go.mod h1:IiSqAXDyP2sWH+J2gs43pNmB/y4FOi2XQPbsb+8qvzc= +github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0yc= +github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-mplex v0.11.0 h1:0vwpLXRSfkTzshEjETIEgJaVxXvg+orbxYoIb3Ty5qM= github.com/libp2p/go-libp2p-mplex v0.11.0/go.mod h1:QrsdNY3lzjpdo9V1goJfPb0O65Nms0sUR8CDAO18f6k= -github.com/libp2p/go-libp2p-pubsub v0.15.0 h1:cG7Cng2BT82WttmPFMi50gDNV+58K626m/wR00vGL1o= -github.com/libp2p/go-libp2p-pubsub v0.15.0/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4= +github.com/libp2p/go-libp2p-pubsub v0.16.1-0.20260611143718-41b11d5cb1a7 h1:UMiJ408NqO9Sf2ANutEM3An8Em3K+qn78eoIgzY3PIY= +github.com/libp2p/go-libp2p-pubsub v0.16.1-0.20260611143718-41b11d5cb1a7/go.mod h1:l00Tc/MXTM/dK69HFxKWIe0yaNwMy5OuU0dyuZaWJa0= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= github.com/libp2p/go-mplex v0.7.0 h1:BDhFZdlk5tbr0oyFq/xv/NPGfjbnrsDam1EvutpBDbY= github.com/libp2p/go-mplex v0.7.0/go.mod h1:rW8ThnRcYWft/Jb2jeORBmPd6xuG3dGxWN/W168L9EU= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= -github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/marcopolo/simnet v0.0.7 h1:DpH8BMGsF9+1w13L8rvCaAhb6nYJdY+dIXncDrssvUs= +github.com/marcopolo/simnet v0.0.7/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mashingan/smapping v0.1.19 h1:SsEtuPn2UcM1croIupPtGLgWgpYRuS0rSQMvKD9g2BQ= @@ -427,10 +400,8 @@ github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY= github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= @@ -442,8 +413,8 @@ github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4S github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= -github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.3.0 h1:HM4pFCSQq/TK+j0/zmorSh5ddh81iDgRgU0BG0Vz/YU= @@ -461,8 +432,6 @@ github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjU github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= @@ -494,8 +463,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -512,13 +479,16 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/paulmach/orb v0.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw= +github.com/paulmach/orb v0.13.0/go.mod h1:6scRWINywA2Jf05dcjOfLfxrUIMECvTSG2MVbRLxu/k= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= +github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec h1:3EiGmeJWoNixU+EwllIn26x6s4njiWRXewdx2zlYa84= +github.com/pingcap/errors v0.11.5-0.20250318082626-8f80e5cb09ec/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= @@ -576,16 +546,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pressly/goose/v3 v3.27.3 h1:pIglVHjw99r4e/hDHHwbl9vfOsDMqUokfkXo6+n/RxA= github.com/pressly/goose/v3 v3.27.3/go.mod h1:Dag+xpV6o20HR2LFY1j0q6MDwc3f7vPUFDA77R+0yGY= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 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.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/protolambda/bls12-381-util v0.1.0 h1:05DU2wJN7DTU7z28+Q+zejXkIsA/MF8JZQGhtBZZiWk= @@ -594,8 +560,8 @@ github.com/protolambda/zrnt v0.34.1 h1:qW55rnhZJDnOb3TwFiFRJZi3yTXFrJdGOFQM7vCwY github.com/protolambda/zrnt v0.34.1/go.mod h1:A0fezkp9Tt3GBLATSPIbuY4ywYESyAuc/FFmPKg8Lqs= github.com/protolambda/ztyp v0.2.2 h1:rVcL3vBu9W/aV646zF6caLS/dyn9BN8NYiuJzicLNyY= github.com/protolambda/ztyp v0.2.2/go.mod h1:9bYgKGqg3wJqT9ac1gI2hnVb0STQq7p/1lapqrqY1dU= -github.com/prysmaticlabs/fastssz v0.0.0-20241008181541-518c4ce73516 h1:xuVAdtz5ShYblG2sPyb4gw01DF8InbOI/kBCQjk7NiM= -github.com/prysmaticlabs/fastssz v0.0.0-20241008181541-518c4ce73516/go.mod h1:h2OlIZD/M6wFvV3YMZbW16lFgh3Rsye00G44J2cwLyU= +github.com/prysmaticlabs/fastssz v0.0.0-20251103153600-259302269bfc h1:ASmh3y4ALne2OoabF5pPL8OcIpBko8gFMg5018MxkBI= +github.com/prysmaticlabs/fastssz v0.0.0-20251103153600-259302269bfc/go.mod h1:h2OlIZD/M6wFvV3YMZbW16lFgh3Rsye00G44J2cwLyU= github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94= github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k= github.com/prysmaticlabs/gohashtree v0.0.5-beta h1:ct41mg7HyIZd7uoSM/ud23f+3DxQG9tlMlQG+BVX23c= @@ -604,12 +570,12 @@ github.com/prysmaticlabs/protoc-gen-go-cast v0.0.0-20230228205207-28762a7b9294 h github.com/prysmaticlabs/protoc-gen-go-cast v0.0.0-20230228205207-28762a7b9294/go.mod h1:ZVEbRdnMkGhp/pu35zq4SXxtvUwWK0J1MATtekZpH2Y= github.com/prysmaticlabs/prysm/v5 v5.3.3 h1:nV/XGB0L8sSAL6fQ8EFJukOYmFowWwRRBvk3+Yilhps= github.com/prysmaticlabs/prysm/v5 v5.3.3/go.mod h1:2SaUMpJ+O8r/pcnNDMHbrk0Ki9ObQXvfRc+rQHovzVk= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= -github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= +github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -627,12 +593,11 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sethvargo/go-retry v0.4.0 h1:9qy1OoIAxBL+gBYnkTnTnWle5wlfsXQlwRzIbbpdqPw= github.com/sethvargo/go-retry v0.4.0/go.mod h1:tvsjdKG6xfiCx4LSiUZ06kcv38xvdVQwv8R6/VnnVWg= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -641,34 +606,10 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -711,7 +652,6 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo= github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38= @@ -724,23 +664,23 @@ github.com/timandy/routine v1.1.6 h1:cueNRVPutK8O6387LL7dmYPLNyS6aKlPCPi5qWCLdc8 github.com/timandy/routine v1.1.6/go.mod h1:kXslgIosdY8LW0byTyPnenDgn4/azt2euufAq9rK51w= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/wealdtech/go-bytesutil v1.2.1 h1:TjuRzcG5KaPwaR5JB7L/OgJqMQWvlrblA1n0GfcXFSY= github.com/wealdtech/go-bytesutil v1.2.1/go.mod h1:RhUDUGT1F4UP4ydqbYp2MWJbAel3M+mKd057Pad7oag= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -751,21 +691,22 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= @@ -783,18 +724,14 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -814,14 +751,9 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -830,15 +762,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -861,15 +786,7 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -878,14 +795,10 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -917,6 +830,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A= @@ -933,7 +847,6 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -945,15 +858,9 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -962,7 +869,6 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -977,26 +883,10 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0= google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1021,14 +911,12 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw= gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1038,10 +926,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= @@ -1053,5 +937,3 @@ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/handlers/epoch.go b/handlers/epoch.go index e62552d55..4290fc867 100644 --- a/handlers/epoch.go +++ b/handlers/epoch.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" "github.com/sirupsen/logrus" + "github.com/ethpandaops/dora/clients/xatu" "github.com/ethpandaops/dora/db" "github.com/ethpandaops/dora/dbtypes" "github.com/ethpandaops/dora/indexer/beacon" @@ -134,6 +135,7 @@ func buildEpochPageData(ctx context.Context, epoch uint64) (*models.EpochPageDat firstSlot := chainState.EpochToSlot(phase0.Epoch(epoch)) lastSlot := chainState.EpochToSlot(phase0.Epoch(epoch+1)) - 1 pageData := &models.EpochPageData{ + XatuEnabled: xatu.GlobalClient != nil, Epoch: epoch, PreviousEpoch: epoch - 1, NextEpoch: nextEpoch, @@ -185,6 +187,7 @@ func buildEpochPageData(ctx context.Context, epoch uint64) (*models.EpochPageDat // load slots pageData.Slots = make([]*models.EpochPageDataSlot, 0) + epochArrival := getEpochArrivalData(phase0.Epoch(epoch)) dbSlots := services.GlobalBeaconService.GetDbBlocksForSlots(ctx, uint64(lastSlot), uint32(specs.SlotsPerEpoch), true, true) dbIdx := 0 dbCnt := len(dbSlots) @@ -289,5 +292,15 @@ func buildEpochPageData(ctx context.Context, epoch uint64) (*models.EpochPageDat default: cacheTimeout = 12 * time.Second } + if epochArrival != nil { + for _, slotData := range pageData.Slots { + if entry, ok := epochArrival.Slots[slotData.Slot]; ok && entry.Nodes > 0 { + slotData.ArrivalNodes = entry.Nodes + slotData.ArrivalMinMs = entry.MinMs + slotData.ArrivalP90Ms = entry.P90Ms + } + } + } + return pageData, cacheTimeout } diff --git a/handlers/epoch_arrival.go b/handlers/epoch_arrival.go new file mode 100644 index 000000000..13ff4bc28 --- /dev/null +++ b/handlers/epoch_arrival.go @@ -0,0 +1,221 @@ +package handlers + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + + "github.com/sirupsen/logrus" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/ethpandaops/dora/clients/xatu" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/types/models" + xch "github.com/ethpandaops/xatu/pkg/proto/clickhouse" +) + +// errorCacheTimeout is how long a failed lookup is remembered, so an outage +// costs one query per epoch per interval instead of one per page build. +const errorCacheTimeout = 30 * time.Second + +// getEpochArrivalData returns the epoch's per-slot arrival summaries through +// the frontend cache, so the epoch page renders them inline without paying a +// ClickHouse round trip on every build. Returns nil when xatu is not +// configured or the data cannot be loaded. +func getEpochArrivalData(epoch phase0.Epoch) *models.EpochArrivalResponse { + if xatu.GlobalClient == nil { + return nil + } + + cacheKey := fmt.Sprintf("epocharrival:%d", epoch) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(cacheKey, true, &models.EpochArrivalResponse{}, func(pageCall *services.FrontendCacheProcessingPage) any { + data, cacheTimeout, buildErr := buildEpochArrivalData(pageCall.CallCtx, epoch) + if buildErr != nil { + logrus.WithError(buildErr).Error("error loading epoch arrival data from xatu") + // brief negative cache: with ClickHouse down the epoch page rebuilds + // every slot, and each rebuild would otherwise spend the full query + // budget failing + pageCall.CacheTimeout = errorCacheTimeout + + // an empty response, not nil: the cache marshals whatever the build + // returns, and a nil value panics there, which would leave the + // negative cache above doing nothing at all + return &models.EpochArrivalResponse{Epoch: uint64(epoch)} + } + + pageCall.CacheTimeout = cacheTimeout + + return data + }) + if pageErr != nil { + logrus.WithError(pageErr).Error("error building epoch arrival data") + return nil + } + + result, _ := pageRes.(*models.EpochArrivalResponse) + if result == nil || len(result.Slots) == 0 { + return nil + } + + return result +} + +// buildEpochArrivalData queries Xatu for all block observations in the epoch +// and aggregates each slot's per-node earliest arrivals into min/p90. The +// engine API series is skipped: a newPayload call never precedes the node's +// own gossip observation, so it cannot change a node's earliest arrival. +func buildEpochArrivalData(ctx context.Context, epoch phase0.Epoch) (*models.EpochArrivalResponse, time.Duration, error) { + client := xatu.GlobalClient + chainState := services.GlobalBeaconService.GetChainState() + + firstSlot := chainState.EpochToSlot(epoch) + lastSlot := chainState.EpochToSlot(epoch+1) - 1 + firstTime := chainState.SlotToTime(firstSlot) + lastTime := chainState.SlotToTime(lastSlot) + + settled := time.Now().After(lastTime.Add(client.SettleDelay())) + + // this runs on the epoch page build path, so keep the budget tight + queryCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + // earliest observation per slot per node across the three arrival series + type slotNode struct { + slot uint32 + node string + } + + earliest := map[slotNode]uint32{} + + record := func(slot uint32, node string, ms uint32) { + key := slotNode{slot: slot, node: node} + if current, ok := earliest[key]; !ok || ms < current { + earliest[key] = ms + } + } + + slotFilter := &xch.UInt32Filter{Filter: &xch.UInt32Filter_Between{Between: &xch.UInt32Range{ + Min: uint32(firstSlot), + Max: wrapperspb.UInt32(uint32(lastSlot)), //nolint:gosec // slot numbers fit + }}} + timeFilter := &xch.UInt32Filter{Filter: &xch.UInt32Filter_Between{Between: &xch.UInt32Range{ + Min: uint32(firstTime.Unix()), //nolint:gosec // unix timestamps fit + Max: wrapperspb.UInt32(uint32(lastTime.Unix())), //nolint:gosec // unix timestamps fit + }}} + networkFilter := &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: client.Network()}} + // late observations are dropped below, so leave them in ClickHouse rather + // than paging them across the wire only to discard them + lateMs := lateThreshold(chainState) + freshFilter := &xch.UInt32Filter{Filter: &xch.UInt32Filter_Lte{Lte: lateMs}} + + pageSize := xatu.MaxQueryPageSize() + + // Every observation in the epoch has to be reduced here: the generated + // builders cannot aggregate, so min/p90 cannot be pushed into ClickHouse. + err := client.QueryPaged(queryCtx, settled, func(pageOffset uint32) (string, []any, error) { + query, err := xch.BuildListBeaconApiEthV1EventsBlockQuery(&xch.ListBeaconApiEthV1EventsBlockRequest{ + MetaNetworkName: networkFilter, Slot: slotFilter, SlotStartDateTime: timeFilter, + PropagationSlotStartDiff: freshFilter, + PageSize: pageSize, PageToken: xatuPageToken(pageOffset), + }) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row xch.BeaconApiEthV1EventsBlockRow + if err := rows.ScanStruct(&row); err != nil { + return err + } + + record(row.Slot, row.MetaClientName, row.PropagationSlotStartDiff) + + return nil + }) + if err != nil { + return nil, -1, fmt.Errorf("api query: %w", err) + } + + err = client.QueryPaged(queryCtx, settled, func(pageOffset uint32) (string, []any, error) { + query, err := xch.BuildListLibp2PGossipsubBeaconBlockQuery(&xch.ListLibp2PGossipsubBeaconBlockRequest{ + MetaNetworkName: networkFilter, Slot: slotFilter, SlotStartDateTime: timeFilter, + PropagationSlotStartDiff: freshFilter, + PageSize: pageSize, PageToken: xatuPageToken(pageOffset), + }) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row xch.Libp2PGossipsubBeaconBlockRow + if err := rows.ScanStruct(&row); err != nil { + return err + } + + record(row.Slot, row.MetaClientName, row.PropagationSlotStartDiff) + + return nil + }) + if err != nil { + return nil, -1, fmt.Errorf("p2p query: %w", err) + } + + err = client.QueryPaged(queryCtx, settled, func(pageOffset uint32) (string, []any, error) { + query, err := xch.BuildListBeaconApiEthV1EventsHeadQuery(&xch.ListBeaconApiEthV1EventsHeadRequest{ + MetaNetworkName: networkFilter, Slot: slotFilter, SlotStartDateTime: timeFilter, + PropagationSlotStartDiff: freshFilter, + PageSize: pageSize, PageToken: xatuPageToken(pageOffset), + }) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row xch.BeaconApiEthV1EventsHeadRow + if err := rows.ScanStruct(&row); err != nil { + return err + } + + record(row.Slot, row.MetaClientName, row.PropagationSlotStartDiff) + + return nil + }) + if err != nil { + return nil, -1, fmt.Errorf("head query: %w", err) + } + + // per-slot values, late observations excluded as on the slot page + slotValues := map[uint32][]uint32{} + + for key, ms := range earliest { + slotValues[key.slot] = append(slotValues[key.slot], ms) + } + + response := &models.EpochArrivalResponse{ + Epoch: uint64(epoch), + Settled: settled, + Slots: make(map[uint64]*models.EpochArrivalSlot, len(slotValues)), + } + + for slot, values := range slotValues { + sort.Slice(values, func(a, b int) bool { return values[a] < values[b] }) + response.Slots[uint64(slot)] = &models.EpochArrivalSlot{ + Nodes: uint32(len(values)), //nolint:gosec // node counts are small + MinMs: values[0], + P90Ms: values[len(values)*9/10], + } + } + + cacheTimeout := time.Duration(-1) + + switch { + case !settled: + cacheTimeout = unsettledCacheTimeout(chainState) + case time.Since(lastTime) < 30*time.Minute: + cacheTimeout = 30 * time.Second + default: + cacheTimeout = time.Hour + } + + return response, cacheTimeout, nil +} diff --git a/handlers/slot.go b/handlers/slot.go index 88bb38f92..ffdba72df 100644 --- a/handlers/slot.go +++ b/handlers/slot.go @@ -29,6 +29,7 @@ import ( "github.com/sirupsen/logrus" "github.com/ethpandaops/dora/blockdb" + "github.com/ethpandaops/dora/clients/xatu" "github.com/ethpandaops/dora/db" "github.com/ethpandaops/dora/dbtypes" "github.com/ethpandaops/dora/indexer/beacon" @@ -59,6 +60,7 @@ func Slot(w http.ResponseWriter, r *http.Request) { "slot/builder_deposit_requests.html", "slot/builder_exit_requests.html", "slot/bids.html", + "slot/arrival.html", "slot/ptc_votes.html", "slot/inclusion_lists.html", "slot/block_access_list.html", @@ -266,6 +268,8 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) ( EpochFinalized: finalizedEpoch >= chainState.EpochOfSlot(slot), Badges: []*models.SlotPageBlockBadge{}, TracoorUrl: utils.Config.Frontend.TracoorUrl, + XatuEnabled: xatu.GlobalClient != nil, + XatuCbtEnabled: xatu.GlobalCbtClient != nil, } var epochStatsValues *beacon.EpochStatsValues diff --git a/handlers/slot_arrival.go b/handlers/slot_arrival.go new file mode 100644 index 000000000..9ab642e38 --- /dev/null +++ b/handlers/slot_arrival.go @@ -0,0 +1,870 @@ +package handlers + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + + "github.com/ethpandaops/dora/clients/consensus" + "github.com/ethpandaops/dora/clients/xatu" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/types/models" + xch "github.com/ethpandaops/xatu/pkg/proto/clickhouse" +) + +// SlotArrival returns block propagation observations for the path slot from +// Xatu, as JSON for the lazy propagation tab on the slot page. +func SlotArrival(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if xatu.GlobalClient == nil { + http.Error(w, "Xatu is not configured", http.StatusNotFound) + return + } + + vars := mux.Vars(r) + slot, err := strconv.ParseUint(vars["slotOrHash"], 10, 64) + if err != nil { + http.Error(w, "Invalid slot", http.StatusBadRequest) + return + } + + // Scope to the block being viewed, so a slot with competing blocks reports + // each block's own propagation rather than merging them. Requests without a + // usable root fall back to the whole slot. + blockRoot := normalizeBlockRoot(r.URL.Query().Get("root")) + + // The execution block identity comes from the page rather than a second + // beacon lookup here; a bad value only misses the EL series. + execHash := normalizeBlockRoot(r.URL.Query().Get("exec")) + execNumber, _ := strconv.ParseUint(r.URL.Query().Get("num"), 10, 64) + + cacheKey := fmt.Sprintf("slotarrival:%d:%s:%s:%d", slot, blockRoot, execHash, execNumber) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(cacheKey, true, &models.SlotArrivalResponse{}, func(pageCall *services.FrontendCacheProcessingPage) any { + data, cacheTimeout, buildErr := buildSlotArrivalData(pageCall.CallCtx, phase0.Slot(slot), blockRoot, execHash, execNumber) + if buildErr != nil { + logrus.WithError(buildErr).Error("error loading slot arrival data from xatu") + pageCall.CacheTimeout = -1 + + return &models.SlotArrivalResponse{Slot: slot} + } + + pageCall.CacheTimeout = cacheTimeout + + return data + }) + if pageErr != nil { + logrus.WithError(pageErr).Error("error building slot arrival data") + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + return + } + + result, ok := pageRes.(*models.SlotArrivalResponse) + if !ok { + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + return + } + + if err := json.NewEncoder(w).Encode(result); err != nil { + logrus.WithError(err).Error("error encoding slot arrival data") + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + } +} + +// arrivalNode accumulates per-node observations across both series. +type arrivalNode struct { + fullName string + implementation string + continent string + country string + countryCode string + apiMs *uint32 + p2pMs *uint32 + headMs *uint32 + npMs *uint32 + npDurMs *uint32 + plMs *uint32 + npStatus string + observations int +} + +// buildSlotArrivalData queries Xatu for the slot's block observations on the +// beacon API event stream and the libp2p gossip layer, and aggregates them +// per observing node. It returns the response and the cache timeout: no +// caching while the ingest pipeline may still receive events for the slot, a +// short timeout for recent slots and a long one for historic slots. +func buildSlotArrivalData(ctx context.Context, slot phase0.Slot, blockRoot, execHash string, execNumber uint64) (*models.SlotArrivalResponse, time.Duration, error) { + client := xatu.GlobalClient + chainState := services.GlobalBeaconService.GetChainState() + slotTime := chainState.SlotToTime(slot) + + settled := time.Now().After(slotTime.Add(client.SettleDelay())) + + queryCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + nodes := map[string]*arrivalNode{} + + apiObservations, err := loadAPIArrivals(queryCtx, client, slot, slotTime, settled, blockRoot, nodes) + if err != nil { + return nil, -1, err + } + + p2pObservations, err := loadP2PArrivals(queryCtx, client, slot, slotTime, settled, blockRoot, nodes) + if err != nil { + return nil, -1, err + } + + headObservations, err := loadHeadArrivals(queryCtx, client, slot, slotTime, settled, blockRoot, nodes) + if err != nil { + return nil, -1, err + } + + npObservations, err := loadEngineTimings(queryCtx, client, slot, slotTime, settled, blockRoot, nodes) + if err != nil { + return nil, -1, err + } + + elObservations, err := loadExecutionEngineTimings(queryCtx, client, execHash, execNumber, slotTime, settled, nodes) + if err != nil { + return nil, -1, err + } + + npObservations += elObservations + + plObservations, err := loadPayloadArrivals(queryCtx, client, slot, slotTime, settled, blockRoot, nodes) + if err != nil { + return nil, -1, err + } + + response := &models.SlotArrivalResponse{ + Slot: uint64(slot), + Settled: settled, + Observations: apiObservations, + P2PObservations: p2pObservations, + HeadObservations: headObservations, + NPObservations: npObservations, + PLObservations: plObservations, + } + + if len(nodes) > 0 { + buildArrivalAggregates(response, nodes, client.Network(), lateThreshold(chainState)) + } + + cacheTimeout := time.Duration(-1) + + switch { + case !settled: + // The pipeline may still be receiving events, so hold the result only + // briefly. Not caching at all would re-query on every tab open, and + // nothing upstream of this endpoint throttles that. + cacheTimeout = unsettledCacheTimeout(chainState) + case time.Since(slotTime) < 30*time.Minute: + cacheTimeout = 30 * time.Second + default: + cacheTimeout = time.Hour + } + + return response, cacheTimeout, nil +} + +// loadAPIArrivals loads the beacon API block event series into nodes. +func loadAPIArrivals(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string, nodes map[string]*arrivalNode) (int, error) { + req := &xch.ListBeaconApiEthV1EventsBlockRequest{ + MetaNetworkName: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: client.Network()}}, + Slot: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{Eq: uint32(slot)}}, + SlotStartDateTime: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{ + Eq: uint32(slotTime.Unix()), + }}, + OrderBy: "propagation_slot_start_diff", + PageSize: xatu.MaxQueryPageSize(), + } + + if blockRoot != "" { + req.Block = &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: blockRoot}} + } + + observations := 0 + + err := client.QueryPaged(ctx, settled, func(pageOffset uint32) (string, []any, error) { + req.PageToken = xatuPageToken(pageOffset) + + query, err := xch.BuildListBeaconApiEthV1EventsBlockQuery(req) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row xch.BeaconApiEthV1EventsBlockRow + if err := rows.ScanStruct(&row); err != nil { + return fmt.Errorf("api scan: %w", err) + } + + observations++ + + node := nodes[row.MetaClientName] + if node == nil { + node = &arrivalNode{fullName: row.MetaClientName} + nodes[row.MetaClientName] = node + } + + node.observations++ + node.implementation = row.MetaConsensusImplementation + node.continent = row.MetaClientGeoContinentCode + node.country = row.MetaClientGeoCountry + node.countryCode = row.MetaClientGeoCountryCode + + // Rows are ordered by propagation time, so the first row per node is + // its earliest observation. + if node.apiMs == nil { + ms := row.PropagationSlotStartDiff + node.apiMs = &ms + } + + return nil + }) + if err != nil { + return 0, fmt.Errorf("api query: %w", err) + } + + return observations, nil +} + +// loadP2PArrivals loads the libp2p gossipsub block series into nodes. +func loadP2PArrivals(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string, nodes map[string]*arrivalNode) (int, error) { + req := &xch.ListLibp2PGossipsubBeaconBlockRequest{ + MetaNetworkName: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: client.Network()}}, + Slot: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{Eq: uint32(slot)}}, + SlotStartDateTime: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{ + Eq: uint32(slotTime.Unix()), + }}, + OrderBy: "propagation_slot_start_diff", + PageSize: xatu.MaxQueryPageSize(), + } + + if blockRoot != "" { + req.Block = &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: blockRoot}} + } + + observations := 0 + + err := client.QueryPaged(ctx, settled, func(pageOffset uint32) (string, []any, error) { + req.PageToken = xatuPageToken(pageOffset) + + query, err := xch.BuildListLibp2PGossipsubBeaconBlockQuery(req) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row xch.Libp2PGossipsubBeaconBlockRow + if err := rows.ScanStruct(&row); err != nil { + return fmt.Errorf("p2p scan: %w", err) + } + + observations++ + + node := nodes[row.MetaClientName] + if node == nil { + node = &arrivalNode{fullName: row.MetaClientName} + nodes[row.MetaClientName] = node + } + + node.observations++ + + if node.continent == "" { + node.continent = row.MetaClientGeoContinentCode + node.country = row.MetaClientGeoCountry + node.countryCode = row.MetaClientGeoCountryCode + } + + if node.implementation == "" { + node.implementation = reduceSidecarName(row.MetaClientImplementation) + } + + if node.p2pMs == nil { + ms := row.PropagationSlotStartDiff + node.p2pMs = &ms + } + + return nil + }) + if err != nil { + return 0, fmt.Errorf("p2p query: %w", err) + } + + return observations, nil +} + +// reduceSidecarName shortens gossip listener implementation names like +// "Xatu Sidecar (lighthouse)" to the client they attach to. +func reduceSidecarName(name string) string { + if open := strings.Index(name, "("); open >= 0 { + if close := strings.Index(name[open:], ")"); close > 1 { + return name[open+1 : open+close] + } + } + + return name +} + +// loadHeadArrivals loads the beacon API head event series into nodes. A head +// event marks the node adopting the block as its head via fork choice, a +// stronger signal than merely having seen the block. +func loadHeadArrivals(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string, nodes map[string]*arrivalNode) (int, error) { + req := &xch.ListBeaconApiEthV1EventsHeadRequest{ + MetaNetworkName: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: client.Network()}}, + Slot: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{Eq: uint32(slot)}}, + SlotStartDateTime: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{ + Eq: uint32(slotTime.Unix()), + }}, + OrderBy: "propagation_slot_start_diff", + PageSize: xatu.MaxQueryPageSize(), + } + + if blockRoot != "" { + req.Block = &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: blockRoot}} + } + + observations := 0 + + err := client.QueryPaged(ctx, settled, func(pageOffset uint32) (string, []any, error) { + req.PageToken = xatuPageToken(pageOffset) + + query, err := xch.BuildListBeaconApiEthV1EventsHeadQuery(req) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row xch.BeaconApiEthV1EventsHeadRow + if err := rows.ScanStruct(&row); err != nil { + return fmt.Errorf("head scan: %w", err) + } + + observations++ + + node := nodes[row.MetaClientName] + if node == nil { + node = &arrivalNode{fullName: row.MetaClientName} + nodes[row.MetaClientName] = node + } + + node.observations++ + + if node.continent == "" { + node.continent = row.MetaClientGeoContinentCode + node.country = row.MetaClientGeoCountry + node.countryCode = row.MetaClientGeoCountryCode + } + + if node.implementation == "" { + node.implementation = row.MetaConsensusImplementation + } + + if node.headMs == nil { + ms := row.PropagationSlotStartDiff + node.headMs = &ms + } + + return nil + }) + if err != nil { + return 0, fmt.Errorf("head query: %w", err) + } + + return observations, nil +} + +// lateThreshold returns the point after slot start beyond which an observation +// is treated as late: typically a syncing or stalled node whose event times say +// nothing about propagation. Derived from the chain's slot length, so it stays +// one slot on chains that do not use twelve second slots. +func lateThreshold(chainState *consensus.ChainState) uint32 { + slotMs := chainState.GetSpecs().SlotDurationMs + if slotMs == 0 { + slotMs = 12000 + } + + return uint32(slotMs) //nolint:gosec // slot lengths are small +} + +// loadEngineTimings loads engine API newPayload calls observed by the +// snooper into nodes: when the consensus client handed the payload to its +// execution client (derived from the observed completion minus the call +// duration), and how long the execution client took to import it. +func loadEngineTimings(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string, nodes map[string]*arrivalNode) (int, error) { + req := &xch.ListConsensusEngineApiNewPayloadRequest{ + MetaNetworkName: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: client.Network()}}, + Slot: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{Eq: uint32(slot)}}, + SlotStartDateTime: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{ + Eq: uint32(slotTime.Unix()), + }}, + OrderBy: "event_date_time", + PageSize: xatu.MaxQueryPageSize(), + } + + if blockRoot != "" { + req.BlockRoot = &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: blockRoot}} + } + + slotStartMs := slotTime.UnixMilli() + observations := 0 + + err := client.QueryPaged(ctx, settled, func(pageOffset uint32) (string, []any, error) { + req.PageToken = xatuPageToken(pageOffset) + + query, err := xch.BuildListConsensusEngineApiNewPayloadQuery(req) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row xch.ConsensusEngineApiNewPayloadRow + if err := rows.ScanStruct(&row); err != nil { + return fmt.Errorf("newpayload scan: %w", err) + } + + observations++ + + node := nodes[row.MetaClientName] + if node == nil { + node = &arrivalNode{fullName: row.MetaClientName} + nodes[row.MetaClientName] = node + } + + node.observations++ + + if node.npMs == nil { + // event_date_time is stamped when the sentry receives the snooper's + // event, which is emitted after the call completes - so the call + // START is the observed completion minus the call duration. + offset := row.EventDateTime/1000 - slotStartMs - int64(row.DurationMs) //nolint:gosec // duration is bounded + if offset < 0 { + offset = 0 + } + + ms := uint32(min(offset, int64(^uint32(0)))) //nolint:gosec // clamped above + dur := uint32(min(row.DurationMs, uint64(^uint32(0)))) //nolint:gosec // clamped + + node.npMs = &ms + node.npDurMs = &dur + node.npStatus = row.Status + } + + return nil + }) + if err != nil { + return 0, fmt.Errorf("newpayload query: %w", err) + } + + return observations, nil +} + +// loadExecutionEngineTimings loads newPayload calls captured on the execution +// side into nodes. It covers what the snooper series cannot: EL-instrumented +// nodes report their own calls, which is the only engine series most devnets +// have. The capture names its clients differently from the beacon sentries, +// so rows merge into existing nodes by their display identity, and the +// snooper's observation wins where both saw the same node. +func loadExecutionEngineTimings(ctx context.Context, client *xatu.Client, execHash string, execNumber uint64, slotTime time.Time, settled bool, nodes map[string]*arrivalNode) (int, error) { + if execHash == "" || execNumber == 0 { + return 0, nil + } + + req := &xch.ListExecutionEngineNewPayloadRequest{ + MetaNetworkName: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: client.Network()}}, + BlockNumber: &xch.UInt64Filter{Filter: &xch.UInt64Filter_Eq{Eq: execNumber}}, + BlockHash: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: execHash}}, + OrderBy: "event_date_time", + PageSize: xatu.MaxQueryPageSize(), + } + + query, err := xch.BuildListExecutionEngineNewPayloadQuery(req) + if err != nil { + return 0, fmt.Errorf("engine el build: %w", err) + } + + rows, err := client.Query(ctx, settled, query.Query, query.Args...) + if err != nil { + return 0, fmt.Errorf("engine el query: %w", err) + } + defer rows.Close() + + // existing nodes indexed by display identity, so the differently-named + // capture rows land on the beacon sentry rows they belong to + byDisplay := map[string]*arrivalNode{} + + for _, node := range nodes { + group, _, display := parseSentryName(node.fullName, client.Network()) + byDisplay[group+"|"+display] = node + } + + slotStartMs := slotTime.UnixMilli() + observations := 0 + + for rows.Next() { + var row xch.ExecutionEngineNewPayloadRow + if err := rows.ScanStruct(&row); err != nil { + return 0, fmt.Errorf("engine el scan: %w", err) + } + + observations++ + + group, _, display := parseSentryName(row.MetaClientName, client.Network()) + + node := byDisplay[group+"|"+display] + if node == nil { + node = nodes[row.MetaClientName] + if node == nil { + node = &arrivalNode{fullName: row.MetaClientName} + nodes[row.MetaClientName] = node + byDisplay[group+"|"+display] = node + } + } + + node.observations++ + + if node.continent == "" { + node.continent = row.MetaClientGeoContinentCode + node.country = row.MetaClientGeoCountry + node.countryCode = row.MetaClientGeoCountryCode + } + + if node.npMs == nil { + // requested_date_time is stamped at the call start, so no + // completion-minus-duration derivation is needed here + offset := row.RequestedDateTime/1000 - slotStartMs + if offset < 0 { + offset = 0 + } + + ms := uint32(min(offset, int64(^uint32(0)))) //nolint:gosec // clamped above + dur := uint32(min(row.DurationMs, uint64(^uint32(0)))) //nolint:gosec // clamped + + node.npMs = &ms + node.npDurMs = &dur + node.npStatus = row.Status + } + } + + if err := rows.Err(); err != nil { + return 0, fmt.Errorf("engine el query: %w", err) + } + + return observations, nil +} + +// payloadGossipQuery reads the gloas execution payload gossip events: when +// each node first saw the separately-gossiped execution payload on the wire. +// This is plain SQL rather than a generated builder because the table only +// exists on networks whose xatu schema carries the gloas migrations, and +// xatu master's generated package cannot include it until the schema lands +// there; swap to the generated builder when it does. One page is plenty: the +// series is one row per observing node. +const payloadGossipQuery = `SELECT meta_client_name, propagation_slot_start_diff +FROM beacon_api_eth_v1_events_execution_payload_gossip +WHERE meta_network_name = ? AND slot_start_date_time = toDateTime(?) AND slot = ?%s +ORDER BY propagation_slot_start_diff +LIMIT 10000` + +// loadPayloadArrivals loads the payload gossip series into nodes. A network +// without the gloas schema has no such table, which reads as no observations +// rather than an error, so pre-gloas networks keep working untouched. +func loadPayloadArrivals(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string, nodes map[string]*arrivalNode) (int, error) { + filter := "" + args := []any{client.Network(), slotTime.Unix(), uint32(slot)} + + if blockRoot != "" { + filter = " AND block_root = ?" + + args = append(args, blockRoot) + } + + rows, err := client.Query(ctx, settled, fmt.Sprintf(payloadGossipQuery, filter), args...) + if err != nil { + if isMissingTableError(err) { + return 0, nil + } + + return 0, fmt.Errorf("payload query: %w", err) + } + defer rows.Close() + + observations := 0 + + for rows.Next() { + var name string + + var ms uint32 + + if err := rows.Scan(&name, &ms); err != nil { + return 0, fmt.Errorf("payload scan: %w", err) + } + + observations++ + + node := nodes[name] + if node == nil { + node = &arrivalNode{fullName: name} + nodes[name] = node + } + + node.observations++ + + if node.plMs == nil { + v := ms + node.plMs = &v + } + } + + if err := rows.Err(); err != nil { + return 0, fmt.Errorf("payload query: %w", err) + } + + return observations, nil +} + +// isMissingTableError matches ClickHouse's unknown-table error (code 60), +// which reaches us as text through the HTTP proxy path. +func isMissingTableError(err error) bool { + msg := err.Error() + + return strings.Contains(msg, "UNKNOWN_TABLE") || strings.Contains(msg, "Code: 60") +} + +// buildArrivalAggregates fills the response's nodes, stats and +// per-continent/group summaries from the accumulated per-node observations. +// Late nodes are listed after on-time nodes and excluded from all summaries. +func buildArrivalAggregates(response *models.SlotArrivalResponse, nodes map[string]*arrivalNode, network string, lateMs uint32) { + nodeList := make([]*models.SlotArrivalNode, 0, len(nodes)) + lateList := make([]*models.SlotArrivalNode, 0) + continents := map[string]*models.SlotArrivalContinent{} + continentHead := map[string][]uint32{} + continentPl := map[string][]uint32{} + groups := map[string][]uint32{} + + for _, node := range nodes { + // Earliest block arrival: beacon API or gossip layer. Head adoption is + // tracked separately and only used as a fallback for head-only nodes. + var minMs uint32 + + switch { + case node.apiMs != nil && node.p2pMs != nil: + minMs = min(*node.apiMs, *node.p2pMs) + case node.apiMs != nil: + minMs = *node.apiMs + case node.p2pMs != nil: + minMs = *node.p2pMs + case node.headMs != nil: + minMs = *node.headMs + case node.npMs != nil: + minMs = *node.npMs + case node.plMs != nil: + minMs = *node.plMs + } + + group, operator, display := parseSentryName(node.fullName, network) + + entry := &models.SlotArrivalNode{ + Name: display, + FullName: node.fullName, + Group: group, + Operator: operator, + Implementation: node.implementation, + Continent: node.continent, + Country: node.country, + CountryCode: node.countryCode, + MinMs: minMs, + APIMs: node.apiMs, + P2PMs: node.p2pMs, + HeadMs: node.headMs, + NPMs: node.npMs, + NPDurMs: node.npDurMs, + PLMs: node.plMs, + NPStatus: node.npStatus, + Observations: node.observations, + } + + if minMs > lateMs { + entry.Late = true + + lateList = append(lateList, entry) + + continue + } + + nodeList = append(nodeList, entry) + + continent := continents[node.continent] + if continent == nil { + continent = &models.SlotArrivalContinent{Code: node.continent} + continents[node.continent] = continent + } + + continent.Nodes++ + + if node.headMs != nil { + continentHead[node.continent] = append(continentHead[node.continent], *node.headMs) + } + + if node.plMs != nil { + continentPl[node.continent] = append(continentPl[node.continent], *node.plMs) + } + + groups[group] = append(groups[group], minMs) + } + + sort.Slice(nodeList, func(a, b int) bool { + return nodeList[a].MinMs < nodeList[b].MinMs + }) + sort.Slice(lateList, func(a, b int) bool { + return lateList[a].MinMs < lateList[b].MinMs + }) + + if len(nodeList) == 0 { + response.Nodes = lateList + response.Stats = &models.SlotArrivalStats{LateNodes: len(lateList)} + + return + } + + response.Stats = &models.SlotArrivalStats{ + UniqueNodes: len(nodeList), + MinMs: nodeList[0].MinMs, + P50Ms: nodeList[len(nodeList)/2].MinMs, + P90Ms: nodeList[len(nodeList)*9/10].MinMs, + MaxMs: nodeList[len(nodeList)-1].MinMs, + LateNodes: len(lateList), + } + response.Nodes = append(nodeList, lateList...) + + // min/p50/p90 of a sorted series; nils when the series has no values, so + // the frontend renders dashes instead of zeros. + arrivalStats := func(values []uint32) (*uint32, *uint32, *uint32) { + if len(values) == 0 { + return nil, nil, nil + } + + sort.Slice(values, func(a, b int) bool { return values[a] < values[b] }) + + return &values[0], &values[len(values)/2], &values[len(values)*9/10] + } + + continentList := make([]*models.SlotArrivalContinent, 0, len(continents)) + + for code, continent := range continents { + continent.HeadMinMs, continent.HeadP50Ms, continent.HeadP90Ms = arrivalStats(continentHead[code]) + continent.PlMinMs, continent.PlP50Ms, continent.PlP90Ms = arrivalStats(continentPl[code]) + + continentList = append(continentList, continent) + } + + // earliest head adoption first; continents without head data sink to the + // bottom, ordered by payload arrival if they have one + continentSortKey := func(c *models.SlotArrivalContinent) uint64 { + switch { + case c.HeadMinMs != nil: + return uint64(*c.HeadMinMs) + case c.PlMinMs != nil: + return uint64(*c.PlMinMs) + (1 << 32) + default: + return 1 << 33 + } + } + + sort.Slice(continentList, func(a, b int) bool { + return continentSortKey(continentList[a]) < continentSortKey(continentList[b]) + }) + + response.Continents = continentList + + groupList := make([]*models.SlotArrivalGroup, 0, len(groups)) + + for name, values := range groups { + sort.Slice(values, func(a, b int) bool { return values[a] < values[b] }) + groupList = append(groupList, &models.SlotArrivalGroup{ + Name: name, + Nodes: len(values), + P50Ms: values[len(values)/2], + }) + } + + sort.Slice(groupList, func(a, b int) bool { + return groupList[a].Nodes > groupList[b].Nodes + }) + + response.Groups = groupList +} + +// unsettledCacheTimeout bounds how stale a pre-settle result can be to one +// slot, which decouples ClickHouse load from request volume without hiding +// events that are still arriving for longer than the slot they belong to. +func unsettledCacheTimeout(chainState *consensus.ChainState) time.Duration { + slotDuration := time.Duration(chainState.GetSpecs().SlotDurationMs) * time.Millisecond + if slotDuration <= 0 { + return 12 * time.Second + } + + return slotDuration +} + +// normalizeBlockRoot returns a lowercase 0x-prefixed 32 byte root, or "" when +// the input is not one. Anything unparsable degrades to a slot-wide query +// rather than being rejected, so a stale link still renders. +func normalizeBlockRoot(root string) string { + root = strings.ToLower(strings.TrimSpace(root)) + if len(root) != 66 || !strings.HasPrefix(root, "0x") { + return "" + } + + if _, err := hex.DecodeString(root[2:]); err != nil { + return "" + } + + return root +} + +// parseSentryName splits a xatu sentry name of the form +// // into a display group, operator and short +// display name. Names without that shape (e.g. locally run xatu instances) +// pass through unchanged. Community and corp contributoor nodes carry a +// hashed suffix that is shortened for display. +func parseSentryName(name, network string) (group, operator, display string) { + parts := strings.Split(name, "/") + if len(parts) != 3 { + return "other", "", name + } + + classifier, mid, node := parts[0], parts[1], parts[2] + + shortHash := func(s string) string { + h := strings.TrimPrefix(s, "hashed-") + if len(h) > 8 { + h = h[:8] + } + + return h + } + + switch { + case classifier == "ethpandaops": + display = node + for _, prefix := range []string{"utility-" + mid + "-", mid + "-", "utility-" + network + "-", network + "-"} { + display = strings.Replace(display, prefix, "", 1) + } + + return "ethpandaops", "ethpandaops", display + case strings.HasPrefix(classifier, "pub-"): + return "community", mid, mid + " #" + shortHash(node) + case strings.HasPrefix(classifier, "corp-"): + return "corp", mid, mid + " #" + shortHash(node) + default: + return "other", mid, node + } +} diff --git a/handlers/slot_arrival_root_test.go b/handlers/slot_arrival_root_test.go new file mode 100644 index 000000000..9966c95ec --- /dev/null +++ b/handlers/slot_arrival_root_test.go @@ -0,0 +1,23 @@ +package handlers + +import "testing" + +func TestNormalizeBlockRoot(t *testing.T) { + valid := "0x" + "ab12" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab" + tests := []struct{ in, want string }{ + {valid, valid}, + {"0xAB12" + "0123456789ABCDEF0123456789abcdef0123456789abcdef0123456789AB", valid}, + {" " + valid + " ", valid}, + {"", ""}, + {"0x1234", ""}, // too short + {valid[2:], ""}, // missing 0x + {"0x" + "zz12" + valid[6:], ""}, // not hex + {valid + "00", ""}, // too long + } + + for _, tt := range tests { + if got := normalizeBlockRoot(tt.in); got != tt.want { + t.Errorf("normalizeBlockRoot(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} diff --git a/handlers/slot_arrival_test.go b/handlers/slot_arrival_test.go new file mode 100644 index 000000000..3d3221007 --- /dev/null +++ b/handlers/slot_arrival_test.go @@ -0,0 +1,91 @@ +package handlers + +import "testing" + +func TestParseSentryName(t *testing.T) { + tests := []struct { + name string + input string + network string + group string + operator string + display string + }{ + { + name: "ethpandaops strips the network prefix", + input: "ethpandaops/mainnet/mainnet-lighthouse-geth-001", + network: "mainnet", + group: "ethpandaops", operator: "ethpandaops", display: "lighthouse-geth-001", + }, + { + name: "ethpandaops strips a utility prefix", + input: "ethpandaops/mainnet/utility-mainnet-bootnode-1", + network: "mainnet", + group: "ethpandaops", operator: "ethpandaops", display: "bootnode-1", + }, + { + name: "community node shortens the hashed suffix", + input: "pub-contributoor/someoperator/hashed-0123456789abcdef", + network: "mainnet", + group: "community", operator: "someoperator", display: "someoperator #01234567", + }, + { + name: "corp node shortens the hashed suffix", + input: "corp-contributoor/bigco/hashed-fedcba9876543210", + network: "mainnet", + group: "corp", operator: "bigco", display: "bigco #fedcba98", + }, + { + name: "short hash is left intact", + input: "pub-contributoor/op/hashed-abc", + network: "mainnet", + group: "community", operator: "op", display: "op #abc", + }, + { + name: "unknown classifier falls back to other", + input: "something-else/op/node-1", + network: "mainnet", + group: "other", operator: "op", display: "node-1", + }, + { + name: "name without the three-part shape passes through", + input: "my-local-xatu", + network: "mainnet", + group: "other", operator: "", display: "my-local-xatu", + }, + { + name: "empty name passes through", + input: "", + network: "mainnet", + group: "other", operator: "", display: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + group, operator, display := parseSentryName(tt.input, tt.network) + if group != tt.group || operator != tt.operator || display != tt.display { + t.Errorf("parseSentryName(%q, %q)\n got group=%q operator=%q display=%q\n want group=%q operator=%q display=%q", + tt.input, tt.network, group, operator, display, tt.group, tt.operator, tt.display) + } + }) + } +} + +func TestReduceSidecarName(t *testing.T) { + tests := []struct { + input string + want string + }{ + {"Xatu Sidecar (lighthouse)", "lighthouse"}, + {"Xatu Sidecar (prysm)", "prysm"}, + {"lighthouse", "lighthouse"}, + {"", ""}, + } + + for _, tt := range tests { + if got := reduceSidecarName(tt.input); got != tt.want { + t.Errorf("reduceSidecarName(%q) = %q, want %q", tt.input, got, tt.want) + } + } +} diff --git a/handlers/slot_waves.go b/handlers/slot_waves.go new file mode 100644 index 000000000..7b526d926 --- /dev/null +++ b/handlers/slot_waves.go @@ -0,0 +1,775 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "time" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + + "github.com/ethpandaops/go-eth2-client/spec/phase0" + + "github.com/ethpandaops/dora/clients/consensus" + "github.com/ethpandaops/dora/clients/xatu" + "github.com/ethpandaops/dora/db" + "github.com/ethpandaops/dora/services" + "github.com/ethpandaops/dora/types/models" + cbtch "github.com/ethpandaops/xatu-cbt/pkg/proto/clickhouse" +) + +// maxWaveRoots caps how many voted block roots the attestation wave reports +// individually. A contested slot rarely splits across more than two or three +// roots; anything beyond the cap is merged into one unnamed entry so a chain +// spam incident cannot inflate the payload. +const maxWaveRoots = 4 + +// SlotWaves returns the xatu-cbt backed slot panels as JSON for the lazy +// propagation tab on the slot page: the attestation first-seen wave and the +// data column propagation strip. +func SlotWaves(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if xatu.GlobalCbtClient == nil { + http.Error(w, "Xatu cbt is not configured", http.StatusNotFound) + return + } + + vars := mux.Vars(r) + slot, err := strconv.ParseUint(vars["slotOrHash"], 10, 64) + if err != nil { + http.Error(w, "Invalid slot", http.StatusBadRequest) + return + } + + // Scope to the block being viewed, so a slot with competing blocks reports + // each block's own wave rather than merging them. Requests without a + // usable root fall back to the whole slot. + blockRoot := normalizeBlockRoot(r.URL.Query().Get("root")) + + // the execution block identity comes from the page, like on the arrival + // endpoint; a bad value only misses the executed series + execHash := normalizeBlockRoot(r.URL.Query().Get("exec")) + execNumber, _ := strconv.ParseUint(r.URL.Query().Get("num"), 10, 64) + + cacheKey := fmt.Sprintf("slotwaves:%d:%s:%s:%d", slot, blockRoot, execHash, execNumber) + pageRes, pageErr := services.GlobalFrontendCache.ProcessCachedPage(cacheKey, true, &models.SlotWavesResponse{}, func(pageCall *services.FrontendCacheProcessingPage) any { + data, cacheTimeout, buildErr := buildSlotWavesData(pageCall.CallCtx, phase0.Slot(slot), blockRoot, execHash, execNumber) + if buildErr != nil { + logrus.WithError(buildErr).Error("error loading slot waves data from xatu-cbt") + pageCall.CacheTimeout = -1 + + return &models.SlotWavesResponse{Slot: slot} + } + + pageCall.CacheTimeout = cacheTimeout + + return data + }) + if pageErr != nil { + logrus.WithError(pageErr).Error("error building slot waves data") + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + return + } + + result, ok := pageRes.(*models.SlotWavesResponse) + if !ok { + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + return + } + + if err := json.NewEncoder(w).Encode(result); err != nil { + logrus.WithError(err).Error("error encoding slot waves data") + http.Error(w, "Internal server error", http.StatusServiceUnavailable) + } +} + +// buildSlotWavesData queries xatu-cbt for the slot's attestation wave and +// data column measurements. It returns the response and the cache timeout, +// following the same ladder as the arrival endpoint: one slot while the cbt +// transformations may still rewrite the slot's rows, then short, then long. +func buildSlotWavesData(ctx context.Context, slot phase0.Slot, blockRoot, execHash string, execNumber uint64) (*models.SlotWavesResponse, time.Duration, error) { + client := xatu.GlobalCbtClient + chainState := services.GlobalBeaconService.GetChainState() + slotTime := chainState.SlotToTime(slot) + + settled := time.Now().After(slotTime.Add(client.SettleDelay())) + + queryCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + attestations, err := loadAttestationWave(queryCtx, client, slot, slotTime, settled, blockRoot) + if err != nil { + return nil, -1, err + } + + if attestations != nil { + attestations.DeadlineMs = attestationDeadlineMs(chainState, slot) + attestations.ExpectedCount = expectedAttesters(queryCtx, chainState, slot) + attestations.ThresholdCount = voteThreshold(chainState, attestations.ExpectedCount) + + } + + columns, err := loadColumnWave(queryCtx, client, slot, slotTime, settled, blockRoot) + if err != nil { + return nil, -1, err + } + + var ptc *models.SlotPtcWave + + var payload, head, executed *models.SlotSeenWave + + if xatu.GlobalClient != nil { + slotMs := lateThreshold(chainState) + + ptc, err = loadPtcWave(queryCtx, xatu.GlobalClient, slot, slotTime, settled, blockRoot) + if err != nil { + // the raw-backed series must not take the cbt panels down + logrus.WithError(err).Warn("error loading ptc wave from xatu") + + ptc = nil + } + + if ptc != nil { + // votes are due within PAYLOAD_ATTESTATION_DUE_BPS of the slot, + // from a committee of PTC_SIZE + ptc.DeadlineMs = bpsOfSlot(chainState, chainState.GetSpecs().PayloadAttestationDueBps, 7500) + ptc.ExpectedCount = int(chainState.GetSpecs().PtcSize) //nolint:gosec // committee sizes are small + } + + payload, err = loadSeenWave(queryCtx, xatu.GlobalClient, "beacon_api_eth_v1_events_execution_payload_gossip", "block_root", slot, slotTime, settled, blockRoot, slotMs) + if err != nil { + logrus.WithError(err).Warn("error loading payload wave from xatu") + + payload = nil + } + + head, err = loadSeenWave(queryCtx, xatu.GlobalClient, "beacon_api_eth_v1_events_head", "block", slot, slotTime, settled, blockRoot, slotMs) + if err != nil { + logrus.WithError(err).Warn("error loading head wave from xatu") + + head = nil + } + + executed, err = loadExecutedWave(queryCtx, xatu.GlobalClient, execHash, execNumber, slotTime, settled, slotMs) + if err != nil { + logrus.WithError(err).Warn("error loading executed wave from xatu") + + executed = nil + } + } + + response := &models.SlotWavesResponse{ + Slot: uint64(slot), + Settled: settled, + SlotMs: lateThreshold(chainState), + Attestations: attestations, + Ptc: ptc, + Payload: payload, + Head: head, + Executed: executed, + Columns: columns, + } + + cacheTimeout := time.Duration(-1) + + switch { + case !settled: + cacheTimeout = unsettledCacheTimeout(chainState) + case time.Since(slotTime) < 30*time.Minute: + cacheTimeout = 30 * time.Second + default: + cacheTimeout = time.Hour + } + + return response, cacheTimeout, nil +} + +// gloasActive reports whether the slot is past the gloas fork. +func gloasActive(chainState *consensus.ChainState, slot phase0.Slot) bool { + specs := chainState.GetSpecs() + + return specs.GloasForkEpoch != nil && chainState.EpochOfSlot(slot) >= phase0.Epoch(*specs.GloasForkEpoch) +} + +// bpsOfSlot resolves a basis-points spec value against the slot duration, +// with the spec's own default for nodes that do not serve the key yet. +func bpsOfSlot(chainState *consensus.ChainState, bps, defaultBps uint64) uint32 { + if bps == 0 { + bps = defaultBps + } + + return uint32(uint64(lateThreshold(chainState)) * bps / 10000) //nolint:gosec // bounded by the slot length +} + +// attestationDeadlineMs is the point where validators attest without a block +// in hand: ATTESTATION_DUE_BPS of the slot, moved earlier by gloas. +func attestationDeadlineMs(chainState *consensus.ChainState, slot phase0.Slot) uint32 { + specs := chainState.GetSpecs() + + if gloasActive(chainState, slot) { + return bpsOfSlot(chainState, specs.AttestationDueBpsGloas, 2500) + } + + return bpsOfSlot(chainState, specs.AttestationDueBps, 3333) +} + +// voteThreshold is the vote count where the block clears the gloas builder +// payment quorum: BUILDER_PAYMENT_THRESHOLD (6/10 by default) of the slot's +// expected attesters. Pre-gloas networks do not serve the keys, so the +// gloas default doubles as a plain supermajority reference there. +func voteThreshold(chainState *consensus.ChainState, expected int) int { + specs := chainState.GetSpecs() + + numerator := specs.BuilderPaymentThresholdNumerator + denominator := specs.BuilderPaymentThresholdDenominator + + if numerator == 0 || denominator == 0 { + numerator, denominator = 6, 10 + } + + return int(uint64(expected) * numerator / denominator) //nolint:gosec // bounded by the validator set +} + +// executedWaveQuery buckets when each node's execution client finished +// importing the payload: the newPayload call start plus its duration, taken +// from the execution-side captures, which are keyed by block rather than +// slot. Bounded to the slot like the other waves. +const executedWaveQuery = `SELECT toUInt32(intDiv(first_ms, 50) * 50) AS chunk, count() AS nodes +FROM ( + SELECT meta_client_name, + min(toUnixTimestamp64Milli(requested_date_time) + toInt64(duration_ms)) - ? AS first_ms + FROM execution_engine_new_payload + WHERE meta_network_name = ? AND block_number = ? AND block_hash = ? + GROUP BY meta_client_name + HAVING first_ms BETWEEN 0 AND ? +) +GROUP BY chunk +ORDER BY chunk` + +// loadExecutedWave loads the per-node payload execution wave, or nil when the +// page provided no execution block or the network lacks the capture table. +func loadExecutedWave(ctx context.Context, client *xatu.Client, execHash string, execNumber uint64, slotTime time.Time, settled bool, slotMs uint32) (*models.SlotSeenWave, error) { + if execHash == "" || execNumber == 0 { + return nil, nil + } + + args := []any{slotTime.UnixMilli(), client.Network(), execNumber, execHash, slotMs} + + rows, err := client.Query(ctx, settled, executedWaveQuery, args...) + if err != nil { + if isMissingTableError(err) { + return nil, nil + } + + return nil, fmt.Errorf("executed wave query: %w", err) + } + defer rows.Close() + + wave := &models.SlotSeenWave{} + + for rows.Next() { + var chunk uint32 + + var nodes uint64 + + if err := rows.Scan(&chunk, &nodes); err != nil { + return nil, fmt.Errorf("executed wave scan: %w", err) + } + + count := int(nodes) //nolint:gosec // bounded by the node fleet + wave.TotalCount += count + wave.Buckets = append(wave.Buckets, &models.SlotAttestationBucket{Ms: chunk, Count: count}) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("executed wave query: %w", err) + } + + if wave.TotalCount == 0 { + return nil, nil + } + + return wave, nil +} + +// expectedAttesters is how many validators were due to attest in the slot: +// the epoch's active validator count spread over its slots. Recent epochs +// come from the in-memory epoch stats; older ones fall back to the epochs +// table, which serves history but only fills when the synchronizer runs. +func expectedAttesters(ctx context.Context, chainState *consensus.ChainState, slot phase0.Slot) int { + epoch := chainState.EpochOfSlot(slot) + + slotsPerEpoch := chainState.GetSpecs().SlotsPerEpoch + if slotsPerEpoch == 0 { + return 0 + } + + beaconIndexer := services.GlobalBeaconService.GetBeaconIndexer() + if epochStats := beaconIndexer.GetEpochStats(epoch, nil); epochStats != nil { + if values := epochStats.GetOrLoadValues(ctx, beaconIndexer, true, false); values != nil && values.ActiveValidators > 0 { + return int(values.ActiveValidators / slotsPerEpoch) //nolint:gosec // bounded by the validator set + } + } + + rows := db.GetEpochs(ctx, uint64(epoch), 1) + if len(rows) == 0 || rows[0].Epoch != uint64(epoch) { + return 0 + } + + return int(rows[0].ValidatorCount / slotsPerEpoch) //nolint:gosec // bounded by the validator set +} + +// seenWaveQuery buckets when each observing node first saw one per-slot +// object, capped to the slot so an hours-late straggler cannot stretch the +// chart. Aggregated in ClickHouse because the generated builders cannot +// express GROUP BY; the object table and its block-root column are +// interpolated from constants, never from user input. +const seenWaveQuery = `SELECT toUInt32(intDiv(first_ms, 50) * 50) AS chunk, count() AS nodes +FROM ( + SELECT meta_client_name, min(propagation_slot_start_diff) AS first_ms + FROM %s + WHERE meta_network_name = ? AND slot_start_date_time = toDateTime(?) AND slot = ? AND propagation_slot_start_diff <= ?%s + GROUP BY meta_client_name +) +GROUP BY chunk +ORDER BY chunk` + +// loadSeenWave loads one first-seen-per-node wave, or nil on networks whose +// xatu schema lacks the table. +func loadSeenWave(ctx context.Context, client *xatu.Client, table, rootColumn string, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string, slotMs uint32) (*models.SlotSeenWave, error) { + filter := "" + args := []any{client.Network(), slotTime.Unix(), uint32(slot), slotMs} + + if blockRoot != "" { + filter = " AND " + rootColumn + " = ?" + + args = append(args, blockRoot) + } + + rows, err := client.Query(ctx, settled, fmt.Sprintf(seenWaveQuery, table, filter), args...) + if err != nil { + if isMissingTableError(err) { + return nil, nil + } + + return nil, fmt.Errorf("%s wave query: %w", table, err) + } + defer rows.Close() + + wave := &models.SlotSeenWave{} + + for rows.Next() { + var chunk uint32 + + var nodes uint64 + + if err := rows.Scan(&chunk, &nodes); err != nil { + return nil, fmt.Errorf("%s wave scan: %w", table, err) + } + + count := int(nodes) //nolint:gosec // bounded by the node fleet + wave.TotalCount += count + wave.Buckets = append(wave.Buckets, &models.SlotAttestationBucket{Ms: chunk, Count: count}) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("%s wave query: %w", table, err) + } + + if wave.TotalCount == 0 { + return nil, nil + } + + return wave, nil +} + +// ptcWaveQuery reduces the gloas payload attestation events to 50ms chunks of +// unique PTC votes, split by the vote's payload_present verdict. Each vote is +// deduplicated to its first sighting across all observing nodes. Plain SQL +// for the same reason as payloadGossipQuery. +const ptcWaveQuery = `SELECT chunk, payload_present, count() AS votes +FROM ( + SELECT validator_index, payload_present, + toUInt32(intDiv(min(propagation_slot_start_diff), 50) * 50) AS chunk + FROM beacon_api_eth_v1_events_payload_attestation + WHERE meta_network_name = ? AND slot_start_date_time = toDateTime(?) AND slot = ?%s + GROUP BY validator_index, payload_present +) +GROUP BY chunk, payload_present +ORDER BY chunk` + +// loadPtcWave loads the payload timeliness committee's voting wave, or nil on +// networks without the gloas schema. +func loadPtcWave(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string) (*models.SlotPtcWave, error) { + filter := "" + args := []any{client.Network(), slotTime.Unix(), uint32(slot)} + + if blockRoot != "" { + filter = " AND beacon_block_root = ?" + + args = append(args, blockRoot) + } + + rows, err := client.Query(ctx, settled, fmt.Sprintf(ptcWaveQuery, filter), args...) + if err != nil { + if isMissingTableError(err) { + return nil, nil + } + + return nil, fmt.Errorf("ptc wave query: %w", err) + } + defer rows.Close() + + buckets := map[uint32]*models.SlotPtcBucket{} + wave := &models.SlotPtcWave{} + + for rows.Next() { + var chunk uint32 + + var present bool + + var votes uint64 + + if err := rows.Scan(&chunk, &present, &votes); err != nil { + return nil, fmt.Errorf("ptc wave scan: %w", err) + } + + bucket := buckets[chunk] + if bucket == nil { + bucket = &models.SlotPtcBucket{Ms: chunk} + buckets[chunk] = bucket + wave.Buckets = append(wave.Buckets, bucket) + } + + count := int(votes) //nolint:gosec // bounded by the committee size + wave.TotalCount += count + + if present { + bucket.Present += count + wave.PresentCount += count + } else { + bucket.Missing += count + } + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("ptc wave query: %w", err) + } + + if wave.TotalCount == 0 { + return nil, nil + } + + sort.Slice(wave.Buckets, func(a, b int) bool { + return wave.Buckets[a].Ms < wave.Buckets[b].Ms + }) + + return wave, nil +} + +// waveRoot accumulates one voted block root's buckets before they are sorted +// into the response. +type waveRoot struct { + root string + count int + buckets map[uint32]int +} + +// loadAttestationWave loads the 50ms attestation first-seen chunks for the +// slot, grouped by voted block root. The cbt tables hold one network per +// database, so no network filter is needed. It returns nil when the table has +// no rows for the slot. +func loadAttestationWave(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string) (*models.SlotAttestationWave, error) { + // Both slot and slot_start_date_time are filtered: the cbt cluster rejects + // queries that cannot use a table's primary key, and which of the two + // leads the key varies between tables. + req := &cbtch.ListFctAttestationFirstSeenChunked50MsRequest{ + Slot: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{Eq: uint32(slot)}}, + SlotStartDateTime: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{ + Eq: uint32(slotTime.Unix()), + }}, + OrderBy: "chunk_slot_start_diff", + PageSize: xatu.MaxQueryPageSize(), + } + + roots := map[string]*waveRoot{} + total := 0 + + err := client.QueryPaged(ctx, settled, func(pageOffset uint32) (string, []any, error) { + req.PageToken = cbtPageToken(pageOffset) + + query, err := cbtch.BuildListFctAttestationFirstSeenChunked50MsQuery(req) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row cbtch.FctAttestationFirstSeenChunked50MsRow + if err := rows.ScanStruct(&row); err != nil { + return fmt.Errorf("attestation wave scan: %w", err) + } + + root := roots[row.BlockRoot] + if root == nil { + root = &waveRoot{root: row.BlockRoot, buckets: map[uint32]int{}} + roots[row.BlockRoot] = root + } + + count := int(row.AttestationCount) + root.count += count + root.buckets[row.ChunkSlotStartDiff] += count + total += count + + return nil + }) + if err != nil { + return nil, fmt.Errorf("attestation wave query: %w", err) + } + + if total == 0 { + return nil, nil + } + + return assembleWaveRoots(roots, total, blockRoot), nil +} + +// assembleWaveRoots orders the accumulated roots by vote count, merges the +// tail beyond maxWaveRoots into one unnamed entry and marks the viewed root. +func assembleWaveRoots(roots map[string]*waveRoot, total int, blockRoot string) *models.SlotAttestationWave { + ordered := make([]*waveRoot, 0, len(roots)) + for _, root := range roots { + ordered = append(ordered, root) + } + + sort.Slice(ordered, func(a, b int) bool { + return ordered[a].count > ordered[b].count + }) + + if len(ordered) > maxWaveRoots { + rest := &waveRoot{buckets: map[uint32]int{}} + + for _, root := range ordered[maxWaveRoots:] { + rest.count += root.count + + for ms, count := range root.buckets { + rest.buckets[ms] += count + } + } + + ordered = append(ordered[:maxWaveRoots], rest) + } + + wave := &models.SlotAttestationWave{TotalCount: total} + + for _, root := range ordered { + entry := &models.SlotAttestationRoot{ + Root: root.root, + Viewed: blockRoot != "" && root.root == blockRoot, + Count: root.count, + Buckets: make([]*models.SlotAttestationBucket, 0, len(root.buckets)), + } + + for ms, count := range root.buckets { + entry.Buckets = append(entry.Buckets, &models.SlotAttestationBucket{Ms: ms, Count: count}) + } + + sort.Slice(entry.Buckets, func(a, b int) bool { + return entry.Buckets[a].Ms < entry.Buckets[b].Ms + }) + + wave.Roots = append(wave.Roots, entry) + } + + return wave +} + +// loadColumnWave loads every node's first sighting of each data column and +// the availability probes for the slot, and reduces them into one entry per +// column index with min/p50/p90 timings. It returns nil when neither table +// has rows, and also for a blobless slot, where there are no columns to +// spread. +func loadColumnWave(ctx context.Context, client *xatu.Client, slot phase0.Slot, slotTime time.Time, settled bool, blockRoot string) (*models.SlotColumnWave, error) { + columns := map[uint32]*models.SlotColumn{} + sightings := map[uint32][]uint32{} + + column := func(index uint32) *models.SlotColumn { + entry := columns[index] + if entry == nil { + entry = &models.SlotColumn{Index: index} + columns[index] = entry + } + + return entry + } + + seenReq := &cbtch.ListFctBlockDataColumnSidecarFirstSeenByNodeRequest{ + Slot: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{Eq: uint32(slot)}}, + SlotStartDateTime: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{ + Eq: uint32(slotTime.Unix()), + }}, + OrderBy: "column_index", + PageSize: xatu.MaxQueryPageSize(), + } + + if blockRoot != "" { + seenReq.BlockRoot = &cbtch.StringFilter{Filter: &cbtch.StringFilter_Eq{Eq: blockRoot}} + } + + blobCount := 0 + + err := client.QueryPaged(ctx, settled, func(pageOffset uint32) (string, []any, error) { + seenReq.PageToken = cbtPageToken(pageOffset) + + query, err := cbtch.BuildListFctBlockDataColumnSidecarFirstSeenByNodeQuery(seenReq) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row cbtch.FctBlockDataColumnSidecarFirstSeenByNodeRow + if err := rows.ScanStruct(&row); err != nil { + return fmt.Errorf("column sightings scan: %w", err) + } + + entry := column(row.ColumnIndex) + sightings[row.ColumnIndex] = append(sightings[row.ColumnIndex], row.SeenSlotStartDiff) + + if entry.FirstSeenMs == nil || row.SeenSlotStartDiff < *entry.FirstSeenMs { + ms := row.SeenSlotStartDiff + entry.FirstSeenMs = &ms + + _, _, display := parseSentryName(row.MetaClientName, client.Network()) + entry.FirstSeenBy = display + entry.CountryCode = row.MetaClientGeoCountryCode + + entry.Implementation = row.MetaConsensusImplementation + if entry.Implementation == "" { + entry.Implementation = reduceSidecarName(row.MetaClientImplementation) + } + } + + if int(row.RowCount) > blobCount { + blobCount = int(row.RowCount) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("column sightings query: %w", err) + } + + // Availability is probed per slot and column, not per block root, so it + // stays unfiltered even when the page views one specific block. + availReq := &cbtch.ListFctDataColumnAvailabilityBySlotRequest{ + Slot: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{Eq: uint32(slot)}}, + SlotStartDateTime: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{ + Eq: uint32(slotTime.Unix()), + }}, + OrderBy: "column_index", + PageSize: xatu.MaxQueryPageSize(), + } + + err = client.QueryPaged(ctx, settled, func(pageOffset uint32) (string, []any, error) { + availReq.PageToken = cbtPageToken(pageOffset) + + query, err := cbtch.BuildListFctDataColumnAvailabilityBySlotQuery(availReq) + + return query.Query, query.Args, err + }, func(rows driver.Rows) error { + var row cbtch.FctDataColumnAvailabilityBySlotRow + if err := rows.ScanStruct(&row); err != nil { + return fmt.Errorf("column availability scan: %w", err) + } + + entry := column(uint32(row.ColumnIndex)) //nolint:gosec // column indexes are 0-127 + + pct := row.AvailabilityPct + entry.AvailabilityPct = &pct + entry.Probes = int(row.ProbeCount) + entry.P50ResponseMs = row.P50ResponseTimeMs + + if int(row.BlobCount) > blobCount { + blobCount = int(row.BlobCount) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("column availability query: %w", err) + } + + if len(columns) == 0 || blobCount == 0 { + return nil, nil + } + + wave := &models.SlotColumnWave{ + BlobCount: blobCount, + MinAvailability: 100, + Columns: make([]*models.SlotColumn, 0, len(columns)), + } + + availabilitySum := float64(0) + allSightings := []uint32{} + + for _, entry := range columns { + wave.Columns = append(wave.Columns, entry) + + if times := sightings[entry.Index]; len(times) > 0 { + sort.Slice(times, func(a, b int) bool { return times[a] < times[b] }) + entry.P50Ms = times[len(times)/2] + entry.P90Ms = times[len(times)*9/10] + entry.Observations = len(times) + + wave.Observations += len(times) + allSightings = append(allSightings, times...) + } + + if entry.FirstSeenMs != nil { + wave.SeenColumns++ + + if wave.FirstMs == 0 || *entry.FirstSeenMs < wave.FirstMs { + wave.FirstMs = *entry.FirstSeenMs + } + + if *entry.FirstSeenMs > wave.LastMs { + wave.LastMs = *entry.FirstSeenMs + } + } + + if entry.AvailabilityPct != nil { + wave.ProbedColumns++ + wave.TotalProbes += entry.Probes + availabilitySum += *entry.AvailabilityPct + + if *entry.AvailabilityPct < wave.MinAvailability { + wave.MinAvailability = *entry.AvailabilityPct + } + + // p50 covers successful probes only, so a fully failed column + // reports zero and cannot be the slowest. + if entry.P50ResponseMs > wave.WorstP50Ms { + wave.WorstP50Ms = entry.P50ResponseMs + } + } + } + + if len(allSightings) > 0 { + sort.Slice(allSightings, func(a, b int) bool { return allSightings[a] < allSightings[b] }) + wave.MedianMs = allSightings[len(allSightings)/2] + } + + if wave.ProbedColumns > 0 { + wave.AvgAvailability = availabilitySum / float64(wave.ProbedColumns) + } else { + wave.MinAvailability = 0 + } + + sort.Slice(wave.Columns, func(a, b int) bool { + return wave.Columns[a].Index < wave.Columns[b].Index + }) + + return wave, nil +} diff --git a/handlers/slot_waves_test.go b/handlers/slot_waves_test.go new file mode 100644 index 000000000..6bfd3b3b2 --- /dev/null +++ b/handlers/slot_waves_test.go @@ -0,0 +1,151 @@ +package handlers + +import ( + "fmt" + "strings" + "testing" + + cbtch "github.com/ethpandaops/xatu-cbt/pkg/proto/clickhouse" +) + +// The cbt cluster runs with force_primary_key, and which of slot or +// slot_start_date_time leads a table's primary key differs between tables and +// replicas. Every cbt query therefore has to filter on both; a builder that +// silently dropped one would only fail in production. +func TestCbtQueriesFilterBothPrimaryKeys(t *testing.T) { + slotFilter := &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{Eq: 123}} + timeFilter := &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{Eq: 456}} + + queries := map[string]func() (cbtch.SQLQuery, error){ + "attestation wave": func() (cbtch.SQLQuery, error) { + return cbtch.BuildListFctAttestationFirstSeenChunked50MsQuery(&cbtch.ListFctAttestationFirstSeenChunked50MsRequest{ + Slot: slotFilter, SlotStartDateTime: timeFilter, + }) + }, + "column first seen": func() (cbtch.SQLQuery, error) { + return cbtch.BuildListFctBlockDataColumnSidecarFirstSeenQuery(&cbtch.ListFctBlockDataColumnSidecarFirstSeenRequest{ + Slot: slotFilter, SlotStartDateTime: timeFilter, + }) + }, + "column availability": func() (cbtch.SQLQuery, error) { + return cbtch.BuildListFctDataColumnAvailabilityBySlotQuery(&cbtch.ListFctDataColumnAvailabilityBySlotRequest{ + Slot: slotFilter, SlotStartDateTime: timeFilter, + }) + }, + } + + for name, build := range queries { + q, err := build() + if err != nil { + t.Fatalf("%s: build: %v", name, err) + } + + for _, column := range []string{"slot =", "slot_start_date_time ="} { + if !strings.Contains(q.Query, column) { + t.Errorf("%s: missing %q filter in %q", name, column, q.Query) + } + } + } +} + +// The paged reader contract from the raw tables holds for the cbt builders +// too: a page token is a row offset, and the ceiling matches MaxQueryPageSize. +func TestCbtPageTokenIsRowOffset(t *testing.T) { + const pageSize = 10000 + + for page, wantOffset := range map[int]string{0: "", 1: "10000", 2: "20000"} { + q, err := cbtch.BuildListFctAttestationFirstSeenChunked50MsQuery(&cbtch.ListFctAttestationFirstSeenChunked50MsRequest{ + Slot: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{Eq: 1}}, + PageSize: pageSize, + PageToken: cbtPageToken(uint32(page * pageSize)), + }) + if err != nil { + t.Fatalf("page %d: build: %v", page, err) + } + + if !strings.Contains(q.Query, "LIMIT 10000") { + t.Errorf("page %d: expected LIMIT 10000 in %q", page, q.Query) + } + + if wantOffset == "" { + if strings.Contains(q.Query, "OFFSET") { + t.Errorf("page 0 should not paginate, got %q", q.Query) + } + + continue + } + + if !strings.Contains(q.Query, "OFFSET "+wantOffset) { + t.Errorf("page %d: expected OFFSET %s, got %q", page, wantOffset, q.Query) + } + } +} + +func TestCbtMaxPageSizeIsTheCeiling(t *testing.T) { + build := func(size int32) error { + _, err := cbtch.BuildListFctAttestationFirstSeenChunked50MsQuery(&cbtch.ListFctAttestationFirstSeenChunked50MsRequest{ + Slot: &cbtch.UInt32Filter{Filter: &cbtch.UInt32Filter_Eq{Eq: 1}}, + PageSize: size, + }) + + return err + } + + if err := build(10000); err != nil { + t.Fatalf("page size 10000 must be accepted: %v", err) + } + + if err := build(10001); err == nil { + t.Fatal("page size 10001 must be rejected") + } +} + +// A slot under attack can carry votes for many roots; the response merges the +// tail so the payload stays bounded, and the viewed root keeps its own entry +// even when it is not the most voted one. +func TestAssembleWaveRootsMergesTheTail(t *testing.T) { + roots := map[string]*waveRoot{} + + for i := 0; i < maxWaveRoots+3; i++ { + root := fmt.Sprintf("0x%064d", i) + roots[root] = &waveRoot{ + root: root, + count: 100 - i, + buckets: map[uint32]int{4000: 100 - i}, + } + } + + total := 0 + for _, root := range roots { + total += root.count + } + + viewedRoot := fmt.Sprintf("0x%064d", 1) + wave := assembleWaveRoots(roots, total, viewedRoot) + + if len(wave.Roots) != maxWaveRoots+1 { + t.Fatalf("expected %d roots after merge, got %d", maxWaveRoots+1, len(wave.Roots)) + } + + last := wave.Roots[len(wave.Roots)-1] + if last.Root != "" { + t.Errorf("merged tail must have an empty root, got %q", last.Root) + } + + if last.Count != (100-maxWaveRoots)+(100-maxWaveRoots-1)+(100-maxWaveRoots-2) { + t.Errorf("merged tail count wrong: %d", last.Count) + } + + if !wave.Roots[1].Viewed || wave.Roots[1].Root != viewedRoot { + t.Errorf("viewed root not marked: %+v", wave.Roots[1]) + } + + sum := 0 + for _, root := range wave.Roots { + sum += root.Count + } + + if sum != total { + t.Errorf("merge lost votes: %d != %d", sum, total) + } +} diff --git a/handlers/xatu_paging.go b/handlers/xatu_paging.go new file mode 100644 index 000000000..86898ec76 --- /dev/null +++ b/handlers/xatu_paging.go @@ -0,0 +1,29 @@ +package handlers + +import ( + cbtch "github.com/ethpandaops/xatu-cbt/pkg/proto/clickhouse" + xch "github.com/ethpandaops/xatu/pkg/proto/clickhouse" +) + +// xatuPageToken encodes a QueryPaged row offset into a page token for the +// xatu raw table builders. The zero offset must stay an empty token: the +// builders treat "" as page one, and a token of zero would be decoded and +// re-applied as OFFSET 0 anyway. +func xatuPageToken(pageOffset uint32) string { + if pageOffset == 0 { + return "" + } + + return xch.EncodePageToken(pageOffset) +} + +// cbtPageToken is xatuPageToken for the xatu-cbt table builders. The two +// generated packages use the same token format, but each builder only decodes +// tokens produced by its own package's helper. +func cbtPageToken(pageOffset uint32) string { + if pageOffset == 0 { + return "" + } + + return cbtch.EncodePageToken(pageOffset) +} diff --git a/handlers/xatu_paging_test.go b/handlers/xatu_paging_test.go new file mode 100644 index 000000000..478939a14 --- /dev/null +++ b/handlers/xatu_paging_test.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "strings" + "testing" + + xch "github.com/ethpandaops/xatu/pkg/proto/clickhouse" +) + +// The paged readers assume a page token is a row offset and that requesting +// page N yields OFFSET N*pageSize. If that contract changes upstream, paging +// would silently re-read or skip rows. +func TestPageTokenIsRowOffset(t *testing.T) { + const pageSize = 10000 + + for page, wantOffset := range map[int]string{0: "", 1: "10000", 2: "20000"} { + token := "" + if page > 0 { + token = xch.EncodePageToken(uint32(page * pageSize)) + } + + q, err := xch.BuildListBeaconApiEthV1EventsBlockQuery(&xch.ListBeaconApiEthV1EventsBlockRequest{ + MetaNetworkName: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: "mainnet"}}, + Slot: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{Eq: 1}}, + PageSize: pageSize, + PageToken: token, + }) + if err != nil { + t.Fatalf("page %d: build: %v", page, err) + } + + if !strings.Contains(q.Query, "LIMIT 10000") { + t.Errorf("page %d: expected LIMIT 10000 in %q", page, q.Query) + } + + if wantOffset == "" { + if strings.Contains(q.Query, "OFFSET") { + t.Errorf("page 0 should not paginate, got %q", q.Query) + } + + continue + } + + if !strings.Contains(q.Query, "OFFSET "+wantOffset) { + t.Errorf("page %d: expected OFFSET %s, got %q", page, wantOffset, q.Query) + } + } +} + +// A page size above the builders' ceiling must fail loudly, since that is what +// MaxQueryPageSize is pinned to. +func TestMaxPageSizeIsTheCeiling(t *testing.T) { + base := func(size int32) error { + _, err := xch.BuildListBeaconApiEthV1EventsBlockQuery(&xch.ListBeaconApiEthV1EventsBlockRequest{ + MetaNetworkName: &xch.StringFilter{Filter: &xch.StringFilter_Eq{Eq: "mainnet"}}, + Slot: &xch.UInt32Filter{Filter: &xch.UInt32Filter_Eq{Eq: 1}}, + PageSize: size, + }) + + return err + } + + if err := base(10000); err != nil { + t.Fatalf("10000 should be accepted: %v", err) + } + + if err := base(10001); err == nil { + t.Fatal("10001 should be rejected, MaxQueryPageSize is no longer the ceiling") + } +} diff --git a/static/images/xatu.png b/static/images/xatu.png new file mode 100644 index 000000000..8dd356186 Binary files /dev/null and b/static/images/xatu.png differ diff --git a/templates/epoch/epoch.html b/templates/epoch/epoch.html index cee597785..2f5fd594f 100644 --- a/templates/epoch/epoch.html +++ b/templates/epoch/epoch.html @@ -182,6 +182,9 @@

Slashings Txs / Blobs Sync Agg % + {{ if .XatuEnabled }} + Arrival min / p90 + {{ end }} Graffiti @@ -220,9 +223,14 @@

{{ if not (eq $slot.Status 0) }}{{ $slot.ProposerSlashingCount }} / {{ $slot.AttesterSlashingCount }}{{ end }} {{ if not (eq $slot.Status 0) }}{{ $slot.EthTransactionCount }} / {{ $slot.BlobCount }}{{ end }} {{ if not (eq $slot.Status 0) }}{{ formatFloat $slot.SyncParticipation 2 }}%{{ end }} + {{ if $epoch.XatuEnabled }} + + {{ if $slot.ArrivalNodes }}{{ formatArrivalMs $slot.ArrivalMinMs }} / {{ formatArrivalMs $slot.ArrivalP90Ms }}{{ else }}{{ end }} + + {{ end }} {{ if not (eq $slot.Status 0) }}{{ formatGraffiti $slot.Graffiti }}{{ end }} {{ else }} - Not indexed yet + Not indexed yet {{ end }} {{ end }} diff --git a/templates/slot/arrival.html b/templates/slot/arrival.html new file mode 100644 index 000000000..d054b6ea8 --- /dev/null +++ b/templates/slot/arrival.html @@ -0,0 +1,1309 @@ +{{ define "block_arrival" }} + + +
+
+
+ Loading propagation data from Xatu... +
+ +
+ Xatu + Data from Xatu +
+
+ + +{{ end }} diff --git a/templates/slot/slot.html b/templates/slot/slot.html index 1eb843339..504f0fefe 100644 --- a/templates/slot/slot.html +++ b/templates/slot/slot.html @@ -138,8 +138,13 @@

Proofs {{ .Block.ExecutionProofsCount }} {{ end }} - {{ if .Block }} + {{ if .XatuEnabled }} + {{ end }} + {{ if .Block }} +