|
| 1 | +use std::{env, fs, path::PathBuf, str::FromStr, sync::Arc}; |
| 2 | + |
| 3 | +use alloy_eips::BlockNumberOrTag; |
| 4 | +use alloy_network::EthereumWallet; |
| 5 | +use alloy_node_bindings::Anvil; |
| 6 | +use alloy_primitives::{Address, B256, U256}; |
| 7 | +use alloy_provider::{Provider, ProviderBuilder}; |
| 8 | +use alloy_signer_local::PrivateKeySigner; |
| 9 | +use alloy_transport_http::reqwest::Url; |
| 10 | +use anyhow::{anyhow, Context, Result}; |
| 11 | +use clap::Parser; |
| 12 | +use fault_proof::contract::{DisputeGameFactory, OPSuccinctFaultDisputeGame, ProposalStatus}; |
| 13 | +use op_succinct_client_utils::{boot::BootInfoStruct, types::u32_to_u8}; |
| 14 | +use op_succinct_elfs::AGGREGATION_ELF; |
| 15 | +use op_succinct_host_utils::{ |
| 16 | + fetcher::OPSuccinctDataFetcher, |
| 17 | + get_agg_proof_stdin, |
| 18 | + host::OPSuccinctHost, |
| 19 | + network::{determine_network_mode, get_network_signer, parse_fulfillment_strategy}, |
| 20 | + witness_generation::WitnessGenerator, |
| 21 | +}; |
| 22 | +use op_succinct_proof_utils::{get_range_elf_embedded, initialize_host}; |
| 23 | +use sp1_sdk::{ |
| 24 | + network::FulfillmentStrategy, utils, HashableKey, Prover, ProverClient, SP1ProofMode, |
| 25 | +}; |
| 26 | +use tracing::info; |
| 27 | + |
| 28 | +#[derive(Parser, Debug)] |
| 29 | +#[command(author, version, about, long_about = None)] |
| 30 | +struct Args { |
| 31 | + /// The environment file path. |
| 32 | + #[arg(long, default_value = ".env.prove_dryrun")] |
| 33 | + env_file: PathBuf, |
| 34 | + |
| 35 | + /// Index of the game to prove. |
| 36 | + #[arg(long, value_parser = clap::value_parser!(u64).range(1..))] |
| 37 | + index: u64, |
| 38 | +} |
| 39 | + |
| 40 | +#[derive(Debug, Clone)] |
| 41 | +struct Config { |
| 42 | + /// The L1 RPC URL. |
| 43 | + pub l1_rpc: Url, |
| 44 | + |
| 45 | + /// The address of the factory contract. |
| 46 | + pub factory_address: Address, |
| 47 | + |
| 48 | + /// Proof fulfillment strategy for range proofs. |
| 49 | + pub range_proof_strategy: FulfillmentStrategy, |
| 50 | + |
| 51 | + /// Proof fulfillment strategy for aggregation proofs. |
| 52 | + pub agg_proof_strategy: FulfillmentStrategy, |
| 53 | + |
| 54 | + // Aggregation proof mode (plonk/groth16) |
| 55 | + pub agg_proof_mode: String, |
| 56 | + |
| 57 | + /// Proposer private key |
| 58 | + pub private_key: String, |
| 59 | + |
| 60 | + /// Whether to expect NETWORK_PRIVATE_KEY to be an AWS KMS key ARN instead of a |
| 61 | + /// plaintext private key. |
| 62 | + pub use_kms_requester: bool, |
| 63 | +} |
| 64 | + |
| 65 | +impl Config { |
| 66 | + pub fn from_env() -> Result<Self> { |
| 67 | + Ok(Self { |
| 68 | + l1_rpc: env::var("L1_RPC") |
| 69 | + .context("L1_RPC must be set")? |
| 70 | + .parse() |
| 71 | + .expect("failed to parse L1_RPC"), |
| 72 | + factory_address: env::var("FACTORY_ADDRESS") |
| 73 | + .context("FACTORY_ADDRESS must be set")? |
| 74 | + .parse() |
| 75 | + .expect("failed to parse FACTORY_ADDRESS"), |
| 76 | + range_proof_strategy: parse_fulfillment_strategy( |
| 77 | + env::var("RANGE_PROOF_STRATEGY").unwrap_or("reserved".to_string()), |
| 78 | + ), |
| 79 | + agg_proof_strategy: parse_fulfillment_strategy( |
| 80 | + env::var("AGG_PROOF_STRATEGY").unwrap_or("reserved".to_string()), |
| 81 | + ), |
| 82 | + agg_proof_mode: env::var("AGG_PROOF_MODE") |
| 83 | + .ok() |
| 84 | + .filter(|s| !s.trim().is_empty()) |
| 85 | + .unwrap_or_else(|| "plonk".to_string()), |
| 86 | + private_key: env::var("PRIVATE_KEY") |
| 87 | + .context("PRIVATE_KEY must be set")? |
| 88 | + .parse() |
| 89 | + .expect("failed to parse PRIVATE_KEY"), |
| 90 | + use_kms_requester: env::var("USE_KMS_REQUESTER") |
| 91 | + .unwrap_or("false".to_string()) |
| 92 | + .parse()?, |
| 93 | + }) |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +/// Preflight check for the OP Succinct Fault Dispute Game. |
| 98 | +#[tokio::main] |
| 99 | +async fn main() -> Result<()> { |
| 100 | + // 1. Set up the environment. |
| 101 | + utils::setup_logger(); |
| 102 | + |
| 103 | + let args = Args::parse(); |
| 104 | + |
| 105 | + dotenv::from_path(&args.env_file) |
| 106 | + .context(format!("Environment file not found: {}", args.env_file.display()))?; |
| 107 | + |
| 108 | + let config = Config::from_env()?; |
| 109 | + |
| 110 | + let wallet = |
| 111 | + PrivateKeySigner::from_str(&config.private_key).context("failed to parse private key")?; |
| 112 | + |
| 113 | + let network_signer = get_network_signer(config.use_kms_requester).await?; |
| 114 | + let network_mode = |
| 115 | + determine_network_mode(config.range_proof_strategy, config.agg_proof_strategy).context( |
| 116 | + "failed to determine network mode from range and agg fulfillment strategies", |
| 117 | + )?; |
| 118 | + |
| 119 | + let data_fetcher = OPSuccinctDataFetcher::new_with_rollup_config().await?; |
| 120 | + |
| 121 | + let factory = DisputeGameFactory::new(config.factory_address, data_fetcher.l1_provider.clone()); |
| 122 | + |
| 123 | + let parent_game = OPSuccinctFaultDisputeGame::new( |
| 124 | + factory |
| 125 | + .gameAtIndex(U256::from(args.index - 1)) |
| 126 | + .call() |
| 127 | + .await |
| 128 | + .with_context(|| { |
| 129 | + format!("failed to fetch the parent game at index {}", args.index - 1) |
| 130 | + })? |
| 131 | + .proxy, |
| 132 | + data_fetcher.l1_provider.clone(), |
| 133 | + ); |
| 134 | + let game = OPSuccinctFaultDisputeGame::new( |
| 135 | + factory |
| 136 | + .gameAtIndex(U256::from(args.index)) |
| 137 | + .call() |
| 138 | + .await |
| 139 | + .with_context(|| format!("failed to fetch the game at index {}", args.index))? |
| 140 | + .proxy, |
| 141 | + data_fetcher.l1_provider.clone(), |
| 142 | + ); |
| 143 | + |
| 144 | + info!("Proving for Game #{} (address: {})", args.index, game.address()); |
| 145 | + |
| 146 | + let l1_head_hash: [u8; 32] = game.l1Head().call().await?.0; |
| 147 | + let l2_start_block = parent_game.l2BlockNumber().call().await?.to::<u64>(); |
| 148 | + let l2_end_block = game.l2BlockNumber().call().await?.to::<u64>(); |
| 149 | + |
| 150 | + let l1_head = data_fetcher |
| 151 | + .l1_provider |
| 152 | + .get_block_by_hash(l1_head_hash.into()) |
| 153 | + .await? |
| 154 | + .expect("failed to fetch L1 head block") |
| 155 | + .header; |
| 156 | + |
| 157 | + info!( |
| 158 | + l2_start_block, |
| 159 | + l2_end_block, |
| 160 | + l1_head_number = l1_head.number, |
| 161 | + l1_head_hash = %l1_head.hash, |
| 162 | + "Proving L2 block range against L1 head" |
| 163 | + ); |
| 164 | + |
| 165 | + // 2. Generate the range proof. |
| 166 | + let host = initialize_host(Arc::new(data_fetcher.clone())); |
| 167 | + let host_args = |
| 168 | + host.fetch(l2_start_block, l2_end_block, Some(l1_head_hash.into()), false).await?; |
| 169 | + |
| 170 | + info!("Generating range proof witness data..."); |
| 171 | + let witness_data = host.run(&host_args).await?; |
| 172 | + info!("Range proof witness data generated successfully"); |
| 173 | + |
| 174 | + info!("Getting range proof stdin..."); |
| 175 | + let range_proof_stdin = host.witness_generator().get_sp1_stdin(witness_data)?; |
| 176 | + info!("Range proof stdin generated successfully"); |
| 177 | + |
| 178 | + // Initialize the network prover. |
| 179 | + let network_prover = |
| 180 | + ProverClient::builder().network_for(network_mode).signer(network_signer.clone()).build(); |
| 181 | + info!("Initialized network prover successfully"); |
| 182 | + |
| 183 | + let (range_pk, range_vk) = network_prover.setup(get_range_elf_embedded()); |
| 184 | + let mut range_proof = network_prover |
| 185 | + .prove(&range_pk, &range_proof_stdin) |
| 186 | + .compressed() |
| 187 | + .strategy(config.range_proof_strategy) |
| 188 | + .run() |
| 189 | + .unwrap(); |
| 190 | + |
| 191 | + // Save the proof to the proof directory corresponding to the chain ID. |
| 192 | + let range_proof_dir = |
| 193 | + format!("data/{}/proofs/range", data_fetcher.get_l2_chain_id().await.unwrap()); |
| 194 | + if !std::path::Path::new(&range_proof_dir).exists() { |
| 195 | + fs::create_dir_all(&range_proof_dir).unwrap(); |
| 196 | + } |
| 197 | + range_proof |
| 198 | + .save(format!("{range_proof_dir}/{l2_start_block}-{l2_end_block}.bin")) |
| 199 | + .expect("saving proof failed"); |
| 200 | + info!("Range proof saved to {range_proof_dir}/{l2_start_block}-{l2_end_block}.bin"); |
| 201 | + |
| 202 | + // Validation |
| 203 | + let boot_info: BootInfoStruct = range_proof.public_values.read(); |
| 204 | + |
| 205 | + info!("BootInfo L1 head: {:?}", boot_info.l1Head); |
| 206 | + info!("Game L1 head: {:?}", l1_head.hash); |
| 207 | + assert_eq!(boot_info.l1Head, l1_head.hash, "L1 head hash mismatch"); |
| 208 | + |
| 209 | + let game_root_claim = game.rootClaim().call().await?; |
| 210 | + info!("Boot Info L2PostRoot: {:?}", boot_info.l2PostRoot); |
| 211 | + info!("Game Root Claim: {:?}", game_root_claim); |
| 212 | + assert_eq!(boot_info.l2PostRoot, game_root_claim, "Root claim mismatch"); |
| 213 | + |
| 214 | + let game_rollup_config_hash = game.rollupConfigHash().call().await?; |
| 215 | + info!("Boot Info Rollup Config Hash: {:?}", boot_info.rollupConfigHash); |
| 216 | + info!("Game Rollup Config Hash: {:?}", game_rollup_config_hash); |
| 217 | + assert_eq!(boot_info.rollupConfigHash, game_rollup_config_hash, "Rollup config hash mismatch"); |
| 218 | + |
| 219 | + let range_vk_hash = B256::from(u32_to_u8(range_vk.vk.hash_u32())); |
| 220 | + let game_range_v_key_hash = game.rangeVkeyCommitment().call().await?; |
| 221 | + info!("Range Verification Key Hash: {:?}", range_vk_hash); |
| 222 | + info!("Game Range Verification Key Hash: {:?}", game_range_v_key_hash); |
| 223 | + assert_eq!(range_vk_hash, game_range_v_key_hash, "Range verification key hash mismatch"); |
| 224 | + |
| 225 | + // 3. Generate the aggregation proof. |
| 226 | + let network_prover = |
| 227 | + ProverClient::builder().network_for(network_mode).signer(network_signer).build(); |
| 228 | + info!("Initialized network prover successfully"); |
| 229 | + |
| 230 | + let agg_proof_stdin = get_agg_proof_stdin( |
| 231 | + vec![range_proof.proof], |
| 232 | + vec![boot_info.clone()], |
| 233 | + vec![l1_head.clone().into()], |
| 234 | + &range_vk, |
| 235 | + boot_info.l1Head, |
| 236 | + wallet.address(), |
| 237 | + ) |
| 238 | + .context("failed to get agg proof stdin")?; |
| 239 | + |
| 240 | + let agg_proof_mode = match config.agg_proof_mode.to_lowercase().as_str() { |
| 241 | + "groth16" => SP1ProofMode::Groth16, |
| 242 | + "plonk" => SP1ProofMode::Plonk, |
| 243 | + other => { |
| 244 | + return Err(anyhow!( |
| 245 | + "Invalid AGG_PROOF_MODE '{}'. Expected one of: plonk, groth16", |
| 246 | + other |
| 247 | + )) |
| 248 | + } |
| 249 | + }; |
| 250 | + info!("Aggregation proof mode: {:?}", agg_proof_mode); |
| 251 | + |
| 252 | + let (agg_pk, agg_vk) = network_prover.setup(AGGREGATION_ELF); |
| 253 | + |
| 254 | + let agg_vk_hash = agg_vk.bytes32(); |
| 255 | + let game_range_aggregation_v_key = game.aggregationVkey().call().await?.to_string(); |
| 256 | + info!("Aggregation Verification Key: {:?}", range_vk_hash); |
| 257 | + info!("Game Aggregation Verification Key: {}", game_range_aggregation_v_key); |
| 258 | + assert_eq!( |
| 259 | + agg_vk_hash, game_range_aggregation_v_key, |
| 260 | + "Aggregation verification key hash mismatch" |
| 261 | + ); |
| 262 | + |
| 263 | + let agg_proof = network_prover |
| 264 | + .prove(&agg_pk, &agg_proof_stdin) |
| 265 | + .mode(agg_proof_mode) |
| 266 | + .strategy(config.agg_proof_strategy) |
| 267 | + .run() |
| 268 | + .unwrap(); |
| 269 | + |
| 270 | + let agg_proof_dir = |
| 271 | + format!("data/{}/proofs/agg", data_fetcher.get_l2_chain_id().await.unwrap()); |
| 272 | + if !std::path::Path::new(&agg_proof_dir).exists() { |
| 273 | + fs::create_dir_all(&agg_proof_dir).unwrap(); |
| 274 | + } |
| 275 | + |
| 276 | + agg_proof.save(format!("{agg_proof_dir}/agg.bin")).expect("saving proof failed"); |
| 277 | + info!("Agg proof saved to {agg_proof_dir}/agg.bin"); |
| 278 | + |
| 279 | + // 4. Spin up anvil. |
| 280 | + let fork_number = l1_head.number + 1; |
| 281 | + |
| 282 | + let anvil = |
| 283 | + Anvil::new().fork(config.l1_rpc).fork_block_number(fork_number).args(["--no-mining"]); |
| 284 | + let anvil_instance = anvil.spawn(); |
| 285 | + let endpoint = anvil_instance.endpoint(); |
| 286 | + info!("Anvil chain started forked from L1 block number: {} at: {}", fork_number, endpoint); |
| 287 | + |
| 288 | + // 5. Run the preflight check. |
| 289 | + let provider_with_signer = ProviderBuilder::new() |
| 290 | + .wallet(EthereumWallet::from(wallet)) |
| 291 | + .connect_http(Url::parse(&endpoint)?); |
| 292 | + |
| 293 | + let game = |
| 294 | + OPSuccinctFaultDisputeGame::new(game.address().clone(), provider_with_signer.clone()); |
| 295 | + |
| 296 | + let tx = game.prove(agg_proof.bytes().into()).send().await?; |
| 297 | + |
| 298 | + let client = provider_with_signer.client(); |
| 299 | + let _: String = client.request("evm_mine", Vec::<serde_json::Value>::new()).await?; |
| 300 | + |
| 301 | + let block = provider_with_signer.get_block_by_number(BlockNumberOrTag::Latest).await?; |
| 302 | + info!("Mined block: {}", block.unwrap().header.number); |
| 303 | + |
| 304 | + let receipt = tx.get_receipt().await?; |
| 305 | + info!("Transaction receipt: {:?}", receipt); |
| 306 | + |
| 307 | + let claim_data = game.claimData().call().await?; |
| 308 | + assert_eq!(claim_data.status, ProposalStatus::UnchallengedAndValidProofProvided); |
| 309 | + |
| 310 | + info!("Prove dry-run completed successfully"); |
| 311 | + |
| 312 | + Ok(()) |
| 313 | +} |
0 commit comments