diff --git a/Cargo.lock b/Cargo.lock index 9202ebe0b20..c3c73ecc345 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1568,13 +1568,12 @@ checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" [[package]] name = "assert_cmd" -version = "2.0.17" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" dependencies = [ "anstyle", "bstr", - "doc-comment", "libc", "predicates", "predicates-core", @@ -4181,6 +4180,7 @@ dependencies = [ "hotshot-types", "rand 0.8.5", "sequencer-utils", + "serde", "serde_json", "surf-disco", "test-log", @@ -4535,9 +4535,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -7837,6 +7837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -10334,6 +10335,7 @@ dependencies = [ "anyhow", "ark-ff 0.5.0", "ark-serialize 0.5.0", + "assert_cmd", "async-channel 2.5.0", "async-lock 3.4.1", "async-once-cell", @@ -10354,6 +10356,7 @@ dependencies = [ "espresso-contract-deployer", "espresso-macros", "espresso-types", + "flate2", "futures", "generic-tests", "hotshot", @@ -10381,6 +10384,7 @@ dependencies = [ "num_enum", "parking_lot", "portpicker", + "predicates", "pretty_assertions", "priority-queue", "rand 0.8.5", @@ -10779,6 +10783,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.5" diff --git a/contracts/rust/deployer/Cargo.toml b/contracts/rust/deployer/Cargo.toml index 4c20b3fd367..31cdc657f6b 100644 --- a/contracts/rust/deployer/Cargo.toml +++ b/contracts/rust/deployer/Cargo.toml @@ -15,6 +15,7 @@ dotenvy = { workspace = true } espresso-types = { path = "../../../types" } hotshot-contract-adapter = { workspace = true } hotshot-types = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } surf-disco = { workspace = true } tide-disco = { workspace = true } diff --git a/contracts/rust/deployer/src/lib.rs b/contracts/rust/deployer/src/lib.rs index 535c6a5dc71..b0fddf440cb 100644 --- a/contracts/rust/deployer/src/lib.rs +++ b/contracts/rust/deployer/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Write, time::Duration}; +use std::{collections::HashMap, fs, io::Write, path::Path, time::Duration}; use alloy::{ contract::RawCallBuilder, @@ -24,6 +24,7 @@ use clap::{builder::OsStr, Parser}; use derive_more::{derive::Deref, Display}; use espresso_types::{v0_1::L1Client, v0_3::Fetcher}; use hotshot_contract_adapter::sol_types::*; +use serde::{Deserialize, Serialize}; pub mod builder; pub mod impersonate_filler; @@ -184,7 +185,7 @@ pub struct DeployedContracts { } /// An identifier for a particular contract. -#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Display, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Contract { #[display("ESPRESSO_SEQUENCER_PLONK_VERIFIER_ADDRESS")] PlonkVerifier, @@ -233,7 +234,7 @@ impl From for OsStr { } /// Cache of contracts predeployed or deployed during this current run. -#[derive(Deref, Debug, Clone, Default)] +#[derive(Deref, derive_more::DerefMut, Debug, Clone, Default)] pub struct Contracts(HashMap); impl From for Contracts { @@ -346,6 +347,121 @@ impl Contracts { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DeployStep { + OpsTimelock, + SafeExitTimelock, + FeeContract, + EspToken, + LightClientV1, + LightClientV2, + LightClientV3, + RewardClaim, + EspTokenV2, + StakeTable, + StakeTableV2, +} + +impl DeployStep { + fn all_steps() -> Vec { + vec![ + DeployStep::OpsTimelock, + DeployStep::SafeExitTimelock, + DeployStep::FeeContract, + DeployStep::EspToken, + DeployStep::LightClientV1, + DeployStep::LightClientV2, + DeployStep::LightClientV3, + DeployStep::RewardClaim, + DeployStep::EspTokenV2, + DeployStep::StakeTable, + DeployStep::StakeTableV2, + ] + } + + fn required_contracts(&self) -> Vec { + match self { + DeployStep::OpsTimelock => vec![Contract::OpsTimelock], + DeployStep::SafeExitTimelock => vec![Contract::SafeExitTimelock], + DeployStep::FeeContract => vec![Contract::FeeContract, Contract::FeeContractProxy], + DeployStep::EspToken => vec![Contract::EspToken, Contract::EspTokenProxy], + DeployStep::RewardClaim => vec![Contract::RewardClaim, Contract::RewardClaimProxy], + DeployStep::EspTokenV2 => vec![Contract::EspTokenV2], + DeployStep::LightClientV1 => { + vec![Contract::LightClient, Contract::LightClientProxy] + }, + DeployStep::LightClientV2 => vec![Contract::LightClientV2], + DeployStep::LightClientV3 => vec![Contract::LightClientV3], + DeployStep::StakeTable => vec![Contract::StakeTable, Contract::StakeTableProxy], + DeployStep::StakeTableV2 => vec![Contract::StakeTableV2], + } + } + + pub fn target_contract(&self) -> Contract { + match self { + DeployStep::OpsTimelock => Contract::OpsTimelock, + DeployStep::SafeExitTimelock => Contract::SafeExitTimelock, + DeployStep::FeeContract => Contract::FeeContractProxy, + DeployStep::EspToken => Contract::EspTokenProxy, + DeployStep::RewardClaim => Contract::RewardClaimProxy, + DeployStep::EspTokenV2 => Contract::EspTokenV2, + DeployStep::LightClientV1 => Contract::LightClientProxy, + DeployStep::LightClientV2 => Contract::LightClientV2, + DeployStep::LightClientV3 => Contract::LightClientV3, + DeployStep::StakeTable => Contract::StakeTableProxy, + DeployStep::StakeTableV2 => Contract::StakeTableV2, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DeploymentState { + pub contracts: HashMap, + pub genesis_lc_state: Option, + pub genesis_st_state: Option, + pub blocks_per_epoch: Option, + pub epoch_start_block: Option, + pub chain_id: u64, +} + +impl DeploymentState { + pub fn new(chain_id: u64) -> Self { + Self { + contracts: HashMap::new(), + genesis_lc_state: None, + genesis_st_state: None, + blocks_per_epoch: None, + epoch_start_block: None, + chain_id, + } + } + + pub fn load(path: impl AsRef) -> Result { + let contents = fs::read_to_string(path)?; + let state = serde_json::from_str(&contents)?; + Ok(state) + } + + pub fn save(&self, path: impl AsRef) -> Result<()> { + let contents = serde_json::to_string_pretty(self)?; + fs::write(path, contents)?; + Ok(()) + } + + pub fn next_step(&self) -> Option { + DeployStep::all_steps().into_iter().find(|step| { + let required = step.required_contracts(); + required + .iter() + .any(|contract| !self.contracts.contains_key(contract)) + }) + } + + pub fn is_complete(&self) -> bool { + self.next_step().is_none() + } +} + /// Default deployment function `LightClient.sol` or `LightClientMock.sol` with `mock: true`. /// /// # NOTE: @@ -398,13 +514,18 @@ pub(crate) async fn deploy_light_client_contract( // Deploy the light client let light_client_addr = if mock { - // for mock, we don't populate the `contracts` since it only track production-ready deployments - let addr = LightClientMock::deploy_builder(&provider) - .map(|req| req.with_deploy_code(lc_linked_bytecode)) - .deploy() - .await?; - tracing::info!("deployed LightClientMock at {addr:#x}"); - addr + if let Some(addr) = contracts.address(Contract::LightClient) { + tracing::info!("skipping deployment of LightClientMock, already deployed at {addr:#x}"); + addr + } else { + let addr = LightClientMock::deploy_builder(&provider) + .map(|req| req.with_deploy_code(lc_linked_bytecode)) + .deploy() + .await?; + tracing::info!("deployed LightClientMock at {addr:#x}"); + contracts.insert(Contract::LightClient, addr); + addr + } } else { contracts .deploy( @@ -544,12 +665,20 @@ pub async fn upgrade_light_client_v2( } }; let lcv2_addr = if is_mock { - let addr = LightClientV2Mock::deploy_builder(&provider) - .map(|req| req.with_deploy_code(lcv2_linked_bytecode)) - .deploy() - .await?; - tracing::info!("deployed LightClientV2Mock at {addr:#x}"); - addr + if let Some(addr) = contracts.address(Contract::LightClientV2) { + tracing::info!( + "skipping deployment of LightClientV2Mock, already deployed at {addr:#x}" + ); + addr + } else { + let addr = LightClientV2Mock::deploy_builder(&provider) + .map(|req| req.with_deploy_code(lcv2_linked_bytecode)) + .deploy() + .await?; + tracing::info!("deployed LightClientV2Mock at {addr:#x}"); + contracts.insert(Contract::LightClientV2, addr); + addr + } } else { contracts .deploy( @@ -683,12 +812,20 @@ pub async fn upgrade_light_client_v3( } }; let lcv3_addr = if is_mock { - let addr = LightClientV3Mock::deploy_builder(&provider) - .map(|req| req.with_deploy_code(lcv3_linked_bytecode)) - .deploy() - .await?; - tracing::info!("deployed LightClientV3Mock at {addr:#x}"); - addr + if let Some(addr) = contracts.address(Contract::LightClientV3) { + tracing::info!( + "skipping deployment of LightClientV3Mock, already deployed at {addr:#x}" + ); + addr + } else { + let addr = LightClientV3Mock::deploy_builder(&provider) + .map(|req| req.with_deploy_code(lcv3_linked_bytecode)) + .deploy() + .await?; + tracing::info!("deployed LightClientV3Mock at {addr:#x}"); + contracts.insert(Contract::LightClientV3, addr); + addr + } } else { contracts .deploy( diff --git a/sequencer/Cargo.toml b/sequencer/Cargo.toml index 2774e4883f6..268fbbf0803 100644 --- a/sequencer/Cargo.toml +++ b/sequencer/Cargo.toml @@ -143,6 +143,9 @@ test-log = { workspace = true } light-client = { workspace = true, features = ["testing"] } sequencer = { path = ".", features = ["testing"] } tempfile = { workspace = true } +assert_cmd = "2.1.1" +predicates = "3.1.3" +flate2 = "1.1.5" [lints] workspace = true diff --git a/sequencer/src/bin/deploy.rs b/sequencer/src/bin/deploy.rs index 949c37c60e8..d6e6d1f191e 100644 --- a/sequencer/src/bin/deploy.rs +++ b/sequencer/src/bin/deploy.rs @@ -15,7 +15,7 @@ use espresso_contract_deployer::{ network_config::{light_client_genesis, light_client_genesis_from_stake_table}, proposals::{multisig::verify_node_js_files, timelock::TimelockOperationType}, provider::connect_ledger, - Contract, Contracts, DeployedContracts, + Contract, Contracts, DeployStep, DeployedContracts, DeploymentState, }; use espresso_types::{config::PublicNetworkConfig, parse_duration}; use hotshot_types::light_client::DEFAULT_STAKE_TABLE_CAPACITY; @@ -348,6 +348,14 @@ struct Options { #[clap(flatten)] logging: logging::Config, + /// Deploy one step then exit (for incremental deployment) + #[clap(long)] + one_step: bool, + + /// Path to state file + #[clap(long, default_value = ".espresso_deploy_state.json")] + state_file: PathBuf, + /// Command to run /// /// For backwards compatibility, the default is to deploy contracts, if no @@ -363,6 +371,256 @@ enum Command { VerifyNodeJsFiles, } +async fn deploy_one_step( + opt: Options, + provider: espresso_contract_deployer::HttpProviderWithWallet, + chain_id: u64, +) -> anyhow::Result<()> { + let mut state = if opt.state_file.exists() + && opt + .state_file + .metadata() + .map(|m| m.len() > 0) + .unwrap_or(false) + { + DeploymentState::load(&opt.state_file)? + } else { + DeploymentState::new(chain_id) + }; + + if state.chain_id != chain_id { + anyhow::bail!( + "Chain ID mismatch: state file has {}, connected to {}", + state.chain_id, + chain_id + ); + } + + let next_step = match state.next_step() { + Some(step) => step, + None => { + println!("All steps complete"); + return Ok(()); + }, + }; + + tracing::info!("Deploying step: {:?}", next_step); + + let mut contracts = Contracts::from(opt.contracts.clone()); + for (contract, address) in &state.contracts { + contracts.insert(*contract, *address); + } + + let mut args_builder = DeployerArgsBuilder::default(); + args_builder + .deployer(provider.clone()) + .mock_light_client(opt.use_mock) + .use_multisig(opt.use_multisig) + .dry_run(opt.dry_run) + .rpc_url(opt.rpc_url.clone()); + + if let Some(multisig) = opt.multisig_address { + args_builder.multisig(multisig); + } + if let Some(multisig_pauser) = opt.multisig_pauser_address { + args_builder.multisig_pauser(multisig_pauser); + } + + if let Some(blocks_per_epoch) = state.blocks_per_epoch.or(opt.blocks_per_epoch) { + args_builder.blocks_per_epoch(blocks_per_epoch); + } + if let Some(epoch_start_block) = state.epoch_start_block.or(opt.epoch_start_block) { + args_builder.epoch_start_block(epoch_start_block); + } + + match next_step { + DeployStep::LightClientV1 => { + let (genesis_state, genesis_stake) = + if let (Some(lc), Some(st)) = (&state.genesis_lc_state, &state.genesis_st_state) { + (lc.clone(), st.clone()) + } else if opt.mock_espresso_live_network { + light_client_genesis_from_stake_table( + &Default::default(), + DEFAULT_STAKE_TABLE_CAPACITY, + )? + } else { + light_client_genesis(&opt.sequencer_url, opt.stake_table_capacity).await? + }; + + state.genesis_lc_state = Some(genesis_state.clone()); + state.genesis_st_state = Some(genesis_stake.clone()); + + args_builder + .genesis_lc_state(genesis_state) + .genesis_st_state(genesis_stake); + + if let Some(prover) = opt.permissioned_prover { + args_builder.permissioned_prover(prover); + } + }, + DeployStep::LightClientV2 | DeployStep::LightClientV3 => { + let (blocks_per_epoch, epoch_start_block) = + if let (Some(bpe), Some(esb)) = (state.blocks_per_epoch, state.epoch_start_block) { + (bpe, esb) + } else if (opt.dry_run && opt.use_multisig) || opt.mock_espresso_live_network { + (10, 22) + } else { + loop { + match surf_disco::Client::>::new( + opt.sequencer_url.clone(), + ) + .get::("config/hotshot") + .send() + .await + { + Ok(resp) => { + let config = resp.hotshot_config(); + break (config.blocks_per_epoch(), config.epoch_start_block()); + }, + Err(e) => { + tracing::error!("Failed to fetch the network config: {e}"); + sleep(Duration::from_secs(5)); + }, + } + } + }; + + state.blocks_per_epoch = Some(blocks_per_epoch); + state.epoch_start_block = Some(epoch_start_block); + + args_builder.blocks_per_epoch(blocks_per_epoch); + args_builder.epoch_start_block(epoch_start_block); + }, + DeployStep::StakeTable => { + if let Some(escrow_period) = opt.exit_escrow_period { + args_builder.exit_escrow_period(U256::from(escrow_period.as_secs())); + } + }, + DeployStep::OpsTimelock => { + let ops_timelock_admin = opt.ops_timelock_admin.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --ops-timelock-admin when deploying ops timelock in one-step \ + mode" + ) + })?; + args_builder.ops_timelock_admin(ops_timelock_admin); + + let ops_timelock_delay = opt.ops_timelock_delay.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --ops-timelock-delay when deploying ops timelock in one-step \ + mode" + ) + })?; + args_builder.ops_timelock_delay(U256::from(ops_timelock_delay)); + + let ops_timelock_executors = opt.ops_timelock_executors.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --ops-timelock-executors when deploying ops timelock in \ + one-step mode" + ) + })?; + args_builder.ops_timelock_executors(ops_timelock_executors.into_iter().collect()); + + let ops_timelock_proposers = opt.ops_timelock_proposers.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --ops-timelock-proposers when deploying ops timelock in \ + one-step mode" + ) + })?; + args_builder.ops_timelock_proposers(ops_timelock_proposers.into_iter().collect()); + }, + DeployStep::SafeExitTimelock => { + let safe_exit_timelock_admin = opt.safe_exit_timelock_admin.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --safe-exit-timelock-admin when deploying safe exit timelock in \ + one-step mode" + ) + })?; + args_builder.safe_exit_timelock_admin(safe_exit_timelock_admin); + + let safe_exit_timelock_delay = opt.safe_exit_timelock_delay.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --safe-exit-timelock-delay when deploying safe exit timelock in \ + one-step mode" + ) + })?; + args_builder.safe_exit_timelock_delay(U256::from(safe_exit_timelock_delay)); + + let safe_exit_timelock_executors = + opt.safe_exit_timelock_executors.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --safe-exit-timelock-executors when deploying safe exit \ + timelock in one-step mode" + ) + })?; + args_builder + .safe_exit_timelock_executors(safe_exit_timelock_executors.into_iter().collect()); + + let safe_exit_timelock_proposers = + opt.safe_exit_timelock_proposers.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --safe-exit-timelock-proposers when deploying safe exit \ + timelock in one-step mode" + ) + })?; + args_builder + .safe_exit_timelock_proposers(safe_exit_timelock_proposers.into_iter().collect()); + }, + DeployStep::EspToken => { + let token_recipient = opt.initial_token_grant_recipient.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --initial-token-grant-recipient when deploying esp token in \ + one-step mode" + ) + })?; + let token_name = opt.token_name.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --token-name when deploying esp token in one-step mode" + ) + })?; + let token_symbol = opt.token_symbol.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --token-symbol when deploying esp token in one-step mode" + ) + })?; + let initial_token_supply = opt.initial_token_supply.ok_or_else(|| { + anyhow::anyhow!( + "Must provide --initial-token-supply when deploying esp token in one-step mode" + ) + })?; + + args_builder.token_name(token_name); + args_builder.token_symbol(token_symbol); + args_builder.initial_token_supply(initial_token_supply); + args_builder.token_recipient(token_recipient); + }, + _ => {}, + } + + if opt.use_timelock_owner { + args_builder.use_timelock_owner(true); + } + + let args = args_builder.build()?; + let target = next_step.target_contract(); + + args.deploy(&mut contracts, target).await?; + + for (contract, address) in contracts.iter() { + state.contracts.insert(*contract, *address); + } + + state.save(&opt.state_file)?; + + tracing::info!( + "Step {:?} complete. Saved state to {:?}", + next_step, + opt.state_file + ); + + Ok(()) +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let opt = Options::parse(); @@ -374,7 +632,6 @@ async fn main() -> anyhow::Result<()> { return Ok(()); }; - let mut contracts = Contracts::from(opt.contracts); let provider = if opt.ledger { let signer = connect_ledger(opt.account_index as usize).await?; tracing::info!("Using ledger for signing, watch ledger device for prompts."); @@ -382,6 +639,7 @@ async fn main() -> anyhow::Result<()> { } else { build_provider( opt.mnemonic + .clone() .expect("Mnemonic provided when not using ledger"), opt.account_index, opt.rpc_url.clone(), @@ -428,6 +686,12 @@ async fn main() -> anyhow::Result<()> { ); } + if opt.one_step { + return deploy_one_step(opt, provider, chain_id).await; + } + + let mut contracts = Contracts::from(opt.contracts); + // First use builder to build constructor input arguments let mut args_builder = DeployerArgsBuilder::default(); args_builder diff --git a/sequencer/tests/deploy_integration.rs b/sequencer/tests/deploy_integration.rs new file mode 100644 index 00000000000..0ac0bffa0fc --- /dev/null +++ b/sequencer/tests/deploy_integration.rs @@ -0,0 +1,185 @@ +use std::io::Read; + +use alloy::{ + node_bindings::Anvil, + providers::{ext::AnvilApi, ProviderBuilder}, +}; +use assert_cmd::Command; +use espresso_contract_deployer::{Contract, DeploymentState}; +use flate2::read::GzDecoder; +use predicates::str; +use serde_json::Value; +use tempfile::NamedTempFile; + +const DEPLOYER_ADDRESS: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + +fn expected_contracts_after_step(step: usize) -> Vec { + let all_steps = [ + vec![Contract::OpsTimelock], + vec![Contract::SafeExitTimelock], + vec![Contract::FeeContract, Contract::FeeContractProxy], + vec![Contract::EspToken, Contract::EspTokenProxy], + vec![ + Contract::PlonkVerifier, + Contract::LightClient, + Contract::LightClientProxy, + ], + vec![Contract::PlonkVerifierV2, Contract::LightClientV2], + vec![Contract::PlonkVerifierV3, Contract::LightClientV3], + vec![Contract::RewardClaim, Contract::RewardClaimProxy], + vec![Contract::EspTokenV2], + vec![Contract::StakeTable, Contract::StakeTableProxy], + vec![Contract::StakeTableV2], + ]; + + all_steps.iter().take(step).flatten().cloned().collect() +} + +#[allow(deprecated)] +fn deploy_cmd(rpc_url: &str, state_path: &str) -> Command { + let mut cmd = Command::cargo_bin("deploy").unwrap(); + cmd.arg("--rpc-url") + .arg(rpc_url) + .arg("--state-file") + .arg(state_path) + .arg("--mock-espresso-live-network") + .args(["--ops-timelock-admin", DEPLOYER_ADDRESS]) + .args(["--ops-timelock-delay", "0"]) + .args(["--ops-timelock-executors", DEPLOYER_ADDRESS]) + .args(["--ops-timelock-proposers", DEPLOYER_ADDRESS]) + .args(["--safe-exit-timelock-admin", DEPLOYER_ADDRESS]) + .args(["--safe-exit-timelock-delay", "0"]) + .args(["--safe-exit-timelock-executors", DEPLOYER_ADDRESS]) + .args(["--safe-exit-timelock-proposers", DEPLOYER_ADDRESS]) + .args(["--initial-token-grant-recipient", DEPLOYER_ADDRESS]) + .args(["--token-name", "Espresso"]) + .args(["--token-symbol", "ESP"]) + .args(["--initial-token-supply", "1000000000000000000000000"]); + cmd +} + +fn deploy_all_steps(rpc_url: &str, state_path: &str) { + let mut iteration = 0; + loop { + iteration += 1; + let assert = deploy_cmd(rpc_url, state_path) + .arg("--one-step") + .assert() + .success(); + + let stdout = String::from_utf8_lossy(&assert.get_output().stdout); + if stdout.contains("All steps complete") { + assert.stdout(str::contains("All steps complete")); + let state = DeploymentState::load(state_path).expect("Failed to load state"); + assert_eq!(state.contracts.len(), 19); + break; + } + + let state = DeploymentState::load(state_path).expect("Failed to load state"); + let expected = expected_contracts_after_step(iteration); + for contract in &expected { + assert!(state.contracts.contains_key(contract)); + } + } +} + +fn get_anvil_accounts(dump: &[u8]) -> Value { + let mut decoder = GzDecoder::new(dump); + let mut json_str = String::new(); + decoder.read_to_string(&mut json_str).unwrap(); + let state: Value = serde_json::from_str(&json_str).unwrap(); + state["accounts"].clone() +} + +#[test_log::test(tokio::test)] +async fn test_deploy_determinism() { + let first_anvil = Anvil::new().spawn(); + let first_rpc_url = first_anvil.endpoint(); + + let first_state_file = NamedTempFile::new().unwrap(); + let first_state_path = first_state_file.path().to_str().unwrap(); + + deploy_all_steps(&first_rpc_url, first_state_path); + + let first_provider = ProviderBuilder::new() + .connect(&first_rpc_url) + .await + .unwrap(); + let first_accounts = get_anvil_accounts(&first_provider.anvil_dump_state().await.unwrap()); + + // Deploy again on fresh anvil, verify same accounts state + let second_anvil = Anvil::new().spawn(); + let second_rpc_url = second_anvil.endpoint(); + + let second_state_file = NamedTempFile::new().unwrap(); + let second_state_path = second_state_file.path().to_str().unwrap(); + + deploy_all_steps(&second_rpc_url, second_state_path); + + let second_provider = ProviderBuilder::new() + .connect(&second_rpc_url) + .await + .unwrap(); + let second_accounts = get_anvil_accounts(&second_provider.anvil_dump_state().await.unwrap()); + + // Find differences + let first_obj = first_accounts.as_object().unwrap(); + let second_obj = second_accounts.as_object().unwrap(); + + for (addr, first_val) in first_obj { + if let Some(second_val) = second_obj.get(addr) { + if first_val != second_val { + let first_acc = first_val.as_object().unwrap(); + let second_acc = second_val.as_object().unwrap(); + println!("Account {addr} differs:"); + if first_acc.get("balance") != second_acc.get("balance") { + println!( + " balance: first={} second={}", + first_acc.get("balance").unwrap(), + second_acc.get("balance").unwrap() + ); + } + if first_acc.get("nonce") != second_acc.get("nonce") { + println!( + " nonce: first={} second={}", + first_acc.get("nonce").unwrap(), + second_acc.get("nonce").unwrap() + ); + } + if first_acc.get("code") != second_acc.get("code") { + println!( + " code differs (lengths: first={} second={})", + first_acc.get("code").unwrap().as_str().unwrap().len(), + second_acc.get("code").unwrap().as_str().unwrap().len() + ); + } + if first_acc.get("storage") != second_acc.get("storage") { + let first_storage = first_acc.get("storage").unwrap().as_object().unwrap(); + let second_storage = second_acc.get("storage").unwrap().as_object().unwrap(); + for (slot, first_v) in first_storage { + if second_storage.get(slot) != Some(first_v) { + println!( + " storage[{slot}]: first={first_v} second={:?}", + second_storage.get(slot) + ); + } + } + for (slot, second_v) in second_storage { + if !first_storage.contains_key(slot) { + println!(" storage[{slot}]: first=None second={second_v}"); + } + } + } + } + } else { + println!("Account {addr} only in first"); + } + } + for addr in second_obj.keys() { + if !first_obj.contains_key(addr) { + println!("Account {addr} only in second"); + } + } + + assert_eq!(first_accounts, second_accounts); +}