From 2e371cb784a8bb77591b69fd04b428d40ba89e43 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Sun, 12 Jul 2026 16:34:50 +0200 Subject: [PATCH] mev-protect-rpc: chain dimension (ethereum/base/bsc), 4 new gateways, consistency answer page --- ...-protection-rpc-is-the-most-consistent.yml | 35 ++++ benchmarks/mev-protect-rpc.yml | 124 +++++++++++++- .../mev-protect-rpc/cmd/script/metrics.go | 12 +- harnesses/mev-protect-rpc/cmd/script/probe.go | 154 ++++++++++++------ 4 files changed, 259 insertions(+), 66 deletions(-) create mode 100644 answers/which-mev-protection-rpc-is-the-most-consistent.yml diff --git a/answers/which-mev-protection-rpc-is-the-most-consistent.yml b/answers/which-mev-protection-rpc-is-the-most-consistent.yml new file mode 100644 index 00000000..8954f80e --- /dev/null +++ b/answers/which-mev-protection-rpc-is-the-most-consistent.yml @@ -0,0 +1,35 @@ +slug: which-mev-protection-rpc-is-the-most-consistent +question: "Which MEV protection RPC is the most consistent?" +short_answer: | + Consistency means the p99 to p50 gap, not the median. On the live OpenChainBench MEV protection benchmark, Blink (formerly Merkle) holds the flattest tail of the cohort: its p99 sits within roughly 10 percent of its p50, while the fastest median gateway spikes an order of magnitude above its own p50 at the tail. For a wallet that fires a balance refresh, a gas estimate and a simulation on every screen, the tail is what users feel. + +benchmark: mev-protect-rpc + +intro: | + Wallet teams picking a default MEV protection RPC usually compare medians, because medians are what marketing pages publish. But a wallet calls its RPC constantly: every open screen triggers balance refreshes, gas estimates and eth_call simulations. At that call volume the user experiences the p99, not the p50. A gateway that answers in 33 ms most of the time but stalls to 400 ms on one call in a hundred produces a wallet that feels randomly broken several times per session. This page reads the live OpenChainBench mev-protect-rpc benchmark through the consistency lens: which of the public keyless gateways (Flashbots Protect, MEV Blocker, Blink, bloXroute Protect, BlockSec, 48 Club, PancakeSwap MEV Guard) keeps its tail closest to its median. + +methodology: | + Same data as the parent benchmark. Every 60 seconds, from us-east, eu-west and Singapore, the harness sends the 7-method wallet set (chainId, blockNumber, gasPrice, getBalance, call, estimateGas, feeHistory) to each public MEV protection gateway, keyless, one request per 1.5 seconds. The per-tick median across served methods feeds a Prometheus gauge; p50, p90 and p99 are quantile_over_time over 24 hours, averaged across regions. Consistency is read as the ratio between the p99 and p50 columns of the same provider. No transactions are sent; this is the read path a wallet exercises all day. + +limitations: + - "Consistency of the read gateway says nothing about inclusion rates or refund economics, which are write-path properties requiring funded transactions to measure. OpenChainBench does not extrapolate them." + - "A flat tail with a moderate median can coexist with a fast median and a heavy tail. Which profile is better depends on the product: trading bots care about the median, consumer wallets about the tail." + - "Success rate must be read next to both numbers. A gateway can post flat latency while throttling a share of calls; throttled calls do not enter the latency distribution." + +faq: + - q: "Which MEV protection RPC has the flattest latency tail?" + a: "Blink (formerly Merkle) currently posts the flattest p99 to p50 ratio of the measured cohort on Ethereum, with a tail within roughly 10 percent of its median. The live columns on the benchmark page update every minute and the ranking can move; the ratio between the p99 and p50 columns is the number to read." + - q: "Why does consistency matter more than median latency for a wallet?" + a: "Because a wallet fires dozens of RPC calls per session. At 50 calls per session, a 1-in-100 stall hits most sessions at least once. The user does not experience the median; they experience the worst call on the screen they are currently looking at." + - q: "Is the fastest median MEV protect RPC also the most consistent?" + a: "Not currently. The gateway with the best p50 on Ethereum shows a p99 an order of magnitude above its own median, while the most consistent gateway trades a slower median for a nearly flat tail. The benchmark page shows both columns side by side so the trade-off is explicit." + - q: "Does this ranking cover Base and BSC too?" + a: "The parent benchmark measures Ethereum, Base and BSC as separate chain tabs. On Base only one MEV protection gateway is measurable keyless today (Blink); on BSC the cohort includes Blink, bloXroute Protect, 48 Club, PancakeSwap MEV Guard and BlockSec, so the consistency comparison is meaningful there as well." + +related: + - drpc-vs-publicnode-vs-1rpc + - alchemy-vs-quicknode-vs-infura-latency + +seo_title: "Most consistent MEV protection RPC 2026" +seo_description: "Which MEV protection RPC has the flattest latency tail? Blink, MEV Blocker, Flashbots Protect and 4 more compared on p99 vs p50, measured live and keyless." +status: live diff --git a/benchmarks/mev-protect-rpc.yml b/benchmarks/mev-protect-rpc.yml index cd4751a4..1001c5ab 100644 --- a/benchmarks/mev-protect-rpc.yml +++ b/benchmarks/mev-protect-rpc.yml @@ -4,8 +4,8 @@ slug: mev-protect-rpc number: "074" title: Fastest MEV protection RPC, live wallet latency and method coverage seo_title: "Best MEV protection RPC 2026" -seo_description: "{{best_name}} leads MEV protection RPC wallet latency at {{best_p50}} (p50, 24h). Flashbots Protect, MEV Blocker, Blink probed on the wallet method set from 3 regions, keyless." -subtitle: "Median latency of the JSON-RPC method set wallets call constantly (balance, gas, call simulation), measured against public MEV protection gateways from three regions. Read-path hygiene only: inclusion rates and refunds are the write path and cannot be measured without sending transactions." +seo_description: "{{best_name}} leads MEV protection RPC wallet latency at {{best_p50}} (p50, 24h). Flashbots, MEV Blocker, Blink, bloXroute, BlockSec, 48 Club probed keyless on Ethereum, Base and BSC." +subtitle: "Median latency of the JSON-RPC method set wallets call constantly (balance, gas, call simulation), measured against public MEV protection gateways on Ethereum, Base and BSC from three regions. Read-path hygiene only: inclusion rates and refunds are the write path and cannot be measured without sending transactions." category: RPCs status: live metric: Wallet call latency @@ -40,11 +40,13 @@ abstract: | than a latency penalty. No transactions are sent. methodology: - - "Providers measured: Flashbots Protect (rpc.flashbots.net), MEV Blocker (rpc.mevblocker.io), Blink, formerly Merkle (ethereum.blinklabs.xyz). SecureRPC probed dead 2026-07-10; the legacy merkle.io hosts alias Blink behind a stricter rate limit and are not probed." + - "Providers measured, one row per (provider, chain), all keyless and live-verified 2026-07-12. Ethereum: Flashbots Protect (rpc.flashbots.net), MEV Blocker (rpc.mevblocker.io), Blink formerly Merkle (ethereum.blinklabs.xyz), bloXroute Protect (eth-protect.rpc.blxrbdn.com), BlockSec Anti-MEV (eth.rpc.blocksec.com). Base: Blink (base.merkle.io) alone, no other MEV-protect gateway exposes a keyless Base endpoint today. BSC: Blink (bsc.merkle.io), bloXroute Protect (bsc.rpc.blxrbdn.com), 48 Club Privacy RPC (rpc.48.club), PancakeSwap MEV Guard (bscrpc.pancakeswap.finance, powered by 48 Club), BlockSec (bsc.rpc.blocksec.com)." + - "Exclusions: SecureRPC probed dead 2026-07-10. Alchemy MEV Protect and GetBlock protected endpoints are key-gated. base.rpc.blxrbdn.com is live but not documented as Protect, so it is not listed as an MEV gateway. Blink's Ethereum row uses the blinklabs.xyz host because the legacy eth.merkle.io alias rate-limits harder; on Base and BSC the merkle.io hosts are Blink's only keyless surface (base/bsc.blinklabs.xyz do not resolve as of 2026-07-12)." + - "Chain dimension: the tabs pin every query to one chain. The All chains headline averages each provider over the chains it actually serves, the same convention as the rpc-capabilities cluster; the Chains covered panel shows the multi-chain footprint explicitly." - "Method set: eth_chainId, eth_blockNumber, eth_gasPrice, eth_getBalance, eth_call (USDC balanceOf), eth_estimateGas, eth_feeHistory. One request per method per tick, 1.5s apart, rotating request ids against body-keyed edge caches." - - "Cadence: every 60 seconds per region (us-east, eu-west, sgp), 7 requests per provider per tick. Deliberately polite: these gateways rate-ban aggressive callers." + - "Cadence: every 60 seconds per region (us-east, eu-west, sgp), 7 requests per gateway per tick, rows probed in parallel so each gateway still sees one request per 1.5s. Deliberately polite: these gateways rate-ban aggressive callers." - "Headline: median latency across the methods the provider served that tick, aggregated over 24h with quantile_over_time. A rejected method does not poison the latency; it lowers the coverage panel instead." - - "Coverage: mev_rpc_methods_supported counts the wallet methods served on the last tick (max 7). Flashbots rejects eth_call on the public endpoint; Blink blocks full-node reads like eth_getBlockByNumber, which keeps it off the general RPC benches but not off this one." + - "Coverage: mev_rpc_methods_supported counts the wallet methods served on the last tick (max 7) per chain. Flashbots rejects eth_call on the public endpoint; Blink blocks full-node reads like eth_getBlockByNumber, which keeps it off the general RPC benches but not off this one." - "Out of scope, disclosed: inclusion rate, refund economics and sandwich protection efficacy are the write path and require funded transactions. See the arXiv study Private MEV Protection RPCs (2505.19708) for a one-off execution-quality comparison." - "Success rate counts transport failures only (timeouts, throttling, network); a method a gateway refuses by policy is a coverage gap shown in the coverage panel, not an outage. Failures increment mev_rpc_call_total{result}; the gauge keeps its last value so the chart shows the outage in the success column rather than a fake zero." @@ -53,6 +55,10 @@ findings: - "{{name:mevblocker}} sits at {{p50:mevblocker}} (p50, 24h) and serves the full 7-method wallet set. MEV Blocker was acquired by Consensys in January 2026; its gateway proxies reads with the lowest overhead of the cohort." - "{{name:flashbots}} clocks {{p50:flashbots}} (p50, 24h) but rejects eth_call on the public endpoint, so wallets relying on it for simulation fall back to another provider for that path." - "{{name:blinklabs}} returns {{p50:blinklabs}} (p50, 24h) across the full wallet set. Blink, formerly Merkle, is a wallet-facing MEV proxy by design: it serves everything a wallet needs while blocking full-node reads." + - "{{name:blinklabs}} is the only gateway in the cohort measurable on all three chains: Ethereum, Base and BSC. Flashbots Protect and MEV Blocker are Ethereum-only by design, bloXroute Protect and BlockSec cover Ethereum plus BSC, and on Base Blink is currently the only keyless MEV-protect endpoint in existence." + - "{{name:bloxroute}} answers at {{p50:bloxroute}} (p50, 24h) across Ethereum and BSC. The Protect RPC is the free tier of a paid MEV infrastructure stack, and the gateway inherits the low-latency backbone bloXroute sells to searchers." + - "{{name:blocksec}} clocks {{p50:blocksec}} (p50, 24h) on Ethereum and BSC. The Anti-MEV RPC is a security-vendor product: same private-routing idea, marketed to users of the Phalcon toolchain." + - "On BSC the local cohort is the real story: {{name:48club}} ({{p50:48club}}) and {{name:pancakeswap}} ({{p50:pancakeswap}}) share the same 48 Club infrastructure behind different edges, and both compete against Blink and bloXroute for the default slot in BSC wallets." - "The p50 to p99 gap is the number wallet teams should read: a protect RPC that stalls on one refresh per hundred makes the whole wallet feel unreliable, whatever its median." source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/mev-protect-rpc @@ -75,6 +81,11 @@ faq: rank_matrix_query: avg by (provider, region) (quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds[24h])) dimensions: + chain: + - { value: all, label: All chains } + - { value: ethereum, label: Ethereum } + - { value: base, label: Base } + - { value: bsc, label: BNB Chain } region: - { value: all, label: All regions } - { value: us-east, label: US-East } @@ -82,6 +93,13 @@ dimensions: - { value: sgp, label: Singapore } metric_panels: + - id: chains + label: Chains covered + metric: count by (provider) (avg by (provider, chain) (mev_rpc_methods_supported{benchmark="mev-protect-rpc"})) + label_key: provider + unit: count + higher_is_better: true + description: "Chains where the provider exposes a keyless MEV-protect gateway we can probe (Ethereum, Base, BSC). Multi-chain wallets need one provider across every network they ship, not one per chain." - id: coverage label: Method coverage metric: avg(mev_rpc_methods_supported{benchmark="mev-protect-rpc"}) @@ -139,8 +157,8 @@ providers: - slug: blinklabs name: Blink - tag: Formerly Merkle, wallet MEV proxy, full wallet set, powers Ledger - formula: "Median latency across the wallet methods served per 60s tick against ethereum.blinklabs.xyz, aggregated over 24h via quantile_over_time; 3-region average." + tag: Formerly Merkle, only gateway live on Ethereum, Base and BSC, powers Ledger + formula: "Median latency across the wallet methods served per 60s tick against ethereum.blinklabs.xyz, base.merkle.io and bsc.merkle.io, aggregated over 24h via quantile_over_time; 3-region average over the chains served." queries: p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="blinklabs"}[24h]) p90: quantile_over_time(0.90, mev_rpc_wallet_latency_milliseconds{provider="blinklabs"}[24h]) @@ -159,3 +177,95 @@ providers: - region: ap-southeast p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="blinklabs", region="sgp"}[24h]) series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="blinklabs", region="sgp"}[1h]) + + - slug: bloxroute + name: bloXroute Protect + tag: Free protect tier of the bloXroute MEV stack, Ethereum + BSC + formula: "Median latency across the wallet methods served per 60s tick against eth-protect.rpc.blxrbdn.com and bsc.rpc.blxrbdn.com, aggregated over 24h via quantile_over_time; 3-region average over the chains served." + queries: + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="bloxroute"}[24h]) + p90: quantile_over_time(0.90, mev_rpc_wallet_latency_milliseconds{provider="bloxroute"}[24h]) + p99: quantile_over_time(0.99, mev_rpc_wallet_latency_milliseconds{provider="bloxroute"}[24h]) + mean: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="bloxroute"}[24h]) + success: sum(increase(mev_rpc_call_total{provider="bloxroute",result="ok"}[24h])) / clamp_min(sum(increase(mev_rpc_call_total{provider="bloxroute",result!~"blocked|method_not_found"}[24h])), 1) + sample_size: sum(increase(mev_rpc_call_total{provider="bloxroute"}[24h])) + series: avg(avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="bloxroute"}[1h])) + regions: + - region: us-east + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="bloxroute", region="us-east"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="bloxroute", region="us-east"}[1h]) + - region: eu-west + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="bloxroute", region="eu-west"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="bloxroute", region="eu-west"}[1h]) + - region: ap-southeast + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="bloxroute", region="sgp"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="bloxroute", region="sgp"}[1h]) + + - slug: blocksec + name: BlockSec Anti-MEV + tag: Security-vendor private routing, Ethereum + BSC + formula: "Median latency across the wallet methods served per 60s tick against eth.rpc.blocksec.com and bsc.rpc.blocksec.com, aggregated over 24h via quantile_over_time; 3-region average over the chains served." + queries: + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="blocksec"}[24h]) + p90: quantile_over_time(0.90, mev_rpc_wallet_latency_milliseconds{provider="blocksec"}[24h]) + p99: quantile_over_time(0.99, mev_rpc_wallet_latency_milliseconds{provider="blocksec"}[24h]) + mean: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="blocksec"}[24h]) + success: sum(increase(mev_rpc_call_total{provider="blocksec",result="ok"}[24h])) / clamp_min(sum(increase(mev_rpc_call_total{provider="blocksec",result!~"blocked|method_not_found"}[24h])), 1) + sample_size: sum(increase(mev_rpc_call_total{provider="blocksec"}[24h])) + series: avg(avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="blocksec"}[1h])) + regions: + - region: us-east + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="blocksec", region="us-east"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="blocksec", region="us-east"}[1h]) + - region: eu-west + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="blocksec", region="eu-west"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="blocksec", region="eu-west"}[1h]) + - region: ap-southeast + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="blocksec", region="sgp"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="blocksec", region="sgp"}[1h]) + + - slug: 48club + name: 48 Club Privacy RPC + tag: BSC validator collective, Puissant successor + formula: "Median latency across the wallet methods served per 60s tick against rpc.48.club, aggregated over 24h via quantile_over_time; 3-region average over the chains served." + queries: + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="48club"}[24h]) + p90: quantile_over_time(0.90, mev_rpc_wallet_latency_milliseconds{provider="48club"}[24h]) + p99: quantile_over_time(0.99, mev_rpc_wallet_latency_milliseconds{provider="48club"}[24h]) + mean: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="48club"}[24h]) + success: sum(increase(mev_rpc_call_total{provider="48club",result="ok"}[24h])) / clamp_min(sum(increase(mev_rpc_call_total{provider="48club",result!~"blocked|method_not_found"}[24h])), 1) + sample_size: sum(increase(mev_rpc_call_total{provider="48club"}[24h])) + series: avg(avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="48club"}[1h])) + regions: + - region: us-east + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="48club", region="us-east"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="48club", region="us-east"}[1h]) + - region: eu-west + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="48club", region="eu-west"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="48club", region="eu-west"}[1h]) + - region: ap-southeast + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="48club", region="sgp"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="48club", region="sgp"}[1h]) + + - slug: pancakeswap + name: PancakeSwap MEV Guard + tag: BSC, powered by 48 Club behind PancakeSwap's edge + formula: "Median latency across the wallet methods served per 60s tick against bscrpc.pancakeswap.finance, aggregated over 24h via quantile_over_time; 3-region average over the chains served." + queries: + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="pancakeswap"}[24h]) + p90: quantile_over_time(0.90, mev_rpc_wallet_latency_milliseconds{provider="pancakeswap"}[24h]) + p99: quantile_over_time(0.99, mev_rpc_wallet_latency_milliseconds{provider="pancakeswap"}[24h]) + mean: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="pancakeswap"}[24h]) + success: sum(increase(mev_rpc_call_total{provider="pancakeswap",result="ok"}[24h])) / clamp_min(sum(increase(mev_rpc_call_total{provider="pancakeswap",result!~"blocked|method_not_found"}[24h])), 1) + sample_size: sum(increase(mev_rpc_call_total{provider="pancakeswap"}[24h])) + series: avg(avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="pancakeswap"}[1h])) + regions: + - region: us-east + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="pancakeswap", region="us-east"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="pancakeswap", region="us-east"}[1h]) + - region: eu-west + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="pancakeswap", region="eu-west"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="pancakeswap", region="eu-west"}[1h]) + - region: ap-southeast + p50: quantile_over_time(0.50, mev_rpc_wallet_latency_milliseconds{provider="pancakeswap", region="sgp"}[24h]) + series: avg_over_time(mev_rpc_wallet_latency_milliseconds{provider="pancakeswap", region="sgp"}[1h]) diff --git a/harnesses/mev-protect-rpc/cmd/script/metrics.go b/harnesses/mev-protect-rpc/cmd/script/metrics.go index 3655633f..d6f78792 100644 --- a/harnesses/mev-protect-rpc/cmd/script/metrics.go +++ b/harnesses/mev-protect-rpc/cmd/script/metrics.go @@ -12,31 +12,31 @@ var ( mevWalletLatency = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "mev_rpc_wallet_latency_milliseconds", Help: "Median latency across the wallet method set served this tick, per MEV-protect provider.", - }, []string{"provider", "region"}) + }, []string{"provider", "chain", "region"}) mevWalletLatencyHist = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "mev_rpc_wallet_latency_milliseconds_histogram", Help: "Distribution of per-tick median wallet latency.", Buckets: prometheus.ExponentialBuckets(25, 2, 10), - }, []string{"provider", "region"}) + }, []string{"provider", "chain", "region"}) mevMethodLatency = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "mev_rpc_method_latency_milliseconds", Help: "Latency of the last successful call per method.", - }, []string{"provider", "method", "region"}) + }, []string{"provider", "chain", "method", "region"}) mevMethodOK = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "mev_rpc_method_ok", Help: "1 when the method succeeded on the last tick, 0 otherwise.", - }, []string{"provider", "method", "region"}) + }, []string{"provider", "chain", "method", "region"}) mevMethodsSupported = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "mev_rpc_methods_supported", Help: "Count of wallet methods served on the last tick (max 7).", - }, []string{"provider", "region"}) + }, []string{"provider", "chain", "region"}) mevCallTotal = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "mev_rpc_call_total", Help: "Probe outcomes per provider/method.", - }, []string{"provider", "method", "region", "result"}) + }, []string{"provider", "chain", "method", "region", "result"}) ) diff --git a/harnesses/mev-protect-rpc/cmd/script/probe.go b/harnesses/mev-protect-rpc/cmd/script/probe.go index 0139b429..25920a4d 100644 --- a/harnesses/mev-protect-rpc/cmd/script/probe.go +++ b/harnesses/mev-protect-rpc/cmd/script/probe.go @@ -8,46 +8,74 @@ import ( "io" "net/http" "sort" + "sync" "time" ) -// Providers: public no-key MEV-protection gateways on Ethereum mainnet. -// SecureRPC (Manifold) probed dead 2026-07-10; the legacy merkle.io -// hosts alias Blink's gateway behind a far more aggressive Cloudflare -// rate limit, so Blink is probed on the blinklabs.xyz host only. +// Providers: public no-key MEV-protection gateways, one row per +// (provider, chain). Cohort verified live 2026-07-12: +// - SecureRPC (Manifold) probed dead 2026-07-10. +// - Blink (ex Merkle): blinklabs.xyz host on Ethereum (the legacy +// eth.merkle.io alias rate-limits harder); on Base and BSC the +// merkle.io hosts are the only keyless surface Blink exposes +// (base/bsc.blinklabs.xyz do not resolve as of 2026-07-12). +// - Flashbots Protect and MEV Blocker are Ethereum-only by design +// (base.rpc.flashbots.net NXDOMAIN), which the coverage panel +// surfaces rather than hides. +// - bloXroute Protect documents ETH + BSC only; base.rpc.blxrbdn.com +// is plain RPC without the Protect path, so it is not listed. +// - PancakeSwap MEV Guard is powered by 48 Club; both rows are kept +// because wallets see two different gateways with different edges. var providers = []struct { - Slug string - URL string + Slug string + Chain string + URL string }{ - {Slug: "flashbots", URL: "https://rpc.flashbots.net"}, - {Slug: "mevblocker", URL: "https://rpc.mevblocker.io"}, - {Slug: "blinklabs", URL: "https://ethereum.blinklabs.xyz"}, + {Slug: "flashbots", Chain: "ethereum", URL: "https://rpc.flashbots.net"}, + {Slug: "mevblocker", Chain: "ethereum", URL: "https://rpc.mevblocker.io"}, + {Slug: "blinklabs", Chain: "ethereum", URL: "https://ethereum.blinklabs.xyz"}, + {Slug: "bloxroute", Chain: "ethereum", URL: "https://eth-protect.rpc.blxrbdn.com"}, + {Slug: "blocksec", Chain: "ethereum", URL: "https://eth.rpc.blocksec.com"}, + {Slug: "blinklabs", Chain: "base", URL: "https://base.merkle.io"}, + {Slug: "blinklabs", Chain: "bsc", URL: "https://bsc.merkle.io"}, + {Slug: "bloxroute", Chain: "bsc", URL: "https://bsc.rpc.blxrbdn.com"}, + {Slug: "48club", Chain: "bsc", URL: "https://rpc.48.club"}, + {Slug: "pancakeswap", Chain: "bsc", URL: "https://bscrpc.pancakeswap.finance"}, + {Slug: "blocksec", Chain: "bsc", URL: "https://bsc.rpc.blocksec.com"}, +} + +// usdcByChain feeds the eth_call probe (balanceOf) with each chain's +// canonical USDC deployment so the simulation path exercises a real +// contract everywhere. +var usdcByChain = map[string]string{ + "ethereum": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "base": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "bsc": "0x8AC76a51cC950d9822D68b83fE1Ad97B32Cd580d", } // walletMethods is the read set wallets fire constantly (balance // refresh, gas estimation, simulation). One request per method per -// provider per tick: 7 req/min/provider/region, polite for gateways +// provider row per tick: 7 req/min/gateway/region, polite for gateways // that rate-ban aggressive callers. var walletMethods = []struct { Name string - Params func() []any + Params func(chain string) []any }{ - {"eth_chainId", func() []any { return []any{} }}, - {"eth_blockNumber", func() []any { return []any{} }}, - {"eth_gasPrice", func() []any { return []any{} }}, - {"eth_getBalance", func() []any { return []any{probeAddress, "latest"} }}, - {"eth_call", func() []any { - return []any{map[string]string{"to": usdcContract, "data": balanceOfData}, "latest"} + {"eth_chainId", func(string) []any { return []any{} }}, + {"eth_blockNumber", func(string) []any { return []any{} }}, + {"eth_gasPrice", func(string) []any { return []any{} }}, + {"eth_getBalance", func(string) []any { return []any{probeAddress, "latest"} }}, + {"eth_call", func(chain string) []any { + return []any{map[string]string{"to": usdcByChain[chain], "data": balanceOfData}, "latest"} }}, - {"eth_estimateGas", func() []any { + {"eth_estimateGas", func(string) []any { return []any{map[string]string{"from": probeAddress, "to": probeAddress, "value": "0x1"}} }}, - {"eth_feeHistory", func() []any { return []any{"0x5", "latest", []int{50}} }}, + {"eth_feeHistory", func(string) []any { return []any{"0x5", "latest", []int{50}} }}, } const ( probeAddress = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" - usdcContract = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" balanceOfData = "0x70a08231000000000000000000000000d8dA6BF26964aF9D7eEd9e03E53415D37aA96045" tickInterval = 60 * time.Second @@ -55,6 +83,12 @@ const ( probeTimeout = 10 * time.Second ) +type providerRow = struct { + Slug string + Chain string + URL string +} + type rpcEnvelope struct { Result json.RawMessage `json:"result"` Error *struct { @@ -78,44 +112,58 @@ func runProbeLoop(ctx context.Context) { } } +// tick probes every (provider, chain) row in parallel; within a row the +// methods stay sequential with methodGap between them, so each gateway +// still sees at most one request per 1.5s while the whole sweep fits +// inside the 60s tick regardless of cohort size. func tick(ctx context.Context, client *http.Client) { + var wg sync.WaitGroup for _, p := range providers { - latencies := make([]float64, 0, len(walletMethods)) - supported := 0 - for _, m := range walletMethods { - select { - case <-ctx.Done(): - return - default: - } - lat, result := call(ctx, client, p.URL, m.Name, m.Params()) - mevCallTotal.WithLabelValues(p.Slug, m.Name, currentRegion, result).Inc() - if result == "ok" { - supported++ - latencies = append(latencies, lat) - mevMethodLatency.WithLabelValues(p.Slug, m.Name, currentRegion).Set(lat) - mevMethodOK.WithLabelValues(p.Slug, m.Name, currentRegion).Set(1) - } else { - mevMethodOK.WithLabelValues(p.Slug, m.Name, currentRegion).Set(0) - } - time.Sleep(methodGap) + wg.Add(1) + go func(p providerRow) { + defer wg.Done() + probeRow(ctx, client, p) + }(p) + } + wg.Wait() +} + +func probeRow(ctx context.Context, client *http.Client, p providerRow) { + latencies := make([]float64, 0, len(walletMethods)) + supported := 0 + for _, m := range walletMethods { + select { + case <-ctx.Done(): + return + default: + } + lat, result := call(ctx, client, p.URL, m.Name, m.Params(p.Chain)) + mevCallTotal.WithLabelValues(p.Slug, p.Chain, m.Name, currentRegion, result).Inc() + if result == "ok" { + supported++ + latencies = append(latencies, lat) + mevMethodLatency.WithLabelValues(p.Slug, p.Chain, m.Name, currentRegion).Set(lat) + mevMethodOK.WithLabelValues(p.Slug, p.Chain, m.Name, currentRegion).Set(1) + } else { + mevMethodOK.WithLabelValues(p.Slug, p.Chain, m.Name, currentRegion).Set(0) } - mevMethodsSupported.WithLabelValues(p.Slug, currentRegion).Set(float64(supported)) - if len(latencies) > 0 { - sort.Float64s(latencies) - median := latencies[len(latencies)/2] - if len(latencies)%2 == 0 { - median = (latencies[len(latencies)/2-1] + latencies[len(latencies)/2]) / 2 - } - // Median across the methods the provider actually serves, - // so coverage gaps (flashbots rejects eth_call) do not - // poison the latency figure; coverage is its own metric. - mevWalletLatency.WithLabelValues(p.Slug, currentRegion).Set(median) - mevWalletLatencyHist.WithLabelValues(p.Slug, currentRegion).Observe(median) + time.Sleep(methodGap) + } + mevMethodsSupported.WithLabelValues(p.Slug, p.Chain, currentRegion).Set(float64(supported)) + if len(latencies) > 0 { + sort.Float64s(latencies) + median := latencies[len(latencies)/2] + if len(latencies)%2 == 0 { + median = (latencies[len(latencies)/2-1] + latencies[len(latencies)/2]) / 2 } - fmt.Printf("[mev-protect][%s][%s] supported=%d/%d median=%s\n", - p.Slug, currentRegion, supported, len(walletMethods), fmtMedian(latencies)) + // Median across the methods the provider actually serves, + // so coverage gaps (flashbots rejects eth_call) do not + // poison the latency figure; coverage is its own metric. + mevWalletLatency.WithLabelValues(p.Slug, p.Chain, currentRegion).Set(median) + mevWalletLatencyHist.WithLabelValues(p.Slug, p.Chain, currentRegion).Observe(median) } + fmt.Printf("[mev-protect][%s/%s][%s] supported=%d/%d median=%s\n", + p.Slug, p.Chain, currentRegion, supported, len(walletMethods), fmtMedian(latencies)) } func fmtMedian(l []float64) string {