diff --git a/.gitignore b/.gitignore index 62e1e125a..638826373 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ perf.data* .DS_Store zerostate.json +zerostate.boc diff --git a/Cargo.lock b/Cargo.lock index 985930ee7..c4ef3a85f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,14 +587,15 @@ dependencies = [ [[package]] name = "everscale-crypto" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b3e4fc7882223c86a7cfd8ccdb58e017b89a9f91d90114beafa0e8d35b45fb" +checksum = "0b0304a55e328ca4f354e59e6816bccb43b03f681b85b31c6bd10ea7233d62b5" dependencies = [ "curve25519-dalek", "generic-array", "hex", "rand", + "serde", "sha2", "tl-proto", ] @@ -602,7 +603,7 @@ dependencies = [ [[package]] name = "everscale-types" version = "0.1.0-rc.6" -source = "git+https://github.com/broxus/everscale-types.git#254e80a9244b6d763f7ca3a47eadbef32fb0bf3c" +source = "git+https://github.com/broxus/everscale-types.git#40f2cd862ede93943a254351fe4eea313be83233" dependencies = [ "ahash", "base64 0.21.7", @@ -622,7 +623,7 @@ dependencies = [ [[package]] name = "everscale-types-proc" version = "0.1.4" -source = "git+https://github.com/broxus/everscale-types.git#254e80a9244b6d763f7ca3a47eadbef32fb0bf3c" +source = "git+https://github.com/broxus/everscale-types.git#40f2cd862ede93943a254351fe4eea313be83233" dependencies = [ "proc-macro2", "quote", @@ -1615,6 +1616,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2106,6 +2117,9 @@ dependencies = [ "rustc_version", "serde", "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", "tikv-jemallocator", "tokio", "tycho-network", diff --git a/Cargo.toml b/Cargo.toml index cc29690e3..1c8bcf22a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ clap = { version = "4.5.3", features = ["derive"] } crc = "3.0.1" dashmap = "5.4" ed25519 = "2.0" -everscale-crypto = { version = "0.2", features = ["tl-proto"] } +everscale-crypto = { version = "0.2", features = ["tl-proto", "serde"] } everscale-types = "0.1.0-rc.6" exponential-backoff = "1" fdlimit = "0.3.0" @@ -64,6 +64,7 @@ rustls = { version = "0.21", features = ["dangerous_configuration"] } rustls-webpki = "0.101" serde = "1.0" serde_json = "1.0.114" +serde_path_to_error = "0.1" sha2 = "0.10.8" smallvec = "1.13.1" socket2 = "0.5" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5e33c2dfb..ce4df01fc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -23,7 +23,10 @@ everscale-types = { workspace = true } hex = { workspace = true } rand = { workspace = true } serde = { workspace = true } +serde_path_to_error = { workspace = true } serde_json = { workspace = true, features = ["preserve_order"] } +sha2 = { workspace = true } +thiserror = { workspace = true } tikv-jemallocator = { workspace = true, features = [ "unprefixed_malloc_on_supported_platforms", "background_threads", diff --git a/cli/src/tools/gen_account.rs b/cli/src/tools/gen_account.rs index 5d73a48c0..24631f658 100644 --- a/cli/src/tools/gen_account.rs +++ b/cli/src/tools/gen_account.rs @@ -295,7 +295,7 @@ impl MultisigBuilder { let mut data = CellBuilder::new(); // Write headers - data.store_u256(HashBytes::wrap(self.pubkey.as_bytes()))?; + data.store_u256(&self.pubkey)?; data.store_u64(0)?; // time data.store_bit_one()?; // constructor flag @@ -368,7 +368,7 @@ impl GiverBuilder { let mut data = CellBuilder::new(); // Append pubkey first - data.store_u256(HashBytes::wrap(self.pubkey.as_bytes()))?; + data.store_u256(&self.pubkey)?; // Append everything except the pubkey let prev_data = state_init diff --git a/cli/src/tools/gen_zerostate.rs b/cli/src/tools/gen_zerostate.rs index 9d47e96c8..2e98af1d9 100644 --- a/cli/src/tools/gen_zerostate.rs +++ b/cli/src/tools/gen_zerostate.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use std::io::IsTerminal; use std::path::PathBuf; +use std::sync::OnceLock; use anyhow::Result; use everscale_crypto::ed25519; @@ -7,7 +9,7 @@ use everscale_types::models::*; use everscale_types::num::Tokens; use everscale_types::prelude::*; use serde::{Deserialize, Serialize}; -use tycho_util::serde_helpers; +use sha2::Digest; use crate::util::compute_storage_used; @@ -16,7 +18,7 @@ use crate::util::compute_storage_used; pub struct Cmd { /// dump the template of the zero state config #[clap(short = 'i', long, exclusive = true)] - init_config: Option, + init_config: Option, /// path to the zero state config #[clap(required_unless_present = "init_config")] @@ -29,48 +31,336 @@ pub struct Cmd { /// explicit unix timestamp of the zero state #[clap(long)] now: Option, + + #[clap(short, long)] + force: bool, } impl Cmd { pub fn run(self) -> Result<()> { match self.init_config { - Some(path) => { - let config = ZerostateConfig::default(); - std::fs::write(path, serde_json::to_string_pretty(&config).unwrap())?; - Ok(()) - } - None => Ok(()), + Some(path) => write_default_config(&path, self.force), + None => generate_zerostate( + &self.config.unwrap(), + &self.output.unwrap(), + self.now.unwrap_or_else(tycho_util::time::now_sec), + self.force, + ), } } } +fn write_default_config(config_path: &PathBuf, force: bool) -> Result<()> { + if config_path.exists() && !force { + anyhow::bail!("config file already exists, use --force to overwrite"); + } + + let config = ZerostateConfig::default(); + std::fs::write(config_path, serde_json::to_string_pretty(&config).unwrap())?; + Ok(()) +} + +fn generate_zerostate( + config_path: &PathBuf, + output_path: &PathBuf, + now: u32, + force: bool, +) -> Result<()> { + if output_path.exists() && !force { + anyhow::bail!("output file already exists, use --force to overwrite"); + } + + let mut config = { + let data = std::fs::read_to_string(config_path)?; + let de = &mut serde_json::Deserializer::from_str(&data); + serde_path_to_error::deserialize::<_, ZerostateConfig>(de)? + }; + + config + .prepare_config_params(now) + .map_err(|e| GenError::new("validator config is invalid", e))?; + + config + .add_required_accounts() + .map_err(|e| GenError::new("failed to add required accounts", e))?; + + let state = config + .build_masterchain_state(now) + .map_err(|e| GenError::new("failed to build masterchain zerostate", e))?; + + let boc = CellBuilder::build_from(&state) + .map_err(|e| GenError::new("failed to serialize zerostate", e))?; + + let root_hash = *boc.repr_hash(); + let data = Boc::encode(&boc); + let file_hash = HashBytes::from(sha2::Sha256::digest(&data)); + + std::fs::write(output_path, data) + .map_err(|e| GenError::new("failed to write masterchain zerostate", e))?; + + let hashes = serde_json::json!({ + "root_hash": root_hash, + "file_hash": file_hash, + }); + + let output = if std::io::stdin().is_terminal() { + serde_json::to_string_pretty(&hashes) + } else { + serde_json::to_string(&hashes) + }?; + println!("{output}"); + Ok(()) +} + #[derive(Serialize, Deserialize)] struct ZerostateConfig { global_id: i32, - #[serde(with = "serde_helpers::public_key")] config_public_key: ed25519::PublicKey, - #[serde(with = "serde_helpers::public_key")] - minter_public_key: ed25519::PublicKey, + #[serde(default)] + minter_public_key: Option, + + config_balance: Tokens, + elector_balance: Tokens, #[serde(with = "serde_account_states")] accounts: HashMap, + validators: Vec, + params: BlockchainConfigParams, } +impl ZerostateConfig { + fn prepare_config_params(&mut self, now: u32) -> Result<()> { + let Some(config_address) = self.params.get::()? else { + anyhow::bail!("config address is not set (param 0)"); + }; + let Some(elector_address) = self.params.get::()? else { + anyhow::bail!("elector address is not set (param 1)"); + }; + let minter_address = self.params.get::()?; + + if self.params.get::()?.is_none() { + self.params + .set::(&ExtraCurrencyCollection::new())?; + } + + anyhow::ensure!( + self.params.get::()?.is_some(), + "required params list is required (param 9)" + ); + + { + let Some(mut workchains) = self.params.get::()? else { + anyhow::bail!("workchains are not set (param 12)"); + }; + + let mut updated = false; + for entry in workchains.clone().iter() { + let (id, mut workchain) = entry?; + anyhow::ensure!( + id != ShardIdent::MASTERCHAIN.workchain(), + "masterchain is not configurable" + ); + + if workchain.zerostate_root_hash != HashBytes::ZERO { + continue; + } + + let shard_ident = ShardIdent::new_full(id); + let shard_state = make_shard_state(self.global_id, shard_ident, now); + + let cell = CellBuilder::build_from(&shard_state)?; + workchain.zerostate_root_hash = *cell.repr_hash(); + let bytes = Boc::encode(&cell); + workchain.zerostate_file_hash = sha2::Sha256::digest(bytes).into(); + + workchains.set(id, &workchain)?; + updated = true; + } + + if updated { + self.params.set_workchains(&workchains)?; + } + } + + { + let mut fundamental_addresses = self.params.get::()?.unwrap_or_default(); + fundamental_addresses.set(config_address, ())?; + fundamental_addresses.set(elector_address, ())?; + if let Some(minter_address) = minter_address { + fundamental_addresses.set(minter_address, ())?; + } + self.params.set::(&fundamental_addresses)?; + } + + for id in 32..=37 { + anyhow::ensure!( + !self.params.contains_raw(id)?, + "config param {id} must not be set manually as it is managed by the tool" + ); + } + + { + const VALIDATOR_WEIGHT: u64 = 1; + + anyhow::ensure!(!self.validators.is_empty(), "validator set is empty"); + + let mut validator_set = ValidatorSet { + utime_since: now, + utime_until: now, + main: (self.validators.len() as u16).try_into().unwrap(), + total_weight: 0, + list: Vec::with_capacity(self.validators.len()), + }; + for pubkey in &self.validators { + validator_set.list.push(ValidatorDescription { + public_key: HashBytes::from(*pubkey.as_bytes()), + weight: VALIDATOR_WEIGHT, + adnl_addr: None, + mc_seqno_since: 0, + prev_total_weight: validator_set.total_weight, + }); + validator_set.total_weight += VALIDATOR_WEIGHT; + } + + self.params.set::(&validator_set)?; + } + + let mandatory_params = self.params.get::()?.unwrap(); + for entry in mandatory_params.keys() { + let id = entry?; + anyhow::ensure!( + self.params.contains_raw(id)?, + "required param {id} is not set" + ); + } + + Ok(()) + } + + fn add_required_accounts(&mut self) -> Result<()> { + // Config + let Some(config_address) = self.params.get::()? else { + anyhow::bail!("config address is not set (param 0)"); + }; + anyhow::ensure!( + &self.config_public_key != zero_public_key(), + "config public key is not set" + ); + self.accounts.insert( + config_address, + build_config_account( + &self.config_public_key, + &config_address, + self.config_balance, + )? + .into(), + ); + + // Elector + let Some(elector_address) = self.params.get::()? else { + anyhow::bail!("elector address is not set (param 1)"); + }; + self.accounts.insert( + elector_address, + build_elector_code(&elector_address, self.elector_balance)?.into(), + ); + + // Minter + match (&self.minter_public_key, self.params.get::()?) { + (Some(public_key), Some(minter_address)) => { + anyhow::ensure!( + public_key != zero_public_key(), + "minter public key is invalid" + ); + self.accounts.insert( + minter_address, + build_minter_account(&public_key, &minter_address)?.into(), + ); + } + (None, Some(_)) => anyhow::bail!("minter_public_key is required"), + (Some(_), None) => anyhow::bail!("minter address is not set (param 2)"), + (None, None) => {} + } + + // Done + Ok(()) + } + + fn build_masterchain_state(&self, now: u32) -> Result { + let mut state = make_shard_state(self.global_id, ShardIdent::MASTERCHAIN, now); + + for account in self.accounts.values() { + if let Some(account) = account.as_ref() { + state.total_balance = state + .total_balance + .checked_add(&account.balance) + .map_err(|e| GenError::new("failed ot compute total balance", e))?; + } + } + + state.custom = Some(Lazy::new(&McStateExtra { + shards: Default::default(), + config: BlockchainConfig { + address: self.params.get::()?.unwrap(), + params: self.params.clone(), + }, + validator_info: ValidatorInfo { + validator_list_hash_short: 0, + catchain_seqno: 0, + nx_cc_updated: true, + }, + prev_blocks: AugDict::new(), + after_key_block: true, + last_key_block: None, + block_create_stats: None, + global_balance: state.total_balance.clone(), + copyleft_rewards: Dict::new(), + })?); + + Ok(state) + } +} + impl Default for ZerostateConfig { fn default() -> Self { Self { global_id: 0, - config_public_key: ed25519::PublicKey::from_bytes([0; 32]).unwrap(), - minter_public_key: ed25519::PublicKey::from_bytes([0; 32]).unwrap(), + config_public_key: *zero_public_key(), + minter_public_key: None, + config_balance: Tokens::new(500_000_000_000), // 500 + elector_balance: Tokens::new(500_000_000_000), // 500 accounts: Default::default(), + validators: Default::default(), params: make_default_params().unwrap(), } } } +fn make_shard_state(global_id: i32, shard_ident: ShardIdent, now: u32) -> ShardStateUnsplit { + ShardStateUnsplit { + global_id, + shard_ident, + seqno: 0, + vert_seqno: 0, + gen_utime: now, + gen_lt: 0, + min_ref_mc_seqno: u32::MAX, + out_msg_queue_info: Default::default(), + before_split: false, + accounts: Lazy::new(&Default::default()).unwrap(), + overload_history: 0, + underload_history: 0, + total_balance: CurrencyCollection::ZERO, + total_validator_fees: CurrencyCollection::ZERO, + libraries: Dict::new(), + master_ref: None, + custom: None, + } +} + fn make_default_params() -> Result { let mut params = BlockchainConfig::new_empty(HashBytes([0x55; 32])).params; @@ -127,7 +417,29 @@ fn make_default_params() -> Result { })?, })?; - // Param 12 will always be overwritten + // Param 12 + { + let mut workchains = Dict::new(); + workchains.set( + 0, + WorkchainDescription { + enabled_since: 0, + actual_min_split: 0, + min_split: 0, + max_split: 3, + active: true, + accept_msgs: true, + zerostate_root_hash: HashBytes::ZERO, + zerostate_file_hash: HashBytes::ZERO, + version: 0, + format: WorkchainFormat::Basic(WorkchainFormatBasic { + vm_version: 0, + vm_mode: 0, + }), + }, + )?; + params.set::(&workchains)?; + } // Param 14 params.set_block_creation_rewards(&BlockCreationRewards { @@ -223,7 +535,7 @@ fn make_default_params() -> Result { // Param 23 (basechain) params.set_block_limits( - true, + false, &BlockLimits { bytes: BlockParamLimits { underload: 131072, @@ -302,42 +614,6 @@ fn make_default_params() -> Result { Ok(params) } -fn build_minter_account(pubkey: &ed25519::PublicKey) -> Result { - const MINTER_STATE: &[u8] = include_bytes!("../../res/minter_state.boc"); - - let mut account = BocRepr::decode::(MINTER_STATE)? - .0 - .expect("invalid minter state"); - - match &mut account.state { - AccountState::Active(state_init) => { - let mut data = CellBuilder::new(); - - // Append pubkey first - data.store_u256(HashBytes::wrap(pubkey.as_bytes()))?; - - // Append everything except the pubkey - let prev_data = state_init - .data - .take() - .expect("minter state must contain data"); - let mut prev_data = prev_data.as_slice()?; - prev_data.advance(256, 0)?; - - data.store_slice(prev_data)?; - - // Update data - state_init.data = Some(data.build()?); - } - _ => unreachable!("saved state is for the active account"), - }; - - account.balance = CurrencyCollection::ZERO; - account.storage_stat.used = compute_storage_used(&account)?; - - Ok(account) -} - fn build_config_account( pubkey: &ed25519::PublicKey, address: &HashBytes, @@ -350,7 +626,7 @@ fn build_config_account( let mut data = CellBuilder::new(); data.store_reference(Cell::empty_cell())?; data.store_u32(0)?; - data.store_u256(HashBytes::wrap(pubkey.as_bytes()))?; + data.store_u256(pubkey)?; data.store_bit_zero()?; let data = data.build()?; @@ -412,15 +688,60 @@ fn build_elector_code(address: &HashBytes, balance: Tokens) -> Result { Ok(account) } +fn build_minter_account(pubkey: &ed25519::PublicKey, address: &HashBytes) -> Result { + const MINTER_STATE: &[u8] = include_bytes!("../../res/minter_state.boc"); + + let mut account = BocRepr::decode::(MINTER_STATE)? + .0 + .expect("invalid minter state"); + + match &mut account.state { + AccountState::Active(state_init) => { + // Append everything except the pubkey + let mut data = CellBuilder::new(); + data.store_u32(0)?; + data.store_u256(pubkey)?; + + // Update data + state_init.data = Some(data.build()?); + } + _ => unreachable!("saved state is for the active account"), + }; + + account.address = StdAddr::new(-1, *address).into(); + account.balance = CurrencyCollection::ZERO; + account.storage_stat.used = compute_storage_used(&account)?; + + Ok(account) +} + +fn zero_public_key() -> &'static ed25519::PublicKey { + static KEY: OnceLock = OnceLock::new(); + KEY.get_or_init(|| ed25519::PublicKey::from_bytes([0; 32]).unwrap()) +} + +#[derive(thiserror::Error, Debug)] +#[error("{context}: {source}")] +struct GenError { + context: String, + #[source] + source: anyhow::Error, +} + +impl GenError { + fn new(context: impl Into, source: impl Into) -> Self { + Self { + context: context.into(), + source: source.into(), + } + } +} + mod serde_account_states { - use std::collections::HashMap; + use super::*; - use everscale_types::boc::BocRepr; - use everscale_types::cell::HashBytes; - use everscale_types::models::OptionalAccount; use serde::de::Deserializer; use serde::ser::{SerializeMap, Serializer}; - use serde::{Deserialize, Serialize}; pub fn serialize( value: &HashMap, @@ -431,9 +752,7 @@ mod serde_account_states { { #[derive(Serialize)] #[repr(transparent)] - struct WrapperValue<'a>( - #[serde(serialize_with = "BocRepr::serialize")] &'a OptionalAccount, - ); + struct WrapperValue<'a>(#[serde(with = "BocRepr")] &'a OptionalAccount); let mut ser = serializer.serialize_map(Some(value.len()))?; for (key, value) in value { diff --git a/util/src/serde_helpers.rs b/util/src/serde_helpers.rs index 9cee7e93d..0d2ddd9ce 100644 --- a/util/src/serde_helpers.rs +++ b/util/src/serde_helpers.rs @@ -5,58 +5,6 @@ use std::str::FromStr; use serde::de::{Error, Expected, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -pub mod public_key { - use everscale_crypto::ed25519; - - use super::*; - - pub fn serialize( - value: &ed25519::PublicKey, - serializer: S, - ) -> Result { - hex_byte_array::serialize(value.as_bytes(), serializer) - } - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result { - hex_byte_array::deserialize(deserializer).and_then(|bytes| { - ed25519::PublicKey::from_bytes(bytes).ok_or_else(|| Error::custom("invalid public key")) - }) - } -} - -pub mod hex_byte_array { - use super::*; - - pub fn serialize( - value: &dyn AsRef<[u8]>, - serializer: S, - ) -> Result { - if serializer.is_human_readable() { - serializer.serialize_str(&hex::encode(value.as_ref())) - } else { - serializer.serialize_bytes(value.as_ref()) - } - } - - pub fn deserialize<'de, D: Deserializer<'de>, const N: usize>( - deserializer: D, - ) -> Result<[u8; N], D::Error> { - if deserializer.is_human_readable() { - deserializer.deserialize_str(HexVisitor).and_then(|bytes| { - let len = bytes.len(); - match <[u8; N]>::try_from(bytes) { - Ok(bytes) => Ok(bytes), - Err(_) => Err(Error::invalid_length(len, &"32 bytes")), - } - }) - } else { - deserializer.deserialize_bytes(BytesVisitor::) - } - } -} - pub mod socket_addr { use std::net::SocketAddr;