1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Provider construction helpers.
//!
//! The eigensdk `get_provider` builds a bare HTTP provider with no transport
//! middleware, so a JSON-RPC node returning `HTTP 429 Too Many Requests`
//! surfaces straight to the caller as a hard error. In mainnet we share a single
//! upstream account (~250 req/s on QuickNode) across every gateway; the periodic
//! operator-discovery refresh (`query_registered_operator_and_fill_db`) fans out
//! a burst of contract reads, and whichever gateway loses the race for the
//! shared budget eats the 429, fails to populate its operator pool, and reports
//! a pool size of 0 while its peers report the correct count.
//!
//! [`get_provider_with_retry`] mirrors `eigensdk::common::get_provider` (same
//! `SdkProvider` return type) and attaches two transport layers:
//!
//! * A **proactive** [`ThrottleLayer`] that paces outbound requests to a
//! per-gateway share of the shared budget (see [`configured_rps`]). This is
//! the layer that actually keeps a gateway under the upstream ceiling — it
//! throttles every request, not just retries. A single rate limiter is shared
//! process-wide (one process == one gateway), so the four providers built per
//! discovery tick draw from one budget rather than four independent ones.
//! * A **reactive** [`RetryBackoffLayer`] that retries `HTTP 429`/`503` with
//! backoff and honors a provider-supplied backoff hint when present. This is a
//! safety net for residual rate-limit responses (e.g. bursts from other RPC
//! paths that share the account, or an under-provisioned throttle); it does
//! not by itself bound the shared budget, and its compute-unit pacing only
//! engages *after* a retryable error inside the retry loop.
//!
//! Retry is layered outside throttle so a retried attempt also waits for a
//! throttle permit rather than bypassing the rate limit.
use ;
use SdkProvider;
use LazyLock;
use info;
/// Maximum number of rate-limit retries before giving up and surfacing the error.
const MAX_RATE_LIMIT_RETRIES: u32 = 5;
/// Initial backoff in milliseconds applied when no provider backoff hint is present.
const INITIAL_BACKOFF_MS: u64 = 200;
/// Compute-units-per-second budget for the retry layer's post-error pacing.
/// This only affects backoff once a retryable error has occurred; it is not a
/// steady-state request cap (that is the throttle layer's job).
const COMPUTE_UNITS_PER_SECOND: u64 = 4_000;
/// Per-gateway request-rate cap for the production deployment.
///
/// Shared upstream budget (~250 req/s) divided across ~10 prod gateways with
/// headroom for other RPC traffic: `250 * 0.8 / 10 ≈ 20`.
const PROD_RPS: u32 = 20;
/// Per-gateway request-rate cap for non-prod deployments (e.g. `stagef`).
///
/// Same budget across ~2 staging gateways: `250 * 0.8 / 2 ≈ 100`. Local anvil
/// runs also resolve here (default `DEPLOYMENT_ENV`), and 100 req/s is well
/// above what local dev/tests need, so it is effectively a no-op there.
const NON_PROD_RPS: u32 = 100;
/// Build a read-only [`SdkProvider`] with throttle + retry transport layers.
///
/// Replacement for `eigensdk::common::get_provider`, returning `Result` instead
/// of panicking on a bad URL (this runs on every discovery refresh tick, not
/// just at startup, so a transient/misconfigured URL should be a skippable
/// error rather than a panic).
/// Process-wide throttle layer, lazily initialized from [`configured_rps`].
///
/// `ThrottleLayer` owns its rate limiter, so a fresh `ThrottleLayer::new` per
/// provider would give each its own budget. We initialize one limiter for the
/// process so all providers (the four built per discovery tick included) are
/// governed together. `None` means throttling is disabled.
static SHARED_THROTTLE: = new;
/// Hand out a clone of the process-wide throttle layer, or `None` if disabled.
///
/// The clone shares the same underlying `Arc` rate limiter, so every provider
/// enforces the one process-wide budget rather than its own.
/// Resolve the per-gateway requests-per-second cap, or `None` to disable.
///
/// Precedence:
/// 1. `RPC_MAX_REQUESTS_PER_SECOND` override — `0` disables throttling, any
/// other positive value is used verbatim (incident retuning without a code
/// change); a non-numeric value is ignored with a warning.
/// 2. Otherwise derived from `DEPLOYMENT_ENV` (`prod` vs everything else).
/// Map a deployment-env name to its per-gateway requests-per-second cap.
///
/// Fails closed: only env names we explicitly recognize as non-prod get the
/// looser staging budget. Everything else — `prod`, an unset/empty var, case
/// drift (`Prod`), or aliases (`production`, `mainnet`) — resolves to the
/// tightest `PROD_RPS`. Running 5x too hot on a prod misconfig (10×100 blows
/// past the shared ~250/s ceiling exactly when something is already wrong) is
/// the dangerous direction; throttling staging slightly too hard is harmless.