1use crate::{
2 chain::{
3 client::{ChainConfig, QuantusClient},
4 quantus_subxt::{self as quantus_node, api::wormhole},
5 },
6 cli::{
7 common::{submit_transaction, ExecutionMode},
8 send::get_balance,
9 },
10 log_error, log_print, log_success, log_verbose,
11 wallet::{password, QuantumKeyPair, WalletManager},
12};
13use clap::Subcommand;
14use indicatif::{ProgressBar, ProgressStyle};
15use plonky2::plonk::proof::ProofWithPublicInputs;
16use qp_rusty_crystals_hdwallet::{
17 derive_wormhole_from_mnemonic, generate_mnemonic, SensitiveBytes32, WormholePair,
18 QUANTUS_WORMHOLE_CHAIN_ID,
19};
20use qp_wormhole_circuit::{
21 inputs::{CircuitInputs, ParseAggregatedPublicInputs, PrivateCircuitInputs},
22 nullifier::Nullifier,
23};
24use qp_wormhole_inputs::{AggregatedPublicCircuitInputs, PublicCircuitInputs};
25use qp_wormhole_prover::WormholeProver;
26use qp_zk_circuits_common::{
27 circuit::{C, D, F},
28 storage_proof::prepare_proof_for_circuit,
29 utils::{digest_felts_to_bytes, BytesDigest},
30};
31use rand::RngCore;
32use sp_core::crypto::{AccountId32, Ss58Codec};
33use std::path::Path;
34use subxt::{
35 backend::legacy::rpc_methods::ReadProof,
36 blocks::Block,
37 ext::{
38 codec::Encode,
39 jsonrpsee::{core::client::ClientT, rpc_params},
40 },
41 utils::{to_hex, AccountId32 as SubxtAccountId},
42 OnlineClient,
43};
44
45pub const NATIVE_ASSET_ID: u32 = 0;
47
48pub const SCALE_DOWN_FACTOR: u128 = 10_000_000_000;
50
51pub const VOLUME_FEE_BPS: u32 = 10;
54
55#[derive(Debug, Clone, serde::Deserialize, Default)]
58pub struct BinaryHashes {
59 pub prover: Option<String>,
60 pub aggregated_common: Option<String>,
61 pub aggregated_verifier: Option<String>,
62 pub dummy_proof: Option<String>,
63}
64
65#[derive(Debug, Clone, serde::Deserialize)]
68pub struct AggregationConfig {
69 pub num_leaf_proofs: usize,
70 #[serde(default)]
71 pub hashes: Option<BinaryHashes>,
72}
73
74impl AggregationConfig {
75 pub fn load_from_bins() -> crate::error::Result<Self> {
77 let config_path = Path::new("generated-bins/config.json");
78 let config_str = std::fs::read_to_string(config_path).map_err(|e| {
79 crate::error::QuantusError::Generic(format!(
80 "Failed to read aggregation config from {}: {}. Run 'quantus developer build-circuits' first.",
81 config_path.display(),
82 e
83 ))
84 })?;
85 serde_json::from_str(&config_str).map_err(|e| {
86 crate::error::QuantusError::Generic(format!(
87 "Failed to parse aggregation config: {}",
88 e
89 ))
90 })
91 }
92
93 pub fn verify_binary_hashes(&self) -> crate::error::Result<()> {
95 use sha2::{Digest, Sha256};
96
97 let Some(ref stored_hashes) = self.hashes else {
98 log_verbose!(" No hashes in config.json, skipping binary verification");
99 return Ok(());
100 };
101
102 let bins_dir = Path::new("generated-bins");
103 let mut mismatches = Vec::new();
104
105 let hash_file = |filename: &str| -> Option<String> {
106 let path = bins_dir.join(filename);
107 std::fs::read(&path).ok().map(|bytes| {
108 let hash = Sha256::digest(&bytes);
109 hex::encode(hash)
110 })
111 };
112
113 let checks = [
114 ("aggregated_common.bin", &stored_hashes.aggregated_common),
115 ("aggregated_verifier.bin", &stored_hashes.aggregated_verifier),
116 ("prover.bin", &stored_hashes.prover),
117 ("dummy_proof.bin", &stored_hashes.dummy_proof),
118 ];
119 for (filename, expected_hash) in checks {
120 if let Some(ref expected) = expected_hash {
121 if let Some(actual) = hash_file(filename) {
122 if expected != &actual {
123 mismatches.push(format!("{}...", filename));
124 }
125 }
126 }
127 }
128
129 if mismatches.is_empty() {
130 log_verbose!(" Binary hashes verified successfully");
131 Ok(())
132 } else {
133 Err(crate::error::QuantusError::Generic(format!(
134 "Binary hash mismatch detected! The circuit binaries do not match config.json.\n\
135 This can happen if binaries were regenerated but the CLI wasn't rebuilt.\n\
136 Mismatches:\n {}\n\n\
137 To fix: Run 'quantus developer build-circuits' and then 'cargo build --release'",
138 mismatches.join("\n ")
139 )))
140 }
141 }
142}
143
144pub fn compute_output_amount(input_amount: u32, fee_bps: u32) -> u32 {
147 ((input_amount as u64) * (10000 - fee_bps as u64) / 10000) as u32
148}
149
150pub fn parse_secret_hex(secret_hex: &str) -> Result<[u8; 32], String> {
152 let secret_bytes = hex::decode(secret_hex.trim_start_matches("0x"))
153 .map_err(|e| format!("Invalid secret hex: {}", e))?;
154
155 if secret_bytes.len() != 32 {
156 return Err(format!("Secret must be exactly 32 bytes, got {} bytes", secret_bytes.len()));
157 }
158
159 secret_bytes
160 .try_into()
161 .map_err(|_| "Failed to convert secret to 32-byte array".to_string())
162}
163
164pub fn parse_exit_account(exit_account_str: &str) -> Result<[u8; 32], String> {
166 if let Some(hex_str) = exit_account_str.strip_prefix("0x") {
167 let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid exit account hex: {}", e))?;
168
169 if bytes.len() != 32 {
170 return Err(format!("Exit account must be 32 bytes, got {} bytes", bytes.len()));
171 }
172
173 bytes.try_into().map_err(|_| "Failed to convert exit account".to_string())
174 } else {
175 let account_id = AccountId32::from_ss58check(exit_account_str)
177 .map_err(|e| format!("Invalid SS58 address: {}", e))?;
178
179 Ok(account_id.into())
180 }
181}
182
183pub fn quantize_funding_amount(amount: u128) -> Result<u32, String> {
186 let quantized = amount / SCALE_DOWN_FACTOR;
187
188 quantized
189 .try_into()
190 .map_err(|_| format!("Funding amount {} too large after quantization", quantized))
191}
192
193pub fn read_proof_file(path: &str) -> Result<Vec<u8>, String> {
195 let proof_hex =
196 std::fs::read_to_string(path).map_err(|e| format!("Failed to read proof file: {}", e))?;
197
198 hex::decode(proof_hex.trim()).map_err(|e| format!("Failed to decode proof hex: {}", e))
199}
200
201pub fn write_proof_file(path: &str, proof_bytes: &[u8]) -> Result<(), String> {
203 let proof_hex = hex::encode(proof_bytes);
204 std::fs::write(path, proof_hex).map_err(|e| format!("Failed to write proof file: {}", e))
205}
206
207pub fn format_balance(amount: u128) -> String {
209 let whole = amount / 1_000_000_000_000;
210 let frac = (amount % 1_000_000_000_000) / 10_000_000_000; format!("{}.{:02} DEV", whole, frac)
212}
213
214pub fn random_partition(total: u128, n: usize, min_per_part: u128) -> Vec<u128> {
218 use rand::Rng;
219
220 if n == 0 {
221 return vec![];
222 }
223 if n == 1 {
224 return vec![total];
225 }
226
227 let min_total = min_per_part * n as u128;
229 if total < min_total {
230 let per_part = total / n as u128;
232 let remainder = total % n as u128;
233 let mut parts: Vec<u128> = vec![per_part; n];
234 parts[n - 1] += remainder;
236 return parts;
237 }
238
239 let distributable = total - min_total;
241
242 let mut rng = rand::rng();
244 let mut cuts: Vec<u128> = (0..n - 1).map(|_| rng.random_range(0..=distributable)).collect();
245 cuts.sort();
246
247 let mut parts = Vec::with_capacity(n);
249 let mut prev = 0u128;
250 for cut in cuts {
251 parts.push(min_per_part + (cut - prev));
252 prev = cut;
253 }
254 parts.push(min_per_part + (distributable - prev));
255
256 let sum: u128 = parts.iter().sum();
259 let diff = total as i128 - sum as i128;
260 if diff != 0 {
261 let idx = rng.random_range(0..n);
263 parts[idx] = (parts[idx] as i128 + diff).max(0) as u128;
264 }
265
266 parts
267}
268
269#[derive(Debug, Clone)]
271pub struct ProofOutputAssignment {
272 pub output_amount_1: u32,
274 pub exit_account_1: [u8; 32],
276 pub output_amount_2: u32,
278 pub exit_account_2: [u8; 32],
280}
281
282pub fn compute_random_output_assignments(
300 input_amounts: &[u128],
301 target_accounts: &[[u8; 32]],
302 fee_bps: u32,
303) -> Vec<ProofOutputAssignment> {
304 use rand::seq::SliceRandom;
305
306 let num_proofs = input_amounts.len();
307 let num_targets = target_accounts.len();
308
309 if num_proofs == 0 || num_targets == 0 {
310 return vec![];
311 }
312
313 let proof_outputs: Vec<u32> = input_amounts
315 .iter()
316 .map(|&input| {
317 let input_quantized = quantize_funding_amount(input).unwrap_or(0);
318 compute_output_amount(input_quantized, fee_bps)
319 })
320 .collect();
321
322 let total_output: u64 = proof_outputs.iter().map(|&x| x as u64).sum();
323
324 let min_per_target = 1u128;
327 let target_amounts_u128 = random_partition(total_output as u128, num_targets, min_per_target);
328 let target_amounts: Vec<u32> = target_amounts_u128.iter().map(|&x| x as u32).collect();
329
330 let mut rng = rand::rng();
344
345 let mut target_remaining: Vec<u32> = target_amounts.clone();
347
348 let mut assignments: Vec<ProofOutputAssignment> = proof_outputs
350 .iter()
351 .map(|&po| ProofOutputAssignment {
352 output_amount_1: po,
353 exit_account_1: [0u8; 32],
354 output_amount_2: 0,
355 exit_account_2: [0u8; 32],
356 })
357 .collect();
358
359 let mut shuffled_targets: Vec<usize> = (0..num_targets).collect();
363 shuffled_targets.shuffle(&mut rng);
364
365 for (assign_idx, &tidx) in shuffled_targets.iter().enumerate() {
366 let proof_idx = assign_idx % num_proofs;
367 let assignment = &mut assignments[proof_idx];
368
369 if assignment.exit_account_1 == [0u8; 32] {
370 let assign = assignment.output_amount_1.min(target_remaining[tidx]);
372 assignment.exit_account_1 = target_accounts[tidx];
373 assignment.output_amount_1 = assign;
375 target_remaining[tidx] -= assign;
376 } else if assignment.exit_account_2 == [0u8; 32] {
377 let avail = proof_outputs[proof_idx].saturating_sub(assignment.output_amount_1);
379 let assign = avail.min(target_remaining[tidx]);
380 assignment.exit_account_2 = target_accounts[tidx];
381 assignment.output_amount_2 = assign;
382 target_remaining[tidx] -= assign;
383 }
384 }
386
387 for proof_idx in 0..num_proofs {
390 let total_proof_output = proof_outputs[proof_idx];
391 let current_sum =
392 assignments[proof_idx].output_amount_1 + assignments[proof_idx].output_amount_2;
393 let mut shortfall = total_proof_output.saturating_sub(current_sum);
394
395 if shortfall > 0 {
396 assignments[proof_idx].output_amount_1 += shortfall;
398 shortfall = 0;
399 }
400
401 if assignments[proof_idx].exit_account_1 == [0u8; 32] && num_targets > 0 {
403 assignments[proof_idx].exit_account_1 = target_accounts[0];
404 }
405
406 let _ = shortfall; }
408
409 assignments
410}
411
412pub struct VerificationResult {
414 pub success: bool,
415 pub exit_amount: Option<u128>,
416 pub error_message: Option<String>,
417}
418
419async fn check_proof_verification_events(
422 client: &subxt::OnlineClient<ChainConfig>,
423 block_hash: &subxt::utils::H256,
424 tx_hash: &subxt::utils::H256,
425 verbose: bool,
426) -> crate::error::Result<VerificationResult> {
427 use crate::chain::quantus_subxt::api::system::events::ExtrinsicFailed;
428 use colored::Colorize;
429
430 let block = client.blocks().at(*block_hash).await.map_err(|e| {
431 crate::error::QuantusError::NetworkError(format!("Failed to get block: {e:?}"))
432 })?;
433
434 let extrinsics = block.extrinsics().await.map_err(|e| {
435 crate::error::QuantusError::NetworkError(format!("Failed to get extrinsics: {e:?}"))
436 })?;
437
438 let our_extrinsic_index = extrinsics
440 .iter()
441 .enumerate()
442 .find(|(_, ext)| ext.hash() == *tx_hash)
443 .map(|(idx, _)| idx);
444
445 let events = block.events().await.map_err(|e| {
446 crate::error::QuantusError::NetworkError(format!("Failed to fetch events: {e:?}"))
447 })?;
448
449 let metadata = client.metadata();
450
451 let mut verification_result =
452 VerificationResult { success: false, exit_amount: None, error_message: None };
453
454 if verbose {
455 log_print!("");
456 log_print!("📋 Transaction Events:");
457 }
458
459 if let Some(ext_idx) = our_extrinsic_index {
460 for event_result in events.iter() {
461 let event = event_result.map_err(|e| {
462 crate::error::QuantusError::NetworkError(format!("Failed to decode event: {e:?}"))
463 })?;
464
465 if let subxt::events::Phase::ApplyExtrinsic(event_ext_idx) = event.phase() {
467 if event_ext_idx != ext_idx as u32 {
468 continue;
469 }
470
471 if verbose {
473 log_print!(
474 " 📌 {}.{}",
475 event.pallet_name().bright_cyan(),
476 event.variant_name().bright_yellow()
477 );
478
479 if let Ok(typed_event) =
481 event.as_root_event::<crate::chain::quantus_subxt::api::Event>()
482 {
483 log_print!(" 📝 {:?}", typed_event);
484 }
485 }
486
487 if let Ok(Some(proof_verified)) =
489 event.as_event::<wormhole::events::ProofVerified>()
490 {
491 verification_result.success = true;
492 verification_result.exit_amount = Some(proof_verified.exit_amount);
493 }
494
495 if let Ok(Some(ExtrinsicFailed { dispatch_error, .. })) =
497 event.as_event::<ExtrinsicFailed>()
498 {
499 let error_msg = format_dispatch_error(&dispatch_error, &metadata);
500 verification_result.success = false;
501 verification_result.error_message = Some(error_msg);
502 }
503 }
504 }
505 }
506
507 if verbose {
508 log_print!("");
509 }
510
511 Ok(verification_result)
512}
513
514fn format_dispatch_error(
516 error: &crate::chain::quantus_subxt::api::runtime_types::sp_runtime::DispatchError,
517 metadata: &subxt::Metadata,
518) -> String {
519 use crate::chain::quantus_subxt::api::runtime_types::sp_runtime::DispatchError;
520
521 match error {
522 DispatchError::Module(module_error) => {
523 let pallet_name = metadata
524 .pallet_by_index(module_error.index)
525 .map(|p| p.name())
526 .unwrap_or("Unknown");
527 let error_index = module_error.error[0];
528 format!("{}::Error[{}]", pallet_name, error_index)
529 },
530 DispatchError::BadOrigin => "BadOrigin".to_string(),
531 DispatchError::CannotLookup => "CannotLookup".to_string(),
532 DispatchError::Other => "Other".to_string(),
533 _ => format!("{:?}", error),
534 }
535}
536
537#[derive(Subcommand, Debug)]
538pub enum WormholeCommands {
539 Address {
541 #[arg(long)]
543 secret: String,
544 },
545 Prove {
547 #[arg(long)]
549 secret: String,
550
551 #[arg(long)]
553 amount: u128,
554
555 #[arg(long)]
557 exit_account: String,
558
559 #[arg(long)]
561 block: String,
562
563 #[arg(long)]
565 transfer_count: u64,
566
567 #[arg(long)]
569 funding_account: String,
570
571 #[arg(short, long, default_value = "proof.hex")]
573 output: String,
574 },
575 Aggregate {
577 #[arg(short, long, num_args = 1..)]
579 proofs: Vec<String>,
580
581 #[arg(short, long, default_value = "aggregated_proof.hex")]
583 output: String,
584 },
585 VerifyAggregated {
587 #[arg(short, long, default_value = "aggregated_proof.hex")]
589 proof: String,
590 },
591 ParseProof {
593 #[arg(short, long)]
595 proof: String,
596
597 #[arg(long)]
599 aggregated: bool,
600
601 #[arg(long)]
603 verify: bool,
604 },
605 Multiround {
607 #[arg(short, long, default_value = "2")]
609 num_proofs: usize,
610
611 #[arg(short, long, default_value = "2")]
613 rounds: usize,
614
615 #[arg(short, long, default_value = "100000000000000")]
617 amount: u128,
618
619 #[arg(short, long)]
621 wallet: String,
622
623 #[arg(short, long)]
625 password: Option<String>,
626
627 #[arg(long)]
629 password_file: Option<String>,
630
631 #[arg(short, long)]
633 keep_files: bool,
634
635 #[arg(short, long, default_value = "/tmp/wormhole_multiround")]
637 output_dir: String,
638
639 #[arg(long)]
641 dry_run: bool,
642 },
643}
644
645pub async fn handle_wormhole_command(
646 command: WormholeCommands,
647 node_url: &str,
648) -> crate::error::Result<()> {
649 match command {
650 WormholeCommands::Address { secret } => show_wormhole_address(secret),
651 WormholeCommands::Prove {
652 secret,
653 amount,
654 exit_account,
655 block,
656 transfer_count,
657 funding_account,
658 output,
659 } => {
660 log_print!("Generating proof from existing transfer...");
661
662 let quantus_client = QuantusClient::new(node_url).await.map_err(|e| {
664 crate::error::QuantusError::Generic(format!("Failed to connect: {}", e))
665 })?;
666
667 let exit_account_bytes =
669 parse_exit_account(&exit_account).map_err(crate::error::QuantusError::Generic)?;
670
671 let input_amount_quantized =
673 quantize_funding_amount(amount).map_err(crate::error::QuantusError::Generic)?;
674 let output_amount = compute_output_amount(input_amount_quantized, VOLUME_FEE_BPS);
675
676 let output_assignment = ProofOutputAssignment {
677 output_amount_1: output_amount,
678 exit_account_1: exit_account_bytes,
679 output_amount_2: 0,
680 exit_account_2: [0u8; 32],
681 };
682
683 let prove_start = std::time::Instant::now();
684 generate_proof(
685 &secret,
686 amount,
687 &output_assignment,
688 &block,
689 transfer_count,
690 &funding_account,
691 &output,
692 &quantus_client,
693 )
694 .await?;
695 let prove_elapsed = prove_start.elapsed();
696 log_print!("Proof generation: {:.2}s", prove_elapsed.as_secs_f64());
697 Ok(())
698 },
699 WormholeCommands::Aggregate { proofs, output } => aggregate_proofs(proofs, output).await,
700 WormholeCommands::VerifyAggregated { proof } =>
701 verify_aggregated_proof(proof, node_url).await,
702 WormholeCommands::ParseProof { proof, aggregated, verify } =>
703 parse_proof_file(proof, aggregated, verify).await,
704 WormholeCommands::Multiround {
705 num_proofs,
706 rounds,
707 amount,
708 wallet,
709 password,
710 password_file,
711 keep_files,
712 output_dir,
713 dry_run,
714 } =>
715 run_multiround(
716 num_proofs,
717 rounds,
718 amount,
719 wallet,
720 password,
721 password_file,
722 keep_files,
723 output_dir,
724 dry_run,
725 node_url,
726 )
727 .await,
728 }
729}
730
731pub type TransferProofKey = (u32, u64, AccountId32, AccountId32, u128);
732
733fn show_wormhole_address(secret_hex: String) -> crate::error::Result<()> {
736 use colored::Colorize;
737
738 let secret_array =
739 parse_secret_hex(&secret_hex).map_err(crate::error::QuantusError::Generic)?;
740 let secret: BytesDigest = secret_array.try_into().map_err(|e| {
741 crate::error::QuantusError::Generic(format!("Failed to convert secret: {:?}", e))
742 })?;
743
744 let unspendable_account =
745 qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
746 .account_id;
747 let unspendable_account_bytes_digest =
748 qp_zk_circuits_common::utils::digest_felts_to_bytes(unspendable_account);
749 let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest
750 .as_ref()
751 .try_into()
752 .expect("BytesDigest is always 32 bytes");
753
754 let account_id = sp_core::crypto::AccountId32::new(unspendable_account_bytes);
755 let ss58_address =
756 account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
757
758 log_print!("{}", "Wormhole Address".bright_cyan());
759 log_print!(" SS58: {}", ss58_address.bright_green());
760 log_print!(" Hex: 0x{}", hex::encode(unspendable_account_bytes));
761 log_print!("");
762 log_print!("To fund this address:");
763 log_print!(" quantus send --from <wallet> --to {} --amount <amount>", ss58_address);
764
765 Ok(())
766}
767
768async fn at_best_block(
769 quantus_client: &QuantusClient,
770) -> anyhow::Result<Block<ChainConfig, OnlineClient<ChainConfig>>> {
771 let best_block = quantus_client.get_latest_block().await?;
772 let block = quantus_client.client().blocks().at(best_block).await?;
773 Ok(block)
774}
775
776async fn aggregate_proofs(
777 proof_files: Vec<String>,
778 output_file: String,
779) -> crate::error::Result<()> {
780 use qp_wormhole_aggregator::aggregator::WormholeProofAggregator;
781 use qp_zk_circuits_common::aggregation::AggregationConfig as AggConfig;
782
783 use std::path::Path;
784
785 log_print!("Aggregating {} proofs...", proof_files.len());
786
787 let bins_dir = Path::new("generated-bins");
789 let agg_config = AggregationConfig::load_from_bins()?;
790
791 log_verbose!("Verifying circuit binary integrity...");
793 agg_config.verify_binary_hashes()?;
794
795 if proof_files.len() > agg_config.num_leaf_proofs {
797 return Err(crate::error::QuantusError::Generic(format!(
798 "Too many proofs: {} provided, max {} supported by circuit",
799 proof_files.len(),
800 agg_config.num_leaf_proofs
801 )));
802 }
803
804 let num_padding_proofs = agg_config.num_leaf_proofs - proof_files.len();
805
806 log_print!(" Loading aggregator and generating {} dummy proofs...", num_padding_proofs);
810
811 let aggr_config = AggConfig::new(agg_config.num_leaf_proofs);
812 let mut aggregator = WormholeProofAggregator::from_prebuilt_dir(bins_dir, aggr_config)
813 .map_err(|e| {
814 crate::error::QuantusError::Generic(format!(
815 "Failed to load aggregator from pre-built bins: {}",
816 e
817 ))
818 })?;
819
820 log_verbose!("Aggregation config: num_leaf_proofs={}", aggregator.config.num_leaf_proofs);
821 let common_data = aggregator.leaf_circuit_data.common.clone();
822
823 for (idx, proof_file) in proof_files.iter().enumerate() {
825 log_verbose!("Loading proof {}/{}: {}", idx + 1, proof_files.len(), proof_file);
826
827 let proof_bytes = read_proof_file(proof_file).map_err(|e| {
828 crate::error::QuantusError::Generic(format!("Failed to load {}: {}", proof_file, e))
829 })?;
830
831 let proof = ProofWithPublicInputs::<F, C, D>::from_bytes(proof_bytes, &common_data)
832 .map_err(|e| {
833 crate::error::QuantusError::Generic(format!(
834 "Failed to deserialize proof from {}: {}",
835 proof_file, e
836 ))
837 })?;
838
839 aggregator.push_proof(proof).map_err(|e| {
840 crate::error::QuantusError::Generic(format!("Failed to add proof: {}", e))
841 })?;
842 }
843
844 log_print!(" Running aggregation...");
845 let agg_start = std::time::Instant::now();
846 let aggregated_proof = aggregator
847 .aggregate()
848 .map_err(|e| crate::error::QuantusError::Generic(format!("Aggregation failed: {}", e)))?;
849 let agg_elapsed = agg_start.elapsed();
850 log_print!(" Aggregation: {:.2}s", agg_elapsed.as_secs_f64());
851
852 let aggregated_public_inputs = AggregatedPublicCircuitInputs::try_from_felts(
854 aggregated_proof.proof.public_inputs.as_slice(),
855 )
856 .map_err(|e| {
857 crate::error::QuantusError::Generic(format!(
858 "Failed to parse aggregated public inputs: {}",
859 e
860 ))
861 })?;
862
863 log_verbose!("Aggregated public inputs: {:#?}", aggregated_public_inputs);
864
865 log_print!(" Exit accounts in aggregated proof:");
867 for (idx, account_data) in aggregated_public_inputs.account_data.iter().enumerate() {
868 let exit_bytes: &[u8] = account_data.exit_account.as_ref();
869 let is_dummy = exit_bytes.iter().all(|&b| b == 0) || account_data.summed_output_amount == 0;
870 if is_dummy {
871 log_verbose!(" [{}] DUMMY (skipped)", idx);
872 } else {
873 let dequantized_amount =
875 (account_data.summed_output_amount as u128) * SCALE_DOWN_FACTOR;
876 log_print!(
877 " [{}] {} -> {} quantized ({} planck = {})",
878 idx,
879 hex::encode(exit_bytes),
880 account_data.summed_output_amount,
881 dequantized_amount,
882 format_balance(dequantized_amount)
883 );
884 }
885 }
886
887 log_verbose!("Verifying aggregated proof locally...");
889 aggregated_proof
890 .circuit_data
891 .verify(aggregated_proof.proof.clone())
892 .map_err(|e| {
893 crate::error::QuantusError::Generic(format!(
894 "Aggregated proof verification failed: {}",
895 e
896 ))
897 })?;
898
899 write_proof_file(&output_file, &aggregated_proof.proof.to_bytes()).map_err(|e| {
901 crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e))
902 })?;
903
904 log_success!("Aggregation complete!");
905 log_success!("Output: {}", output_file);
906 log_print!(
907 "Aggregated {} proofs into 1 proof with {} exit accounts",
908 proof_files.len(),
909 aggregated_public_inputs.account_data.len()
910 );
911
912 Ok(())
913}
914
915#[derive(Debug, Clone, Copy)]
916enum IncludedAt {
917 Best,
918 Finalized,
919}
920
921impl IncludedAt {
922 fn label(self) -> &'static str {
923 match self {
924 IncludedAt::Best => "best block",
925 IncludedAt::Finalized => "finalized block",
926 }
927 }
928}
929
930fn read_hex_proof_file_to_bytes(proof_file: &str) -> crate::error::Result<Vec<u8>> {
931 let proof_hex = std::fs::read_to_string(proof_file).map_err(|e| {
932 crate::error::QuantusError::Generic(format!("Failed to read proof file: {}", e))
933 })?;
934
935 let proof_bytes = hex::decode(proof_hex.trim())
936 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to decode hex: {}", e)))?;
937
938 Ok(proof_bytes)
939}
940
941async fn submit_unsigned_verify_aggregated_proof(
944 quantus_client: &QuantusClient,
945 proof_bytes: Vec<u8>,
946) -> crate::error::Result<(IncludedAt, subxt::utils::H256, subxt::utils::H256)> {
947 use subxt::tx::TxStatus;
948
949 let verify_tx = quantus_node::api::tx().wormhole().verify_aggregated_proof(proof_bytes);
950
951 let unsigned_tx = quantus_client.client().tx().create_unsigned(&verify_tx).map_err(|e| {
952 crate::error::QuantusError::Generic(format!("Failed to create unsigned tx: {}", e))
953 })?;
954
955 let mut tx_progress = unsigned_tx
956 .submit_and_watch()
957 .await
958 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to submit tx: {}", e)))?;
959
960 while let Some(Ok(status)) = tx_progress.next().await {
961 match status {
962 TxStatus::InBestBlock(tx_in_block) => {
963 return Ok((
964 IncludedAt::Best,
965 tx_in_block.block_hash(),
966 tx_in_block.extrinsic_hash(),
967 ));
968 },
969 TxStatus::InFinalizedBlock(tx_in_block) => {
970 return Ok((
971 IncludedAt::Finalized,
972 tx_in_block.block_hash(),
973 tx_in_block.extrinsic_hash(),
974 ));
975 },
976 TxStatus::Error { message } | TxStatus::Invalid { message } => {
977 return Err(crate::error::QuantusError::Generic(format!(
978 "Transaction failed: {}",
979 message
980 )));
981 },
982 _ => continue,
983 }
984 }
985
986 Err(crate::error::QuantusError::Generic("Transaction stream ended unexpectedly".to_string()))
987}
988
989async fn collect_wormhole_events_for_extrinsic(
992 quantus_client: &QuantusClient,
993 block_hash: subxt::utils::H256,
994 tx_hash: subxt::utils::H256,
995) -> crate::error::Result<(bool, Vec<wormhole::events::NativeTransferred>)> {
996 use crate::chain::quantus_subxt::api::system::events::ExtrinsicFailed;
997
998 let block =
999 quantus_client.client().blocks().at(block_hash).await.map_err(|e| {
1000 crate::error::QuantusError::Generic(format!("Failed to get block: {}", e))
1001 })?;
1002
1003 let events = block
1004 .events()
1005 .await
1006 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get events: {}", e)))?;
1007
1008 let extrinsics = block.extrinsics().await.map_err(|e| {
1009 crate::error::QuantusError::Generic(format!("Failed to get extrinsics: {}", e))
1010 })?;
1011
1012 let our_ext_idx = extrinsics
1013 .iter()
1014 .enumerate()
1015 .find(|(_, ext)| ext.hash() == tx_hash)
1016 .map(|(idx, _)| idx as u32)
1017 .ok_or_else(|| {
1018 crate::error::QuantusError::Generic(
1019 "Could not find submitted extrinsic in included block".to_string(),
1020 )
1021 })?;
1022
1023 let mut transfer_events = Vec::new();
1024 let mut found_proof_verified = false;
1025
1026 log_verbose!(" Events for our extrinsic (idx={}):", our_ext_idx);
1027
1028 for event_result in events.iter() {
1029 let event = event_result.map_err(|e| {
1030 crate::error::QuantusError::Generic(format!("Failed to decode event: {}", e))
1031 })?;
1032
1033 if let subxt::events::Phase::ApplyExtrinsic(ext_idx) = event.phase() {
1034 if ext_idx == our_ext_idx {
1035 log_print!(" Event: {}::{}", event.pallet_name(), event.variant_name());
1036
1037 if let Ok(Some(ExtrinsicFailed { dispatch_error, .. })) =
1039 event.as_event::<ExtrinsicFailed>()
1040 {
1041 let metadata = quantus_client.client().metadata();
1042 let error_msg = format_dispatch_error(&dispatch_error, &metadata);
1043 log_print!(" DispatchError: {}", error_msg);
1044 }
1045
1046 if let Ok(Some(_)) = event.as_event::<wormhole::events::ProofVerified>() {
1047 found_proof_verified = true;
1048 }
1049
1050 if let Ok(Some(transfer)) = event.as_event::<wormhole::events::NativeTransferred>()
1051 {
1052 transfer_events.push(transfer);
1053 }
1054 }
1055 }
1056 }
1057
1058 Ok((found_proof_verified, transfer_events))
1059}
1060
1061async fn verify_aggregated_proof(proof_file: String, node_url: &str) -> crate::error::Result<()> {
1062 log_print!("Verifying aggregated wormhole proof on-chain...");
1063
1064 let proof_bytes = read_hex_proof_file_to_bytes(&proof_file)?;
1065 log_verbose!("Aggregated proof size: {} bytes", proof_bytes.len());
1066
1067 let quantus_client = QuantusClient::new(node_url)
1069 .await
1070 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to connect: {}", e)))?;
1071 log_verbose!("Connected to node");
1072
1073 log_verbose!("Submitting unsigned aggregated verification transaction...");
1074
1075 let (included_at, block_hash, tx_hash) =
1076 submit_unsigned_verify_aggregated_proof(&quantus_client, proof_bytes).await?;
1077
1078 let result = check_proof_verification_events(
1080 quantus_client.client(),
1081 &block_hash,
1082 &tx_hash,
1083 crate::log::is_verbose(),
1084 )
1085 .await?;
1086
1087 if result.success {
1088 log_success!("Aggregated proof verified successfully on-chain!");
1089 if let Some(amount) = result.exit_amount {
1090 log_success!("Total exit amount: {}", format_balance(amount));
1091 }
1092
1093 log_verbose!("Included in {}: {:?}", included_at.label(), block_hash);
1094 return Ok(());
1095 }
1096
1097 let error_msg = result.error_message.unwrap_or_else(|| {
1098 "Aggregated proof verification failed - no ProofVerified event found".to_string()
1099 });
1100 log_error!("❌ {}", error_msg);
1101 Err(crate::error::QuantusError::Generic(error_msg))
1102}
1103
1104#[derive(Debug, Clone)]
1110#[allow(dead_code)]
1111struct TransferInfo {
1112 block_hash: subxt::utils::H256,
1114 transfer_count: u64,
1116 amount: u128,
1118 wormhole_address: SubxtAccountId,
1120 funding_account: SubxtAccountId,
1122}
1123
1124fn derive_wormhole_secret(
1127 mnemonic: &str,
1128 round: usize,
1129 index: usize,
1130) -> Result<WormholePair, crate::error::QuantusError> {
1131 let path = format!("m/44'/{}/0'/{}'/{}'", QUANTUS_WORMHOLE_CHAIN_ID, round, index);
1133 derive_wormhole_from_mnemonic(mnemonic, None, &path)
1134 .map_err(|e| crate::error::QuantusError::Generic(format!("HD derivation failed: {:?}", e)))
1135}
1136
1137fn calculate_round_amount(initial_amount: u128, round: usize) -> u128 {
1141 let mut amount = initial_amount;
1142 for _ in 0..round {
1143 amount = amount * 9990 / 10000;
1145 }
1146 amount
1147}
1148
1149async fn get_minting_account(
1151 client: &OnlineClient<ChainConfig>,
1152) -> Result<SubxtAccountId, crate::error::QuantusError> {
1153 let minting_account_addr = quantus_node::api::constants().wormhole().minting_account();
1154 let minting_account = client.constants().at(&minting_account_addr).map_err(|e| {
1155 crate::error::QuantusError::Generic(format!("Failed to get minting account: {}", e))
1156 })?;
1157 Ok(minting_account)
1158}
1159
1160fn parse_transfer_events(
1163 events: &[wormhole::events::NativeTransferred],
1164 expected_addresses: &[SubxtAccountId],
1165 block_hash: subxt::utils::H256,
1166) -> Result<Vec<TransferInfo>, crate::error::QuantusError> {
1167 let mut transfer_infos = Vec::new();
1168
1169 for expected_addr in expected_addresses {
1170 let matching_event = events.iter().find(|e| &e.to == expected_addr).ok_or_else(|| {
1172 crate::error::QuantusError::Generic(format!(
1173 "No transfer event found for address {:?}",
1174 expected_addr
1175 ))
1176 })?;
1177
1178 transfer_infos.push(TransferInfo {
1179 block_hash,
1180 transfer_count: matching_event.transfer_count,
1181 amount: matching_event.amount,
1182 wormhole_address: expected_addr.clone(),
1183 funding_account: matching_event.from.clone(),
1184 });
1185 }
1186
1187 Ok(transfer_infos)
1188}
1189
1190struct MultiroundConfig {
1192 num_proofs: usize,
1193 rounds: usize,
1194 amount: u128,
1195 output_dir: String,
1196 keep_files: bool,
1197}
1198
1199struct MultiroundWalletContext {
1201 wallet_name: String,
1202 wallet_address: String,
1203 wallet_account_id: SubxtAccountId,
1204 keypair: QuantumKeyPair,
1205 mnemonic: String,
1206}
1207
1208fn validate_multiround_params(
1210 num_proofs: usize,
1211 rounds: usize,
1212 max_proofs: usize,
1213) -> crate::error::Result<()> {
1214 if !(1..=max_proofs).contains(&num_proofs) {
1215 return Err(crate::error::QuantusError::Generic(format!(
1216 "num_proofs must be between 1 and {} (got: {})",
1217 max_proofs, num_proofs
1218 )));
1219 }
1220 if rounds < 1 {
1221 return Err(crate::error::QuantusError::Generic(format!(
1222 "rounds must be at least 1 (got: {})",
1223 rounds
1224 )));
1225 }
1226 Ok(())
1227}
1228
1229fn load_multiround_wallet(
1231 wallet_name: &str,
1232 password: Option<String>,
1233 password_file: Option<String>,
1234) -> crate::error::Result<MultiroundWalletContext> {
1235 let wallet_manager = WalletManager::new()?;
1236 let wallet_password = password::get_wallet_password(wallet_name, password, password_file)?;
1237 let wallet_data = wallet_manager.load_wallet(wallet_name, &wallet_password)?;
1238 let wallet_address = wallet_data.keypair.to_account_id_ss58check();
1239 let wallet_account_id = SubxtAccountId(wallet_data.keypair.to_account_id_32().into());
1240
1241 let mnemonic = match wallet_data.mnemonic {
1243 Some(m) => {
1244 log_verbose!("Using wallet mnemonic for HD derivation");
1245 m
1246 },
1247 None => {
1248 log_print!("Wallet has no mnemonic - generating random mnemonic for wormhole secrets");
1249 let mut entropy = [0u8; 32];
1250 rand::rng().fill_bytes(&mut entropy);
1251 let sensitive_entropy = SensitiveBytes32::from(&mut entropy);
1252 let m = generate_mnemonic(sensitive_entropy).map_err(|e| {
1253 crate::error::QuantusError::Generic(format!("Failed to generate mnemonic: {:?}", e))
1254 })?;
1255 log_verbose!("Generated mnemonic (not saved): {}", m);
1256 m
1257 },
1258 };
1259
1260 Ok(MultiroundWalletContext {
1261 wallet_name: wallet_name.to_string(),
1262 wallet_address,
1263 wallet_account_id,
1264 keypair: wallet_data.keypair,
1265 mnemonic,
1266 })
1267}
1268
1269fn print_multiround_config(
1271 config: &MultiroundConfig,
1272 wallet: &MultiroundWalletContext,
1273 agg_config: &AggregationConfig,
1274) {
1275 use colored::Colorize;
1276
1277 log_print!("{}", "Configuration:".bright_cyan());
1278 log_print!(" Wallet: {}", wallet.wallet_name);
1279 log_print!(" Wallet address: {}", wallet.wallet_address);
1280 log_print!(
1281 " Total amount: {} ({}) - randomly partitioned across {} proofs",
1282 config.amount,
1283 format_balance(config.amount),
1284 config.num_proofs
1285 );
1286 log_print!(" Proofs per round: {}", config.num_proofs);
1287 log_print!(" Rounds: {}", config.rounds);
1288 log_print!(" Aggregation: num_leaf_proofs={}", agg_config.num_leaf_proofs);
1289 log_print!(" Output directory: {}", config.output_dir);
1290 log_print!(" Keep files: {}", config.keep_files);
1291 log_print!("");
1292
1293 log_print!("{}", "Expected amounts per round:".bright_cyan());
1295 for r in 1..=config.rounds {
1296 let round_amount = calculate_round_amount(config.amount, r);
1297 log_print!(" Round {}: {} ({})", r, round_amount, format_balance(round_amount));
1298 }
1299 log_print!("");
1300}
1301
1302async fn execute_initial_transfers(
1307 quantus_client: &QuantusClient,
1308 wallet: &MultiroundWalletContext,
1309 secrets: &[WormholePair],
1310 amount: u128,
1311 num_proofs: usize,
1312) -> crate::error::Result<Vec<TransferInfo>> {
1313 use colored::Colorize;
1314 use quantus_node::api::runtime_types::{
1315 pallet_balances::pallet::Call as BalancesCall, quantus_runtime::RuntimeCall,
1316 };
1317
1318 log_print!("{}", "Step 1: Sending batched transfer to wormhole addresses...".bright_yellow());
1319
1320 let partition_amounts = random_partition(amount, num_proofs, 2 * SCALE_DOWN_FACTOR);
1324 log_print!(" Random partition of {} ({}):", amount, format_balance(amount));
1325 for (i, &amt) in partition_amounts.iter().enumerate() {
1326 log_print!(" Proof {}: {} ({})", i + 1, amt, format_balance(amt));
1327 }
1328
1329 let mut calls = Vec::with_capacity(num_proofs);
1331 for (i, secret) in secrets.iter().enumerate() {
1332 let wormhole_address = SubxtAccountId(secret.address);
1333 let transfer_call = RuntimeCall::Balances(BalancesCall::transfer_allow_death {
1334 dest: subxt::ext::subxt_core::utils::MultiAddress::Id(wormhole_address),
1335 value: partition_amounts[i],
1336 });
1337 calls.push(transfer_call);
1338 }
1339
1340 let batch_tx = quantus_node::api::tx().utility().batch(calls);
1341
1342 let quantum_keypair = QuantumKeyPair {
1343 public_key: wallet.keypair.public_key.clone(),
1344 private_key: wallet.keypair.private_key.clone(),
1345 };
1346
1347 log_print!(" Submitting batch of {} transfers...", num_proofs);
1348
1349 submit_transaction(
1350 quantus_client,
1351 &quantum_keypair,
1352 batch_tx,
1353 None,
1354 ExecutionMode { finalized: false, wait_for_transaction: true },
1355 )
1356 .await
1357 .map_err(|e| crate::error::QuantusError::Generic(format!("Batch transfer failed: {}", e)))?;
1358
1359 let client = quantus_client.client();
1361 let block = at_best_block(quantus_client)
1362 .await
1363 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?;
1364 let block_hash = block.hash();
1365
1366 let events_api =
1367 client.events().at(block_hash).await.map_err(|e| {
1368 crate::error::QuantusError::Generic(format!("Failed to get events: {}", e))
1369 })?;
1370
1371 let funding_account: SubxtAccountId = SubxtAccountId(wallet.keypair.to_account_id_32().into());
1373 let mut transfers = Vec::with_capacity(num_proofs);
1374
1375 for (i, secret) in secrets.iter().enumerate() {
1376 let event = events_api
1377 .find::<wormhole::events::NativeTransferred>()
1378 .find(|e| if let Ok(evt) = e { evt.to.0 == secret.address } else { false })
1379 .ok_or_else(|| {
1380 crate::error::QuantusError::Generic(format!(
1381 "No NativeTransferred event found for wormhole address {} (proof {})",
1382 hex::encode(secret.address),
1383 i + 1
1384 ))
1385 })?
1386 .map_err(|e| {
1387 crate::error::QuantusError::Generic(format!("Event decode error: {}", e))
1388 })?;
1389
1390 transfers.push(TransferInfo {
1391 block_hash,
1392 transfer_count: event.transfer_count,
1393 amount: partition_amounts[i],
1394 wormhole_address: SubxtAccountId(secret.address),
1395 funding_account: funding_account.clone(),
1396 });
1397 }
1398
1399 log_success!(
1400 " {} transfers submitted in a single batch (block {})",
1401 num_proofs,
1402 hex::encode(block_hash.0)
1403 );
1404
1405 Ok(transfers)
1406}
1407
1408async fn generate_round_proofs(
1410 quantus_client: &QuantusClient,
1411 secrets: &[WormholePair],
1412 transfers: &[TransferInfo],
1413 exit_accounts: &[SubxtAccountId],
1414 round_dir: &str,
1415 num_proofs: usize,
1416) -> crate::error::Result<Vec<String>> {
1417 use colored::Colorize;
1418
1419 log_print!("{}", "Step 2: Generating proofs...".bright_yellow());
1420
1421 let proof_block = at_best_block(quantus_client)
1423 .await
1424 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?;
1425 let proof_block_hash = proof_block.hash();
1426 log_print!(" Using block {} for all proofs", hex::encode(proof_block_hash.0));
1427
1428 let input_amounts: Vec<u128> = transfers.iter().map(|t| t.amount).collect();
1430 let exit_account_bytes: Vec<[u8; 32]> = exit_accounts.iter().map(|a| a.0).collect();
1431
1432 let output_assignments =
1434 compute_random_output_assignments(&input_amounts, &exit_account_bytes, VOLUME_FEE_BPS);
1435
1436 log_print!(" Random output partition:");
1438 for (i, assignment) in output_assignments.iter().enumerate() {
1439 let amt1_planck = (assignment.output_amount_1 as u128) * SCALE_DOWN_FACTOR;
1440 if assignment.output_amount_2 > 0 {
1441 let amt2_planck = (assignment.output_amount_2 as u128) * SCALE_DOWN_FACTOR;
1442 log_print!(
1443 " Proof {}: {} ({}) -> 0x{}..., {} ({}) -> 0x{}...",
1444 i + 1,
1445 assignment.output_amount_1,
1446 format_balance(amt1_planck),
1447 hex::encode(&assignment.exit_account_1[..4]),
1448 assignment.output_amount_2,
1449 format_balance(amt2_planck),
1450 hex::encode(&assignment.exit_account_2[..4])
1451 );
1452 } else {
1453 log_print!(
1454 " Proof {}: {} ({}) -> 0x{}...",
1455 i + 1,
1456 assignment.output_amount_1,
1457 format_balance(amt1_planck),
1458 hex::encode(&assignment.exit_account_1[..4])
1459 );
1460 }
1461 }
1462
1463 let pb = ProgressBar::new(num_proofs as u64);
1464 pb.set_style(
1465 ProgressStyle::default_bar()
1466 .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
1467 .unwrap()
1468 .progress_chars("#>-"),
1469 );
1470
1471 let proof_gen_start = std::time::Instant::now();
1472 let mut proof_files = Vec::new();
1473 for (i, (secret, transfer)) in secrets.iter().zip(transfers.iter()).enumerate() {
1474 pb.set_message(format!("Proof {}/{}", i + 1, num_proofs));
1475
1476 let proof_file = format!("{}/proof_{}.hex", round_dir, i + 1);
1477
1478 let funding_account_hex = format!("0x{}", hex::encode(transfer.funding_account.0));
1480
1481 let single_start = std::time::Instant::now();
1482
1483 generate_proof(
1485 &hex::encode(secret.secret),
1486 transfer.amount, &output_assignments[i],
1488 &format!("0x{}", hex::encode(proof_block_hash.0)),
1489 transfer.transfer_count,
1490 &funding_account_hex,
1491 &proof_file,
1492 quantus_client,
1493 )
1494 .await?;
1495
1496 let single_elapsed = single_start.elapsed();
1497 log_verbose!(" Proof {} generated in {:.2}s", i + 1, single_elapsed.as_secs_f64());
1498
1499 proof_files.push(proof_file);
1500 pb.inc(1);
1501 }
1502 pb.finish_with_message("Proofs generated");
1503 let proof_gen_elapsed = proof_gen_start.elapsed();
1504 log_print!(
1505 " Proof generation: {:.2}s ({} proofs, {:.2}s avg)",
1506 proof_gen_elapsed.as_secs_f64(),
1507 num_proofs,
1508 proof_gen_elapsed.as_secs_f64() / num_proofs as f64,
1509 );
1510
1511 Ok(proof_files)
1512}
1513
1514fn derive_round_secrets(
1516 mnemonic: &str,
1517 round: usize,
1518 num_proofs: usize,
1519) -> crate::error::Result<Vec<WormholePair>> {
1520 let pb = ProgressBar::new(num_proofs as u64);
1521 pb.set_style(
1522 ProgressStyle::default_bar()
1523 .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
1524 .unwrap()
1525 .progress_chars("#>-"),
1526 );
1527 pb.set_message("Deriving secrets...");
1528
1529 let mut secrets = Vec::new();
1530 for i in 1..=num_proofs {
1531 let secret = derive_wormhole_secret(mnemonic, round, i)?;
1532 secrets.push(secret);
1533 pb.inc(1);
1534 }
1535 pb.finish_with_message("Secrets derived");
1536
1537 Ok(secrets)
1538}
1539
1540fn verify_final_balance(
1542 initial_balance: u128,
1543 final_balance: u128,
1544 total_sent: u128,
1545 rounds: usize,
1546 num_proofs: usize,
1547) {
1548 use colored::Colorize;
1549
1550 log_print!("{}", "Balance Verification:".bright_cyan());
1551
1552 let total_received = calculate_round_amount(total_sent, rounds);
1554
1555 let expected_change = total_received as i128 - total_sent as i128;
1557 let actual_change = final_balance as i128 - initial_balance as i128;
1558
1559 log_print!(" Initial balance: {} ({})", initial_balance, format_balance(initial_balance));
1560 log_print!(" Final balance: {} ({})", final_balance, format_balance(final_balance));
1561 log_print!("");
1562 log_print!(" Total sent (round 1): {} ({})", total_sent, format_balance(total_sent));
1563 log_print!(
1564 " Total received (round {}): {} ({})",
1565 rounds,
1566 total_received,
1567 format_balance(total_received)
1568 );
1569 log_print!("");
1570
1571 let expected_change_str = if expected_change >= 0 {
1573 format!("+{}", expected_change)
1574 } else {
1575 format!("{}", expected_change)
1576 };
1577 let actual_change_str = if actual_change >= 0 {
1578 format!("+{}", actual_change)
1579 } else {
1580 format!("{}", actual_change)
1581 };
1582
1583 log_print!(" Expected change: {} planck", expected_change_str);
1584 log_print!(" Actual change: {} planck", actual_change_str);
1585 log_print!("");
1586
1587 let tolerance = (total_sent / 100).max(1_000_000_000_000); let diff = (actual_change - expected_change).unsigned_abs();
1591 if diff <= tolerance {
1592 log_success!(
1593 " {} Balance verification PASSED (within tolerance of {} planck)",
1594 "✓".bright_green(),
1595 tolerance
1596 );
1597 } else {
1598 log_print!(
1599 " {} Balance verification: difference of {} planck (tolerance: {} planck)",
1600 "!".bright_yellow(),
1601 diff,
1602 tolerance
1603 );
1604 log_print!(
1605 " Note: Transaction fees for {} initial transfers may account for the difference",
1606 num_proofs
1607 );
1608 }
1609 log_print!("");
1610}
1611
1612#[allow(clippy::too_many_arguments)]
1614async fn run_multiround(
1615 num_proofs: usize,
1616 rounds: usize,
1617 amount: u128,
1618 wallet_name: String,
1619 password: Option<String>,
1620 password_file: Option<String>,
1621 keep_files: bool,
1622 output_dir: String,
1623 dry_run: bool,
1624 node_url: &str,
1625) -> crate::error::Result<()> {
1626 use colored::Colorize;
1627
1628 log_print!("");
1629 log_print!("==================================================");
1630 log_print!(" Wormhole Multi-Round Flow Test");
1631 log_print!("==================================================");
1632 log_print!("");
1633
1634 let agg_config = AggregationConfig::load_from_bins()?;
1636
1637 validate_multiround_params(num_proofs, rounds, agg_config.num_leaf_proofs)?;
1639
1640 let wallet = load_multiround_wallet(&wallet_name, password, password_file)?;
1642
1643 let config =
1645 MultiroundConfig { num_proofs, rounds, amount, output_dir: output_dir.clone(), keep_files };
1646
1647 print_multiround_config(&config, &wallet, &agg_config);
1649 log_print!(" Dry run: {}", dry_run);
1650 log_print!("");
1651
1652 std::fs::create_dir_all(&output_dir).map_err(|e| {
1654 crate::error::QuantusError::Generic(format!("Failed to create output directory: {}", e))
1655 })?;
1656
1657 if dry_run {
1658 return run_multiround_dry_run(
1659 &wallet.mnemonic,
1660 num_proofs,
1661 rounds,
1662 amount,
1663 &wallet.wallet_address,
1664 );
1665 }
1666
1667 let quantus_client = QuantusClient::new(node_url).await.map_err(|e| {
1669 crate::error::QuantusError::Generic(format!("Failed to connect to node: {}", e))
1670 })?;
1671 let client = quantus_client.client();
1672
1673 let minting_account = get_minting_account(client).await?;
1675 log_verbose!("Minting account: {:?}", minting_account);
1676
1677 let initial_balance = get_balance(&quantus_client, &wallet.wallet_address).await?;
1679 log_print!("{}", "Initial Balance:".bright_cyan());
1680 log_print!(" Wallet balance: {} ({})", initial_balance, format_balance(initial_balance));
1681 log_print!("");
1682
1683 let mut current_transfers: Vec<TransferInfo> = Vec::new();
1685
1686 for round in 1..=rounds {
1687 let is_final = round == rounds;
1688
1689 log_print!("");
1690 log_print!("--------------------------------------------------");
1691 log_print!(
1692 " {} Round {} of {} {}",
1693 ">>>".bright_blue(),
1694 round,
1695 rounds,
1696 "<<<".bright_blue()
1697 );
1698 log_print!("--------------------------------------------------");
1699 log_print!("");
1700
1701 let round_dir = format!("{}/round{}", output_dir, round);
1703 std::fs::create_dir_all(&round_dir).map_err(|e| {
1704 crate::error::QuantusError::Generic(format!("Failed to create round directory: {}", e))
1705 })?;
1706
1707 let secrets = derive_round_secrets(&wallet.mnemonic, round, num_proofs)?;
1709
1710 let exit_accounts: Vec<SubxtAccountId> = if is_final {
1712 log_print!("Final round - all proofs exit to wallet: {}", wallet.wallet_address);
1713 vec![wallet.wallet_account_id.clone(); num_proofs]
1714 } else {
1715 log_print!(
1716 "Intermediate round - proofs exit to round {} wormhole addresses",
1717 round + 1
1718 );
1719 let mut addrs = Vec::new();
1720 for i in 1..=num_proofs {
1721 let next_secret = derive_wormhole_secret(&wallet.mnemonic, round + 1, i)?;
1722 addrs.push(SubxtAccountId(next_secret.address));
1723 }
1724 addrs
1725 };
1726
1727 if round == 1 {
1730 current_transfers =
1731 execute_initial_transfers(&quantus_client, &wallet, &secrets, amount, num_proofs)
1732 .await?;
1733
1734 let balance_after_funding =
1736 get_balance(&quantus_client, &wallet.wallet_address).await?;
1737 let funding_deducted = initial_balance.saturating_sub(balance_after_funding);
1738 log_print!(
1739 " Balance after funding: {} ({}) [deducted: {} planck]",
1740 balance_after_funding,
1741 format_balance(balance_after_funding),
1742 funding_deducted
1743 );
1744 } else {
1745 log_print!("{}", "Step 1: Using transfer info from previous round...".bright_yellow());
1746 log_print!(" Found {} transfer(s) from previous round", current_transfers.len());
1747 }
1748
1749 let proof_files = generate_round_proofs(
1751 &quantus_client,
1752 &secrets,
1753 ¤t_transfers,
1754 &exit_accounts,
1755 &round_dir,
1756 num_proofs,
1757 )
1758 .await?;
1759
1760 log_print!("{}", "Step 3: Aggregating proofs...".bright_yellow());
1762
1763 let aggregated_file = format!("{}/aggregated.hex", round_dir);
1764 aggregate_proofs(proof_files, aggregated_file.clone()).await?;
1765
1766 log_print!(" Aggregated proof saved to {}", aggregated_file);
1767
1768 log_print!("{}", "Step 4: Submitting aggregated proof on-chain...".bright_yellow());
1770
1771 let (verification_block, transfer_events) =
1772 verify_aggregated_and_get_events(&aggregated_file, &quantus_client).await?;
1773
1774 log_print!(
1775 " {} Proof verified in block {}",
1776 "✓".bright_green(),
1777 hex::encode(verification_block.0)
1778 );
1779
1780 if !is_final {
1782 log_print!("{}", "Step 5: Capturing transfer info for next round...".bright_yellow());
1783
1784 let next_round_addresses: Vec<SubxtAccountId> = (1..=num_proofs)
1786 .map(|i| {
1787 let next_secret =
1788 derive_wormhole_secret(&wallet.mnemonic, round + 1, i).unwrap();
1789 SubxtAccountId(next_secret.address)
1790 })
1791 .collect();
1792
1793 current_transfers =
1794 parse_transfer_events(&transfer_events, &next_round_addresses, verification_block)?;
1795
1796 log_print!(
1797 " Captured {} transfer(s) for round {}",
1798 current_transfers.len(),
1799 round + 1
1800 );
1801 }
1802
1803 let balance_after_round = get_balance(&quantus_client, &wallet.wallet_address).await?;
1805 let change_from_initial = balance_after_round as i128 - initial_balance as i128;
1806 let change_str = if change_from_initial >= 0 {
1807 format!("+{}", change_from_initial)
1808 } else {
1809 format!("{}", change_from_initial)
1810 };
1811 log_print!("");
1812 log_print!(
1813 " Balance after round {}: {} ({}) [change: {} planck]",
1814 round,
1815 balance_after_round,
1816 format_balance(balance_after_round),
1817 change_str
1818 );
1819
1820 log_print!("");
1821 log_print!(" {} Round {} complete!", "✓".bright_green(), round);
1822 }
1823
1824 log_print!("");
1825 log_print!("==================================================");
1826 log_success!(" All {} rounds completed successfully!", rounds);
1827 log_print!("==================================================");
1828 log_print!("");
1829
1830 let final_balance = get_balance(&quantus_client, &wallet.wallet_address).await?;
1832 verify_final_balance(initial_balance, final_balance, amount, rounds, num_proofs);
1833
1834 if keep_files {
1835 log_print!("Proof files preserved in: {}", output_dir);
1836 } else {
1837 log_print!("Cleaning up proof files...");
1838 std::fs::remove_dir_all(&output_dir).ok();
1839 }
1840
1841 Ok(())
1842}
1843
1844async fn generate_proof(
1846 secret_hex: &str,
1847 funding_amount: u128,
1848 output_assignment: &ProofOutputAssignment,
1849 block_hash_str: &str,
1850 transfer_count: u64,
1851 funding_account_str: &str,
1852 output_file: &str,
1853 quantus_client: &QuantusClient,
1854) -> crate::error::Result<()> {
1855 let secret_array = parse_secret_hex(secret_hex).map_err(crate::error::QuantusError::Generic)?;
1857 let secret: BytesDigest = secret_array.try_into().map_err(|e| {
1858 crate::error::QuantusError::Generic(format!("Failed to convert secret: {:?}", e))
1859 })?;
1860
1861 let funding_account_bytes =
1863 parse_exit_account(funding_account_str).map_err(crate::error::QuantusError::Generic)?;
1864 let funding_account = AccountId32::new(funding_account_bytes);
1865
1866 let hash_bytes = hex::decode(block_hash_str.trim_start_matches("0x"))
1868 .map_err(|e| crate::error::QuantusError::Generic(format!("Invalid block hash: {}", e)))?;
1869 if hash_bytes.len() != 32 {
1870 return Err(crate::error::QuantusError::Generic(format!(
1871 "Block hash must be 32 bytes, got {}",
1872 hash_bytes.len()
1873 )));
1874 }
1875 let hash: [u8; 32] = hash_bytes.try_into().unwrap();
1876 let block_hash = subxt::utils::H256::from(hash);
1877
1878 let client = quantus_client.client();
1879
1880 let unspendable_account =
1882 qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
1883 .account_id;
1884 let unspendable_account_bytes_digest =
1885 qp_zk_circuits_common::utils::digest_felts_to_bytes(unspendable_account);
1886 let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest
1887 .as_ref()
1888 .try_into()
1889 .expect("BytesDigest is always 32 bytes");
1890
1891 let from_account = funding_account.clone();
1892 let to_account = AccountId32::new(unspendable_account_bytes);
1893
1894 let blocks =
1896 client.blocks().at(block_hash).await.map_err(|e| {
1897 crate::error::QuantusError::Generic(format!("Failed to get block: {}", e))
1898 })?;
1899
1900 let leaf_hash = qp_poseidon::PoseidonHasher::hash_storage::<TransferProofKey>(
1902 &(
1903 NATIVE_ASSET_ID,
1904 transfer_count,
1905 from_account.clone(),
1906 to_account.clone(),
1907 funding_amount,
1908 )
1909 .encode(),
1910 );
1911
1912 let proof_address = quantus_node::api::storage().wormhole().transfer_proof((
1913 NATIVE_ASSET_ID,
1914 transfer_count,
1915 SubxtAccountId(from_account.clone().into()),
1916 SubxtAccountId(to_account.clone().into()),
1917 funding_amount,
1918 ));
1919
1920 let mut final_key = proof_address.to_root_bytes();
1921 final_key.extend_from_slice(&leaf_hash);
1922
1923 let storage_api = client.storage().at(block_hash);
1925 let val = storage_api
1926 .fetch_raw(final_key.clone())
1927 .await
1928 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1929 if val.is_none() {
1930 return Err(crate::error::QuantusError::Generic(
1931 "Storage key not found - transfer may not exist in this block".to_string(),
1932 ));
1933 }
1934
1935 let proof_params = rpc_params![vec![to_hex(&final_key)], block_hash];
1937 let read_proof: ReadProof<sp_core::H256> = quantus_client
1938 .rpc_client()
1939 .request("state_getReadProof", proof_params)
1940 .await
1941 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1942
1943 let header = blocks.header();
1944 let state_root = BytesDigest::try_from(header.state_root.as_bytes())
1945 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1946 let parent_hash = BytesDigest::try_from(header.parent_hash.as_bytes())
1947 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1948 let extrinsics_root = BytesDigest::try_from(header.extrinsics_root.as_bytes())
1949 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1950 let digest =
1951 header.digest.encode().try_into().map_err(|_| {
1952 crate::error::QuantusError::Generic("Failed to encode digest".to_string())
1953 })?;
1954 let block_number = header.number;
1955
1956 let processed_storage_proof = prepare_proof_for_circuit(
1958 read_proof.proof.iter().map(|proof| proof.0.clone()).collect(),
1959 hex::encode(header.state_root.0),
1960 leaf_hash,
1961 )
1962 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1963
1964 let input_amount_quantized: u32 =
1966 quantize_funding_amount(funding_amount).map_err(crate::error::QuantusError::Generic)?;
1967
1968 let inputs = CircuitInputs {
1970 private: PrivateCircuitInputs {
1971 secret,
1972 transfer_count,
1973 funding_account: BytesDigest::try_from(funding_account.as_ref() as &[u8])
1974 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
1975 storage_proof: processed_storage_proof,
1976 unspendable_account: unspendable_account_bytes_digest,
1977 parent_hash,
1978 state_root,
1979 extrinsics_root,
1980 digest,
1981 input_amount: input_amount_quantized,
1982 },
1983 public: PublicCircuitInputs {
1984 output_amount_1: output_assignment.output_amount_1,
1985 output_amount_2: output_assignment.output_amount_2,
1986 volume_fee_bps: VOLUME_FEE_BPS,
1987 nullifier: digest_felts_to_bytes(Nullifier::from_preimage(secret, transfer_count).hash),
1988 exit_account_1: BytesDigest::try_from(output_assignment.exit_account_1.as_ref())
1989 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
1990 exit_account_2: BytesDigest::try_from(output_assignment.exit_account_2.as_ref())
1991 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
1992 block_hash: BytesDigest::try_from(block_hash.as_ref())
1993 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?,
1994 block_number,
1995 asset_id: NATIVE_ASSET_ID,
1996 },
1997 };
1998
1999 let bins_dir = Path::new("generated-bins");
2001 let prover =
2002 WormholeProver::new_from_files(&bins_dir.join("prover.bin"), &bins_dir.join("common.bin"))
2003 .map_err(|e| {
2004 crate::error::QuantusError::Generic(format!("Failed to load prover: {}", e))
2005 })?;
2006 let prover_next = prover
2007 .commit(&inputs)
2008 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
2009 let proof: ProofWithPublicInputs<_, _, 2> = prover_next.prove().map_err(|e| {
2010 crate::error::QuantusError::Generic(format!("Proof generation failed: {}", e))
2011 })?;
2012
2013 let proof_hex = hex::encode(proof.to_bytes());
2014 std::fs::write(output_file, proof_hex).map_err(|e| {
2015 crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e))
2016 })?;
2017
2018 Ok(())
2019}
2020
2021async fn verify_aggregated_and_get_events(
2023 proof_file: &str,
2024 quantus_client: &QuantusClient,
2025) -> crate::error::Result<(subxt::utils::H256, Vec<wormhole::events::NativeTransferred>)> {
2026 use qp_wormhole_verifier::WormholeVerifier;
2027
2028 let proof_bytes = read_hex_proof_file_to_bytes(proof_file)?;
2029
2030 log_verbose!("Verifying aggregated proof locally before on-chain submission...");
2032 let bins_dir = Path::new("generated-bins");
2033 let verifier = WormholeVerifier::new_from_files(
2034 &bins_dir.join("aggregated_verifier.bin"),
2035 &bins_dir.join("aggregated_common.bin"),
2036 )
2037 .map_err(|e| {
2038 crate::error::QuantusError::Generic(format!("Failed to load aggregated verifier: {}", e))
2039 })?;
2040
2041 let proof = qp_wormhole_verifier::ProofWithPublicInputs::<
2042 qp_wormhole_verifier::F,
2043 qp_wormhole_verifier::C,
2044 { qp_wormhole_verifier::D },
2045 >::from_bytes(proof_bytes.clone(), &verifier.circuit_data.common)
2046 .map_err(|e| {
2047 crate::error::QuantusError::Generic(format!(
2048 "Failed to deserialize aggregated proof: {}",
2049 e
2050 ))
2051 })?;
2052
2053 verifier.verify(proof).map_err(|e| {
2054 crate::error::QuantusError::Generic(format!(
2055 "Local aggregated proof verification failed: {}",
2056 e
2057 ))
2058 })?;
2059 log_verbose!("Local verification passed!");
2060
2061 let (included_at, block_hash, tx_hash) =
2063 submit_unsigned_verify_aggregated_proof(quantus_client, proof_bytes).await?;
2064
2065 log_verbose!(
2066 "Submitted tx included in {}: block={:?}, tx={:?}",
2067 included_at.label(),
2068 block_hash,
2069 tx_hash
2070 );
2071
2072 let (found_proof_verified, transfer_events) =
2074 collect_wormhole_events_for_extrinsic(quantus_client, block_hash, tx_hash).await?;
2075
2076 if !found_proof_verified {
2077 return Err(crate::error::QuantusError::Generic(
2078 "Proof verification failed - no ProofVerified event".to_string(),
2079 ));
2080 }
2081
2082 log_print!(" Tokens minted (from NativeTransferred events):");
2084 for (idx, transfer) in transfer_events.iter().enumerate() {
2085 let to_hex = hex::encode(transfer.to.0);
2086 log_print!(
2087 " [{}] {} -> {} planck ({})",
2088 idx,
2089 to_hex,
2090 transfer.amount,
2091 format_balance(transfer.amount)
2092 );
2093 }
2094
2095 Ok((block_hash, transfer_events))
2096}
2097
2098fn run_multiround_dry_run(
2100 mnemonic: &str,
2101 num_proofs: usize,
2102 rounds: usize,
2103 amount: u128,
2104 wallet_address: &str,
2105) -> crate::error::Result<()> {
2106 use colored::Colorize;
2107
2108 log_print!("");
2109 log_print!("{}", "=== DRY RUN MODE ===".bright_yellow());
2110 log_print!("No transactions will be executed.");
2111 log_print!("");
2112
2113 for round in 1..=rounds {
2114 let is_final = round == rounds;
2115 let round_amount = calculate_round_amount(amount, round);
2116
2117 log_print!("");
2118 log_print!("{}", format!("Round {}", round).bright_cyan());
2119 log_print!(" Total amount: {} ({})", round_amount, format_balance(round_amount));
2120
2121 if round == 1 {
2123 let partition = random_partition(amount, num_proofs, 2 * SCALE_DOWN_FACTOR);
2124 log_print!(" Sample random partition (actual partition will differ):");
2125 for (i, &amt) in partition.iter().enumerate() {
2126 log_print!(" Proof {}: {} ({})", i + 1, amt, format_balance(amt));
2127 }
2128 }
2129 log_print!("");
2130
2131 log_print!(" Wormhole addresses (to be funded):");
2132 for i in 1..=num_proofs {
2133 let secret = derive_wormhole_secret(mnemonic, round, i)?;
2134 let address = sp_core::crypto::AccountId32::new(secret.address)
2135 .to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
2136 log_print!(" [{}] {}", i, address);
2137 log_verbose!(" secret: 0x{}", hex::encode(secret.secret));
2138 }
2139
2140 log_print!("");
2141 log_print!(" Exit accounts:");
2142 if is_final {
2143 log_print!(" All proofs exit to wallet: {}", wallet_address);
2144 } else {
2145 for i in 1..=num_proofs {
2146 let next_secret = derive_wormhole_secret(mnemonic, round + 1, i)?;
2147 let address = sp_core::crypto::AccountId32::new(next_secret.address)
2148 .to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
2149 log_print!(" [{}] {} (round {} wormhole)", i, address, round + 1);
2150 }
2151 }
2152 }
2153
2154 log_print!("");
2155 log_print!("{}", "=== END DRY RUN ===".bright_yellow());
2156 log_print!("");
2157
2158 Ok(())
2159}
2160
2161async fn parse_proof_file(
2163 proof_file: String,
2164 aggregated: bool,
2165 verify: bool,
2166) -> crate::error::Result<()> {
2167 use qp_wormhole_verifier::WormholeVerifier;
2168
2169 log_print!("Parsing proof file: {}", proof_file);
2170
2171 let proof_bytes = read_proof_file(&proof_file)
2173 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to read proof: {}", e)))?;
2174
2175 log_print!("Proof size: {} bytes", proof_bytes.len());
2176
2177 let bins_dir = Path::new("generated-bins");
2178
2179 if aggregated {
2180 let verifier = WormholeVerifier::new_from_files(
2182 &bins_dir.join("aggregated_verifier.bin"),
2183 &bins_dir.join("aggregated_common.bin"),
2184 )
2185 .map_err(|e| {
2186 crate::error::QuantusError::Generic(format!("Failed to load verifier: {}", e))
2187 })?;
2188
2189 let proof = qp_wormhole_verifier::ProofWithPublicInputs::<
2191 qp_wormhole_verifier::F,
2192 qp_wormhole_verifier::C,
2193 { qp_wormhole_verifier::D },
2194 >::from_bytes(proof_bytes.clone(), &verifier.circuit_data.common)
2195 .map_err(|e| {
2196 crate::error::QuantusError::Generic(format!(
2197 "Failed to deserialize aggregated proof: {:?}",
2198 e
2199 ))
2200 })?;
2201
2202 log_print!("\nPublic inputs count: {}", proof.public_inputs.len());
2203 log_verbose!("\nPublic inputs count: {}", proof.public_inputs.len());
2204
2205 match qp_wormhole_verifier::parse_aggregated_public_inputs(&proof) {
2207 Ok(agg_inputs) => {
2208 log_print!("\n=== Parsed Aggregated Public Inputs ===");
2209 log_print!("Asset ID: {}", agg_inputs.asset_id);
2210 log_print!("Volume Fee BPS: {}", agg_inputs.volume_fee_bps);
2211 log_print!(
2212 "Block Hash: 0x{}",
2213 hex::encode(agg_inputs.block_data.block_hash.as_ref())
2214 );
2215 log_print!("Block Number: {}", agg_inputs.block_data.block_number);
2216 log_print!("\nAccount Data ({} accounts):", agg_inputs.account_data.len());
2217 for (i, acct) in agg_inputs.account_data.iter().enumerate() {
2218 log_print!(
2219 " [{}] amount={}, exit=0x{}",
2220 i,
2221 acct.summed_output_amount,
2222 hex::encode(acct.exit_account.as_ref())
2223 );
2224 }
2225 log_print!("\nNullifiers ({} nullifiers):", agg_inputs.nullifiers.len());
2226 for (i, nullifier) in agg_inputs.nullifiers.iter().enumerate() {
2227 log_print!(" [{}] 0x{}", i, hex::encode(nullifier.as_ref()));
2228 }
2229 },
2230 Err(e) => {
2231 log_print!("Failed to parse as aggregated inputs: {}", e);
2232 },
2233 }
2234
2235 if verify {
2237 log_print!("\n=== Verifying Proof ===");
2238 match verifier.verify(proof) {
2239 Ok(()) => {
2240 log_success!("Proof verification PASSED");
2241 },
2242 Err(e) => {
2243 log_error!("Proof verification FAILED: {}", e);
2244 return Err(crate::error::QuantusError::Generic(format!(
2245 "Proof verification failed: {}",
2246 e
2247 )));
2248 },
2249 }
2250 }
2251 } else {
2252 let verifier = WormholeVerifier::new_from_files(
2254 &bins_dir.join("verifier.bin"),
2255 &bins_dir.join("common.bin"),
2256 )
2257 .map_err(|e| {
2258 crate::error::QuantusError::Generic(format!("Failed to load verifier: {}", e))
2259 })?;
2260
2261 let proof = qp_wormhole_verifier::ProofWithPublicInputs::<
2263 qp_wormhole_verifier::F,
2264 qp_wormhole_verifier::C,
2265 { qp_wormhole_verifier::D },
2266 >::from_bytes(proof_bytes, &verifier.circuit_data.common)
2267 .map_err(|e| {
2268 crate::error::QuantusError::Generic(format!("Failed to deserialize proof: {:?}", e))
2269 })?;
2270
2271 log_print!("\nPublic inputs count: {}", proof.public_inputs.len());
2272
2273 let pi = qp_wormhole_verifier::parse_public_inputs(&proof).map_err(|e| {
2274 crate::error::QuantusError::Generic(format!("Failed to parse public inputs: {}", e))
2275 })?;
2276
2277 log_print!("\n=== Parsed Leaf Public Inputs ===");
2278 log_print!("Asset ID: {}", pi.asset_id);
2279 log_print!("Output Amount 1: {}", pi.output_amount_1);
2280 log_print!("Output Amount 2: {}", pi.output_amount_2);
2281 log_print!("Volume Fee BPS: {}", pi.volume_fee_bps);
2282 log_print!("Nullifier: 0x{}", hex::encode(pi.nullifier.as_ref()));
2283 log_print!("Exit Account 1: 0x{}", hex::encode(pi.exit_account_1.as_ref()));
2284 log_print!("Exit Account 2: 0x{}", hex::encode(pi.exit_account_2.as_ref()));
2285 log_print!("Block Hash: 0x{}", hex::encode(pi.block_hash.as_ref()));
2286 log_print!("Block Number: {}", pi.block_number);
2287
2288 if verify {
2290 log_print!("\n=== Verifying Proof ===");
2291 match verifier.verify(proof) {
2292 Ok(()) => {
2293 log_success!("Proof verification PASSED");
2294 },
2295 Err(e) => {
2296 log_error!("Proof verification FAILED: {}", e);
2297 return Err(crate::error::QuantusError::Generic(format!(
2298 "Proof verification failed: {}",
2299 e
2300 )));
2301 },
2302 }
2303 }
2304 }
2305
2306 Ok(())
2307}
2308
2309#[cfg(test)]
2310mod tests {
2311 use super::*;
2312 use std::collections::HashSet;
2313 use tempfile::NamedTempFile;
2314
2315 #[test]
2316 fn test_compute_output_amount() {
2317 assert_eq!(compute_output_amount(1000, 10), 999);
2319 assert_eq!(compute_output_amount(10000, 10), 9990);
2320
2321 assert_eq!(compute_output_amount(1000, 100), 990);
2323 assert_eq!(compute_output_amount(10000, 100), 9900);
2324
2325 assert_eq!(compute_output_amount(1000, 0), 1000);
2327
2328 assert_eq!(compute_output_amount(0, 10), 0);
2330 assert_eq!(compute_output_amount(1, 10), 0); assert_eq!(compute_output_amount(100, 10), 99);
2332 }
2333
2334 #[test]
2335 fn test_parse_secret_hex() {
2336 let secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
2338 assert!(parse_secret_hex(secret).is_ok());
2339 assert!(parse_secret_hex(&format!("0x{}", secret)).is_ok());
2340
2341 assert!(parse_secret_hex("0123456789abcdef").unwrap_err().contains("32 bytes"));
2343
2344 assert!(parse_secret_hex("ghij".repeat(16).as_str())
2346 .unwrap_err()
2347 .contains("Invalid secret hex"));
2348 }
2349
2350 #[test]
2351 fn test_parse_exit_account() {
2352 let hex = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
2354 assert!(parse_exit_account(hex).is_ok());
2355
2356 assert!(parse_exit_account("0x0123456789abcdef").unwrap_err().contains("32 bytes"));
2358
2359 assert!(parse_exit_account("not_valid").unwrap_err().contains("Invalid SS58"));
2361 }
2362
2363 #[test]
2364 fn test_quantize_funding_amount() {
2365 assert_eq!(quantize_funding_amount(1_000_000_000_000).unwrap(), 100);
2367
2368 assert_eq!(quantize_funding_amount(0).unwrap(), 0);
2370 assert_eq!(quantize_funding_amount(5_000_000_000).unwrap(), 0); let max_valid = (u32::MAX as u128) * SCALE_DOWN_FACTOR;
2374 assert_eq!(quantize_funding_amount(max_valid).unwrap(), u32::MAX);
2375 assert!(quantize_funding_amount(max_valid + SCALE_DOWN_FACTOR)
2376 .unwrap_err()
2377 .contains("too large"));
2378 }
2379
2380 #[test]
2381 fn test_proof_file_roundtrip() {
2382 let temp_file = NamedTempFile::new().unwrap();
2383 let path = temp_file.path().to_str().unwrap();
2384 let proof_bytes = vec![0x01, 0x02, 0x03, 0xaa, 0xbb, 0xcc];
2385
2386 write_proof_file(path, &proof_bytes).unwrap();
2387 assert_eq!(read_proof_file(path).unwrap(), proof_bytes);
2388 }
2389
2390 #[test]
2391 fn test_read_proof_file_errors() {
2392 assert!(read_proof_file("/nonexistent/path/proof.hex")
2394 .unwrap_err()
2395 .contains("Failed to read"));
2396
2397 let temp_file = NamedTempFile::new().unwrap();
2399 std::fs::write(temp_file.path(), "not valid hex!").unwrap();
2400 assert!(read_proof_file(temp_file.path().to_str().unwrap())
2401 .unwrap_err()
2402 .contains("Failed to decode"));
2403 }
2404
2405 #[test]
2406 fn test_fee_calculation_edge_cases() {
2407 let input_small: u32 = 100;
2412 let output_small = compute_output_amount(input_small, VOLUME_FEE_BPS);
2413 assert_eq!(output_small, 99);
2414 assert!(
2416 (output_small as u64) * 10000 <= (input_small as u64) * (10000 - VOLUME_FEE_BPS as u64)
2417 );
2418
2419 let input_medium: u32 = 10000;
2421 let output_medium = compute_output_amount(input_medium, VOLUME_FEE_BPS);
2422 assert_eq!(output_medium, 9990);
2423 assert!(
2424 (output_medium as u64) * 10000 <=
2425 (input_medium as u64) * (10000 - VOLUME_FEE_BPS as u64)
2426 );
2427
2428 let input_large: u32 = u32::MAX / 2;
2430 let output_large = compute_output_amount(input_large, VOLUME_FEE_BPS);
2431 assert!(
2432 (output_large as u64) * 10000 <= (input_large as u64) * (10000 - VOLUME_FEE_BPS as u64)
2433 );
2434
2435 for fee_bps in [0u32, 1, 10, 50, 100, 500, 1000] {
2437 let input: u32 = 100000;
2438 let output = compute_output_amount(input, fee_bps);
2439 assert!(
2440 (output as u64) * 10000 <= (input as u64) * (10000 - fee_bps as u64),
2441 "Fee constraint violated for fee_bps={}: {} * 10000 > {} * {}",
2442 fee_bps,
2443 output,
2444 input,
2445 10000 - fee_bps
2446 );
2447 }
2448 }
2449
2450 #[test]
2451 fn test_nullifier_determinism() {
2452 use qp_wormhole_circuit::nullifier::Nullifier;
2453 use qp_zk_circuits_common::utils::BytesDigest;
2454
2455 let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret");
2456 let transfer_count = 42u64;
2457
2458 let nullifier1 = Nullifier::from_preimage(secret, transfer_count);
2460 let nullifier2 = Nullifier::from_preimage(secret, transfer_count);
2461 let nullifier3 = Nullifier::from_preimage(secret, transfer_count);
2462
2463 assert_eq!(nullifier1.hash, nullifier2.hash);
2464 assert_eq!(nullifier2.hash, nullifier3.hash);
2465
2466 let nullifier_different = Nullifier::from_preimage(secret, transfer_count + 1);
2468 assert_ne!(nullifier1.hash, nullifier_different.hash);
2469
2470 let different_secret: BytesDigest = [2u8; 32].try_into().expect("valid secret");
2472 let nullifier_different_secret = Nullifier::from_preimage(different_secret, transfer_count);
2473 assert_ne!(nullifier1.hash, nullifier_different_secret.hash);
2474 }
2475
2476 #[test]
2477 fn test_unspendable_account_determinism() {
2478 use qp_wormhole_circuit::unspendable_account::UnspendableAccount;
2479 use qp_zk_circuits_common::utils::BytesDigest;
2480
2481 let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret");
2482
2483 let account1 = UnspendableAccount::from_secret(secret);
2485 let account2 = UnspendableAccount::from_secret(secret);
2486
2487 assert_eq!(account1.account_id, account2.account_id);
2488
2489 let different_secret: BytesDigest = [2u8; 32].try_into().expect("valid secret");
2491 let account_different = UnspendableAccount::from_secret(different_secret);
2492 assert_ne!(account1.account_id, account_different.account_id);
2493 }
2494
2495 #[test]
2501 fn test_public_inputs_structure() {
2502 use qp_wormhole_inputs::{
2503 ASSET_ID_INDEX, BLOCK_HASH_END_INDEX, BLOCK_HASH_START_INDEX, BLOCK_NUMBER_INDEX,
2504 EXIT_ACCOUNT_1_END_INDEX, EXIT_ACCOUNT_1_START_INDEX, EXIT_ACCOUNT_2_END_INDEX,
2505 EXIT_ACCOUNT_2_START_INDEX, NULLIFIER_END_INDEX, NULLIFIER_START_INDEX,
2506 OUTPUT_AMOUNT_1_INDEX, OUTPUT_AMOUNT_2_INDEX, PUBLIC_INPUTS_FELTS_LEN,
2507 VOLUME_FEE_BPS_INDEX,
2508 };
2509
2510 assert_eq!(PUBLIC_INPUTS_FELTS_LEN, 21, "Public inputs should be 21 field elements");
2512 assert_eq!(ASSET_ID_INDEX, 0, "Asset ID should be first");
2513 assert_eq!(OUTPUT_AMOUNT_1_INDEX, 1, "Output amount 1 should be at index 1");
2514 assert_eq!(OUTPUT_AMOUNT_2_INDEX, 2, "Output amount 2 should be at index 2");
2515 assert_eq!(VOLUME_FEE_BPS_INDEX, 3, "Volume fee BPS should be at index 3");
2516 assert_eq!(NULLIFIER_START_INDEX, 4, "Nullifier should start at index 4");
2517 assert_eq!(NULLIFIER_END_INDEX, 8, "Nullifier should end at index 8");
2518 assert_eq!(EXIT_ACCOUNT_1_START_INDEX, 8, "Exit account 1 should start at index 8");
2519 assert_eq!(EXIT_ACCOUNT_1_END_INDEX, 12, "Exit account 1 should end at index 12");
2520 assert_eq!(EXIT_ACCOUNT_2_START_INDEX, 12, "Exit account 2 should start at index 12");
2521 assert_eq!(EXIT_ACCOUNT_2_END_INDEX, 16, "Exit account 2 should end at index 16");
2522 assert_eq!(BLOCK_HASH_START_INDEX, 16, "Block hash should start at index 16");
2523 assert_eq!(BLOCK_HASH_END_INDEX, 20, "Block hash should end at index 20");
2524 assert_eq!(BLOCK_NUMBER_INDEX, 20, "Block number should be at index 20");
2525 }
2526
2527 #[test]
2529 fn test_constants_match_chain_config() {
2530 assert_eq!(VOLUME_FEE_BPS, 10, "Volume fee should be 10 bps");
2532
2533 assert_eq!(NATIVE_ASSET_ID, 0, "Native asset ID should be 0");
2535
2536 assert_eq!(SCALE_DOWN_FACTOR, 10_000_000_000, "Scale down factor should be 10^10");
2538
2539 let one_token_12_decimals: u128 = 1_000_000_000_000;
2542 let quantized = quantize_funding_amount(one_token_12_decimals).unwrap();
2543 assert_eq!(quantized, 100, "1 token should quantize to 100 (1.00 with 2 decimals)");
2544 }
2545
2546 #[test]
2547 fn test_volume_fee_bps_constant() {
2548 assert_eq!(VOLUME_FEE_BPS, 10);
2550 }
2551
2552 #[test]
2553 fn test_aggregation_config_deserialization_matches_upstream_format() {
2554 let json = r#"{
2558 "num_leaf_proofs": 8,
2559 "hashes": {
2560 "common": "aabbcc",
2561 "verifier": "ddeeff",
2562 "prover": "112233",
2563 "aggregated_common": "445566",
2564 "aggregated_verifier": "778899"
2565 }
2566 }"#;
2567
2568 let config: AggregationConfig = serde_json::from_str(json).unwrap();
2569 assert_eq!(config.num_leaf_proofs, 8);
2570
2571 let hashes = config.hashes.unwrap();
2572 assert_eq!(hashes.prover.as_deref(), Some("112233"));
2573 assert_eq!(hashes.aggregated_common.as_deref(), Some("445566"));
2574 assert_eq!(hashes.aggregated_verifier.as_deref(), Some("778899"));
2575 }
2576
2577 fn mk_accounts(n: usize) -> Vec<[u8; 32]> {
2578 (0..n)
2579 .map(|i| {
2580 let mut a = [0u8; 32];
2581 a[0] = (i as u8).wrapping_add(1); a
2583 })
2584 .collect()
2585 }
2586
2587 fn proof_outputs_for_inputs(input_amounts: &[u128], fee_bps: u32) -> Vec<u32> {
2588 input_amounts
2589 .iter()
2590 .map(|&input| {
2591 let input_quantized = quantize_funding_amount(input).unwrap_or(0);
2592 compute_output_amount(input_quantized, fee_bps)
2593 })
2594 .collect()
2595 }
2596
2597 fn total_output_for_inputs(input_amounts: &[u128], fee_bps: u32) -> u64 {
2598 proof_outputs_for_inputs(input_amounts, fee_bps)
2599 .into_iter()
2600 .map(|x| x as u64)
2601 .sum()
2602 }
2603
2604 fn find_input_for_min_output(fee_bps: u32, min_out: u32) -> u128 {
2607 let mut input: u128 = 1;
2608 for _ in 0..80 {
2609 let q = quantize_funding_amount(input).unwrap_or(0);
2610 let out = compute_output_amount(q, fee_bps);
2611 if out >= min_out {
2612 return input;
2613 }
2614 input = input.saturating_mul(10);
2616 }
2617 panic!("Could not find input producing output >= {}", min_out);
2618 }
2619
2620 #[test]
2625 fn random_partition_n0() {
2626 let parts = random_partition(100, 0, 1);
2627 assert!(parts.is_empty());
2628 }
2629
2630 #[test]
2631 fn random_partition_n1() {
2632 let total = 12345u128;
2633 let parts = random_partition(total, 1, 9999);
2634 assert_eq!(parts, vec![total]);
2635 }
2636
2637 #[test]
2638 fn random_partition_total_less_than_min_total_falls_back_to_equalish() {
2639 let total = 5u128;
2641 let n = 10usize;
2642 let min_per_part = 1u128;
2643
2644 let parts = random_partition(total, n, min_per_part);
2645
2646 assert_eq!(parts.len(), n);
2647 assert_eq!(parts.iter().sum::<u128>(), total);
2648
2649 for part in parts.iter().take(n - 1) {
2651 assert_eq!(*part, 0);
2652 }
2653 assert_eq!(parts[n - 1], 5);
2654 }
2655
2656 #[test]
2657 fn random_partition_min_achievable_invariants_hold() {
2658 let total = 100u128;
2659 let n = 10usize;
2660 let min_per_part = 3u128;
2661
2662 for _ in 0..200 {
2663 let parts = random_partition(total, n, min_per_part);
2664 assert_eq!(parts.len(), n);
2665 assert_eq!(parts.iter().sum::<u128>(), total);
2666 assert!(parts.iter().all(|&p| p >= min_per_part));
2667 }
2668 }
2669
2670 #[test]
2671 fn random_partition_distributable_zero_all_min() {
2672 let n = 10usize;
2673 let min_per_part = 3u128;
2674 let total = min_per_part * n as u128;
2675
2676 let parts = random_partition(total, n, min_per_part);
2677
2678 assert_eq!(parts.len(), n);
2679 assert_eq!(parts.iter().sum::<u128>(), total);
2680 assert!(parts.iter().all(|&p| p == min_per_part));
2681 }
2682
2683 #[test]
2688 fn compute_random_output_assignments_empty_inputs_or_targets() {
2689 let targets = mk_accounts(3);
2690 assert!(compute_random_output_assignments(&[], &targets, 0).is_empty());
2691
2692 let inputs = vec![1u128, 2u128, 3u128];
2693 assert!(compute_random_output_assignments(&inputs, &[], 0).is_empty());
2694 }
2695
2696 #[test]
2697 fn compute_random_output_assignments_basic_invariants() {
2698 let fee_bps = 0u32;
2699
2700 let input = find_input_for_min_output(fee_bps, 5);
2702 let input_amounts = vec![input, input, input, input, input];
2703 let targets = mk_accounts(4);
2704
2705 let assignments = compute_random_output_assignments(&input_amounts, &targets, fee_bps);
2706 assert_eq!(assignments.len(), input_amounts.len());
2707
2708 let proof_outputs = proof_outputs_for_inputs(&input_amounts, fee_bps);
2709
2710 for (i, a) in assignments.iter().enumerate() {
2712 let per_proof_sum = a.output_amount_1 as u64 + a.output_amount_2 as u64;
2713 assert_eq!(per_proof_sum, proof_outputs[i] as u64);
2714
2715 if a.output_amount_1 > 0 {
2717 assert!(targets.contains(&a.exit_account_1));
2718 } else {
2719 }
2722 if a.output_amount_2 > 0 {
2723 assert!(targets.contains(&a.exit_account_2));
2724 assert_ne!(a.exit_account_2, a.exit_account_1); } else {
2726 assert_eq!(a.exit_account_2, [0u8; 32]);
2728 }
2729 }
2730
2731 let total_assigned: u64 = assignments
2733 .iter()
2734 .map(|a| a.output_amount_1 as u64 + a.output_amount_2 as u64)
2735 .sum();
2736
2737 let total_expected = total_output_for_inputs(&input_amounts, fee_bps);
2738 assert_eq!(total_assigned, total_expected);
2739 }
2740
2741 #[test]
2742 fn compute_random_output_assignments_more_targets_than_capacity_still_conserves_funds() {
2743 let fee_bps = 0u32;
2747 let num_proofs = 1usize;
2748 let num_targets = 5usize;
2749
2750 let input = find_input_for_min_output(fee_bps, 10); let input_amounts = vec![input; num_proofs];
2752 let targets = mk_accounts(num_targets);
2753
2754 let assignments = compute_random_output_assignments(&input_amounts, &targets, fee_bps);
2755 assert_eq!(assignments.len(), num_proofs);
2756
2757 let total_assigned: u64 = assignments
2759 .iter()
2760 .map(|a| a.output_amount_1 as u64 + a.output_amount_2 as u64)
2761 .sum();
2762 let total_expected = total_output_for_inputs(&input_amounts, fee_bps);
2763 assert_eq!(total_assigned, total_expected);
2764
2765 let mut used = HashSet::new();
2767 for a in &assignments {
2768 if a.output_amount_1 > 0 {
2769 used.insert(a.exit_account_1);
2770 }
2771 if a.output_amount_2 > 0 {
2772 used.insert(a.exit_account_2);
2773 }
2774 }
2775 assert!(used.len() <= 2 * num_proofs);
2776 assert!(used.len() < num_targets);
2777 }
2778
2779 #[test]
2780 fn compute_random_output_assignments_total_output_less_than_num_targets_does_not_panic_and_conserves(
2781 ) {
2782 let fee_bps = 0u32;
2786
2787 let num_targets = 50usize;
2788 let targets = mk_accounts(num_targets);
2789
2790 let input = find_input_for_min_output(fee_bps, 1);
2793 let input_amounts = vec![input, input];
2794
2795 let assignments = compute_random_output_assignments(&input_amounts, &targets, fee_bps);
2796 assert_eq!(assignments.len(), input_amounts.len());
2797
2798 let total_assigned: u64 = assignments
2799 .iter()
2800 .map(|a| a.output_amount_1 as u64 + a.output_amount_2 as u64)
2801 .sum();
2802 let total_expected = total_output_for_inputs(&input_amounts, fee_bps);
2803 assert_eq!(total_assigned, total_expected);
2804
2805 for a in &assignments {
2807 if a.output_amount_1 > 0 {
2808 assert!(targets.contains(&a.exit_account_1));
2809 }
2810 if a.output_amount_2 > 0 {
2811 assert!(targets.contains(&a.exit_account_2));
2812 assert_ne!(a.exit_account_2, a.exit_account_1);
2813 }
2814 }
2815 }
2816}