diff --git a/Cargo.lock b/Cargo.lock index 9ccdd41..4411351 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1107,6 +1107,12 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -3117,6 +3123,10 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -3639,18 +3649,20 @@ dependencies = [ [[package]] name = "qp-plonky2" -version = "1.1.6" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74c09b03472d90b14309122930a9efb80541e0df2b83eae0ee280dc60d09657" +checksum = "414a0203c2d4787ad76be16aaa6a3e5bedf4ac5baa66ecfab46ed40c88d10af9" dependencies = [ "ahash", "anyhow", + "critical-section", "getrandom 0.2.17", "hashbrown 0.14.5", "itertools 0.11.0", "keccak-hash 0.8.0", "log", "num", + "once_cell", "p3-field", "p3-goldilocks", "p3-poseidon2", @@ -3671,9 +3683,9 @@ dependencies = [ [[package]] name = "qp-plonky2-core" -version = "1.1.6" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4b67b899994eae278d802c9d8735fc824f4ecfc1b61f8a2a9163b88384162a" +checksum = "e97d56a59ca75de58414a058195e6d8630b524ffb333d1a4555f2b49f4113cdd" dependencies = [ "ahash", "anyhow", @@ -3697,9 +3709,9 @@ dependencies = [ [[package]] name = "qp-plonky2-field" -version = "1.1.6" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c90a6abc681568deeb0a1f238225de5d3bf11e89373894e5199395268c68cd8" +checksum = "bf15d2ccea0cb80e61ee27315bdcb272fdbbb19c1826936569887c4c5b499627" dependencies = [ "anyhow", "itertools 0.11.0", @@ -3714,17 +3726,19 @@ dependencies = [ [[package]] name = "qp-plonky2-verifier" -version = "1.1.6" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5283d4b8c9bf9f5488643f46b8a9886161acfc654ef8b7d77c53745f3c6e0f56" +checksum = "7741059492bd163a1eeaec7471e2cf3d00b403e1ddec476fa5248237f7493298" dependencies = [ "ahash", "anyhow", + "critical-section", "hashbrown 0.14.5", "itertools 0.11.0", "keccak-hash 0.8.0", "log", "num", + "once_cell", "p3-field", "p3-goldilocks", "p3-poseidon2", @@ -3816,9 +3830,9 @@ dependencies = [ [[package]] name = "qp-wormhole-aggregator" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b1c758c298bc5b19a3389fe7606a620603a690cfdd19a79a487fa080cb11d2" +checksum = "2a96cf6018afa939953257a738e503ce305546c85d8777aa66153f21dc535ffe" dependencies = [ "anyhow", "hex", @@ -3835,9 +3849,9 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e303f0f9076d27518b5ee21f25ddb1c1fc75bbea733f06abdedd08074392fa4" +checksum = "b86a9f070a7cc9d00e69caed517d2add5913484ce05052e3fb9849cd9c0587d4" dependencies = [ "anyhow", "hex", @@ -3848,9 +3862,9 @@ dependencies = [ [[package]] name = "qp-wormhole-circuit-builder" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242e1e510e2f6cf3d8797fa1b92cf7c06e53f121fe8515d26d57c9458f87d249" +checksum = "177ce48156dd5fe8d84e581ba70ffaf4ee1334986697f58305b04c11b9d3566c" dependencies = [ "anyhow", "clap", @@ -3862,18 +3876,18 @@ dependencies = [ [[package]] name = "qp-wormhole-inputs" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39fcd1a9f1161d6c2f7b723e35ca76425c41853f582355ddba9ccfd1f9d04b23" +checksum = "6b81b137f4cf98b62f4437694fb1849e07b65f2b556dddeea6e91e7209cbf168" dependencies = [ "anyhow", ] [[package]] name = "qp-wormhole-prover" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ff0f27b5de4c41e3984a31ae12d212494de9a55cb22a503bce294d5ab1dfab" +checksum = "4fae60b441328ecf956c5bd34c98810cb7d536bccdc65ceccfed5bf2b46344eb" dependencies = [ "anyhow", "qp-plonky2", @@ -3884,9 +3898,9 @@ dependencies = [ [[package]] name = "qp-wormhole-verifier" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245908bc1b88872566931e8c7c5b00bc6bd6aa042fdbcb1703c4fe289e7e3bdf" +checksum = "3f9d517f9570578944a22a53f514ce9f7643857f28b256252c6ba82cbebc308a" dependencies = [ "anyhow", "qp-plonky2-verifier", @@ -3895,9 +3909,9 @@ dependencies = [ [[package]] name = "qp-zk-circuits-common" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2d4ac7c42d0f118af80e7d9aec273432999268ea76a8a982ad7d780bb8bf98" +checksum = "9d0649182166496ef48ad23f9c94062baf5d98499c409ff9a92f784bf5fa4efe" dependencies = [ "anyhow", "hex", diff --git a/Cargo.toml b/Cargo.toml index a3f10e2..01d72bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,18 +77,18 @@ subxt-metadata = "0.44" # ZK proof generation (aligned with chain) anyhow = "1.0" -qp-wormhole-circuit = { version = "1.3.0", default-features = false, features = ["std"] } -qp-wormhole-prover = { version = "1.3.0", default-features = false, features = ["std"] } -qp-wormhole-verifier = { version = "1.3.0", default-features = false, features = ["std"] } -qp-wormhole-aggregator = { version = "1.3.0", default-features = false, features = ["rayon", "std"] } -qp-wormhole-inputs = { version = "1.3.0", default-features = false, features = ["std"] } -qp-zk-circuits-common = { version = "1.3.0", default-features = false, features = ["std"] } -qp-plonky2 = { version = "1.1.6", default-features = false, features = ["rand", "std"] } -qp-wormhole-circuit-builder = { version = "1.3.0" } +qp-plonky2 = { version = "1.4.0", default-features = false, features = ["rand", "std"] } +qp-wormhole-circuit = { version = "1.4.0", default-features = false, features = ["std"] } +qp-wormhole-prover = { version = "1.4.0", default-features = false, features = ["std"] } +qp-wormhole-verifier = { version = "1.4.0", default-features = false, features = ["std"] } +qp-wormhole-aggregator = { version = "1.4.0", default-features = false, features = ["rayon", "std"] } +qp-wormhole-inputs = { version = "1.4.0", default-features = false, features = ["std"] } +qp-zk-circuits-common = { version = "1.4.0", default-features = false, features = ["std"] } +qp-wormhole-circuit-builder = { version = "1.4.0" } [build-dependencies] -qp-wormhole-circuit-builder = { version = "1.3.0" } +qp-wormhole-circuit-builder = { version = "1.4.0" } [dev-dependencies] tempfile = "3.8.1" diff --git a/build.rs b/build.rs index d16bfeb..6183c55 100644 --- a/build.rs +++ b/build.rs @@ -22,7 +22,8 @@ fn main() { .parse() .expect("QP_NUM_LEAF_PROOFS must be a valid usize"); - println!("cargo:rerun-if-changed=build.rs"); + // Don't emit any rerun-if-changed directives - this forces the build script + // to run on every build. Circuit generation is fast enough in release mode. println!( "cargo:warning=[quantus-cli] Generating ZK circuit binaries (num_leaf_proofs={})...", diff --git a/src/cli/address_format.rs b/src/cli/address_format.rs index 08ee1e2..99dde5e 100644 --- a/src/cli/address_format.rs +++ b/src/cli/address_format.rs @@ -29,3 +29,15 @@ impl QuantusSS58 for subxt::ext::subxt_core::utils::AccountId32 { sp_account_id.to_ss58check_with_version(quantus_ss58_format()) } } + +/// Convert raw 32-byte account to Quantus SS58 format +pub fn bytes_to_quantus_ss58(bytes: &[u8; 32]) -> String { + let sp_account_id = sp_core::crypto::AccountId32::from(*bytes); + sp_account_id.to_ss58check_with_version(quantus_ss58_format()) +} + +/// Convert a byte slice to Quantus SS58 format (panics if not 32 bytes) +pub fn slice_to_quantus_ss58(bytes: &[u8]) -> String { + let arr: [u8; 32] = bytes.try_into().expect("account must be 32 bytes"); + bytes_to_quantus_ss58(&arr) +} diff --git a/src/cli/wormhole.rs b/src/cli/wormhole.rs index b3d96a9..e1cb2b1 100644 --- a/src/cli/wormhole.rs +++ b/src/cli/wormhole.rs @@ -4,6 +4,7 @@ use crate::{ quantus_subxt::{self as quantus_node, api::wormhole}, }, cli::{ + address_format::{bytes_to_quantus_ss58, slice_to_quantus_ss58}, common::{submit_transaction, ExecutionMode}, send::get_balance, }, @@ -880,10 +881,11 @@ async fn aggregate_proofs( // De-quantize to show actual amount that will be minted let dequantized_amount = (account_data.summed_output_amount as u128) * SCALE_DOWN_FACTOR; + let ss58_address = slice_to_quantus_ss58(exit_bytes); log_print!( " [{}] {} -> {} quantized ({} planck = {})", idx, - hex::encode(exit_bytes), + ss58_address, account_data.summed_output_amount, dequantized_amount, format_balance(dequantized_amount) @@ -1304,8 +1306,13 @@ fn print_multiround_config( /// Execute initial transfers from wallet to wormhole addresses (round 1 only). /// -/// Sends all transfers in a single batched extrinsic using `utility.batch()`, -/// then parses the `NativeTransferred` events to extract transfer info for proof generation. +/// Sends all transfers in a single batched extrinsic using `utility.batch()`. +/// Transfer counts are queried from chain storage (not events) to determine the +/// `transfer_count` for each recipient, which is needed for proof generation. +/// +/// Note: We query `TransferCount` storage BEFORE submitting the batch because +/// the transfer_count in the proof must be the value at the time of transfer +/// (i.e., before the increment that happens during `record_transfer`). async fn execute_initial_transfers( quantus_client: &QuantusClient, wallet: &MultiroundWalletContext, @@ -1349,6 +1356,33 @@ async fn execute_initial_transfers( log_print!(" Submitting batch of {} transfers...", num_proofs); + // Query transfer counts BEFORE submitting the batch. + // The transfer_count used in the proof is the count at the time of transfer, + // which equals the count before the transfer (since it increments after). + let client = quantus_client.client(); + let mut transfer_counts_before: Vec = Vec::with_capacity(num_proofs); + for secret in secrets.iter() { + let wormhole_address = SubxtAccountId(secret.address); + let count = client + .storage() + .at_latest() + .await + .map_err(|e| { + crate::error::QuantusError::Generic(format!("Failed to get storage: {}", e)) + })? + .fetch(&quantus_node::api::storage().wormhole().transfer_count(wormhole_address)) + .await + .map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to fetch transfer count for {}: {}", + hex::encode(secret.address), + e + )) + })? + .unwrap_or(0); + transfer_counts_before.push(count); + } + submit_transaction( quantus_client, &quantum_keypair, @@ -1359,40 +1393,20 @@ async fn execute_initial_transfers( .await .map_err(|e| crate::error::QuantusError::Generic(format!("Batch transfer failed: {}", e)))?; - // Get the block and find all NativeTransferred events - let client = quantus_client.client(); + // Get the block hash for the transfer info (any recent block works since we use storage) let block = at_best_block(quantus_client) .await .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?; let block_hash = block.hash(); - let events_api = - client.events().at(block_hash).await.map_err(|e| { - crate::error::QuantusError::Generic(format!("Failed to get events: {}", e)) - })?; - - // Match each secret's wormhole address to its NativeTransferred event + // Build transfer info using the transfer counts we captured before the batch let funding_account: SubxtAccountId = SubxtAccountId(wallet.keypair.to_account_id_32().into()); let mut transfers = Vec::with_capacity(num_proofs); for (i, secret) in secrets.iter().enumerate() { - let event = events_api - .find::() - .find(|e| if let Ok(evt) = e { evt.to.0 == secret.address } else { false }) - .ok_or_else(|| { - crate::error::QuantusError::Generic(format!( - "No NativeTransferred event found for wormhole address {} (proof {})", - hex::encode(secret.address), - i + 1 - )) - })? - .map_err(|e| { - crate::error::QuantusError::Generic(format!("Event decode error: {}", e)) - })?; - transfers.push(TransferInfo { block_hash, - transfer_count: event.transfer_count, + transfer_count: transfer_counts_before[i], amount: partition_amounts[i], wormhole_address: SubxtAccountId(secret.address), funding_account: funding_account.clone(), @@ -1440,25 +1454,27 @@ async fn generate_round_proofs( log_print!(" Random output partition:"); for (i, assignment) in output_assignments.iter().enumerate() { let amt1_planck = (assignment.output_amount_1 as u128) * SCALE_DOWN_FACTOR; + let ss58_1 = bytes_to_quantus_ss58(&assignment.exit_account_1); if assignment.output_amount_2 > 0 { let amt2_planck = (assignment.output_amount_2 as u128) * SCALE_DOWN_FACTOR; + let ss58_2 = bytes_to_quantus_ss58(&assignment.exit_account_2); log_print!( - " Proof {}: {} ({}) -> 0x{}..., {} ({}) -> 0x{}...", + " Proof {}: {} ({}) -> {}, {} ({}) -> {}", i + 1, assignment.output_amount_1, format_balance(amt1_planck), - hex::encode(&assignment.exit_account_1[..4]), + ss58_1, assignment.output_amount_2, format_balance(amt2_planck), - hex::encode(&assignment.exit_account_2[..4]) + ss58_2 ); } else { log_print!( - " Proof {}: {} ({}) -> 0x{}...", + " Proof {}: {} ({}) -> {}", i + 1, assignment.output_amount_1, format_balance(amt1_planck), - hex::encode(&assignment.exit_account_1[..4]) + ss58_1 ); } } @@ -1978,6 +1994,25 @@ async fn verify_aggregated_and_get_events( // Verify locally before submitting on-chain log_verbose!("Verifying aggregated proof locally before on-chain submission..."); let bins_dir = Path::new("generated-bins"); + + // Log circuit binary hashes for debugging + let common_bytes = std::fs::read(bins_dir.join("aggregated_common.bin")).map_err(|e| { + crate::error::QuantusError::Generic(format!("Failed to read aggregated_common.bin: {}", e)) + })?; + let verifier_bytes = std::fs::read(bins_dir.join("aggregated_verifier.bin")).map_err(|e| { + crate::error::QuantusError::Generic(format!( + "Failed to read aggregated_verifier.bin: {}", + e + )) + })?; + println!( + "[quantus-cli] Circuit binaries: common_bytes.len={}, verifier_bytes.len={}, common_hash={}, verifier_hash={}", + common_bytes.len(), + verifier_bytes.len(), + hex::encode(blake3::hash(&common_bytes).as_bytes()), + hex::encode(blake3::hash(&verifier_bytes).as_bytes()), + ); + let verifier = WormholeVerifier::new_from_files( &bins_dir.join("aggregated_verifier.bin"), &bins_dir.join("aggregated_common.bin"), @@ -2030,11 +2065,11 @@ async fn verify_aggregated_and_get_events( // Log minted amounts log_print!(" Tokens minted (from NativeTransferred events):"); for (idx, transfer) in transfer_events.iter().enumerate() { - let to_hex = hex::encode(transfer.to.0); + let ss58_address = bytes_to_quantus_ss58(&transfer.to.0); log_print!( " [{}] {} -> {} planck ({})", idx, - to_hex, + ss58_address, transfer.amount, format_balance(transfer.amount) );