diff --git a/Cargo.lock b/Cargo.lock index 1f747e059..e345d655e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8533,6 +8533,7 @@ dependencies = [ "futures", "hotshot-contract-adapter", "portpicker", + "serde", "serde_json", "surf", "tempfile", diff --git a/builder/src/non_permissioned.rs b/builder/src/non_permissioned.rs index 8b3a411c0..8f662bf5e 100644 --- a/builder/src/non_permissioned.rs +++ b/builder/src/non_permissioned.rs @@ -158,7 +158,7 @@ impl BuilderConfig { maximize_txns_count_timeout_duration, instance_state .chain_config - .base_fee() + .base_fee .as_u64() .context("the base fee exceeds the maximum amount that a builder can pay (defined by u64::MAX)")?, Arc::new(instance_state), diff --git a/builder/src/permissioned.rs b/builder/src/permissioned.rs index 3aeb2cadc..a69985d3e 100644 --- a/builder/src/permissioned.rs +++ b/builder/src/permissioned.rs @@ -443,7 +443,7 @@ impl Payload { block_size += size_of::() as u64; } - if block_size > chain_config.max_block_size { + if block_size > *chain_config.max_block_size { break; } @@ -373,7 +373,7 @@ mod test { let n_txs = target_payload_total as u64 / tx_size; let chain_config = ChainConfig { - max_block_size, + max_block_size: max_block_size.into(), ..Default::default() }; diff --git a/sequencer/src/chain_config.rs b/sequencer/src/chain_config.rs index 09c03749a..15c06aa38 100644 --- a/sequencer/src/chain_config.rs +++ b/sequencer/src/chain_config.rs @@ -2,41 +2,86 @@ use crate::{ options::parse_size, state::{FeeAccount, FeeAmount}, }; -use anyhow::{bail, Context}; use committable::{Commitment, Committable}; -use derive_more::{From, Into}; +use derive_more::{Deref, Display, From, Into}; use ethers::types::{Address, U256}; use itertools::Either; -use sequencer_utils::impl_to_fixed_bytes; +use sequencer_utils::{ + impl_serde_from_string_or_integer, impl_to_fixed_bytes, serde::FromStringOrInteger, +}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -#[derive(Default, Hash, Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, From, Into)] +#[derive(Default, Hash, Copy, Clone, Debug, Display, PartialEq, Eq, From, Into)] +#[display(fmt = "{_0}")] pub struct ChainId(U256); +impl_serde_from_string_or_integer!(ChainId); impl_to_fixed_bytes!(ChainId, U256); +impl FromStringOrInteger for ChainId { + type Binary = U256; + type Integer = u64; + + fn from_binary(b: Self::Binary) -> anyhow::Result { + Ok(Self(b)) + } + + fn from_integer(i: Self::Integer) -> anyhow::Result { + Ok(i.into()) + } + + fn from_string(s: String) -> anyhow::Result { + if s.starts_with("0x") { + Ok(Self(U256::from_str(&s)?)) + } else { + Ok(Self(U256::from_dec_str(&s)?)) + } + } + + fn to_binary(&self) -> anyhow::Result { + Ok(self.0) + } + + fn to_string(&self) -> anyhow::Result { + Ok(format!("{self}")) + } +} + impl From for ChainId { fn from(id: u64) -> Self { Self(id.into()) } } -impl ChainId { - pub fn from_toml(toml: &toml::Value) -> anyhow::Result { - if let Some(s) = toml.as_str() { - if s.starts_with("0x") { - Ok(Self(U256::from_str(s)?)) - } else { - Ok(Self(U256::from_dec_str(s)?)) - } - } else if let Some(n) = toml.as_integer() { - Ok(u64::try_from(n) - .context("must be an unsigned integer")? - .into()) - } else { - bail!("must be an integer or an integral string"); - } +#[derive(Hash, Copy, Clone, Debug, Default, Display, PartialEq, Eq, From, Into, Deref)] +#[display(fmt = "{_0}")] +pub struct BlockSize(u64); + +impl_serde_from_string_or_integer!(BlockSize); + +impl FromStringOrInteger for BlockSize { + type Binary = u64; + type Integer = u64; + + fn from_binary(b: Self::Binary) -> anyhow::Result { + Ok(Self(b)) + } + + fn from_integer(i: Self::Integer) -> anyhow::Result { + Ok(Self(i)) + } + + fn from_string(s: String) -> anyhow::Result { + Ok(parse_size(&s)?.into()) + } + + fn to_binary(&self) -> anyhow::Result { + Ok(self.0) + } + + fn to_string(&self) -> anyhow::Result { + Ok(format!("{self}")) } } @@ -47,7 +92,7 @@ pub struct ChainConfig { pub chain_id: ChainId, /// Maximum size in bytes of a block - pub max_block_size: u64, + pub max_block_size: BlockSize, /// Minimum fee in WEI per byte of payload pub base_fee: FeeAmount, @@ -71,7 +116,7 @@ impl Default for ChainConfig { fn default() -> Self { Self { chain_id: U256::from(35353).into(), // arbitrarily chosen chain ID - max_block_size: 10240, + max_block_size: 10240.into(), base_fee: 0.into(), fee_contract: None, fee_recipient: Default::default(), @@ -79,55 +124,6 @@ impl Default for ChainConfig { } } -impl ChainConfig { - pub fn from_toml(toml: &toml::Value) -> anyhow::Result { - let cfg = toml.as_table().context("must be table")?; - let chain_id = ChainId::from_toml(cfg.get("chain_id").context("missing chain_id")?) - .context("invalid chain ID")?; - let max_block_size = match cfg - .get("max_block_size") - .context("missing max_block_size")? - { - toml::Value::String(s) => parse_size(s).context("invalid max block size")?, - toml::Value::Integer(n) => (*n) - .try_into() - .context("max_block_size must be an unsigned integer")?, - _ => bail!("max_block_size must be an integer or an integral string"), - }; - let base_fee = FeeAmount::from_toml(cfg.get("base_fee").context("missing base_fee")?) - .context("invalid base fee")?; - let fee_contract = match cfg.get("fee_contract") { - Some(toml::Value::String(s)) => { - Some(s.parse().context("invalid fee_contract address")?) - } - Some(_) => bail!("fee_contract must be an address string"), - None => None, - }; - let fee_recipient = cfg - .get("fee_recipient") - .context("missing fee_recipient")? - .as_str() - .context("fee_recipient must be an address string")? - .parse() - .context("invalid fee_recipient")?; - Ok(Self { - chain_id, - max_block_size, - base_fee, - fee_contract, - fee_recipient, - }) - } - - pub fn max_block_size(&self) -> u64 { - self.max_block_size - } - - pub fn base_fee(&self) -> FeeAmount { - self.base_fee - } -} - impl Committable for ChainConfig { fn tag() -> String { "CHAIN_CONFIG".to_string() @@ -136,7 +132,7 @@ impl Committable for ChainConfig { fn commit(&self) -> Commitment { let comm = committable::RawCommitmentBuilder::new(&Self::tag()) .fixed_size_field("chain_id", &self.chain_id.to_fixed_bytes()) - .u64_field("max_block_size", self.max_block_size) + .u64_field("max_block_size", *self.max_block_size) .fixed_size_field("base_fee", &self.base_fee.to_fixed_bytes()) .fixed_size_field("fee_recipient", &self.fee_recipient.to_fixed_bytes()); let comm = if let Some(addr) = self.fee_contract { diff --git a/sequencer/src/genesis.rs b/sequencer/src/genesis.rs index 69b5d29a0..3991cd2a0 100644 --- a/sequencer/src/genesis.rs +++ b/sequencer/src/genesis.rs @@ -5,7 +5,7 @@ use crate::{ }; use anyhow::Context; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, path::Path, str::FromStr}; +use std::{collections::HashMap, path::Path}; /// Initial configuration of an Espresso stake table. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] @@ -17,9 +17,10 @@ pub struct StakeTableConfig { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Genesis { pub chain_config: ChainConfig, + pub stake_table: StakeTableConfig, + #[serde(default)] pub accounts: HashMap, pub l1_finalized: Option, - pub stake_table: StakeTableConfig, } impl Genesis { @@ -27,52 +28,7 @@ impl Genesis { let path = path.as_ref(); let bytes = std::fs::read(path).context(format!("genesis file {}", path.display()))?; let text = std::str::from_utf8(&bytes).context("genesis file must be UTF-8")?; - let toml: toml::Value = toml::from_str(text).context("malformed genesis file")?; - Self::from_toml(&toml).context("malformed genesis file") - } - - pub fn from_toml(toml: &toml::Value) -> anyhow::Result { - let genesis = toml.as_table().context("must be a TOML table")?; - let chain_config = ChainConfig::from_toml( - genesis - .get("chain_config") - .context("missing chain_config section")?, - ) - .context("invalid chain config section")?; - let accounts = match toml.get("accounts") { - Some(accounts) => { - let accounts = accounts - .as_table() - .context("accounts section must be a table")?; - accounts - .iter() - .map(|(account, value)| { - Ok(( - FeeAccount::from_str(account) - .context(format!("invalid account {account}"))?, - FeeAmount::from_toml(value) - .context(format!("invalid value for account {account}"))?, - )) - }) - .collect::>()? - } - None => Default::default(), - }; - let l1_finalized = toml - .get("l1_finalized") - .map(|toml| L1BlockInfo::from_toml(toml).context("ivnalid L1 finalized block")) - .transpose()?; - let stake_table = toml::from_str(&toml::to_string( - toml.get("stake_table").context("missing stake_table")?, - )?) - .context("invalid stake table")?; - - Ok(Self { - chain_config, - accounts, - l1_finalized, - stake_table, - }) + toml::from_str(text).context("malformed genesis file") } } @@ -104,15 +60,15 @@ mod test { timestamp = "0x123def" hash = "0x80f5dd11f2bdda2814cb1ad94ef30a47de02cf28ad68c89e104c00c4e51bb7a5" } - .into(); + .to_string(); - let genesis = Genesis::from_toml(&toml).unwrap(); + let genesis: Genesis = toml::from_str(&toml).unwrap(); assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 }); assert_eq!( genesis.chain_config, ChainConfig { chain_id: 12345.into(), - max_block_size: 30000, + max_block_size: 30000.into(), base_fee: 1.into(), fee_recipient: FeeAccount::default(), fee_contract: Some(Address::default()) @@ -159,15 +115,15 @@ mod test { base_fee = 1 fee_recipient = "0x0000000000000000000000000000000000000000" } - .into(); + .to_string(); - let genesis = Genesis::from_toml(&toml).unwrap(); + let genesis: Genesis = toml::from_str(&toml).unwrap(); assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 }); assert_eq!( genesis.chain_config, ChainConfig { chain_id: 12345.into(), - max_block_size: 30000, + max_block_size: 30000.into(), base_fee: 1.into(), fee_recipient: FeeAccount::default(), fee_contract: None, @@ -189,11 +145,11 @@ mod test { base_fee = "1 gwei" fee_recipient = "0x0000000000000000000000000000000000000000" } - .into(); + .to_string(); - let genesis = Genesis::from_toml(&toml).unwrap(); + let genesis: Genesis = toml::from_str(&toml).unwrap(); assert_eq!(genesis.stake_table, StakeTableConfig { capacity: 10 }); - assert_eq!(genesis.chain_config.max_block_size, 30000000); + assert_eq!(*genesis.chain_config.max_block_size, 30000000); assert_eq!(genesis.chain_config.base_fee, 1_000_000_000.into()); } } diff --git a/sequencer/src/l1_client.rs b/sequencer/src/l1_client.rs index cdca5546a..a2e031593 100644 --- a/sequencer/src/l1_client.rs +++ b/sequencer/src/l1_client.rs @@ -41,12 +41,6 @@ pub struct L1BlockInfo { pub hash: H256, } -impl L1BlockInfo { - pub fn from_toml(toml: &toml::Value) -> anyhow::Result { - Ok(toml::from_str(&toml::to_string(toml)?)?) - } -} - impl PartialOrd for L1BlockInfo { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) diff --git a/sequencer/src/state.rs b/sequencer/src/state.rs index b4c13394f..48f6415b8 100644 --- a/sequencer/src/state.rs +++ b/sequencer/src/state.rs @@ -42,7 +42,9 @@ use jf_merkle_tree::{ }; use jf_vid::VidScheme; use num_traits::CheckedSub; -use sequencer_utils::impl_to_fixed_bytes; +use sequencer_utils::{ + impl_serde_from_string_or_integer, impl_to_fixed_bytes, serde::FromStringOrInteger, +}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::time::Duration; @@ -225,7 +227,7 @@ pub fn validate_proposal( // validate block size and fee let block_size = VidSchemeType::get_payload_byte_len(vid_common) as u64; anyhow::ensure!( - block_size < expected_chain_config.max_block_size, + block_size < *expected_chain_config.max_block_size, anyhow::anyhow!( "Invalid Payload Size: local={:?}, proposal={:?}", expected_chain_config, @@ -921,8 +923,6 @@ impl Committable for FeeInfo { Clone, Debug, Display, - Deserialize, - Serialize, PartialEq, Eq, PartialOrd, @@ -936,6 +936,7 @@ impl Committable for FeeInfo { #[display(fmt = "{_0}")] pub struct FeeAmount(U256); +impl_serde_from_string_or_integer!(FeeAmount); impl_to_fixed_bytes!(FeeAmount, U256); impl From for FeeAmount { @@ -964,6 +965,56 @@ impl FromStr for FeeAmount { } } +impl FromStringOrInteger for FeeAmount { + type Binary = U256; + type Integer = u64; + + fn from_binary(b: Self::Binary) -> anyhow::Result { + Ok(Self(b)) + } + + fn from_integer(i: Self::Integer) -> anyhow::Result { + Ok(i.into()) + } + + fn from_string(s: String) -> anyhow::Result { + // Interpret the integer as hex if the string starts with 0x. + let (s, hex) = match s.strip_prefix("0x") { + Some(s) => (s, true), + None => (s.as_str(), false), + }; + // Strip an optional non-numeric suffix, which will be interpreted as a unit. + let (s, multiplier) = match s.split_once(char::is_whitespace) { + Some((s, unit)) => { + let multiplier = match unit.to_lowercase().as_str() { + "wei" => 1u64, + "gwei" => 1_000_000_000, + "eth" | "ether" => 1_000_000_000_000_000_000, + unit => bail!("unrecognized unit {unit}"), + }; + (s, multiplier) + } + None => (s, 1), + }; + // Parse the base amount as an integer. + let base = if hex { + s.parse()? + } else { + U256::from_dec_str(s)? + }; + + Ok(Self(base * multiplier)) + } + + fn to_binary(&self) -> anyhow::Result { + Ok(self.0) + } + + fn to_string(&self) -> anyhow::Result { + Ok(format!("{self}")) + } +} + impl FeeAmount { pub fn as_u64(&self) -> Option { if self.0 <= u64::MAX.into() { @@ -972,43 +1023,6 @@ impl FeeAmount { None } } - - pub fn from_toml(toml: &toml::Value) -> anyhow::Result { - match toml { - toml::Value::String(s) => { - // Interpret the integer as hex if the string starts with 0x. - let (s, hex) = match s.strip_prefix("0x") { - Some(s) => (s, true), - None => (s.as_str(), false), - }; - // Strip an optional non-numeric suffix, which will be interpreted as a unit. - let (s, multiplier) = match s.split_once(char::is_whitespace) { - Some((s, unit)) => { - let multiplier = match unit.to_lowercase().as_str() { - "wei" => 1u64, - "gwei" => 1_000_000_000, - "eth" | "ether" => 1_000_000_000_000_000_000, - unit => bail!("unrecognized unit {unit}"), - }; - (s, multiplier) - } - None => (s, 1), - }; - // Parse the base amount as an integer. - let base = if hex { - s.parse()? - } else { - U256::from_dec_str(s)? - }; - - Ok(Self(base * multiplier)) - } - toml::Value::Integer(n) => Ok(u64::try_from(*n) - .context("must be an unsigned integer")? - .into()), - _ => bail!("must be an integer or an integral string"), - } - } } // New Type for `Address` in order to implement `CanonicalSerialize` and @@ -1327,7 +1341,7 @@ mod test { let state = ValidatedState::default(); let instance = NodeState::mock().with_chain_config(ChainConfig { - max_block_size: MAX_BLOCK_SIZE as u64, + max_block_size: (MAX_BLOCK_SIZE as u64).into(), base_fee: 0.into(), ..Default::default() }); @@ -1352,7 +1366,7 @@ mod test { let state = ValidatedState::default(); let instance = NodeState::mock().with_chain_config(ChainConfig { base_fee: 1000.into(), // High base fee - max_block_size, + max_block_size: max_block_size.into(), ..Default::default() }); let parent = Leaf::genesis(&instance); diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 7dd22b841..bee687076 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -17,6 +17,7 @@ ethers = { workspace = true } futures = { workspace = true } hotshot-contract-adapter ={ path = "../contracts/rust/adapter" } portpicker = { workspace = true } +serde = { workspace = true } serde_json = "^1.0.113" surf = "2.3.2" tempfile = "3.9.0" diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 62069d3f7..f4e986f71 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -21,6 +21,7 @@ use tempfile::TempDir; use url::Url; pub mod deployer; +pub mod serde; pub mod test_utils; pub type Signer = SignerMiddleware, LocalWallet>; diff --git a/utils/src/serde.rs b/utils/src/serde.rs new file mode 100644 index 000000000..edbc96f89 --- /dev/null +++ b/utils/src/serde.rs @@ -0,0 +1,89 @@ +use serde::{ + de::{DeserializeOwned, Deserializer, Error as _}, + ser::{Error as _, Serializer}, + Deserialize, Serialize, +}; + +/// Types which can be deserialized from either integers or strings. +/// +/// Some types can be represented as an integer or a string in human-readable formats like JSON or +/// TOML. For example, 1 GWEI might be represented by the integer `1000000000` or the string `"1 +/// gwei"`. Such types can implement `FromStringOrInteger` and then use [`impl_string_or_integer`] +/// to derive this user-friendly serialization. +/// +/// These types are assumed to have an efficient representation as an integral type in Rust -- +/// [`Self::Binary`] -- and will be serialized to and from this type when using a non-human-readable +/// encoding. With human readable encodings, serialization is always to a string. +pub trait FromStringOrInteger: Sized { + type Binary: Serialize + DeserializeOwned; + type Integer: Serialize + DeserializeOwned; + + fn from_binary(b: Self::Binary) -> anyhow::Result; + fn from_string(s: String) -> anyhow::Result; + fn from_integer(i: Self::Integer) -> anyhow::Result; + + fn to_binary(&self) -> anyhow::Result; + fn to_string(&self) -> anyhow::Result; +} + +/// Deserialize a type from either a string or integer in human-readable encodings. +/// +/// This macro implements serde `Serialize` and `DeserializeOwned` traits with a friendly +/// deserialization mechanism that can handle strings and integers when using human-readable +/// formats. It works with any [`FromStringOrInteger`] type. +#[macro_export] +macro_rules! impl_serde_from_string_or_integer { + ($t:ty) => { + impl serde::Serialize for $t { + fn serialize(&self, s: S) -> Result { + $crate::serde::string_or_integer::serialize(self, s) + } + } + + impl<'de> serde::Deserialize<'de> for $t { + fn deserialize>(d: D) -> Result { + $crate::serde::string_or_integer::deserialize(d) + } + } + }; +} +pub use crate::impl_serde_from_string_or_integer; + +/// Deserialize a type from either a string or integer in human-readable encodings. +/// +/// This serialization module can be used with any [`FromStringOrInteger`] type. It is usually used +/// only indirectly by the expansion of the [`impl_string_or_integer`] macro. +pub mod string_or_integer { + use super::*; + + #[derive(Debug, Deserialize)] + #[serde(untagged)] + enum StringOrInteger { + String(String), + Integer(I), + } + + pub fn serialize( + t: &T, + s: S, + ) -> Result { + if s.is_human_readable() { + t.to_string().map_err(S::Error::custom)?.serialize(s) + } else { + t.to_binary().map_err(S::Error::custom)?.serialize(s) + } + } + + pub fn deserialize<'a, T: FromStringOrInteger, D: Deserializer<'a>>( + d: D, + ) -> Result { + if d.is_human_readable() { + match StringOrInteger::deserialize(d)? { + StringOrInteger::String(s) => T::from_string(s).map_err(D::Error::custom), + StringOrInteger::Integer(i) => T::from_integer(i).map_err(D::Error::custom), + } + } else { + T::from_binary(T::Binary::deserialize(d)?).map_err(D::Error::custom) + } + } +}