diff --git a/sequencer/src/genesis.rs b/sequencer/src/genesis.rs index c71ee7deeb..03f0c1b46e 100644 --- a/sequencer/src/genesis.rs +++ b/sequencer/src/genesis.rs @@ -4,13 +4,13 @@ use std::{ }; use alloy::primitives::Address; -use anyhow::{Context, Ok}; +use anyhow::{ensure, Context, Ok}; use espresso_types::{ - v0_3::ChainConfig, FeeAccount, FeeAmount, GenesisHeader, L1BlockInfo, L1Client, Timestamp, - Upgrade, + v0_3::ChainConfig, EpochVersion, FeeAccount, FeeAmount, FeeVersion, GenesisHeader, L1BlockInfo, + L1Client, Timestamp, Upgrade, }; use serde::{Deserialize, Serialize}; -use vbs::version::Version; +use vbs::version::{StaticVersionType, Version}; /// Initial configuration of an Espresso stake table. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] @@ -131,6 +131,87 @@ impl Genesis { // TODO: it's optional for the fee contract to be included in a proxy in v1 so no need to panic but revisit this after v1 https://github.com/EspressoSystems/espresso-sequencer/pull/2000#discussion_r1765174702 Ok(()) } + + pub async fn validate(&self, l1: &L1Client) -> anyhow::Result<()> { + self.validate_fee_contract(l1).await?; + self.validate_base_version_config()?; + self.validate_upgrades_config()?; + Ok(()) + } + + fn validate_base_version_config(&self) -> anyhow::Result<()> { + if self.base_version >= FeeVersion::version() { + Self::validate_address( + self.chain_config.fee_contract, + "fee_contract", + "base_version", + &self.base_version, + )?; + } + + if self.base_version >= EpochVersion::version() { + ensure!( + self.epoch_height.is_some(), + "epoch_height must be provided for base_version {} and above", + self.base_version + ); + + ensure!( + self.epoch_start_block.is_some(), + "epoch_start_block must be provided for base_version {} and above", + self.base_version + ); + + Self::validate_address( + self.chain_config.stake_table_contract, + "stake_table_contract", + "base_version", + &self.base_version, + )?; + } + + Ok(()) + } + + fn validate_upgrades_config(&self) -> anyhow::Result<()> { + for (version, upgrade) in &self.upgrades { + let chain_config = upgrade.upgrade_type.chain_config(); + + if *version >= FeeVersion::version() { + Self::validate_address( + chain_config.and_then(|c| c.fee_contract), + "fee_contract", + "upgrade version", + version, + )?; + } + + if *version >= EpochVersion::version() { + Self::validate_address( + chain_config.and_then(|c| c.stake_table_contract), + "stake_table_contract", + "upgrade version", + version, + )?; + } + } + Ok(()) + } + + fn validate_address( + opt: Option
, + field: &str, + kind: &str, + version: &Version, + ) -> anyhow::Result<()> { + match opt { + Some(addr) if addr == Address::default() => { + anyhow::bail!("{field} cannot be the zero address in {kind} {version} and above") + }, + Some(_) => Ok(()), + None => anyhow::bail!("{field} must be provided in {kind} {version} and above"), + } + } } mod version_ser { @@ -1130,4 +1211,123 @@ mod test { toml::from_str::(&toml).unwrap(); } + + fn mock_genesis() -> Genesis { + Genesis { + base_version: Version { major: 0, minor: 0 }, + upgrade_version: Version { major: 0, minor: 0 }, + genesis_version: Version { major: 0, minor: 0 }, + epoch_height: None, + drb_difficulty: None, + drb_upgrade_difficulty: None, + epoch_start_block: None, + stake_table_capacity: None, + chain_config: Default::default(), + stake_table: StakeTableConfig { capacity: 10 }, + accounts: HashMap::new(), + l1_finalized: L1Finalized::Number { number: 0 }, + header: GenesisHeader::default(), + upgrades: BTreeMap::new(), + } + } + + #[test] + fn test_base_version_fee_contract_missing_should_fail() { + let mut genesis = mock_genesis(); + genesis.base_version = FeeVersion::version(); + genesis.chain_config.fee_contract = None; + + assert!(genesis.validate_base_version_config().is_err()) + } + + #[test] + fn test_base_version_fee_contract_zero_should_fail() { + let mut genesis = mock_genesis(); + genesis.base_version = FeeVersion::version(); + genesis.chain_config.fee_contract = Some(Address::default()); + + assert!(genesis.validate_base_version_config().is_err()); + } + + #[test] + fn test_base_version_fee_contract_ok() { + let mut genesis = mock_genesis(); + genesis.base_version = FeeVersion::version(); + genesis.chain_config.fee_contract = Some(Address::random()); + + assert!(genesis.validate_base_version_config().is_ok()); + } + + #[test] + fn test_base_version_epoch_fields_missing_should_fail() { + let mut genesis = mock_genesis(); + genesis.base_version = EpochVersion::version(); + genesis.chain_config.stake_table_contract = Some(Address::random()); + + assert!(genesis.validate_base_version_config().is_err()); + } + + #[test] + fn test_base_version_epoch_fields_ok() { + let mut genesis = mock_genesis(); + genesis.base_version = EpochVersion::version(); + genesis.epoch_height = Some(10); + genesis.epoch_start_block = Some(5); + genesis.chain_config.stake_table_contract = Some(Address::random()); + genesis.chain_config.fee_contract = Some(Address::random()); + + assert!(genesis.validate_base_version_config().is_ok()); + } + + #[test] + fn test_upgrade_with_zero_stake_table_should_fail() { + let mut genesis = mock_genesis(); + genesis.upgrades.insert( + EpochVersion::version(), + Upgrade { + mode: UpgradeMode::View(ViewBasedUpgrade { + start_proposing_view: 0, + stop_proposing_view: 0, + start_voting_view: None, + stop_voting_view: None, + }), + upgrade_type: UpgradeType::Epoch { + chain_config: ChainConfig { + base_fee: 1.into(), + fee_contract: Some(Address::random()), + stake_table_contract: Some(Address::default()), + ..Default::default() + }, + }, + }, + ); + + assert!(genesis.validate_upgrades_config().is_err()); + } + + #[test] + fn test_upgrade_with_valid_stake_table_should_pass() { + let mut genesis = mock_genesis(); + genesis.upgrades.insert( + EpochVersion::version(), + Upgrade { + mode: UpgradeMode::View(ViewBasedUpgrade { + start_proposing_view: 0, + stop_proposing_view: 0, + start_voting_view: None, + stop_voting_view: None, + }), + upgrade_type: UpgradeType::Epoch { + chain_config: ChainConfig { + base_fee: 1.into(), + fee_contract: Some(Address::random()), + stake_table_contract: Some(Address::random()), + ..Default::default() + }, + }, + }, + ); + + assert!(genesis.validate_upgrades_config().is_ok()); + } } diff --git a/sequencer/src/lib.rs b/sequencer/src/lib.rs index 3121de2db5..7fdeecfac4 100644 --- a/sequencer/src/lib.rs +++ b/sequencer/src/lib.rs @@ -467,7 +467,7 @@ where .with_metrics(metrics) .connect(l1_params.urls) .with_context(|| "failed to create L1 client")?; - genesis.validate_fee_contract(&l1_client).await?; + genesis.validate(&l1_client).await?; l1_client.spawn_tasks().await; let l1_genesis = match genesis.l1_finalized { diff --git a/sequencer/src/restart_tests.rs b/sequencer/src/restart_tests.rs index 6f88d31af2..173bae8cd6 100755 --- a/sequencer/src/restart_tests.rs +++ b/sequencer/src/restart_tests.rs @@ -647,7 +647,7 @@ impl TestNetwork { let genesis_file_path = tmp.path().join("genesis.toml"); let mut genesis = Genesis { - chain_config: Default::default(), + chain_config: ChainConfig::default(), // TODO we apparently have two `capacity` configurations stake_table: StakeTableConfig { capacity: STAKE_TABLE_CAPACITY_FOR_TEST, @@ -742,12 +742,13 @@ impl TestNetwork { }; // Deploy stake contracts and delegate. - let stake_table_address = network.deploy(&genesis).await.unwrap(); + let (fee_proxy, stake_table_address) = network.deploy(&genesis).await.unwrap(); // Add contract address to `ChainConfig`. let chain_config = ChainConfig { base_fee: 1.into(), stake_table_contract: Some(stake_table_address), + fee_contract: Some(fee_proxy), ..Default::default() }; genesis.chain_config = chain_config; @@ -783,7 +784,7 @@ impl TestNetwork { } /// Deploy stake contracts and delegate. - async fn deploy(&self, genesis: &Genesis) -> anyhow::Result
{ + async fn deploy(&self, genesis: &Genesis) -> anyhow::Result<(Address, Address)> { let stake_table_version = StakeTableContractVersion::V2; let delegation_config = DelegationConfig::EqualAmounts; @@ -857,6 +858,9 @@ impl TestNetwork { } .context("failed to deploy contracts")?; + let fee = contracts + .address(Contract::FeeContractProxy) + .expect("FeeContractProxy address not found "); let stake_table_address = contracts .address(Contract::StakeTableProxy) .expect("StakeTableProxy address not found"); @@ -881,7 +885,7 @@ impl TestNetwork { .await .expect("interval mining"); - Ok(stake_table_address) + Ok((fee, stake_table_address)) } async fn wait_for_epoch(&self) { diff --git a/types/src/v0/impls/instance_state.rs b/types/src/v0/impls/instance_state.rs index 8acd22a20f..db24c5e95b 100644 --- a/types/src/v0/impls/instance_state.rs +++ b/types/src/v0/impls/instance_state.rs @@ -208,7 +208,7 @@ impl NodeState { Arc::new(mock::MockStateCatchup::default()), StaticVersion::<0, 2>::version(), coordinator, - Version { major: 0, minor: 1 }, + Version { major: 0, minor: 2 }, ) } @@ -238,7 +238,7 @@ impl NodeState { mock::MockStateCatchup::default(), StaticVersion::<0, 3>::version(), coordinator, - Version { major: 0, minor: 1 }, + Version { major: 0, minor: 3 }, ) }