From c78ca1ccf3d5ab059eb5d74bbfcd1533acf72785 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 7 Jan 2025 14:04:41 -0500 Subject: [PATCH] refactor(katana): store genesis in `ChainSpec` as its JSON repr (#2871) To allow specifying the genesis file in the chain spec file in its JSON format. Basically, in the same format as if when the genesis file is passed using `--genesis`. The JSON repr is a more user facing format compared to its internal counterpart (ie `Genesis`). --- Cargo.lock | 2 + crates/katana/chain-spec/Cargo.toml | 4 + crates/katana/chain-spec/src/lib.rs | 45 +++-- crates/katana/primitives/src/class.rs | 4 + crates/katana/primitives/src/genesis/json.rs | 196 ++++++++++++++----- crates/katana/primitives/src/genesis/mod.rs | 8 +- 6 files changed, 193 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d1b036f61..44e7be1c09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8486,7 +8486,9 @@ dependencies = [ "lazy_static", "serde", "serde_json", + "similar-asserts", "starknet 0.12.0", + "tempfile", "url", ] diff --git a/crates/katana/chain-spec/Cargo.toml b/crates/katana/chain-spec/Cargo.toml index 96ae05dcaf..6657aeb3e5 100644 --- a/crates/katana/chain-spec/Cargo.toml +++ b/crates/katana/chain-spec/Cargo.toml @@ -16,5 +16,9 @@ serde_json.workspace = true starknet.workspace = true url.workspace = true +[dev-dependencies] +similar-asserts.workspace = true +tempfile.workspace = true + [features] controller = [ "katana-primitives/controller" ] diff --git a/crates/katana/chain-spec/src/lib.rs b/crates/katana/chain-spec/src/lib.rs index 0741221cbd..229d1c4bea 100644 --- a/crates/katana/chain-spec/src/lib.rs +++ b/crates/katana/chain-spec/src/lib.rs @@ -18,6 +18,7 @@ use katana_primitives::genesis::constant::{ DEFAULT_STRK_FEE_TOKEN_ADDRESS, DEFAULT_UDC_ADDRESS, ERC20_DECIMAL_STORAGE_SLOT, ERC20_NAME_STORAGE_SLOT, ERC20_SYMBOL_STORAGE_SLOT, ERC20_TOTAL_SUPPLY_STORAGE_SLOT, }; +use katana_primitives::genesis::json::GenesisJson; use katana_primitives::genesis::Genesis; use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::utils::split_u256; @@ -30,6 +31,7 @@ use url::Url; /// The rollup chain specification. #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] pub struct ChainSpec { /// The rollup network chain id. pub id: ChainId, @@ -53,6 +55,7 @@ pub struct ChainSpec { /// supported on Starknet. // TODO: include both l1 and l2 addresses #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] pub struct FeeContracts { /// L2 ETH fee token address. Used for paying pre-V3 transactions. pub eth: ContractAddress, @@ -61,6 +64,7 @@ pub struct FeeContracts { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] #[serde(tag = "type", rename_all = "camelCase")] pub enum SettlementLayer { Ethereum { @@ -102,7 +106,10 @@ impl ChainSpec { let cs = serde_json::from_str::(&content)?; let file = File::open(&cs.genesis).context("failed to open genesis file")?; - let genesis: Genesis = serde_json::from_reader(BufReader::new(file))?; + + // the genesis file is stored as its JSON representation + let genesis_json: GenesisJson = serde_json::from_reader(BufReader::new(file))?; + let genesis = Genesis::try_from(genesis_json)?; Ok(Self { genesis, @@ -126,8 +133,11 @@ impl ChainSpec { fee_contracts: self.fee_contracts, }; + // convert the genesis to its JSON representation and store it + let genesis_json = GenesisJson::try_from(self.genesis)?; + serde_json::to_writer_pretty(File::create(cfg_path)?, &stored)?; - serde_json::to_writer_pretty(File::create(stored.genesis)?, &self.genesis)?; + serde_json::to_writer_pretty(File::create(stored.genesis)?, &genesis_json)?; Ok(()) } @@ -367,6 +377,20 @@ mod tests { use super::*; + #[test] + fn chainspec_load_store_rt() { + let chainspec = ChainSpec::default(); + + // Create a temporary file and store the ChainSpec + let temp = tempfile::NamedTempFile::new().unwrap(); + chainspec.clone().store(temp.path()).unwrap(); + + // Load the ChainSpec back from the file + let loaded_chainspec = ChainSpec::load(temp.path()).unwrap(); + + similar_asserts::assert_eq!(chainspec, loaded_chainspec); + } + #[test] fn genesis_block_and_state_updates() { // setup initial states to test @@ -488,22 +512,7 @@ mod tests { let actual_block = chain_spec.block(); let actual_state_updates = chain_spec.state_updates(); - // assert individual fields of the block - - assert_eq!(actual_block.header.number, expected_block.header.number); - assert_eq!(actual_block.header.timestamp, expected_block.header.timestamp); - assert_eq!(actual_block.header.parent_hash, expected_block.header.parent_hash); - assert_eq!(actual_block.header.sequencer_address, expected_block.header.sequencer_address); - assert_eq!(actual_block.header.l1_gas_prices, expected_block.header.l1_gas_prices); - assert_eq!( - actual_block.header.l1_data_gas_prices, - expected_block.header.l1_data_gas_prices - ); - assert_eq!(actual_block.header.l1_da_mode, expected_block.header.l1_da_mode); - assert_eq!(actual_block.header.protocol_version, expected_block.header.protocol_version); - assert_eq!(actual_block.header.transaction_count, expected_block.header.transaction_count); - assert_eq!(actual_block.header.events_count, expected_block.header.events_count); - assert_eq!(actual_block.body, expected_block.body); + similar_asserts::assert_eq!(actual_block, expected_block); if cfg!(feature = "controller") { assert!(actual_state_updates.classes.len() == 4); diff --git a/crates/katana/primitives/src/class.rs b/crates/katana/primitives/src/class.rs index 780e956639..a385801016 100644 --- a/crates/katana/primitives/src/class.rs +++ b/crates/katana/primitives/src/class.rs @@ -30,6 +30,10 @@ pub enum ContractClassCompilationError { SierraCompilation(#[from] StarknetSierraCompilationError), } +// NOTE: +// Ideally, we can implement this enum as an untagged `serde` enum, so that we can deserialize from +// the raw JSON class artifact directly into this (ie +// `serde_json::from_str::(json)`). But that is not possible due to a limitation with untagged enums derivation (see https://github.com/serde-rs/serde/pull/2781). #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] diff --git a/crates/katana/primitives/src/genesis/json.rs b/crates/katana/primitives/src/genesis/json.rs index 837a6ae107..927d739dd1 100644 --- a/crates/katana/primitives/src/genesis/json.rs +++ b/crates/katana/primitives/src/genesis/json.rs @@ -13,13 +13,11 @@ use std::sync::Arc; use alloy_primitives::U256; use base64::prelude::*; use katana_cairo::cairo_vm::types::errors::program_errors::ProgramError; -use katana_cairo::lang::starknet_classes::casm_contract_class::StarknetSierraCompilationError; use serde::de::value::MapAccessDeserializer; use serde::de::Visitor; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use starknet::core::types::contract::legacy::LegacyContractClass; -use starknet::core::types::contract::{ComputeClassHashError, JsonError}; +use starknet::core::types::contract::JsonError; use super::allocation::{ DevGenesisAccount, GenesisAccount, GenesisAccountAlloc, GenesisContractAlloc, @@ -31,10 +29,12 @@ use super::constant::{ }; use super::{Genesis, GenesisAllocation}; use crate::block::{BlockHash, BlockNumber, GasPrices}; -use crate::class::{ClassHash, ContractClass, SierraContractClass}; +use crate::class::{ + ClassHash, ComputeClassHashError, ContractClass, ContractClassCompilationError, + LegacyContractClass, SierraContractClass, +}; use crate::contract::{ContractAddress, StorageKey, StorageValue}; use crate::genesis::GenesisClass; -use crate::utils::class::{parse_compiled_class_v1, parse_deprecated_compiled_class}; use crate::Felt; type Object = Map; @@ -175,9 +175,6 @@ pub enum GenesisJsonError { #[error(transparent)] ComputeClassHash(#[from] ComputeClassHashError), - #[error(transparent)] - SierraCompilation(#[from] StarknetSierraCompilationError), - #[error(transparent)] ProgramError(#[from] ProgramError), @@ -202,6 +199,9 @@ pub enum GenesisJsonError { #[error("Class name '{0}' not found in the genesis classes")] UnknownClassName(String), + #[error(transparent)] + ContractClassCompilation(#[from] ContractClassCompilationError), + #[error(transparent)] Other(#[from] anyhow::Error), } @@ -286,16 +286,22 @@ impl TryFrom for Genesis { let mut classes: BTreeMap = BTreeMap::new(); #[cfg(feature = "controller")] - // Merely a band aid fix for now. - // Adding this by default so that we can support mounting the genesis file from k8s - // ConfigMap when we embed the Controller class, and its capacity is only limited to 1MiB. - classes.insert( - CONTROLLER_CLASS_HASH, - GenesisClass { - class: CONTROLLER_ACCOUNT_CLASS.clone().into(), - compiled_class_hash: CONTROLLER_CLASS_HASH, - }, - ); + { + // Merely a band aid fix for now. + // Adding this by default so that we can support mounting the genesis file from k8s + // ConfigMap when we embed the Controller class, and its capacity is only limited to + // 1MiB. + classes.insert( + CONTROLLER_CLASS_HASH, + GenesisClass { + class: CONTROLLER_ACCOUNT_CLASS.clone().into(), + compiled_class_hash: CONTROLLER_ACCOUNT_CLASS + .clone() + .compile()? + .class_hash()?, + }, + ); + } for entry in value.classes { let GenesisClassJson { class, class_hash, name } = entry; @@ -309,36 +315,28 @@ impl TryFrom for Genesis { } }; - let sierra = serde_json::from_value::(artifact.clone()); - - let (class_hash, compiled_class_hash, class) = match sierra { - Ok(sierra) => { - let casm = parse_compiled_class_v1(artifact)?; + let (class_hash, compiled_class_hash, class) = + match serde_json::from_value::(artifact.clone()) { + Ok(class) => { + let class = ContractClass::Class(class); + let class_hash = + if let Some(hash) = class_hash { hash } else { class.class_hash()? }; + let compiled_hash = class.clone().compile()?.class_hash()?; - // check if the class hash is provided, otherwise compute it from the - // artifacts - let class = ContractClass::Class(sierra); - let class_hash = class_hash - .unwrap_or_else(|| class.class_hash().expect("failed to compute hash")); - let compiled_hash = casm.compiled_class_hash(); - - (class_hash, compiled_hash, Arc::new(class)) - } + (class_hash, compiled_hash, Arc::new(class)) + } - // if the artifact is not a sierra contract, we check if it's a legacy contract - Err(_) => { - let casm = parse_deprecated_compiled_class(artifact.clone())?; + // if the artifact is not a sierra contract, we check if it's a legacy contract + Err(_) => { + let class = serde_json::from_value::(artifact)?; - let class_hash = if let Some(class_hash) = class_hash { - class_hash - } else { - let casm: LegacyContractClass = serde_json::from_value(artifact.clone())?; - casm.class_hash()? - }; + let class = ContractClass::Legacy(class); + let class_hash = + if let Some(hash) = class_hash { hash } else { class.class_hash()? }; - (class_hash, class_hash, Arc::new(ContractClass::Legacy(casm))) - } - }; + (class_hash, class_hash, Arc::new(class)) + } + }; // if the class has a name, we add it to the lookup table to use later when we're // parsing the contracts @@ -473,6 +471,86 @@ impl TryFrom for Genesis { } } +impl TryFrom for GenesisJson { + type Error = GenesisJsonError; + + fn try_from(value: Genesis) -> Result { + let mut contracts = BTreeMap::new(); + let mut accounts = BTreeMap::new(); + let mut classes = Vec::with_capacity(value.classes.len()); + + for (hash, class) in value.classes { + // Convert the class to an artifact Value + let artifact = match &*class.class { + ContractClass::Legacy(casm) => serde_json::to_value(casm)?, + ContractClass::Class(sierra) => serde_json::to_value(sierra)?, + }; + + classes.push(GenesisClassJson { + class: PathOrFullArtifact::Artifact(artifact), + class_hash: Some(hash), + name: None, + }); + } + + for (address, allocation) in value.allocations { + match allocation { + GenesisAllocation::Account(account) => match account { + GenesisAccountAlloc::Account(acc) => { + accounts.insert( + address, + GenesisAccountJson { + nonce: acc.nonce, + private_key: None, + storage: acc.storage, + balance: acc.balance, + public_key: acc.public_key, + class: Some(ClassNameOrHash::Hash(acc.class_hash)), + }, + ); + } + GenesisAccountAlloc::DevAccount(dev_acc) => { + accounts.insert( + address, + GenesisAccountJson { + nonce: dev_acc.inner.nonce, + balance: dev_acc.inner.balance, + storage: dev_acc.inner.storage, + public_key: dev_acc.inner.public_key, + private_key: Some(dev_acc.private_key), + class: Some(ClassNameOrHash::Hash(dev_acc.inner.class_hash)), + }, + ); + } + }, + GenesisAllocation::Contract(contract) => { + contracts.insert( + address, + GenesisContractJson { + nonce: contract.nonce, + balance: contract.balance, + storage: contract.storage, + class: contract.class_hash.map(ClassNameOrHash::Hash), + }, + ); + } + } + } + + Ok(GenesisJson { + parent_hash: value.parent_hash, + state_root: value.state_root, + number: value.number, + timestamp: value.timestamp, + sequencer_address: value.sequencer_address, + gas_prices: value.gas_prices, + classes, + accounts, + contracts, + }) + } +} + impl FromStr for GenesisJson { type Err = GenesisJsonError; fn from_str(s: &str) -> Result { @@ -729,8 +807,13 @@ mod tests { ( CONTROLLER_CLASS_HASH, GenesisClass { - compiled_class_hash: CONTROLLER_CLASS_HASH, class: CONTROLLER_ACCOUNT_CLASS.clone().into(), + compiled_class_hash: CONTROLLER_ACCOUNT_CLASS + .clone() + .compile() + .unwrap() + .class_hash() + .unwrap(), }, ), ]); @@ -858,6 +941,22 @@ mod tests { } } + // We don't care what the intermediate JSON format looks like as long as the + // conversion back and forth between GenesisJson and Genesis results in equivalent Genesis + // structs + #[test] + fn genesis_conversion_rt() { + let path = PathBuf::from("./src/genesis/test-genesis.json"); + + let json = GenesisJson::load(path).unwrap(); + let genesis = Genesis::try_from(json.clone()).unwrap(); + + let json_again = GenesisJson::try_from(genesis.clone()).unwrap(); + let genesis_again = Genesis::try_from(json_again.clone()).unwrap(); + + similar_asserts::assert_eq!(genesis, genesis_again); + } + #[test] fn default_genesis_try_from_json() { let json = r#" @@ -904,7 +1003,12 @@ mod tests { CONTROLLER_CLASS_HASH, GenesisClass { class: CONTROLLER_ACCOUNT_CLASS.clone().into(), - compiled_class_hash: CONTROLLER_CLASS_HASH, + compiled_class_hash: CONTROLLER_ACCOUNT_CLASS + .clone() + .compile() + .unwrap() + .class_hash() + .unwrap(), }, ), ]); diff --git a/crates/katana/primitives/src/genesis/mod.rs b/crates/katana/primitives/src/genesis/mod.rs index a057b2d734..17597bb2f3 100644 --- a/crates/katana/primitives/src/genesis/mod.rs +++ b/crates/katana/primitives/src/genesis/mod.rs @@ -40,7 +40,6 @@ impl core::fmt::Debug for GenesisClass { } /// Genesis block configuration. -#[serde_with::serde_as] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Genesis { /// The genesis block parent hash. @@ -128,7 +127,12 @@ impl Default for Genesis { ( CONTROLLER_CLASS_HASH, GenesisClass { - compiled_class_hash: CONTROLLER_CLASS_HASH, + compiled_class_hash: CONTROLLER_ACCOUNT_CLASS + .clone() + .compile() + .expect("failed to compile") + .class_hash() + .expect("failed to compute class hash"), class: CONTROLLER_ACCOUNT_CLASS.clone().into(), }, ),