arb_node/
chainspec.rs

1//! Chain spec parser that pre-populates the genesis `alloc` with
2//! ArbOS state when the spec declares `config.arbitrum.InitialArbOSVersion`
3//! but does not include the ArbOS state account in the alloc.
4
5use std::{path::Path, str::FromStr, sync::Arc};
6
7use alloy_genesis::GenesisAccount;
8use alloy_primitives::{Address, B256, U256};
9use eyre::eyre;
10use reth_chainspec::ChainSpec;
11use reth_cli::chainspec::ChainSpecParser;
12use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
13use revm::database::{EmptyDB, State, StateBuilder};
14use revm_database::states::bundle_state::BundleRetention;
15use serde_json::Value;
16
17use arbos::arbos_types::ParsedInitMessage;
18
19use crate::genesis;
20
21#[derive(Debug, Clone, Default)]
22#[non_exhaustive]
23pub struct ArbChainSpecParser;
24
25impl ChainSpecParser for ArbChainSpecParser {
26    type ChainSpec = ChainSpec;
27
28    const SUPPORTED_CHAINS: &'static [&'static str] = EthereumChainSpecParser::SUPPORTED_CHAINS;
29
30    fn parse(s: &str) -> eyre::Result<Arc<ChainSpec>> {
31        if EthereumChainSpecParser::SUPPORTED_CHAINS.contains(&s) {
32            return EthereumChainSpecParser::parse(s);
33        }
34
35        let raw = if Path::new(s).exists() {
36            std::fs::read_to_string(s).map_err(|e| eyre!("read chain spec {s}: {e}"))?
37        } else {
38            s.to_string()
39        };
40
41        let mut value: Value =
42            serde_json::from_str(&raw).map_err(|e| eyre!("parse chain spec JSON: {e}"))?;
43
44        let initial_arbos = value
45            .pointer("/config/arbitrum/InitialArbOSVersion")
46            .and_then(Value::as_u64)
47            .unwrap_or(0);
48        let chain_id = value
49            .pointer("/config/chainId")
50            .and_then(Value::as_u64)
51            .unwrap_or(0);
52        let initial_owner = value
53            .pointer("/config/arbitrum/InitialChainOwner")
54            .and_then(Value::as_str)
55            .and_then(|s| Address::from_str(s.trim_start_matches("0x")).ok())
56            .unwrap_or(Address::ZERO);
57        let arbos_init = parse_arbos_init(&value);
58
59        let allow_debug = value
60            .pointer("/config/arbitrum/AllowDebugPrecompiles")
61            .and_then(Value::as_bool)
62            .unwrap_or(false);
63        arb_precompiles::set_allow_debug_precompiles(allow_debug);
64
65        if initial_arbos > 0 && chain_id > 0 {
66            inject_arbos_alloc(
67                &mut value,
68                chain_id,
69                initial_arbos,
70                initial_owner,
71                arbos_init,
72            )?;
73        }
74
75        let augmented = serde_json::to_string(&value)?;
76        EthereumChainSpecParser::parse(&augmented)
77    }
78}
79
80fn parse_arbos_init(value: &Value) -> genesis::ArbOSInit {
81    let native = value
82        .pointer("/config/arbitrum/ArbOSInit/nativeTokenSupplyManagementEnabled")
83        .or_else(|| value.pointer("/config/arbitrum/nativeTokenSupplyManagementEnabled"))
84        .and_then(Value::as_bool)
85        .unwrap_or(false);
86    let filtering = value
87        .pointer("/config/arbitrum/ArbOSInit/transactionFilteringEnabled")
88        .or_else(|| value.pointer("/config/arbitrum/transactionFilteringEnabled"))
89        .and_then(Value::as_bool)
90        .unwrap_or(false);
91    genesis::ArbOSInit {
92        native_token_supply_management_enabled: native,
93        transaction_filtering_enabled: filtering,
94    }
95}
96
97fn inject_arbos_alloc(
98    value: &mut Value,
99    chain_id: u64,
100    arbos_version: u64,
101    chain_owner: Address,
102    arbos_init: genesis::ArbOSInit,
103) -> eyre::Result<()> {
104    let alloc_obj = value
105        .as_object_mut()
106        .ok_or_else(|| eyre!("chain spec is not a JSON object"))?
107        .entry("alloc")
108        .or_insert_with(|| Value::Object(serde_json::Map::new()))
109        .as_object_mut()
110        .ok_or_else(|| eyre!("alloc is not a JSON object"))?;
111
112    let entries = compute_arbos_alloc(chain_id, arbos_version, chain_owner, arbos_init)?;
113    for (addr, account) in entries {
114        let key = address_lower_no_prefix(addr);
115        let prefixed = format!("0x{key}");
116        let existing_key = if alloc_obj.contains_key(&key) {
117            Some(key.clone())
118        } else if alloc_obj.contains_key(&prefixed) {
119            Some(prefixed.clone())
120        } else {
121            None
122        };
123        let injected = serde_json::to_value(&account)?;
124        match existing_key {
125            None => {
126                alloc_obj.insert(prefixed, injected);
127            }
128            Some(k) => {
129                // Merge injected entry into the user-supplied one. User-set
130                // fields (balance, nonce, code, individual storage slots)
131                // win on conflict so fixture overrides replace bootstrap
132                // values; injected fields fill in anything the user didn't
133                // specify.
134                let user = alloc_obj.get_mut(&k).unwrap();
135                if !user.is_object() || !injected.is_object() {
136                    continue;
137                }
138                let user_obj = user.as_object_mut().unwrap();
139                let injected_obj = injected.as_object().unwrap();
140                for (field, val) in injected_obj {
141                    if field == "storage" {
142                        continue;
143                    }
144                    user_obj.entry(field.clone()).or_insert(val.clone());
145                }
146                let injected_storage = injected
147                    .get("storage")
148                    .and_then(|s| s.as_object())
149                    .cloned()
150                    .unwrap_or_default();
151                let storage = user_obj
152                    .entry("storage")
153                    .or_insert_with(|| Value::Object(serde_json::Map::new()))
154                    .as_object_mut()
155                    .ok_or_else(|| eyre!("alloc[{k}].storage is not an object"))?;
156                for (slot, val) in injected_storage {
157                    storage.entry(slot).or_insert(val);
158                }
159            }
160        }
161    }
162    Ok(())
163}
164
165fn address_lower_no_prefix(addr: Address) -> String {
166    let s = format!("{addr:x}");
167    let mut padded = String::with_capacity(40);
168    for _ in 0..(40 - s.len()) {
169        padded.push('0');
170    }
171    padded.push_str(&s);
172    padded
173}
174
175/// Run [`genesis::initialize_arbos_state`] in a scratch in-memory state
176/// and dump the resulting account/storage map. Returns one entry per
177/// account touched (the ArbOS state address plus all genesis precompile
178/// markers).
179pub fn compute_arbos_alloc(
180    chain_id: u64,
181    arbos_version: u64,
182    chain_owner: Address,
183    arbos_init: genesis::ArbOSInit,
184) -> eyre::Result<Vec<(Address, GenesisAccount)>> {
185    let mut state: State<EmptyDB> = StateBuilder::new()
186        .with_database(EmptyDB::default())
187        .with_bundle_update()
188        .build();
189
190    let init_msg = ParsedInitMessage {
191        chain_id: U256::from(chain_id),
192        initial_l1_base_fee: U256::ZERO,
193        serialized_chain_config: Vec::new(),
194    };
195
196    genesis::initialize_arbos_state(
197        &mut state,
198        &init_msg,
199        chain_id,
200        arbos_version,
201        chain_owner,
202        arbos_init,
203    )
204    .map_err(|e| eyre!("initialize_arbos_state: {e}"))?;
205
206    state.merge_transitions(BundleRetention::PlainState);
207    let bundle = state.take_bundle();
208
209    let mut out = Vec::new();
210    for (addr, account) in bundle.state.iter() {
211        let info = match &account.info {
212            Some(info) => info,
213            None => continue,
214        };
215
216        let mut storage = std::collections::BTreeMap::new();
217        for (slot, slot_value) in account.storage.iter() {
218            if slot_value.present_value.is_zero() {
219                continue;
220            }
221            storage.insert(
222                B256::from(slot.to_be_bytes::<32>()),
223                B256::from(slot_value.present_value.to_be_bytes::<32>()),
224            );
225        }
226
227        let code = match &info.code {
228            Some(c) if !c.original_bytes().is_empty() => Some(c.original_bytes()),
229            _ => None,
230        };
231
232        let entry = GenesisAccount {
233            balance: info.balance,
234            nonce: Some(info.nonce),
235            code,
236            storage: if storage.is_empty() {
237                None
238            } else {
239                Some(storage)
240            },
241            private_key: None,
242        };
243        out.push((*addr, entry));
244    }
245    out.sort_by_key(|(a, _)| *a);
246    Ok(out)
247}