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";
24pub const MAX_BODY_BYTES: u64 = 64 * 1024 * 1024;
26
27pub struct JsonRpcSource {
30 url: String,
31 client: reqwest::Client,
32}
33
34#[derive(Deserialize)]
35struct RpcResponse {
36 result: Option<Value>,
37 error: Option<RpcError>,
38}
39
40#[derive(Deserialize)]
41struct RpcError {
42 code: i64,
43 message: String,
44}
45
46impl JsonRpcSource {
47 pub fn new(url: impl Into<String>) -> Self {
51 let client = reqwest::Client::builder()
52 .timeout(std::time::Duration::from_secs(30))
53 .redirect(reqwest::redirect::Policy::none())
54 .build()
55 .unwrap_or_default();
56 Self {
57 url: url.into(),
58 client,
59 }
60 }
61
62 pub async fn call(&self, method: &str, params: Value) -> Result<Value> {
64 let body = json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params});
65 let http = self
66 .client
67 .post(&self.url)
68 .json(&body)
69 .send()
70 .await
71 .map_err(|e| SourceError::Transport(e.to_string()))?;
72 let status = http.status();
74 if !status.is_success() {
75 return Err(SourceError::Transport(format!(
76 "HTTP {} from {} for {method}",
77 status.as_u16(),
78 self.url
79 )));
80 }
81 if let Some(len) = http.content_length() {
85 if len > MAX_BODY_BYTES {
86 return Err(SourceError::Transport(format!(
87 "{method}: response of {len} bytes exceeds the {MAX_BODY_BYTES}-byte limit"
88 )));
89 }
90 }
91 let mut http = http;
92 let mut body: Vec<u8> = Vec::new();
93 while let Some(chunk) = http
94 .chunk()
95 .await
96 .map_err(|e| SourceError::Transport(format!("{method}: {e}")))?
97 {
98 if body.len() + chunk.len() > MAX_BODY_BYTES as usize {
99 return Err(SourceError::Transport(format!(
100 "{method}: response exceeds the {MAX_BODY_BYTES}-byte limit"
101 )));
102 }
103 body.extend_from_slice(&chunk);
104 }
105 let resp: RpcResponse = serde_json::from_slice(&body)
106 .map_err(|e| SourceError::Malformed(format!("{method}: {e}")))?;
107 if let Some(e) = resp.error {
108 return Err(SourceError::Rpc {
109 code: e.code,
110 message: e.message,
111 });
112 }
113 Ok(resp.result.unwrap_or(Value::Null))
114 }
115
116 async fn raw_block(&self, tag: Value) -> Result<Value> {
117 let v = self
118 .call("eth_getBlockByNumber", json!([tag, false]))
119 .await?;
120 if v.is_null() {
121 return Err(SourceError::BlockNotFound(match &tag {
122 Value::String(s) => parse_hex_u64(s).unwrap_or(u64::MAX),
123 _ => u64::MAX,
124 }));
125 }
126 Ok(v)
127 }
128
129 pub async fn bal(&self, number: u64) -> Result<BlockAccessList> {
131 let v = self
132 .call(BAL_METHOD, json!([format!("{number:#x}")]))
133 .await?;
134 if v.is_null() {
135 return Err(SourceError::NoBal(number));
136 }
137 Ok(BlockAccessList::from_rpc_json(&v)?)
138 }
139
140 async fn probe_block(&self, tag: Value) -> BalProbe {
143 let v = match self.raw_block(tag).await {
144 Ok(v) => v,
145 Err(e) => return BalProbe::Error(e.to_string()),
146 };
147 let header = match parse_header(&v) {
148 Ok(h) => h,
149 Err(e) => return BalProbe::Error(e.to_string()),
150 };
151 let bal = match self.bal(header.number).await {
152 Ok(b) => b,
153 Err(SourceError::NoBal(n)) => return BalProbe::Missing(n),
154 Err(e) => return BalProbe::Error(e.to_string()),
155 };
156 let computed = bal.hash();
157 match header.block_access_list_hash {
158 None => BalProbe::NoHashInHeader {
159 block: header.number,
160 accounts: bal.accounts.len(),
161 },
162 Some(expected) if expected == computed => BalProbe::Verified {
163 block: header.number,
164 accounts: bal.accounts.len(),
165 hash: computed,
166 },
167 Some(expected) => BalProbe::Mismatch {
168 block: header.number,
169 computed,
170 expected,
171 },
172 }
173 }
174
175 pub async fn probe(&self, old_block_age: u64) -> Result<ProbeReport> {
178 let head_v = self.raw_block(json!("latest")).await?;
179 let head = parse_header(&head_v)?;
180 let old_number = head.number.saturating_sub(old_block_age).max(1);
181 let chain_id = self
182 .call("eth_chainId", json!([]))
183 .await
184 .ok()
185 .and_then(|v| v.as_str().and_then(|s| parse_hex_u64(s).ok()));
186 let client_version = self
187 .call("web3_clientVersion", json!([]))
188 .await
189 .ok()
190 .and_then(|v| v.as_str().map(String::from));
191 let head_fields = head_v
192 .as_object()
193 .map(|o| o.keys().cloned().collect())
194 .unwrap_or_default();
195 let head_probe = self.probe_block(json!(format!("{:#x}", head.number))).await;
196 let old_probe = self.probe_block(json!(format!("{old_number:#x}"))).await;
197 let earliest_probe = self.probe_block(json!("0x1")).await;
198 let proof_window = self.measure_proof_window(head.number).await;
199
200 Ok(ProbeReport {
201 client_version,
202 chain_id,
203 head: head.number,
204 head_fields,
205 head_probe,
206 old_probe,
207 earliest_probe,
208 proof_window,
209 })
210 }
211
212 pub async fn measure_proof_window(&self, head: u64) -> std::result::Result<u64, String> {
216 let mut ok: Option<u64> = None;
217 for k in [0u64, 1, 2, 4, 8, 16, 32, 64, 128, 256, 1024, 4096] {
218 if k > head {
219 break;
220 }
221 let r = self
222 .call(
223 "eth_getProof",
224 json!([
225 Address::ZERO,
226 Vec::<B256>::new(),
227 format!("{:#x}", head - k)
228 ]),
229 )
230 .await;
231 match r {
232 Ok(_) => ok = Some(k),
233 Err(e) => {
234 if ok.is_none() {
235 return Err(e.to_string());
236 }
237 break;
238 }
239 }
240 }
241 Ok(ok.unwrap_or(0))
242 }
243}
244
245#[derive(Debug, Clone)]
247pub enum BalProbe {
248 Verified {
250 block: u64,
252 accounts: usize,
254 hash: B256,
256 },
257 NoHashInHeader {
259 block: u64,
261 accounts: usize,
263 },
264 Mismatch {
266 block: u64,
268 computed: B256,
270 expected: B256,
272 },
273 Missing(u64),
275 Error(String),
277}
278
279#[derive(Debug, Clone)]
281pub struct ProbeReport {
282 pub client_version: Option<String>,
284 pub chain_id: Option<u64>,
286 pub head: u64,
288 pub head_fields: Vec<String>,
290 pub head_probe: BalProbe,
292 pub old_probe: BalProbe,
294 pub earliest_probe: BalProbe,
296 pub proof_window: std::result::Result<u64, String>,
299}
300
301fn parse_hex_u64(s: &str) -> Result<u64> {
302 let s = s.strip_prefix("0x").unwrap_or(s);
303 u64::from_str_radix(s, 16).map_err(|e| SourceError::Malformed(format!("u64 {s}: {e}")))
304}
305
306fn parse_b256(v: &Value, name: &str) -> Result<B256> {
307 let s = v
308 .as_str()
309 .ok_or_else(|| SourceError::Malformed(format!("{name}: not a string")))?;
310 s.parse::<B256>()
311 .map_err(|e| SourceError::Malformed(format!("{name}: {e}")))
312}
313
314fn field<'a>(v: &'a Value, name: &str) -> Result<&'a Value> {
315 v.get(name)
316 .ok_or_else(|| SourceError::Malformed(format!("missing field {name}")))
317}
318
319fn parse_header(v: &Value) -> Result<Header> {
320 let num = field(v, "number")?
321 .as_str()
322 .ok_or_else(|| SourceError::Malformed("number".into()))?;
323 let ts = field(v, "timestamp")?
324 .as_str()
325 .ok_or_else(|| SourceError::Malformed("timestamp".into()))?;
326 let bal_hash = v
327 .get(BAL_HASH_FIELD)
328 .filter(|x| !x.is_null())
329 .map(|x| parse_b256(x, BAL_HASH_FIELD))
330 .transpose()?;
331 Ok(Header {
332 number: parse_hex_u64(num)?,
333 hash: parse_b256(field(v, "hash")?, "hash")?,
334 parent_hash: parse_b256(field(v, "parentHash")?, "parentHash")?,
335 state_root: parse_b256(field(v, "stateRoot")?, "stateRoot")?,
336 timestamp: parse_hex_u64(ts)?,
337 block_access_list_hash: bal_hash,
338 })
339}
340
341#[async_trait]
342impl BalSource for JsonRpcSource {
343 async fn header(&self, number: u64) -> Result<Header> {
344 let v = self.raw_block(json!(format!("{number:#x}"))).await?;
345 parse_header(&v)
346 }
347
348 async fn head(&self) -> Result<u64> {
349 let v = self.call("eth_blockNumber", json!([])).await?;
350 parse_hex_u64(
351 v.as_str()
352 .ok_or_else(|| SourceError::Malformed("blockNumber".into()))?,
353 )
354 }
355
356 async fn finalized(&self) -> Result<u64> {
357 let v = self.raw_block(json!("finalized")).await?;
358 Ok(parse_header(&v)?.number)
359 }
360
361 async fn block(&self, number: u64) -> Result<SourcedBlock> {
362 let v = self.raw_block(json!(format!("{number:#x}"))).await?;
363 let header = parse_header(&v)?;
364 let bal = JsonRpcSource::bal(self, number).await?;
365 Ok(SourcedBlock { header, bal })
366 }
367
368 async fn bal(&self, number: u64) -> Result<BlockAccessList> {
369 JsonRpcSource::bal(self, number).await
370 }
371}
372
373#[derive(Deserialize)]
374#[serde(rename_all = "camelCase")]
375struct ProofResp {
376 balance: U256,
377 nonce: U256,
378 code_hash: B256,
379 storage_hash: B256,
380 account_proof: Vec<Bytes>,
381 storage_proof: Vec<StorageProofResp>,
382}
383
384#[derive(Deserialize)]
385struct StorageProofResp {
386 key: U256,
387 value: U256,
388 proof: Vec<Bytes>,
389}
390
391#[async_trait]
392impl StateSource for JsonRpcSource {
393 async fn proof(&self, addr: Address, slots: &[B256], block: u64) -> Result<AccountProof> {
394 let v = self
395 .call("eth_getProof", json!([addr, slots, format!("{block:#x}")]))
396 .await?;
397 let p: ProofResp =
398 serde_json::from_value(v).map_err(|e| SourceError::Malformed(format!("proof: {e}")))?;
399 let nonce: u64 = p
400 .nonce
401 .try_into()
402 .map_err(|_| SourceError::Malformed(format!("proof nonce {} exceeds u64", p.nonce)))?;
403 Ok(AccountProof {
404 address: addr,
405 balance: p.balance,
406 nonce,
407 code_hash: p.code_hash,
408 storage_hash: p.storage_hash,
409 account_proof: p.account_proof,
410 storage_proofs: p
411 .storage_proof
412 .into_iter()
413 .map(|s| StorageProof {
414 key: B256::from(s.key.to_be_bytes::<32>()),
415 value: s.value,
416 proof: s.proof,
417 })
418 .collect(),
419 })
420 }
421}