1use crate::{
12 AccountProof, BalSource, Header, Result, SourceError, SourcedBlock, StateSource, StorageProof,
13};
14use alloy_primitives::{Address, Bytes, B256, U256};
15use async_trait::async_trait;
16use bal_codec::BlockAccessList;
17use serde::Deserialize;
18use serde_json::{json, Value};
19
20pub const BAL_HASH_FIELD: &str = "blockAccessListHash";
22pub const BAL_METHOD: &str = "eth_getBlockAccessList";
24const RETRIES: u32 = 4;
27
28pub const MAX_BODY_BYTES: u64 = 64 * 1024 * 1024;
30
31pub struct JsonRpcSource {
34 url: String,
35 client: reqwest::Client,
36}
37
38#[derive(Deserialize)]
39struct RpcResponse {
40 result: Option<Value>,
41 error: Option<RpcError>,
42}
43
44#[derive(Deserialize)]
45struct RpcError {
46 code: i64,
47 message: String,
48}
49
50impl JsonRpcSource {
51 pub fn new(url: impl Into<String>) -> Self {
55 let client = Self::hardened_client().unwrap_or_default();
59 Self {
60 url: url.into(),
61 client,
62 }
63 }
64
65 fn hardened_client() -> reqwest::Result<reqwest::Client> {
66 reqwest::Client::builder()
67 .timeout(std::time::Duration::from_secs(30))
68 .redirect(reqwest::redirect::Policy::none())
69 .build()
70 }
71
72 pub fn try_new(url: impl Into<String>) -> Result<Self> {
74 let client = Self::hardened_client()
75 .map_err(|e| SourceError::Transport(format!("http client: {e}")))?;
76 Ok(Self {
77 url: url.into(),
78 client,
79 })
80 }
81
82 pub async fn call(&self, method: &str, params: Value) -> Result<Value> {
87 let mut delay = std::time::Duration::from_millis(400);
88 for attempt in 0..RETRIES {
89 match self.call_once(method, params.clone()).await {
90 Err(SourceError::Transport(e)) if attempt + 1 < RETRIES => {
91 tracing::debug!(method, attempt, %e, "transport error; retrying");
92 tokio::time::sleep(delay).await;
93 delay *= 2;
94 }
95 other => return other,
96 }
97 }
98 unreachable!("the loop returns on its last attempt")
99 }
100
101 async fn call_once(&self, method: &str, params: Value) -> Result<Value> {
102 let body = json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params});
103 let http = self
104 .client
105 .post(&self.url)
106 .json(&body)
107 .send()
108 .await
109 .map_err(|e| SourceError::Transport(e.to_string()))?;
110 let status = http.status();
112 if !status.is_success() {
113 return Err(SourceError::Transport(format!(
114 "HTTP {} from {} for {method}",
115 status.as_u16(),
116 self.url
117 )));
118 }
119 if let Some(len) = http.content_length() {
123 if len > MAX_BODY_BYTES {
124 return Err(SourceError::Transport(format!(
125 "{method}: response of {len} bytes exceeds the {MAX_BODY_BYTES}-byte limit"
126 )));
127 }
128 }
129 let mut http = http;
130 let mut body: Vec<u8> = Vec::new();
131 while let Some(chunk) = http
132 .chunk()
133 .await
134 .map_err(|e| SourceError::Transport(format!("{method}: {e}")))?
135 {
136 if body.len() + chunk.len() > MAX_BODY_BYTES as usize {
137 return Err(SourceError::Transport(format!(
138 "{method}: response exceeds the {MAX_BODY_BYTES}-byte limit"
139 )));
140 }
141 body.extend_from_slice(&chunk);
142 }
143 let resp: RpcResponse = serde_json::from_slice(&body)
144 .map_err(|e| SourceError::Malformed(format!("{method}: {e}")))?;
145 if let Some(e) = resp.error {
146 return Err(SourceError::Rpc {
147 code: e.code,
148 message: e.message,
149 });
150 }
151 Ok(resp.result.unwrap_or(Value::Null))
152 }
153
154 async fn raw_block(&self, tag: Value) -> Result<Value> {
155 let v = self
156 .call("eth_getBlockByNumber", json!([tag, false]))
157 .await?;
158 if v.is_null() {
159 return Err(SourceError::BlockNotFound(match &tag {
160 Value::String(s) => parse_hex_u64(s).unwrap_or(u64::MAX),
161 _ => u64::MAX,
162 }));
163 }
164 Ok(v)
165 }
166
167 pub async fn bal(&self, number: u64) -> Result<BlockAccessList> {
169 let v = self
170 .call(BAL_METHOD, json!([format!("{number:#x}")]))
171 .await?;
172 if v.is_null() {
173 return Err(SourceError::NoBal(number));
174 }
175 Ok(BlockAccessList::from_rpc_json(&v)?)
176 }
177
178 async fn probe_block(&self, tag: Value) -> BalProbe {
181 let v = match self.raw_block(tag).await {
182 Ok(v) => v,
183 Err(e) => return BalProbe::Error(e.to_string()),
184 };
185 let header = match parse_header(&v) {
186 Ok(h) => h,
187 Err(e) => return BalProbe::Error(e.to_string()),
188 };
189 let bal = match self.bal(header.number).await {
190 Ok(b) => b,
191 Err(SourceError::NoBal(n)) => return BalProbe::Missing(n),
192 Err(e) => return BalProbe::Error(e.to_string()),
193 };
194 let computed = bal.hash();
195 match header.block_access_list_hash {
196 None => BalProbe::NoHashInHeader {
197 block: header.number,
198 accounts: bal.accounts.len(),
199 },
200 Some(expected) if expected == computed => BalProbe::Verified {
201 block: header.number,
202 accounts: bal.accounts.len(),
203 hash: computed,
204 },
205 Some(expected) => BalProbe::Mismatch {
206 block: header.number,
207 computed,
208 expected,
209 },
210 }
211 }
212
213 pub async fn probe(&self, old_block_age: u64) -> Result<ProbeReport> {
216 let head_v = self.raw_block(json!("latest")).await?;
217 let head = parse_header(&head_v)?;
218 let old_number = head.number.saturating_sub(old_block_age).max(1);
219 let chain_id = self
220 .call("eth_chainId", json!([]))
221 .await
222 .ok()
223 .and_then(|v| v.as_str().and_then(|s| parse_hex_u64(s).ok()));
224 let client_version = self
225 .call("web3_clientVersion", json!([]))
226 .await
227 .ok()
228 .and_then(|v| v.as_str().map(String::from));
229 let head_fields = head_v
230 .as_object()
231 .map(|o| o.keys().cloned().collect())
232 .unwrap_or_default();
233 let head_probe = self.probe_block(json!(format!("{:#x}", head.number))).await;
234 let old_probe = self.probe_block(json!(format!("{old_number:#x}"))).await;
235 let earliest_probe = self.probe_block(json!("0x1")).await;
236 let proof_window = self.measure_proof_window(head.number).await;
237
238 Ok(ProbeReport {
239 client_version,
240 chain_id,
241 head: head.number,
242 head_fields,
243 head_probe,
244 old_probe,
245 earliest_probe,
246 proof_window,
247 })
248 }
249
250 pub async fn measure_proof_window(&self, head: u64) -> std::result::Result<u64, String> {
254 let mut ok: Option<u64> = None;
255 for k in [0u64, 1, 2, 4, 8, 16, 32, 64, 128, 256, 1024, 4096] {
256 if k > head {
257 break;
258 }
259 let r = self
260 .call(
261 "eth_getProof",
262 json!([
263 Address::ZERO,
264 Vec::<B256>::new(),
265 format!("{:#x}", head - k)
266 ]),
267 )
268 .await;
269 match r {
270 Ok(_) => ok = Some(k),
271 Err(e) => {
272 if ok.is_none() {
273 return Err(e.to_string());
274 }
275 break;
276 }
277 }
278 }
279 Ok(ok.unwrap_or(0))
280 }
281}
282
283#[derive(Debug, Clone)]
285pub enum BalProbe {
286 Verified {
288 block: u64,
290 accounts: usize,
292 hash: B256,
294 },
295 NoHashInHeader {
297 block: u64,
299 accounts: usize,
301 },
302 Mismatch {
304 block: u64,
306 computed: B256,
308 expected: B256,
310 },
311 Missing(u64),
313 Error(String),
315}
316
317#[derive(Debug, Clone)]
319pub struct ProbeReport {
320 pub client_version: Option<String>,
322 pub chain_id: Option<u64>,
324 pub head: u64,
326 pub head_fields: Vec<String>,
328 pub head_probe: BalProbe,
330 pub old_probe: BalProbe,
332 pub earliest_probe: BalProbe,
334 pub proof_window: std::result::Result<u64, String>,
337}
338
339fn expect_number(h: Header, requested: u64) -> Result<Header> {
342 if h.number != requested {
343 return Err(SourceError::Malformed(format!(
344 "asked for block {requested}, node answered with block {}",
345 h.number
346 )));
347 }
348 Ok(h)
349}
350
351fn parse_hex_u64(s: &str) -> Result<u64> {
352 let s = s.strip_prefix("0x").unwrap_or(s);
353 u64::from_str_radix(s, 16).map_err(|e| SourceError::Malformed(format!("u64 {s}: {e}")))
354}
355
356fn parse_b256(v: &Value, name: &str) -> Result<B256> {
357 let s = v
358 .as_str()
359 .ok_or_else(|| SourceError::Malformed(format!("{name}: not a string")))?;
360 s.parse::<B256>()
361 .map_err(|e| SourceError::Malformed(format!("{name}: {e}")))
362}
363
364fn field<'a>(v: &'a Value, name: &str) -> Result<&'a Value> {
365 v.get(name)
366 .ok_or_else(|| SourceError::Malformed(format!("missing field {name}")))
367}
368
369fn parse_header(v: &Value) -> Result<Header> {
370 let num = field(v, "number")?
371 .as_str()
372 .ok_or_else(|| SourceError::Malformed("number".into()))?;
373 let ts = field(v, "timestamp")?
374 .as_str()
375 .ok_or_else(|| SourceError::Malformed("timestamp".into()))?;
376 let bal_hash = v
377 .get(BAL_HASH_FIELD)
378 .filter(|x| !x.is_null())
379 .map(|x| parse_b256(x, BAL_HASH_FIELD))
380 .transpose()?;
381 Ok(Header {
382 number: parse_hex_u64(num)?,
383 hash: parse_b256(field(v, "hash")?, "hash")?,
384 parent_hash: parse_b256(field(v, "parentHash")?, "parentHash")?,
385 state_root: parse_b256(field(v, "stateRoot")?, "stateRoot")?,
386 timestamp: parse_hex_u64(ts)?,
387 block_access_list_hash: bal_hash,
388 })
389}
390
391#[async_trait]
392impl BalSource for JsonRpcSource {
393 async fn header(&self, number: u64) -> Result<Header> {
394 let v = self.raw_block(json!(format!("{number:#x}"))).await?;
395 expect_number(parse_header(&v)?, number)
396 }
397
398 async fn head(&self) -> Result<u64> {
399 let v = self.call("eth_blockNumber", json!([])).await?;
400 parse_hex_u64(
401 v.as_str()
402 .ok_or_else(|| SourceError::Malformed("blockNumber".into()))?,
403 )
404 }
405
406 async fn finalized(&self) -> Result<u64> {
407 let v = self.raw_block(json!("finalized")).await?;
408 Ok(parse_header(&v)?.number)
409 }
410
411 async fn block(&self, number: u64) -> Result<SourcedBlock> {
412 let v = self.raw_block(json!(format!("{number:#x}"))).await?;
413 let header = expect_number(parse_header(&v)?, number)?;
414 let bal = JsonRpcSource::bal(self, number).await?;
415 Ok(SourcedBlock { header, bal })
416 }
417
418 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
419 JsonRpcSource::bal(self, number).await
420 }
421}
422
423#[derive(Deserialize)]
424#[serde(rename_all = "camelCase")]
425struct ProofResp {
426 balance: U256,
427 nonce: U256,
428 code_hash: B256,
429 storage_hash: B256,
430 account_proof: Vec<Bytes>,
431 storage_proof: Vec<StorageProofResp>,
432}
433
434#[derive(Deserialize)]
435struct StorageProofResp {
436 key: U256,
437 value: U256,
438 proof: Vec<Bytes>,
439}
440
441#[async_trait]
442impl StateSource for JsonRpcSource {
443 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
444 let v = self
445 .call("eth_getProof", json!([addr, slots, format!("{block:#x}")]))
446 .await?;
447 let p: ProofResp =
448 serde_json::from_value(v).map_err(|e| SourceError::Malformed(format!("proof: {e}")))?;
449 let nonce: u64 = p
450 .nonce
451 .try_into()
452 .map_err(|_| SourceError::Malformed(format!("proof nonce {} exceeds u64", p.nonce)))?;
453 Ok(AccountProof {
454 address: addr,
455 balance: p.balance,
456 nonce,
457 code_hash: p.code_hash,
458 storage_hash: p.storage_hash,
459 account_proof: p.account_proof,
460 storage_proofs: p
461 .storage_proof
462 .into_iter()
463 .map(|s| StorageProof {
464 key: B256::from(s.key.to_be_bytes::<32>()),
465 value: s.value,
466 proof: s.proof,
467 })
468 .collect(),
469 })
470 }
471}