Skip to main content

bal_codec/
json.rs

1//! JSON form of a BAL as returned by `eth_getBlockAccessList`
2//! (execution-apis). Field names observed on Platåberget (reth 2.5.0),
3//! 2026-08-29:
4//!
5//! ```json
6//! [{ "address": "0x…",
7//!    "storageChanges": [{ "key": "0x1e29", "changes": [{ "index": "0x0", "value": "0x…" }] }],
8//!    "storageReads":   ["0x…"],
9//!    "balanceChanges": [{ "index": "0x2a", "value": "0x…" }],
10//!    "nonceChanges":   [{ "index": "0x16", "value": "0x1809" }],
11//!    "codeChanges":    [{ "index": "0x3e", "code": "0x…" }] }]
12//! ```
13//!
14//! Quantities are minimal hex. The result is validated exactly like the RLP
15//! path, and its hash is expected to equal the header's — that equality is
16//! the proof that this mapping is right.
17
18use crate::{
19    AccountChanges, BalanceChange, BlockAccessList, CodeChange, CodecError, NonceChange,
20    SlotChanges, StorageChange,
21};
22use alloy_primitives::{Address, Bytes, U256};
23use serde::Deserialize;
24
25#[derive(Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27struct RpcAccount {
28    address: Address,
29    #[serde(default)]
30    storage_changes: Vec<RpcSlot>,
31    #[serde(default)]
32    storage_reads: Vec<U256>,
33    #[serde(default)]
34    balance_changes: Vec<RpcIndexed>,
35    #[serde(default)]
36    nonce_changes: Vec<RpcIndexed>,
37    #[serde(default)]
38    code_changes: Vec<RpcCode>,
39}
40
41#[derive(Deserialize)]
42#[serde(rename_all = "camelCase", deny_unknown_fields)]
43struct RpcSlot {
44    key: U256,
45    changes: Vec<RpcIndexed>,
46}
47
48#[derive(Deserialize)]
49#[serde(deny_unknown_fields)]
50struct RpcIndexed {
51    index: U256,
52    value: U256,
53}
54
55#[derive(Deserialize)]
56#[serde(deny_unknown_fields)]
57struct RpcCode {
58    index: U256,
59    code: Bytes,
60}
61
62fn idx(u: U256) -> Result<u32, CodecError> {
63    u.try_into()
64        .map_err(|_| CodecError::Json(format!("block access index {u} exceeds u32")))
65}
66
67impl BlockAccessList {
68    /// Decode the JSON result of `eth_getBlockAccessList` and validate it
69    /// like RLP input. `null` (block not found) is an error here; callers
70    /// handle "not found" before reaching the codec.
71    pub fn from_rpc_json(v: &serde_json::Value) -> Result<Self, CodecError> {
72        // `&Value` is itself a Deserializer: no clone of the (possibly tens
73        // of MB) tree.
74        let accounts: Vec<RpcAccount> = serde::Deserialize::deserialize(v)
75            .map_err(|e: serde_json::Error| CodecError::Json(e.to_string()))?;
76        let mut out = Vec::with_capacity(accounts.len());
77        for a in accounts {
78            out.push(AccountChanges {
79                address: a.address,
80                storage_changes: a
81                    .storage_changes
82                    .into_iter()
83                    .map(|s| {
84                        Ok(SlotChanges {
85                            slot: s.key,
86                            changes: s
87                                .changes
88                                .into_iter()
89                                .map(|c| {
90                                    Ok(StorageChange {
91                                        block_access_index: idx(c.index)?,
92                                        value: c.value,
93                                    })
94                                })
95                                .collect::<Result<_, CodecError>>()?,
96                        })
97                    })
98                    .collect::<Result<_, CodecError>>()?,
99                storage_reads: a.storage_reads,
100                balance_changes: a
101                    .balance_changes
102                    .into_iter()
103                    .map(|c| {
104                        Ok(BalanceChange {
105                            block_access_index: idx(c.index)?,
106                            post_balance: c.value,
107                        })
108                    })
109                    .collect::<Result<_, CodecError>>()?,
110                nonce_changes: a
111                    .nonce_changes
112                    .into_iter()
113                    .map(|c| {
114                        Ok(NonceChange {
115                            block_access_index: idx(c.index)?,
116                            new_nonce: c.value.try_into().map_err(|_| {
117                                CodecError::Json(format!("nonce {} exceeds u64", c.value))
118                            })?,
119                        })
120                    })
121                    .collect::<Result<_, CodecError>>()?,
122                code_changes: a
123                    .code_changes
124                    .into_iter()
125                    .map(|c| {
126                        Ok(CodeChange {
127                            block_access_index: idx(c.index)?,
128                            new_code: c.code,
129                        })
130                    })
131                    .collect::<Result<_, CodecError>>()?,
132            });
133        }
134        let bal = BlockAccessList { accounts: out };
135        crate::validate::validate(&bal)?;
136        Ok(bal)
137    }
138}