1use std::{path::Path, str::FromStr, sync::Arc};
6
7use alloy_genesis::GenesisAccount;
8use alloy_primitives::{Address, B256, U256, hex};
9use arbos::arbos_types::ParsedInitMessage;
10use eyre::eyre;
11use reth_chainspec::ChainSpec;
12use reth_cli::chainspec::ChainSpecParser;
13use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
14use revm::database::{EmptyDB, State, StateBuilder};
15use revm_database::states::bundle_state::BundleRetention;
16use serde_json::Value;
17
18use crate::genesis;
19
20const NITRO_GENESIS_GAS_LIMIT: u64 = 1 << 50;
22const NITRO_GENESIS_BASE_FEE: u64 = 100_000_000;
24const SKIP_GENESIS_INJECTION_POINTER: &str = "/config/arbitrum/SkipGenesisInjection";
27const DEFAULT_INITIAL_L1_BASE_FEE_WEI: u64 = 50_000_000_000;
29
30#[derive(Debug, Clone, Default)]
31#[non_exhaustive]
32pub struct ArbChainSpecParser;
33
34impl ChainSpecParser for ArbChainSpecParser {
35 type ChainSpec = ChainSpec;
36
37 const SUPPORTED_CHAINS: &'static [&'static str] = EthereumChainSpecParser::SUPPORTED_CHAINS;
38
39 fn parse(s: &str) -> eyre::Result<Arc<ChainSpec>> {
40 if EthereumChainSpecParser::SUPPORTED_CHAINS.contains(&s) {
41 return EthereumChainSpecParser::parse(s);
42 }
43
44 let raw = if Path::new(s).exists() {
45 std::fs::read_to_string(s).map_err(|e| eyre!("read chain spec {s}: {e}"))?
46 } else {
47 s.to_string()
48 };
49
50 let mut value: Value =
51 serde_json::from_str(&raw).map_err(|e| eyre!("parse chain spec JSON: {e}"))?;
52
53 let initial_arbos = value
54 .pointer("/config/arbitrum/InitialArbOSVersion")
55 .and_then(Value::as_u64)
56 .unwrap_or(0);
57 let chain_id = value
58 .pointer("/config/chainId")
59 .and_then(Value::as_u64)
60 .unwrap_or(0);
61 let initial_owner = value
62 .pointer("/config/arbitrum/InitialChainOwner")
63 .and_then(Value::as_str)
64 .and_then(|s| Address::from_str(s.trim_start_matches("0x")).ok())
65 .unwrap_or(Address::ZERO);
66 let arbos_init = parse_arbos_init(&value);
67
68 let skip_injection = value
69 .pointer(SKIP_GENESIS_INJECTION_POINTER)
70 .and_then(Value::as_bool)
71 .unwrap_or(false);
72
73 if initial_arbos > 0 && chain_id > 0 {
74 let _ = skip_injection;
80 inject_arbos_alloc(
81 &mut value,
82 chain_id,
83 initial_arbos,
84 initial_owner,
85 arbos_init,
86 )?;
87 override_arbos_genesis_header(&mut value, initial_arbos)?;
88 }
89
90 let augmented = serde_json::to_string(&value)?;
91 EthereumChainSpecParser::parse(&augmented)
92 }
93}
94
95pub fn allow_debug_precompiles(chain_spec: &ChainSpec) -> bool {
98 chain_spec
99 .genesis()
100 .config
101 .extra_fields
102 .get_deserialized::<serde_json::Value>("arbitrum")
103 .and_then(|v| v.ok())
104 .and_then(|arb| arb.get("AllowDebugPrecompiles").and_then(Value::as_bool))
105 .unwrap_or(false)
106}
107
108fn override_arbos_genesis_header(value: &mut Value, arbos_version: u64) -> eyre::Result<()> {
112 let obj = value
113 .as_object_mut()
114 .ok_or_else(|| eyre!("chain spec is not a JSON object"))?;
115
116 obj.insert("nonce".into(), Value::String("0x1".into()));
118
119 obj.insert(
121 "extraData".into(),
122 Value::String(format!("0x{}", hex::encode([0u8; 32]))),
123 );
124
125 let mut mix_hash = [0u8; 32];
128 mix_hash[16..24].copy_from_slice(&arbos_version.to_be_bytes());
129 obj.insert(
130 "mixHash".into(),
131 Value::String(format!("0x{}", hex::encode(mix_hash))),
132 );
133
134 obj.insert("difficulty".into(), Value::String("0x1".into()));
135 obj.insert(
136 "gasLimit".into(),
137 Value::String(format!("{NITRO_GENESIS_GAS_LIMIT:#x}")),
138 );
139 obj.insert(
140 "baseFeePerGas".into(),
141 Value::String(format!("{NITRO_GENESIS_BASE_FEE:#x}")),
142 );
143 obj.insert(
144 "coinbase".into(),
145 Value::String(format!("0x{}", hex::encode([0u8; 20]))),
146 );
147
148 Ok(())
149}
150
151fn parse_arbos_init(value: &Value) -> genesis::ArbOSInit {
152 let native = value
153 .pointer("/config/arbitrum/ArbOSInit/nativeTokenSupplyManagementEnabled")
154 .or_else(|| value.pointer("/config/arbitrum/nativeTokenSupplyManagementEnabled"))
155 .and_then(Value::as_bool)
156 .unwrap_or(false);
157 let filtering = value
158 .pointer("/config/arbitrum/ArbOSInit/transactionFilteringEnabled")
159 .or_else(|| value.pointer("/config/arbitrum/transactionFilteringEnabled"))
160 .and_then(Value::as_bool)
161 .unwrap_or(false);
162 genesis::ArbOSInit {
163 native_token_supply_management_enabled: native,
164 transaction_filtering_enabled: filtering,
165 }
166}
167
168fn inject_arbos_alloc(
169 value: &mut Value,
170 chain_id: u64,
171 arbos_version: u64,
172 chain_owner: Address,
173 arbos_init: genesis::ArbOSInit,
174) -> eyre::Result<()> {
175 let serialized_chain_config = value
179 .get("config")
180 .map(serialize_chain_config_go_style)
181 .unwrap_or_default();
182
183 let alloc_obj = value
184 .as_object_mut()
185 .ok_or_else(|| eyre!("chain spec is not a JSON object"))?
186 .entry("alloc")
187 .or_insert_with(|| Value::Object(serde_json::Map::new()))
188 .as_object_mut()
189 .ok_or_else(|| eyre!("alloc is not a JSON object"))?;
190
191 let entries = compute_arbos_alloc_with_config(
192 chain_id,
193 arbos_version,
194 chain_owner,
195 arbos_init,
196 serialized_chain_config,
197 U256::from(DEFAULT_INITIAL_L1_BASE_FEE_WEI),
198 )?;
199 for (addr, account) in entries {
200 let key = address_lower_no_prefix(addr);
201 let prefixed = format!("0x{key}");
202 let existing_key = if alloc_obj.contains_key(&key) {
203 Some(key.clone())
204 } else if alloc_obj.contains_key(&prefixed) {
205 Some(prefixed.clone())
206 } else {
207 None
208 };
209 let injected = serde_json::to_value(&account)?;
210 match existing_key {
211 None => {
212 alloc_obj.insert(prefixed, injected);
213 }
214 Some(k) => {
215 let user = alloc_obj
221 .get_mut(&k)
222 .ok_or_else(|| eyre!("alloc[{k}] disappeared while merging ArbOS state"))?;
223 let user_obj = user
224 .as_object_mut()
225 .ok_or_else(|| eyre!("alloc[{k}] is not an object"))?;
226 let injected_obj = injected
227 .as_object()
228 .ok_or_else(|| eyre!("generated ArbOS alloc[{k}] is not an object"))?;
229 for (field, val) in injected_obj {
230 if field == "storage" {
231 continue;
232 }
233 user_obj.entry(field.clone()).or_insert(val.clone());
234 }
235 let injected_storage = injected
236 .get("storage")
237 .and_then(|s| s.as_object())
238 .cloned()
239 .unwrap_or_default();
240 let storage = user_obj
241 .entry("storage")
242 .or_insert_with(|| Value::Object(serde_json::Map::new()))
243 .as_object_mut()
244 .ok_or_else(|| eyre!("alloc[{k}].storage is not an object"))?;
245 for (slot, val) in injected_storage {
246 storage.entry(slot).or_insert(val);
247 }
248 }
249 }
250 }
251 Ok(())
252}
253
254fn address_lower_no_prefix(addr: Address) -> String {
255 let s = format!("{addr:x}");
256 let mut padded = String::with_capacity(40);
257 for _ in 0..(40 - s.len()) {
258 padded.push('0');
259 }
260 padded.push_str(&s);
261 padded
262}
263
264pub fn compute_arbos_alloc(
272 chain_id: u64,
273 arbos_version: u64,
274 chain_owner: Address,
275 arbos_init: genesis::ArbOSInit,
276) -> eyre::Result<Vec<(Address, GenesisAccount)>> {
277 compute_arbos_alloc_with_config(
278 chain_id,
279 arbos_version,
280 chain_owner,
281 arbos_init,
282 Vec::new(),
283 U256::ZERO,
284 )
285}
286
287pub fn compute_arbos_alloc_with_config(
294 chain_id: u64,
295 arbos_version: u64,
296 chain_owner: Address,
297 arbos_init: genesis::ArbOSInit,
298 serialized_chain_config: Vec<u8>,
299 initial_l1_base_fee: U256,
300) -> eyre::Result<Vec<(Address, GenesisAccount)>> {
301 let mut state: State<EmptyDB> = StateBuilder::new()
302 .with_database(EmptyDB::default())
303 .with_bundle_update()
304 .build();
305
306 let init_msg = ParsedInitMessage {
307 chain_id: U256::from(chain_id),
308 initial_l1_base_fee,
309 serialized_chain_config,
310 };
311
312 genesis::initialize_arbos_state(
313 &mut state,
314 &init_msg,
315 chain_id,
316 arbos_version,
317 chain_owner,
318 arbos_init,
319 )
320 .map_err(|e| eyre!("initialize_arbos_state: {e}"))?;
321
322 state.merge_transitions(BundleRetention::PlainState);
323 let bundle = state.take_bundle();
324
325 let mut out = Vec::new();
326 for (addr, account) in bundle.state.iter() {
327 let info = match &account.info {
328 Some(info) => info,
329 None => continue,
330 };
331
332 let mut storage = std::collections::BTreeMap::new();
333 for (slot, slot_value) in account.storage.iter() {
334 if slot_value.present_value.is_zero() {
335 continue;
336 }
337 storage.insert(
338 B256::from(slot.to_be_bytes::<32>()),
339 B256::from(slot_value.present_value.to_be_bytes::<32>()),
340 );
341 }
342
343 let code = match &info.code {
344 Some(c) if !c.original_bytes().is_empty() => Some(c.original_bytes()),
345 _ => None,
346 };
347
348 let entry = GenesisAccount {
349 balance: info.balance,
350 nonce: Some(info.nonce),
351 code,
352 storage: if storage.is_empty() {
353 None
354 } else {
355 Some(storage)
356 },
357 private_key: None,
358 };
359 out.push((*addr, entry));
360 }
361 out.sort_by_key(|(a, _)| *a);
362 Ok(out)
363}
364
365pub fn serialize_chain_config_go_style(config: &Value) -> Vec<u8> {
371 let mut out = Vec::with_capacity(512);
372 out.push(b'{');
373 let mut writer = JsonWriter::new(&mut out);
374
375 let cfg = config.as_object();
376
377 writer.write_required_chain_id(cfg);
381
382 for (name, json_key) in BIG_INT_BLOCK_FIELDS {
383 writer.write_optional_big_int(cfg, json_key, name);
384 }
385
386 if let Some(map) = cfg
387 && map.get("daoForkSupport").and_then(Value::as_bool) == Some(true)
388 {
389 writer.write_bool_field("daoForkSupport", true);
390 }
391
392 for (name, json_key) in BIG_INT_BLOCK_FIELDS_AFTER_DAO {
393 writer.write_optional_big_int(cfg, json_key, name);
394 }
395
396 for (name, json_key) in TIME_FIELDS {
397 writer.write_optional_uint64(cfg, json_key, name);
398 }
399
400 writer.write_optional_big_int(cfg, "terminalTotalDifficulty", "terminalTotalDifficulty");
401
402 writer.write_address_field(
405 cfg,
406 "depositContractAddress",
407 "depositContractAddress",
408 true,
409 );
410
411 if let Some(map) = cfg
412 && map.get("enableVerkleAtGenesis").and_then(Value::as_bool) == Some(true)
413 {
414 writer.write_bool_field("enableVerkleAtGenesis", true);
415 }
416
417 if let Some(eth) = cfg.and_then(|m| m.get("ethash")).filter(|v| v.is_object()) {
421 writer.write_raw_object("ethash", eth);
422 }
423 if let Some(clique) = cfg.and_then(|m| m.get("clique")).filter(|v| v.is_object()) {
424 writer.write_clique("clique", clique);
425 }
426
427 let arbitrum = cfg
430 .and_then(|m| m.get("arbitrum"))
431 .filter(|v| v.is_object());
432 writer.write_arbitrum(arbitrum);
433
434 out.push(b'}');
435 out
436}
437
438const BIG_INT_BLOCK_FIELDS: &[(&str, &str)] = &[
440 ("homesteadBlock", "homesteadBlock"),
441 ("daoForkBlock", "daoForkBlock"),
442];
443
444const BIG_INT_BLOCK_FIELDS_AFTER_DAO: &[(&str, &str)] = &[
446 ("eip150Block", "eip150Block"),
447 ("eip155Block", "eip155Block"),
448 ("eip158Block", "eip158Block"),
449 ("byzantiumBlock", "byzantiumBlock"),
450 ("constantinopleBlock", "constantinopleBlock"),
451 ("petersburgBlock", "petersburgBlock"),
452 ("istanbulBlock", "istanbulBlock"),
453 ("muirGlacierBlock", "muirGlacierBlock"),
454 ("berlinBlock", "berlinBlock"),
455 ("londonBlock", "londonBlock"),
456 ("arrowGlacierBlock", "arrowGlacierBlock"),
457 ("grayGlacierBlock", "grayGlacierBlock"),
458 ("mergeNetsplitBlock", "mergeNetsplitBlock"),
459];
460
461const TIME_FIELDS: &[(&str, &str)] = &[
463 ("shanghaiTime", "shanghaiTime"),
464 ("cancunTime", "cancunTime"),
465 ("pragueTime", "pragueTime"),
466 ("osakaTime", "osakaTime"),
467 ("bpo1Time", "bpo1Time"),
468 ("bpo2Time", "bpo2Time"),
469 ("bpo3Time", "bpo3Time"),
470 ("bpo4Time", "bpo4Time"),
471 ("bpo5Time", "bpo5Time"),
472 ("amsterdamTime", "amsterdamTime"),
473 ("verkleTime", "verkleTime"),
474];
475
476struct JsonWriter<'a> {
478 buf: &'a mut Vec<u8>,
479 first: bool,
480}
481
482impl<'a> JsonWriter<'a> {
483 fn new(buf: &'a mut Vec<u8>) -> Self {
484 Self { buf, first: true }
485 }
486
487 fn comma(&mut self) {
488 if self.first {
489 self.first = false;
490 } else {
491 self.buf.push(b',');
492 }
493 }
494
495 fn write_key(&mut self, name: &str) {
496 self.comma();
497 self.buf.push(b'"');
498 self.buf.extend_from_slice(name.as_bytes());
499 self.buf.extend_from_slice(b"\":");
500 }
501
502 fn write_required_chain_id(&mut self, cfg: Option<&serde_json::Map<String, Value>>) {
503 let chain_id = cfg
504 .and_then(|m| m.get("chainId"))
505 .map(value_to_decimal_int)
506 .unwrap_or_else(|| "0".to_string());
507 self.write_key("chainId");
508 self.buf.extend_from_slice(chain_id.as_bytes());
509 }
510
511 fn write_optional_big_int(
512 &mut self,
513 cfg: Option<&serde_json::Map<String, Value>>,
514 json_key: &str,
515 name: &str,
516 ) {
517 let map = match cfg {
518 Some(m) => m,
519 None => return,
520 };
521 let v = match map.get(json_key) {
522 Some(v) if !v.is_null() => v,
523 _ => return,
524 };
525 let s = value_to_decimal_int(v);
526 self.write_key(name);
527 self.buf.extend_from_slice(s.as_bytes());
528 }
529
530 fn write_optional_uint64(
531 &mut self,
532 cfg: Option<&serde_json::Map<String, Value>>,
533 json_key: &str,
534 name: &str,
535 ) {
536 let map = match cfg {
537 Some(m) => m,
538 None => return,
539 };
540 let v = match map.get(json_key) {
541 Some(v) if !v.is_null() => v,
542 _ => return,
543 };
544 let s = value_to_decimal_int(v);
547 self.write_key(name);
548 self.buf.extend_from_slice(s.as_bytes());
549 }
550
551 fn write_bool_field(&mut self, name: &str, val: bool) {
552 self.write_key(name);
553 self.buf
554 .extend_from_slice(if val { b"true" } else { b"false" });
555 }
556
557 fn write_address_field(
558 &mut self,
559 cfg: Option<&serde_json::Map<String, Value>>,
560 json_key: &str,
561 name: &str,
562 emit_zero: bool,
563 ) {
564 let addr = cfg
565 .and_then(|m| m.get(json_key))
566 .and_then(Value::as_str)
567 .map(|s| s.trim_start_matches("0x").to_lowercase())
568 .unwrap_or_default();
569 let normalized = pad_address_lower(&addr);
570 if !emit_zero && normalized == "0".repeat(40) {
571 return;
572 }
573 self.write_key(name);
574 self.buf.push(b'"');
575 self.buf.extend_from_slice(b"0x");
576 self.buf.extend_from_slice(normalized.as_bytes());
577 self.buf.push(b'"');
578 }
579
580 fn write_raw_object(&mut self, name: &str, val: &Value) {
581 self.write_key(name);
584 if let Some(obj) = val.as_object() {
585 if obj.is_empty() {
586 self.buf.extend_from_slice(b"{}");
587 return;
588 }
589 let bytes = serde_json::to_vec(val).unwrap_or_else(|_| b"{}".to_vec());
591 self.buf.extend_from_slice(&bytes);
592 } else {
593 self.buf.extend_from_slice(b"null");
594 }
595 }
596
597 fn write_clique(&mut self, name: &str, val: &Value) {
598 self.write_key(name);
599 let map = val.as_object();
600 let period = map
601 .and_then(|m| m.get("period"))
602 .map(value_to_decimal_int)
603 .unwrap_or_else(|| "0".to_string());
604 let epoch = map
605 .and_then(|m| m.get("epoch"))
606 .map(value_to_decimal_int)
607 .unwrap_or_else(|| "0".to_string());
608 self.buf.extend_from_slice(b"{\"period\":");
609 self.buf.extend_from_slice(period.as_bytes());
610 self.buf.extend_from_slice(b",\"epoch\":");
611 self.buf.extend_from_slice(epoch.as_bytes());
612 self.buf.push(b'}');
613 }
614
615 fn write_arbitrum(&mut self, arbitrum: Option<&Value>) {
616 self.write_key("arbitrum");
617 self.buf.push(b'{');
618 let mut inner = JsonWriter::new(self.buf);
619 let map = arbitrum.and_then(Value::as_object);
620
621 let always_bool = [
624 "EnableArbOS",
625 "AllowDebugPrecompiles",
626 "DataAvailabilityCommittee",
627 ];
628 for key in always_bool {
629 let v = map
630 .and_then(|m| m.get(key))
631 .and_then(Value::as_bool)
632 .unwrap_or(false);
633 inner.write_bool_field(key, v);
634 }
635
636 let initial_arbos = map
637 .and_then(|m| m.get("InitialArbOSVersion"))
638 .map(value_to_decimal_int)
639 .unwrap_or_else(|| "0".to_string());
640 inner.write_key("InitialArbOSVersion");
641 inner.buf.extend_from_slice(initial_arbos.as_bytes());
642
643 inner.write_address_field(map, "InitialChainOwner", "InitialChainOwner", true);
644
645 let genesis_block = map
646 .and_then(|m| m.get("GenesisBlockNum"))
647 .map(value_to_decimal_int)
648 .unwrap_or_else(|| "0".to_string());
649 inner.write_key("GenesisBlockNum");
650 inner.buf.extend_from_slice(genesis_block.as_bytes());
651
652 for json_key in ["MaxCodeSize", "MaxInitCodeSize", "MaxUncompressedBatchSize"] {
655 if let Some(v) = map.and_then(|m| m.get(json_key)) {
656 let n = value_to_decimal_int(v);
657 if n != "0" {
658 inner.write_key(json_key);
659 inner.buf.extend_from_slice(n.as_bytes());
660 }
661 }
662 }
663
664 self.buf.push(b'}');
665 }
666}
667
668fn value_to_decimal_int(v: &Value) -> String {
669 match v {
670 Value::Number(n) => {
671 if let Some(u) = n.as_u64() {
672 return u.to_string();
673 }
674 if let Some(i) = n.as_i64() {
675 return i.to_string();
676 }
677 n.to_string()
678 }
679 Value::String(s) => {
680 if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
681 return U256::from_str_radix(rest, 16)
682 .map(|x| x.to_string())
683 .unwrap_or_else(|_| "0".to_string());
684 }
685 s.clone()
686 }
687 Value::Bool(b) => (*b as u64).to_string(),
688 _ => "0".to_string(),
689 }
690}
691
692fn pad_address_lower(s: &str) -> String {
693 let trimmed = s.trim_start_matches("0x").to_lowercase();
694 if trimmed.len() >= 40 {
695 return trimmed[trimmed.len() - 40..].to_string();
696 }
697 let mut out = String::with_capacity(40);
698 for _ in 0..(40 - trimmed.len()) {
699 out.push('0');
700 }
701 out.push_str(&trimmed);
702 out
703}
704
705#[cfg(test)]
706mod tests {
707 use serde_json::json;
708
709 use super::*;
710
711 #[test]
712 fn inject_arbos_alloc_rejects_non_object_account() {
713 let key = address_lower_no_prefix(arb_storage::ARBOS_STATE_ADDRESS);
714 let mut alloc = serde_json::Map::new();
715 alloc.insert(key.clone(), Value::String("malformed".into()));
716 let mut chain_spec = json!({
717 "config": { "chainId": 421614 },
718 "alloc": alloc,
719 });
720
721 let error = inject_arbos_alloc(
722 &mut chain_spec,
723 421614,
724 10,
725 Address::ZERO,
726 genesis::ArbOSInit::default(),
727 )
728 .expect_err("a non-object alloc entry must be rejected");
729
730 assert_eq!(error.to_string(), format!("alloc[{key}] is not an object"));
731 }
732
733 #[test]
734 fn serialize_chain_config_matches_v10_default_layout() {
735 let cfg = json!({
738 "chainId": 421614,
739 "homesteadBlock": 0,
740 "daoForkSupport": true,
741 "eip150Block": 0,
742 "eip155Block": 0,
743 "eip158Block": 0,
744 "byzantiumBlock": 0,
745 "constantinopleBlock": 0,
746 "petersburgBlock": 0,
747 "istanbulBlock": 0,
748 "muirGlacierBlock": 0,
749 "berlinBlock": 0,
750 "londonBlock": 0,
751 "depositContractAddress": "0x0000000000000000000000000000000000000000",
752 "clique": {"period": 0, "epoch": 0},
753 "arbitrum": {
754 "EnableArbOS": true,
755 "AllowDebugPrecompiles": false,
756 "DataAvailabilityCommittee": false,
757 "InitialArbOSVersion": 10,
758 "InitialChainOwner": "0x71B61c2E250AFa05dFc36304D6c91501bE0965D8",
759 "GenesisBlockNum": 0u64,
760 }
761 });
762 let bytes = serialize_chain_config_go_style(&cfg);
763 let s = std::str::from_utf8(&bytes).unwrap();
764
765 let expected = "{\"chainId\":421614,\"homesteadBlock\":0,\"daoForkSupport\":true,\"eip150Block\":0,\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"berlinBlock\":0,\"londonBlock\":0,\"depositContractAddress\":\"0x0000000000000000000000000000000000000000\",\"clique\":{\"period\":0,\"epoch\":0},\"arbitrum\":{\"EnableArbOS\":true,\"AllowDebugPrecompiles\":false,\"DataAvailabilityCommittee\":false,\"InitialArbOSVersion\":10,\"InitialChainOwner\":\"0x71b61c2e250afa05dfc36304d6c91501be0965d8\",\"GenesisBlockNum\":0}}";
766 assert_eq!(s, expected, "canonical chain config bytes mismatch");
767 assert_eq!(bytes.len(), 549, "expected 549-byte serialization");
768 }
769
770 #[test]
771 fn serialize_skips_null_and_default_fields() {
772 let cfg = json!({
775 "chainId": 421614,
776 "homesteadBlock": 0,
777 "daoForkBlock": null,
778 "daoForkSupport": false,
779 "eip150Block": 0,
780 "arbitrum": {
781 "InitialArbOSVersion": 10,
782 }
783 });
784 let bytes = serialize_chain_config_go_style(&cfg);
785 let s = std::str::from_utf8(&bytes).unwrap();
786 assert!(!s.contains("daoForkBlock"));
787 assert!(!s.contains("daoForkSupport"));
788 assert!(s.contains("\"EnableArbOS\":false"));
790 assert!(s.contains("\"GenesisBlockNum\":0"));
791 assert!(s.contains("\"InitialChainOwner\":\"0x0000000000000000000000000000000000000000\""));
792 }
793
794 #[test]
795 fn serialize_includes_terminal_total_difficulty_when_set() {
796 let cfg = json!({
797 "chainId": 421614,
798 "terminalTotalDifficulty": 0,
799 "arbitrum": { "InitialArbOSVersion": 10 }
800 });
801 let bytes = serialize_chain_config_go_style(&cfg);
802 let s = std::str::from_utf8(&bytes).unwrap();
803 assert!(s.contains("\"terminalTotalDifficulty\":0"));
804 }
805
806 #[test]
807 fn serialize_lowercases_addresses_and_strips_prefix() {
808 let cfg = json!({
809 "chainId": 1,
810 "depositContractAddress": "0xABCDEF0000000000000000000000000000000123",
811 "arbitrum": {
812 "InitialChainOwner": "0xABCDEF0000000000000000000000000000000123",
813 }
814 });
815 let bytes = serialize_chain_config_go_style(&cfg);
816 let s = std::str::from_utf8(&bytes).unwrap();
817 assert!(
818 s.contains("\"depositContractAddress\":\"0xabcdef0000000000000000000000000000000123\"")
819 );
820 assert!(s.contains("\"InitialChainOwner\":\"0xabcdef0000000000000000000000000000000123\""));
821 }
822}