diff --git a/benchmarks/bridge-execution-latency.yml b/benchmarks/bridge-execution-latency.yml index e856cb522..ebaad506a 100644 --- a/benchmarks/bridge-execution-latency.yml +++ b/benchmarks/bridge-execution-latency.yml @@ -52,16 +52,17 @@ abstract: | methodology: - "Route: a self-conserving all-USDC triangle. Solana USDC to Base USDC, Base USDC to Arbitrum USDC, Arbitrum USDC to Solana USDC. Each executing bridge completes the full triangle before the next starts, so inventory returns to its origin and only fees burn." - "Ticket sizes: $3 daily and $30 twice weekly, on fixed UTC schedules. Amounts are deliberately small: this measures settlement reliability, not large-ticket cost." - - "Latency: execution latency is wall-clock from broadcast to the destination credit; end-to-end latency adds the quote round-trip. Histogram buckets 1s, 5s, 10s, 30s, 60s, 120s, 300s, 600s; p50, p90 and p99 via histogram_quantile over 24h." + - "Latency: execution latency is wall-clock from broadcast to the destination credit; end-to-end latency adds the quote round-trip. p50, p90 and p99 are quantile_over_time over the exact observed latencies of the last 24h (a per-execution gauge), not a histogram-bucket interpolation, so the figures are the real values rather than a bucket midpoint." - "Outcome classification: settled (funds received), reverted or refunded (capital returned to source), or errored (never broadcast). Only settled transactions contribute to the latency distribution; reverts and refunds count against the success rate." - "Region: EU-West only at present. The execution node runs a single wallet, so a second origin would race the same inventory; additional origins are not planned for the execution loop." - - "Cohort: Mobula, Relay and LI.FI. deBridge is quote-only in this loop. Providers without a symmetric route on all three legs are excluded so the triangle conserves." + - "Cohort: Mobula, Relay, LI.FI and Near Intents (Near Intents runs its own conserving USDC triangle via the 1Click flow). deBridge is quote-only in this loop. Providers without a symmetric route on all three legs are excluded so the triangle conserves." - "Cost metrics captured per real execution (beyond latency and success): realized output on-chain, execution slippage vs the quote (realized fee minus quote-projected fee), our own on-chain gas paid (approve + deposit, measured as source-chain native balance delta), and the refund rate split from hard-fail rate. These surface the true all-in cost and the gap between a quote and what actually settles." findings: - "{{name:mobula}} settles at {{p50:mobula}} (p50, broadcast to funds received) with a {{success:mobula}} success rate over the last 24 hours." - "{{name:relay}} settles at {{p50:relay}} with a {{success:relay}} success rate." - "{{name:lifi}} settles at {{p50:lifi}} with a {{success:lifi}} success rate." + - "{{name:near-intents}} settles at {{p50:near-intents}} with a {{success:near-intents}} success rate; intent settlement runs longer than lock-mint fills, so its latency sits above the others while staying reliable." faq: - q: "How is this different from the bridge quote and bridge fee benchmarks?" @@ -87,36 +88,49 @@ providers: tag: Aggregator + intent layer formula: "p50 over 24h of on-chain execution latency (broadcast to funds received, ms) for real USDC-triangle transfers executed through Mobula from EU-West, at the selected ticket size and corridor." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="mobula", region="eu-west"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="mobula", region="eu-west"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="mobula", region="eu-west"}[24h]))) - mean: sum(rate(bridge_execution_latency_ms_sum{bridge="mobula", region="eu-west"}[24h])) / clamp_min(sum(rate(bridge_execution_latency_ms_count{bridge="mobula", region="eu-west"}[24h])), 1) + p50: avg(quantile_over_time(0.50, bridge_exec_latency_ms{bridge="mobula", region="eu-west"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_exec_latency_ms{bridge="mobula", region="eu-west"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_exec_latency_ms{bridge="mobula", region="eu-west"}[24h])) + mean: avg(avg_over_time(bridge_exec_latency_ms{bridge="mobula", region="eu-west"}[24h])) success: sum(increase(bridge_success_total{bridge="mobula", region="eu-west"}[24h])) / clamp_min(sum(increase(bridge_success_total{bridge="mobula", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="mobula", region="eu-west"}[24h])), 1) sample_size: sum(increase(bridge_success_total{bridge="mobula", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="mobula", region="eu-west"}[24h])) - series: histogram_quantile(0.50, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="mobula", region="eu-west"}[1h]))) + series: avg_over_time(bridge_exec_latency_ms{bridge="mobula", region="eu-west"}[1h]) - slug: relay name: Relay tag: Cross-chain relay formula: "p50 over 24h of on-chain execution latency (broadcast to funds received, ms) for real USDC-triangle transfers executed through Relay from EU-West, at the selected ticket size and corridor." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="relay", region="eu-west"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="relay", region="eu-west"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="relay", region="eu-west"}[24h]))) - mean: sum(rate(bridge_execution_latency_ms_sum{bridge="relay", region="eu-west"}[24h])) / clamp_min(sum(rate(bridge_execution_latency_ms_count{bridge="relay", region="eu-west"}[24h])), 1) + p50: avg(quantile_over_time(0.50, bridge_exec_latency_ms{bridge="relay", region="eu-west"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_exec_latency_ms{bridge="relay", region="eu-west"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_exec_latency_ms{bridge="relay", region="eu-west"}[24h])) + mean: avg(avg_over_time(bridge_exec_latency_ms{bridge="relay", region="eu-west"}[24h])) success: sum(increase(bridge_success_total{bridge="relay", region="eu-west"}[24h])) / clamp_min(sum(increase(bridge_success_total{bridge="relay", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="relay", region="eu-west"}[24h])), 1) sample_size: sum(increase(bridge_success_total{bridge="relay", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="relay", region="eu-west"}[24h])) - series: histogram_quantile(0.50, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="relay", region="eu-west"}[1h]))) + series: avg_over_time(bridge_exec_latency_ms{bridge="relay", region="eu-west"}[1h]) - slug: lifi name: LI.FI tag: Aggregator formula: "p50 over 24h of on-chain execution latency (broadcast to funds received, ms) for real USDC-triangle transfers executed through LI.FI from EU-West, at the selected ticket size and corridor." queries: - p50: histogram_quantile(0.50, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="lifi", region="eu-west"}[24h]))) - p90: histogram_quantile(0.90, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="lifi", region="eu-west"}[24h]))) - p99: histogram_quantile(0.99, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="lifi", region="eu-west"}[24h]))) - mean: sum(rate(bridge_execution_latency_ms_sum{bridge="lifi", region="eu-west"}[24h])) / clamp_min(sum(rate(bridge_execution_latency_ms_count{bridge="lifi", region="eu-west"}[24h])), 1) + p50: avg(quantile_over_time(0.50, bridge_exec_latency_ms{bridge="lifi", region="eu-west"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_exec_latency_ms{bridge="lifi", region="eu-west"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_exec_latency_ms{bridge="lifi", region="eu-west"}[24h])) + mean: avg(avg_over_time(bridge_exec_latency_ms{bridge="lifi", region="eu-west"}[24h])) success: sum(increase(bridge_success_total{bridge="lifi", region="eu-west"}[24h])) / clamp_min(sum(increase(bridge_success_total{bridge="lifi", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="lifi", region="eu-west"}[24h])), 1) sample_size: sum(increase(bridge_success_total{bridge="lifi", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="lifi", region="eu-west"}[24h])) - series: histogram_quantile(0.50, sum by (le) (rate(bridge_execution_latency_ms_bucket{bridge="lifi", region="eu-west"}[1h]))) + series: avg_over_time(bridge_exec_latency_ms{bridge="lifi", region="eu-west"}[1h]) + + - slug: near-intents + name: Near Intents + tag: Intent layer (NEAR) + formula: "p50 over 24h of exact on-chain execution latency (broadcast to funds received, ms) for real USDC-triangle transfers executed through the Near Intents 1Click flow from EU-West, at the selected ticket size and corridor." + queries: + p50: avg(quantile_over_time(0.50, bridge_exec_latency_ms{bridge="near-intents", region="eu-west"}[24h])) + p90: avg(quantile_over_time(0.90, bridge_exec_latency_ms{bridge="near-intents", region="eu-west"}[24h])) + p99: avg(quantile_over_time(0.99, bridge_exec_latency_ms{bridge="near-intents", region="eu-west"}[24h])) + mean: avg(avg_over_time(bridge_exec_latency_ms{bridge="near-intents", region="eu-west"}[24h])) + success: sum(increase(bridge_success_total{bridge="near-intents", region="eu-west"}[24h])) / clamp_min(sum(increase(bridge_success_total{bridge="near-intents", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="near-intents", region="eu-west"}[24h])), 1) + sample_size: sum(increase(bridge_success_total{bridge="near-intents", region="eu-west"}[24h])) + sum(increase(bridge_reverts_total{bridge="near-intents", region="eu-west"}[24h])) + series: avg_over_time(bridge_exec_latency_ms{bridge="near-intents", region="eu-west"}[1h]) diff --git a/harnesses/bridge-monitor/cmd/monitor/executor.go b/harnesses/bridge-monitor/cmd/monitor/executor.go index 2fd46e3d6..32a4946d6 100644 --- a/harnesses/bridge-monitor/cmd/monitor/executor.go +++ b/harnesses/bridge-monitor/cmd/monitor/executor.go @@ -938,6 +938,11 @@ func (e *Executor) recordExecutionMetrics(result *ExecutionResult) { bridgeQuoteLatency.WithLabelValues(labels...).Observe(float64(result.QuoteLatencyMs)) bridgeExecutionLatency.WithLabelValues(labels...).Observe(float64(result.ExecutionLatencyMs)) bridgeE2ELatency.WithLabelValues(labels...).Observe(float64(result.E2ELatencyMs)) + // Exact latency gauge (only on a real fill): lets the bench read the true + // observed value via quantile_over_time instead of a coarse bucket midpoint. + if result.Success && result.ExecutionLatencyMs > 0 { + bridgeExecLatencyMs.WithLabelValues(labels...).Set(float64(result.ExecutionLatencyMs)) + } // Record success/revert + consecutive-failure streak (used for paging alerts). if result.Success { diff --git a/harnesses/bridge-monitor/cmd/monitor/metrics.go b/harnesses/bridge-monitor/cmd/monitor/metrics.go index b393dd5a6..3d5025fee 100644 --- a/harnesses/bridge-monitor/cmd/monitor/metrics.go +++ b/harnesses/bridge-monitor/cmd/monitor/metrics.go @@ -105,6 +105,18 @@ var ( Help: "On-chain gas we paid (approve + deposit), source-chain native balance delta in USD", }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) + // Exact execution latency as a gauge (last value per corridor). The + // histogram above can only place a value in a bucket, so with a handful of + // samples all landing in the same (5s,10s] bucket histogram_quantile + // interpolates every bridge to the 7.5s midpoint. This gauge lets the bench + // take quantile_over_time / avg_over_time over the REAL observed latencies + // (same pattern as the gauge-backed fee + realized-cost benches), so the + // numbers are exact instead of bucketed. + bridgeExecLatencyMs = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "bridge_exec_latency_ms", + Help: "Exact execution latency (broadcast to funds received) in ms, last value per corridor", + }, []string{"bridge", "from_chain", "to_chain", "from_token", "to_token", "amount_usd", "region", "chain"}) + // Error counter bridgeErrors = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "bridge_errors_total", @@ -253,21 +265,32 @@ func preseedExecutionMetrics(region string) { if region == "" { return } - bridges := []string{"mobula", "relay", "lifi"} amounts := []string{"3", "30"} - for _, route := range GetTriangleRoutes() { - for _, bridge := range bridges { - for _, amt := range amounts { - labels := []string{bridge, route.FromChain, route.ToChain, route.FromToken, route.ToToken, amt, region, route.ToChain} - bridgeSuccess.WithLabelValues(labels...).Add(0) - bridgeReverts.WithLabelValues(labels...).Add(0) - bridgeRefunds.WithLabelValues(labels...).Add(0) - bridgeStuck.WithLabelValues(labels...).Add(0) - // Instantiate the histogram children so their _bucket / _sum / - // _count series exist at 0 before the first Observe. - bridgeExecutionLatency.WithLabelValues(labels...) - bridgeE2ELatency.WithLabelValues(labels...) - bridgeRefundLatency.WithLabelValues(labels...) + // mobula/relay/lifi run the stable triangle; near-intents runs its OWN + // USDC triangle (separate route set), so seed each on the routes it + // actually executes or the labels won't match the real increments. + seedSets := []struct { + bridges []string + routes []TestRoute + }{ + {[]string{"mobula", "relay", "lifi"}, GetTriangleRoutes()}, + {[]string{"near-intents"}, GetNearIntentsTriangle()}, + } + for _, ss := range seedSets { + for _, route := range ss.routes { + for _, bridge := range ss.bridges { + for _, amt := range amounts { + labels := []string{bridge, route.FromChain, route.ToChain, route.FromToken, route.ToToken, amt, region, route.ToChain} + bridgeSuccess.WithLabelValues(labels...).Add(0) + bridgeReverts.WithLabelValues(labels...).Add(0) + bridgeRefunds.WithLabelValues(labels...).Add(0) + bridgeStuck.WithLabelValues(labels...).Add(0) + // Instantiate the histogram children so their _bucket / _sum / + // _count series exist at 0 before the first Observe. + bridgeExecutionLatency.WithLabelValues(labels...) + bridgeE2ELatency.WithLabelValues(labels...) + bridgeRefundLatency.WithLabelValues(labels...) + } } } }