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_aggregator::{
21 aggregator::{AggregationBackend, CircuitType},
22 config::CircuitBinsConfig,
23};
24use qp_wormhole_circuit::{
25 inputs::{CircuitInputs, ParseAggregatedPublicInputs, PrivateCircuitInputs},
26 nullifier::Nullifier,
27};
28use qp_wormhole_inputs::{AggregatedPublicCircuitInputs, PublicCircuitInputs};
29use qp_wormhole_prover::WormholeProver;
30use qp_zk_circuits_common::{
31 circuit::{C, D, F},
32 storage_proof::prepare_proof_for_circuit,
33 utils::{digest_to_bytes, BytesDigest},
34};
35use rand::RngCore;
36use sp_core::crypto::{AccountId32, Ss58Codec};
37use std::path::Path;
38use subxt::{
39 backend::legacy::rpc_methods::ReadProof,
40 blocks::Block,
41 ext::{
42 codec::Encode,
43 jsonrpsee::{core::client::ClientT, rpc_params},
44 },
45 utils::{to_hex, AccountId32 as SubxtAccountId},
46 OnlineClient,
47};
48
49use crate::wormhole_lib;
51pub use crate::wormhole_lib::{
52 compute_output_amount, NATIVE_ASSET_ID, SCALE_DOWN_FACTOR, VOLUME_FEE_BPS,
53};
54
55pub fn parse_secret_hex(secret_hex: &str) -> Result<[u8; 32], String> {
57 let secret_bytes = hex::decode(secret_hex.trim_start_matches("0x"))
58 .map_err(|e| format!("Invalid secret hex: {}", e))?;
59
60 if secret_bytes.len() != 32 {
61 return Err(format!("Secret must be exactly 32 bytes, got {} bytes", secret_bytes.len()));
62 }
63
64 secret_bytes
65 .try_into()
66 .map_err(|_| "Failed to convert secret to 32-byte array".to_string())
67}
68
69pub fn parse_exit_account(exit_account_str: &str) -> Result<[u8; 32], String> {
71 if let Some(hex_str) = exit_account_str.strip_prefix("0x") {
72 let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid exit account hex: {}", e))?;
73
74 if bytes.len() != 32 {
75 return Err(format!("Exit account must be 32 bytes, got {} bytes", bytes.len()));
76 }
77
78 bytes.try_into().map_err(|_| "Failed to convert exit account".to_string())
79 } else {
80 let account_id = AccountId32::from_ss58check(exit_account_str)
82 .map_err(|e| format!("Invalid SS58 address: {}", e))?;
83
84 Ok(account_id.into())
85 }
86}
87
88pub fn quantize_funding_amount(amount: u128) -> Result<u32, String> {
91 wormhole_lib::quantize_amount(amount).map_err(|e| e.message)
92}
93
94pub fn read_proof_file(path: &str) -> Result<Vec<u8>, String> {
96 let proof_hex =
97 std::fs::read_to_string(path).map_err(|e| format!("Failed to read proof file: {}", e))?;
98
99 hex::decode(proof_hex.trim()).map_err(|e| format!("Failed to decode proof hex: {}", e))
100}
101
102pub fn write_proof_file(path: &str, proof_bytes: &[u8]) -> Result<(), String> {
104 let proof_hex = hex::encode(proof_bytes);
105 std::fs::write(path, proof_hex).map_err(|e| format!("Failed to write proof file: {}", e))
106}
107
108pub fn format_balance(amount: u128) -> String {
110 let whole = amount / 1_000_000_000_000;
111 let frac = (amount % 1_000_000_000_000) / 10_000_000_000; format!("{}.{:02} DEV", whole, frac)
113}
114
115pub fn random_partition(total: u128, n: usize, min_per_part: u128) -> Vec<u128> {
119 use rand::Rng;
120
121 if n == 0 {
122 return vec![];
123 }
124 if n == 1 {
125 return vec![total];
126 }
127
128 let min_total = min_per_part * n as u128;
130 if total < min_total {
131 let per_part = total / n as u128;
133 let remainder = total % n as u128;
134 let mut parts: Vec<u128> = vec![per_part; n];
135 parts[n - 1] += remainder;
137 return parts;
138 }
139
140 let distributable = total - min_total;
142
143 let mut rng = rand::rng();
145 let mut cuts: Vec<u128> = (0..n - 1).map(|_| rng.random_range(0..=distributable)).collect();
146 cuts.sort();
147
148 let mut parts = Vec::with_capacity(n);
150 let mut prev = 0u128;
151 for cut in cuts {
152 parts.push(min_per_part + (cut - prev));
153 prev = cut;
154 }
155 parts.push(min_per_part + (distributable - prev));
156
157 let sum: u128 = parts.iter().sum();
160 let diff = total as i128 - sum as i128;
161 if diff != 0 {
162 let idx = rng.random_range(0..n);
164 parts[idx] = (parts[idx] as i128 + diff).max(0) as u128;
165 }
166
167 parts
168}
169
170#[derive(Debug, Clone)]
172pub struct ProofOutputAssignment {
173 pub output_amount_1: u32,
175 pub exit_account_1: [u8; 32],
177 pub output_amount_2: u32,
179 pub exit_account_2: [u8; 32],
181}
182
183pub fn compute_random_output_assignments(
201 input_amounts: &[u128],
202 target_accounts: &[[u8; 32]],
203 fee_bps: u32,
204) -> Vec<ProofOutputAssignment> {
205 use rand::seq::SliceRandom;
206
207 let num_proofs = input_amounts.len();
208 let num_targets = target_accounts.len();
209
210 if num_proofs == 0 || num_targets == 0 {
211 return vec![];
212 }
213
214 let proof_outputs: Vec<u32> = input_amounts
216 .iter()
217 .map(|&input| {
218 let input_quantized = quantize_funding_amount(input).unwrap_or(0);
219 compute_output_amount(input_quantized, fee_bps)
220 })
221 .collect();
222
223 let total_output: u64 = proof_outputs.iter().map(|&x| x as u64).sum();
224
225 let min_per_target = 3u128;
231 let target_amounts_u128 = random_partition(total_output as u128, num_targets, min_per_target);
232 let target_amounts: Vec<u32> = target_amounts_u128.iter().map(|&x| x as u32).collect();
233
234 let mut rng = rand::rng();
248
249 let mut target_remaining: Vec<u32> = target_amounts.clone();
251
252 let mut assignments: Vec<ProofOutputAssignment> = proof_outputs
254 .iter()
255 .map(|&po| ProofOutputAssignment {
256 output_amount_1: po,
257 exit_account_1: [0u8; 32],
258 output_amount_2: 0,
259 exit_account_2: [0u8; 32],
260 })
261 .collect();
262
263 let mut shuffled_targets: Vec<usize> = (0..num_targets).collect();
267 shuffled_targets.shuffle(&mut rng);
268
269 for (assign_idx, &tidx) in shuffled_targets.iter().enumerate() {
270 let proof_idx = assign_idx % num_proofs;
271 let assignment = &mut assignments[proof_idx];
272
273 if assignment.exit_account_1 == [0u8; 32] {
274 let assign = assignment.output_amount_1.min(target_remaining[tidx]);
276 assignment.exit_account_1 = target_accounts[tidx];
277 assignment.output_amount_1 = assign;
279 target_remaining[tidx] -= assign;
280 } else if assignment.exit_account_2 == [0u8; 32] {
281 let avail = proof_outputs[proof_idx].saturating_sub(assignment.output_amount_1);
283 let assign = avail.min(target_remaining[tidx]);
284 assignment.exit_account_2 = target_accounts[tidx];
285 assignment.output_amount_2 = assign;
286 target_remaining[tidx] -= assign;
287 }
288 }
290
291 for proof_idx in 0..num_proofs {
294 let total_proof_output = proof_outputs[proof_idx];
295 let current_sum =
296 assignments[proof_idx].output_amount_1 + assignments[proof_idx].output_amount_2;
297 let mut shortfall = total_proof_output.saturating_sub(current_sum);
298
299 if shortfall > 0 {
300 assignments[proof_idx].output_amount_1 += shortfall;
302 shortfall = 0;
303 }
304
305 if assignments[proof_idx].exit_account_1 == [0u8; 32] && num_targets > 0 {
307 assignments[proof_idx].exit_account_1 = target_accounts[0];
308 }
309
310 let _ = shortfall; }
312
313 assignments
314}
315
316pub struct VerificationResult {
318 pub success: bool,
319 pub exit_amount: Option<u128>,
320 pub error_message: Option<String>,
321}
322
323async fn check_proof_verification_events(
326 client: &subxt::OnlineClient<ChainConfig>,
327 block_hash: &subxt::utils::H256,
328 tx_hash: &subxt::utils::H256,
329 verbose: bool,
330) -> crate::error::Result<VerificationResult> {
331 use crate::chain::quantus_subxt::api::system::events::ExtrinsicFailed;
332 use colored::Colorize;
333
334 let block = client.blocks().at(*block_hash).await.map_err(|e| {
335 crate::error::QuantusError::NetworkError(format!("Failed to get block: {e:?}"))
336 })?;
337
338 let extrinsics = block.extrinsics().await.map_err(|e| {
339 crate::error::QuantusError::NetworkError(format!("Failed to get extrinsics: {e:?}"))
340 })?;
341
342 let our_extrinsic_index = extrinsics
344 .iter()
345 .enumerate()
346 .find(|(_, ext)| ext.hash() == *tx_hash)
347 .map(|(idx, _)| idx);
348
349 let events = block.events().await.map_err(|e| {
350 crate::error::QuantusError::NetworkError(format!("Failed to fetch events: {e:?}"))
351 })?;
352
353 let metadata = client.metadata();
354
355 let mut verification_result =
356 VerificationResult { success: false, exit_amount: None, error_message: None };
357
358 if verbose {
359 log_print!("");
360 log_print!("📋 Transaction Events:");
361 }
362
363 if let Some(ext_idx) = our_extrinsic_index {
364 for event_result in events.iter() {
365 let event = event_result.map_err(|e| {
366 crate::error::QuantusError::NetworkError(format!("Failed to decode event: {e:?}"))
367 })?;
368
369 if let subxt::events::Phase::ApplyExtrinsic(event_ext_idx) = event.phase() {
371 if event_ext_idx != ext_idx as u32 {
372 continue;
373 }
374
375 if verbose {
377 log_print!(
378 " 📌 {}.{}",
379 event.pallet_name().bright_cyan(),
380 event.variant_name().bright_yellow()
381 );
382
383 if let Ok(typed_event) =
385 event.as_root_event::<crate::chain::quantus_subxt::api::Event>()
386 {
387 log_print!(" 📝 {:?}", typed_event);
388 }
389 }
390
391 if let Ok(Some(proof_verified)) =
393 event.as_event::<wormhole::events::ProofVerified>()
394 {
395 verification_result.success = true;
396 verification_result.exit_amount = Some(proof_verified.exit_amount);
397 }
398
399 if let Ok(Some(ExtrinsicFailed { dispatch_error, .. })) =
401 event.as_event::<ExtrinsicFailed>()
402 {
403 let error_msg = format_dispatch_error(&dispatch_error, &metadata);
404 verification_result.success = false;
405 verification_result.error_message = Some(error_msg);
406 }
407 }
408 }
409 }
410
411 if verbose {
412 log_print!("");
413 }
414
415 Ok(verification_result)
416}
417
418fn format_dispatch_error(
420 error: &crate::chain::quantus_subxt::api::runtime_types::sp_runtime::DispatchError,
421 metadata: &subxt::Metadata,
422) -> String {
423 use crate::chain::quantus_subxt::api::runtime_types::sp_runtime::DispatchError;
424
425 match error {
426 DispatchError::Module(module_error) => {
427 let pallet_name = metadata
428 .pallet_by_index(module_error.index)
429 .map(|p| p.name())
430 .unwrap_or("Unknown");
431 let error_index = module_error.error[0];
432
433 let error_info = metadata.pallet_by_index(module_error.index).and_then(|p| {
435 p.error_variant_by_index(error_index)
436 .map(|v| (v.name.clone(), v.docs.join(" ")))
437 });
438
439 match error_info {
440 Some((name, docs)) if !docs.is_empty() => {
441 format!("{}::{} ({})", pallet_name, name, docs)
442 },
443 Some((name, _)) => format!("{}::{}", pallet_name, name),
444 None => format!("{}::Error[{}]", pallet_name, error_index),
445 }
446 },
447 DispatchError::BadOrigin => "BadOrigin".to_string(),
448 DispatchError::CannotLookup => "CannotLookup".to_string(),
449 DispatchError::Other => "Other".to_string(),
450 _ => format!("{:?}", error),
451 }
452}
453
454#[derive(Subcommand, Debug)]
455pub enum WormholeCommands {
456 Address {
458 #[arg(long)]
460 secret: String,
461 },
462 Prove {
464 #[arg(long)]
466 secret: String,
467
468 #[arg(long)]
470 amount: u128,
471
472 #[arg(long)]
474 exit_account: String,
475
476 #[arg(long)]
478 block: String,
479
480 #[arg(long)]
482 transfer_count: u64,
483
484 #[arg(long)]
486 funding_account: String,
487
488 #[arg(short, long, default_value = "proof.hex")]
490 output: String,
491 },
492 Aggregate {
494 #[arg(short, long, num_args = 1..)]
496 proofs: Vec<String>,
497
498 #[arg(short, long, default_value = "aggregated_proof.hex")]
500 output: String,
501 },
502 VerifyAggregated {
504 #[arg(short, long, default_value = "aggregated_proof.hex")]
506 proof: String,
507 },
508 ParseProof {
510 #[arg(short, long)]
512 proof: String,
513
514 #[arg(long)]
516 aggregated: bool,
517
518 #[arg(long)]
520 verify: bool,
521 },
522 Multiround {
524 #[arg(short, long, default_value = "2")]
526 num_proofs: usize,
527
528 #[arg(short, long, default_value = "2")]
530 rounds: usize,
531
532 #[arg(short, long, default_value = "100")]
534 amount: f64,
535
536 #[arg(short, long)]
538 wallet: String,
539
540 #[arg(short, long)]
542 password: Option<String>,
543
544 #[arg(long)]
546 password_file: Option<String>,
547
548 #[arg(short, long)]
550 keep_files: bool,
551
552 #[arg(short, long, default_value = "/tmp/wormhole_multiround")]
554 output_dir: String,
555
556 #[arg(long)]
558 dry_run: bool,
559 },
560 Dissolve {
567 #[arg(short, long)]
569 amount: f64,
570
571 #[arg(short, long, default_value = "1.0")]
573 target_size: f64,
574
575 #[arg(short, long)]
577 wallet: String,
578
579 #[arg(short, long)]
581 password: Option<String>,
582
583 #[arg(long)]
585 password_file: Option<String>,
586
587 #[arg(short, long)]
589 keep_files: bool,
590
591 #[arg(short, long, default_value = "/tmp/wormhole_dissolve")]
593 output_dir: String,
594 },
595 Fuzz {
597 #[arg(short, long)]
599 wallet: String,
600
601 #[arg(short, long)]
603 password: Option<String>,
604
605 #[arg(long)]
607 password_file: Option<String>,
608
609 #[arg(short, long, default_value = "1.0")]
611 amount: f64,
612 },
613}
614
615pub async fn handle_wormhole_command(
616 command: WormholeCommands,
617 node_url: &str,
618) -> crate::error::Result<()> {
619 match command {
620 WormholeCommands::Address { secret } => show_wormhole_address(secret),
621 WormholeCommands::Prove {
622 secret,
623 amount,
624 exit_account,
625 block,
626 transfer_count,
627 funding_account,
628 output,
629 } => {
630 log_print!("Generating proof from existing transfer...");
631
632 let quantus_client = QuantusClient::new(node_url).await.map_err(|e| {
634 crate::error::QuantusError::Generic(format!("Failed to connect: {}", e))
635 })?;
636
637 let exit_account_bytes =
639 parse_exit_account(&exit_account).map_err(crate::error::QuantusError::Generic)?;
640
641 let input_amount_quantized =
643 quantize_funding_amount(amount).map_err(crate::error::QuantusError::Generic)?;
644 let output_amount = compute_output_amount(input_amount_quantized, VOLUME_FEE_BPS);
645
646 let output_assignment = ProofOutputAssignment {
647 output_amount_1: output_amount,
648 exit_account_1: exit_account_bytes,
649 output_amount_2: 0,
650 exit_account_2: [0u8; 32],
651 };
652
653 let prove_start = std::time::Instant::now();
654 generate_proof(
655 &secret,
656 amount,
657 &output_assignment,
658 &block,
659 transfer_count,
660 &funding_account,
661 &output,
662 &quantus_client,
663 )
664 .await?;
665 let prove_elapsed = prove_start.elapsed();
666 log_print!("Proof generation: {:.2}s", prove_elapsed.as_secs_f64());
667 Ok(())
668 },
669 WormholeCommands::Aggregate { proofs, output } => aggregate_proofs(proofs, output).await,
670 WormholeCommands::VerifyAggregated { proof } =>
671 verify_aggregated_proof(proof, node_url).await,
672 WormholeCommands::ParseProof { proof, aggregated, verify } =>
673 parse_proof_file(proof, aggregated, verify).await,
674 WormholeCommands::Multiround {
675 num_proofs,
676 rounds,
677 amount,
678 wallet,
679 password,
680 password_file,
681 keep_files,
682 output_dir,
683 dry_run,
684 } => {
685 let amount_planck = (amount * 1_000_000_000_000.0) as u128;
687 let amount_aligned = (amount_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR;
688 run_multiround(
689 num_proofs,
690 rounds,
691 amount_aligned,
692 wallet,
693 password,
694 password_file,
695 keep_files,
696 output_dir,
697 dry_run,
698 node_url,
699 )
700 .await
701 },
702 WormholeCommands::Dissolve {
703 amount,
704 target_size,
705 wallet,
706 password,
707 password_file,
708 keep_files,
709 output_dir,
710 } => {
711 let amount_planck = (amount * 1_000_000_000_000.0) as u128;
712 let amount_aligned = (amount_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR;
713 let target_planck = (target_size * 1_000_000_000_000.0) as u128;
714 let target_aligned = (target_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR;
715 run_dissolve(
716 amount_aligned,
717 target_aligned,
718 wallet,
719 password,
720 password_file,
721 keep_files,
722 output_dir,
723 node_url,
724 )
725 .await
726 },
727 WormholeCommands::Fuzz { wallet, password, password_file, amount } => {
728 let amount_planck = (amount * 1_000_000_000_000.0) as u128;
729 let amount_aligned = (amount_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR;
730 run_fuzz_test(wallet, password, password_file, amount_aligned, node_url).await
731 },
732 }
733}
734
735pub type TransferProofKey = (AccountId32, u64);
739
740pub type TransferProofData = (u32, u64, AccountId32, AccountId32, u128);
743
744fn show_wormhole_address(secret_hex: String) -> crate::error::Result<()> {
747 use colored::Colorize;
748
749 let secret_array =
750 parse_secret_hex(&secret_hex).map_err(crate::error::QuantusError::Generic)?;
751 let secret: BytesDigest = secret_array.try_into().map_err(|e| {
752 crate::error::QuantusError::Generic(format!("Failed to convert secret: {:?}", e))
753 })?;
754
755 let unspendable_account =
756 qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
757 .account_id;
758 let unspendable_account_bytes_digest =
759 qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account);
760 let unspendable_account_bytes: [u8; 32] = unspendable_account_bytes_digest
761 .as_ref()
762 .try_into()
763 .expect("BytesDigest is always 32 bytes");
764
765 let account_id = sp_core::crypto::AccountId32::new(unspendable_account_bytes);
766 let ss58_address =
767 account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
768
769 log_print!("{}", "Wormhole Address".bright_cyan());
770 log_print!(" SS58: {}", ss58_address.bright_green());
771 log_print!(" Hex: 0x{}", hex::encode(unspendable_account_bytes));
772 log_print!("");
773 log_print!("To fund this address:");
774 log_print!(" quantus send --from <wallet> --to {} --amount <amount>", ss58_address);
775
776 Ok(())
777}
778
779async fn at_best_block(
780 quantus_client: &QuantusClient,
781) -> anyhow::Result<Block<ChainConfig, OnlineClient<ChainConfig>>> {
782 let best_block = quantus_client.get_latest_block().await?;
783 let block = quantus_client.client().blocks().at(best_block).await?;
784 Ok(block)
785}
786
787async fn aggregate_proofs(
788 proof_files: Vec<String>,
789 output_file: String,
790) -> crate::error::Result<()> {
791 use qp_wormhole_aggregator::aggregator::{AggregationBackend, CircuitType, Layer0Aggregator};
792
793 use std::path::Path;
794
795 log_print!("Aggregating {} proofs...", proof_files.len());
796
797 let bins_dir = Path::new("generated-bins");
799 let agg_config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
800 crate::error::QuantusError::Generic(format!(
801 "Failed to load circuit bins config from {:?}: {}",
802 bins_dir, e
803 ))
804 })?;
805
806 if proof_files.len() > agg_config.num_leaf_proofs {
808 return Err(crate::error::QuantusError::Generic(format!(
809 "Too many proofs: {} provided, max {} supported by circuit",
810 proof_files.len(),
811 agg_config.num_leaf_proofs
812 )));
813 }
814
815 let num_padding_proofs = agg_config.num_leaf_proofs - proof_files.len();
816
817 log_print!(" Loading aggregator and generating {} dummy proofs...", num_padding_proofs);
818
819 let mut aggregator = Layer0Aggregator::new(bins_dir).map_err(|e| {
820 crate::error::QuantusError::Generic(format!(
821 "Failed to load aggregator from pre-built bins: {}",
822 e
823 ))
824 })?;
825
826 log_verbose!("Aggregation config: num_leaf_proofs={}", aggregator.batch_size());
827 let common_data = aggregator.load_common_data(CircuitType::Leaf).map_err(|e| {
828 crate::error::QuantusError::Generic(format!("Failed to load leaf circuit data: {}", e))
829 })?;
830
831 for (idx, proof_file) in proof_files.iter().enumerate() {
833 log_verbose!("Loading proof {}/{}: {}", idx + 1, proof_files.len(), proof_file);
834
835 let proof_bytes = read_proof_file(proof_file).map_err(|e| {
836 crate::error::QuantusError::Generic(format!("Failed to load {}: {}", proof_file, e))
837 })?;
838
839 let proof = ProofWithPublicInputs::<F, C, D>::from_bytes(proof_bytes, &common_data)
840 .map_err(|e| {
841 crate::error::QuantusError::Generic(format!(
842 "Failed to deserialize proof from {}: {}",
843 proof_file, e
844 ))
845 })?;
846
847 aggregator.push_proof(proof).map_err(|e| {
848 crate::error::QuantusError::Generic(format!("Failed to add proof: {}", e))
849 })?;
850 }
851
852 log_print!(" Running aggregation...");
853 let agg_start = std::time::Instant::now();
854 let aggregated_proof = aggregator
855 .aggregate()
856 .map_err(|e| crate::error::QuantusError::Generic(format!("Aggregation failed: {}", e)))?;
857 let agg_elapsed = agg_start.elapsed();
858 log_print!(" Aggregation: {:.2}s", agg_elapsed.as_secs_f64());
859
860 let aggregated_public_inputs =
862 AggregatedPublicCircuitInputs::try_from_felts(aggregated_proof.public_inputs.as_slice())
863 .map_err(|e| {
864 crate::error::QuantusError::Generic(format!(
865 "Failed to parse aggregated public inputs: {}",
866 e
867 ))
868 })?;
869
870 log_verbose!("Aggregated public inputs: {:#?}", aggregated_public_inputs);
871
872 log_print!(" Exit accounts in aggregated proof:");
874 for (idx, account_data) in aggregated_public_inputs.account_data.iter().enumerate() {
875 let exit_bytes: &[u8] = account_data.exit_account.as_ref();
876 let is_dummy = exit_bytes.iter().all(|&b| b == 0) || account_data.summed_output_amount == 0;
877 if is_dummy {
878 log_verbose!(" [{}] DUMMY (skipped)", idx);
879 } else {
880 let dequantized_amount =
882 (account_data.summed_output_amount as u128) * SCALE_DOWN_FACTOR;
883 log_print!(
884 " [{}] {} -> {} quantized ({} planck = {})",
885 idx,
886 hex::encode(exit_bytes),
887 account_data.summed_output_amount,
888 dequantized_amount,
889 format_balance(dequantized_amount)
890 );
891 }
892 }
893
894 log_verbose!("Verifying aggregated proof locally...");
896 aggregator.verify(aggregated_proof.clone()).map_err(|e| {
897 crate::error::QuantusError::Generic(format!("Aggregated proof verification failed: {}", e))
898 })?;
899
900 write_proof_file(&output_file, &aggregated_proof.to_bytes()).map_err(|e| {
902 crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e))
903 })?;
904
905 log_success!("Aggregation complete!");
906 log_success!("Output: {}", output_file);
907 log_print!(
908 "Aggregated {} proofs into 1 proof with {} exit accounts",
909 proof_files.len(),
910 aggregated_public_inputs.account_data.len()
911 );
912
913 Ok(())
914}
915
916#[derive(Debug, Clone, Copy)]
917enum IncludedAt {
918 Best,
919 Finalized,
920}
921
922impl IncludedAt {
923 fn label(self) -> &'static str {
924 match self {
925 IncludedAt::Best => "best block",
926 IncludedAt::Finalized => "finalized block",
927 }
928 }
929}
930
931fn read_hex_proof_file_to_bytes(proof_file: &str) -> crate::error::Result<Vec<u8>> {
932 let proof_hex = std::fs::read_to_string(proof_file).map_err(|e| {
933 crate::error::QuantusError::Generic(format!("Failed to read proof file: {}", e))
934 })?;
935
936 let proof_bytes = hex::decode(proof_hex.trim())
937 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to decode hex: {}", e)))?;
938
939 Ok(proof_bytes)
940}
941
942async fn submit_unsigned_verify_aggregated_proof(
945 quantus_client: &QuantusClient,
946 proof_bytes: Vec<u8>,
947) -> crate::error::Result<(IncludedAt, subxt::utils::H256, subxt::utils::H256)> {
948 use subxt::tx::TxStatus;
949
950 let verify_tx = quantus_node::api::tx().wormhole().verify_aggregated_proof(proof_bytes);
951
952 let unsigned_tx = quantus_client.client().tx().create_unsigned(&verify_tx).map_err(|e| {
953 crate::error::QuantusError::Generic(format!("Failed to create unsigned tx: {}", e))
954 })?;
955
956 let mut tx_progress = unsigned_tx
957 .submit_and_watch()
958 .await
959 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to submit tx: {}", e)))?;
960
961 while let Some(Ok(status)) = tx_progress.next().await {
962 match status {
963 TxStatus::InBestBlock(tx_in_block) => {
964 return Ok((
965 IncludedAt::Best,
966 tx_in_block.block_hash(),
967 tx_in_block.extrinsic_hash(),
968 ));
969 },
970 TxStatus::InFinalizedBlock(tx_in_block) => {
971 return Ok((
972 IncludedAt::Finalized,
973 tx_in_block.block_hash(),
974 tx_in_block.extrinsic_hash(),
975 ));
976 },
977 TxStatus::Error { message } | TxStatus::Invalid { message } => {
978 return Err(crate::error::QuantusError::Generic(format!(
979 "Transaction failed: {}",
980 message
981 )));
982 },
983 _ => continue,
984 }
985 }
986
987 Err(crate::error::QuantusError::Generic("Transaction stream ended unexpectedly".to_string()))
988}
989
990async fn collect_wormhole_events_for_extrinsic(
993 quantus_client: &QuantusClient,
994 block_hash: subxt::utils::H256,
995 tx_hash: subxt::utils::H256,
996) -> crate::error::Result<(bool, Vec<wormhole::events::NativeTransferred>)> {
997 use crate::chain::quantus_subxt::api::system::events::ExtrinsicFailed;
998
999 let block =
1000 quantus_client.client().blocks().at(block_hash).await.map_err(|e| {
1001 crate::error::QuantusError::Generic(format!("Failed to get block: {}", e))
1002 })?;
1003
1004 let events = block
1005 .events()
1006 .await
1007 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get events: {}", e)))?;
1008
1009 let extrinsics = block.extrinsics().await.map_err(|e| {
1010 crate::error::QuantusError::Generic(format!("Failed to get extrinsics: {}", e))
1011 })?;
1012
1013 let our_ext_idx = extrinsics
1014 .iter()
1015 .enumerate()
1016 .find(|(_, ext)| ext.hash() == tx_hash)
1017 .map(|(idx, _)| idx as u32)
1018 .ok_or_else(|| {
1019 crate::error::QuantusError::Generic(
1020 "Could not find submitted extrinsic in included block".to_string(),
1021 )
1022 })?;
1023
1024 let mut transfer_events = Vec::new();
1025 let mut found_proof_verified = false;
1026
1027 log_verbose!(" Events for our extrinsic (idx={}):", our_ext_idx);
1028
1029 for event_result in events.iter() {
1030 let event = event_result.map_err(|e| {
1031 crate::error::QuantusError::Generic(format!("Failed to decode event: {}", e))
1032 })?;
1033
1034 if let subxt::events::Phase::ApplyExtrinsic(ext_idx) = event.phase() {
1035 if ext_idx == our_ext_idx {
1036 log_print!(" Event: {}::{}", event.pallet_name(), event.variant_name());
1037
1038 if let Ok(Some(ExtrinsicFailed { dispatch_error, .. })) =
1040 event.as_event::<ExtrinsicFailed>()
1041 {
1042 let metadata = quantus_client.client().metadata();
1043 let error_msg = format_dispatch_error(&dispatch_error, &metadata);
1044 log_print!(" DispatchError: {}", error_msg);
1045 }
1046
1047 if let Ok(Some(_)) = event.as_event::<wormhole::events::ProofVerified>() {
1048 found_proof_verified = true;
1049 }
1050
1051 if let Ok(Some(transfer)) = event.as_event::<wormhole::events::NativeTransferred>()
1052 {
1053 transfer_events.push(transfer);
1054 }
1055 }
1056 }
1057 }
1058
1059 Ok((found_proof_verified, transfer_events))
1060}
1061
1062async fn verify_aggregated_proof(proof_file: String, node_url: &str) -> crate::error::Result<()> {
1063 log_print!("Verifying aggregated wormhole proof on-chain...");
1064
1065 let proof_bytes = read_hex_proof_file_to_bytes(&proof_file)?;
1066 log_verbose!("Aggregated proof size: {} bytes", proof_bytes.len());
1067
1068 let quantus_client = QuantusClient::new(node_url)
1070 .await
1071 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to connect: {}", e)))?;
1072 log_verbose!("Connected to node");
1073
1074 log_verbose!("Submitting unsigned aggregated verification transaction...");
1075
1076 let (included_at, block_hash, tx_hash) =
1077 submit_unsigned_verify_aggregated_proof(&quantus_client, proof_bytes).await?;
1078
1079 let result = check_proof_verification_events(
1081 quantus_client.client(),
1082 &block_hash,
1083 &tx_hash,
1084 crate::log::is_verbose(),
1085 )
1086 .await?;
1087
1088 if result.success {
1089 log_success!("Aggregated proof verified successfully on-chain!");
1090 if let Some(amount) = result.exit_amount {
1091 log_success!("Total exit amount: {}", format_balance(amount));
1092 }
1093
1094 log_print!(" Block: 0x{}", hex::encode(block_hash.0));
1095 log_print!(" Extrinsic: 0x{}", hex::encode(tx_hash.0));
1096 log_verbose!("Included in {}: {:?}", included_at.label(), block_hash);
1097 return Ok(());
1098 }
1099
1100 let error_msg = result.error_message.unwrap_or_else(|| {
1101 "Aggregated proof verification failed - no ProofVerified event found".to_string()
1102 });
1103 log_error!("❌ {}", error_msg);
1104 Err(crate::error::QuantusError::Generic(error_msg))
1105}
1106
1107#[derive(Debug, Clone)]
1113#[allow(dead_code)]
1114struct TransferInfo {
1115 block_hash: subxt::utils::H256,
1117 transfer_count: u64,
1119 amount: u128,
1121 wormhole_address: SubxtAccountId,
1123 funding_account: SubxtAccountId,
1125}
1126
1127fn derive_wormhole_secret(
1130 mnemonic: &str,
1131 round: usize,
1132 index: usize,
1133) -> Result<WormholePair, crate::error::QuantusError> {
1134 let path = format!("m/44'/{}/0'/{}'/{}'", QUANTUS_WORMHOLE_CHAIN_ID, round, index);
1136 derive_wormhole_from_mnemonic(mnemonic, None, &path)
1137 .map_err(|e| crate::error::QuantusError::Generic(format!("HD derivation failed: {:?}", e)))
1138}
1139
1140fn calculate_round_amount(initial_amount: u128, round: usize) -> u128 {
1144 let mut amount = initial_amount;
1145 for _ in 0..round {
1146 amount = amount * 9990 / 10000;
1148 }
1149 amount
1150}
1151
1152async fn get_minting_account(
1154 client: &OnlineClient<ChainConfig>,
1155) -> Result<SubxtAccountId, crate::error::QuantusError> {
1156 let minting_account_addr = quantus_node::api::constants().wormhole().minting_account();
1157 let minting_account = client.constants().at(&minting_account_addr).map_err(|e| {
1158 crate::error::QuantusError::Generic(format!("Failed to get minting account: {}", e))
1159 })?;
1160 Ok(minting_account)
1161}
1162
1163fn parse_transfer_events(
1166 events: &[wormhole::events::NativeTransferred],
1167 expected_addresses: &[SubxtAccountId],
1168 block_hash: subxt::utils::H256,
1169) -> Result<Vec<TransferInfo>, crate::error::QuantusError> {
1170 let mut transfer_infos = Vec::new();
1171
1172 for expected_addr in expected_addresses {
1173 let matching_event = events.iter().find(|e| &e.to == expected_addr).ok_or_else(|| {
1175 crate::error::QuantusError::Generic(format!(
1176 "No transfer event found for address {:?}",
1177 expected_addr
1178 ))
1179 })?;
1180
1181 transfer_infos.push(TransferInfo {
1182 block_hash,
1183 transfer_count: matching_event.transfer_count,
1184 amount: matching_event.amount,
1185 wormhole_address: expected_addr.clone(),
1186 funding_account: matching_event.from.clone(),
1187 });
1188 }
1189
1190 Ok(transfer_infos)
1191}
1192
1193struct MultiroundConfig {
1195 num_proofs: usize,
1196 rounds: usize,
1197 amount: u128,
1198 output_dir: String,
1199 keep_files: bool,
1200}
1201
1202struct MultiroundWalletContext {
1204 wallet_name: String,
1205 wallet_address: String,
1206 wallet_account_id: SubxtAccountId,
1207 keypair: QuantumKeyPair,
1208 mnemonic: String,
1209}
1210
1211fn validate_multiround_params(
1213 num_proofs: usize,
1214 rounds: usize,
1215 max_proofs: usize,
1216) -> crate::error::Result<()> {
1217 if !(1..=max_proofs).contains(&num_proofs) {
1218 return Err(crate::error::QuantusError::Generic(format!(
1219 "num_proofs must be between 1 and {} (got: {})",
1220 max_proofs, num_proofs
1221 )));
1222 }
1223 if rounds < 1 {
1224 return Err(crate::error::QuantusError::Generic(format!(
1225 "rounds must be at least 1 (got: {})",
1226 rounds
1227 )));
1228 }
1229 Ok(())
1230}
1231
1232fn load_multiround_wallet(
1234 wallet_name: &str,
1235 password: Option<String>,
1236 password_file: Option<String>,
1237) -> crate::error::Result<MultiroundWalletContext> {
1238 let wallet_manager = WalletManager::new()?;
1239 let wallet_password = password::get_wallet_password(wallet_name, password, password_file)?;
1240 let wallet_data = wallet_manager.load_wallet(wallet_name, &wallet_password)?;
1241 let wallet_address = wallet_data.keypair.to_account_id_ss58check();
1242 let wallet_account_id = SubxtAccountId(wallet_data.keypair.to_account_id_32().into());
1243
1244 let mnemonic = match wallet_data.mnemonic {
1246 Some(m) => {
1247 log_verbose!("Using wallet mnemonic for HD derivation");
1248 m
1249 },
1250 None => {
1251 log_print!("Wallet has no mnemonic - generating random mnemonic for wormhole secrets");
1252 let mut entropy = [0u8; 32];
1253 rand::rng().fill_bytes(&mut entropy);
1254 let sensitive_entropy = SensitiveBytes32::from(&mut entropy);
1255 let m = generate_mnemonic(sensitive_entropy).map_err(|e| {
1256 crate::error::QuantusError::Generic(format!("Failed to generate mnemonic: {:?}", e))
1257 })?;
1258 log_verbose!("Generated mnemonic (not saved): {}", m);
1259 m
1260 },
1261 };
1262
1263 Ok(MultiroundWalletContext {
1264 wallet_name: wallet_name.to_string(),
1265 wallet_address,
1266 wallet_account_id,
1267 keypair: wallet_data.keypair,
1268 mnemonic,
1269 })
1270}
1271
1272fn print_multiround_config(
1274 config: &MultiroundConfig,
1275 wallet: &MultiroundWalletContext,
1276 num_leaf_proofs: usize,
1277) {
1278 use colored::Colorize;
1279
1280 log_print!("{}", "Configuration:".bright_cyan());
1281 log_print!(" Wallet: {}", wallet.wallet_name);
1282 log_print!(" Wallet address: {}", wallet.wallet_address);
1283 log_print!(
1284 " Total amount: {} ({}) - randomly partitioned across {} proofs",
1285 config.amount,
1286 format_balance(config.amount),
1287 config.num_proofs
1288 );
1289 log_print!(" Proofs per round: {}", config.num_proofs);
1290 log_print!(" Rounds: {}", config.rounds);
1291 log_print!(" Aggregation: num_leaf_proofs={}", num_leaf_proofs);
1292 log_print!(" Output directory: {}", config.output_dir);
1293 log_print!(" Keep files: {}", config.keep_files);
1294 log_print!("");
1295
1296 log_print!("{}", "Expected amounts per round:".bright_cyan());
1298 for r in 1..=config.rounds {
1299 let round_amount = calculate_round_amount(config.amount, r);
1300 log_print!(" Round {}: {} ({})", r, round_amount, format_balance(round_amount));
1301 }
1302 log_print!("");
1303}
1304
1305async fn execute_initial_transfers(
1310 quantus_client: &QuantusClient,
1311 wallet: &MultiroundWalletContext,
1312 secrets: &[WormholePair],
1313 amount: u128,
1314 num_proofs: usize,
1315) -> crate::error::Result<Vec<TransferInfo>> {
1316 use colored::Colorize;
1317 use quantus_node::api::runtime_types::{
1318 pallet_balances::pallet::Call as BalancesCall, quantus_runtime::RuntimeCall,
1319 };
1320
1321 log_print!("{}", "Step 1: Sending batched transfer to wormhole addresses...".bright_yellow());
1322
1323 let partition_amounts = random_partition(amount, num_proofs, 3 * SCALE_DOWN_FACTOR);
1327 log_print!(" Random partition of {} ({}):", amount, format_balance(amount));
1328 for (i, &amt) in partition_amounts.iter().enumerate() {
1329 log_print!(" Proof {}: {} ({})", i + 1, amt, format_balance(amt));
1330 }
1331
1332 let mut calls = Vec::with_capacity(num_proofs);
1334 for (i, secret) in secrets.iter().enumerate() {
1335 let wormhole_address = SubxtAccountId(secret.address);
1336 let transfer_call = RuntimeCall::Balances(BalancesCall::transfer_allow_death {
1337 dest: subxt::ext::subxt_core::utils::MultiAddress::Id(wormhole_address),
1338 value: partition_amounts[i],
1339 });
1340 calls.push(transfer_call);
1341 }
1342
1343 let batch_tx = quantus_node::api::tx().utility().batch(calls);
1344
1345 let quantum_keypair = QuantumKeyPair {
1346 public_key: wallet.keypair.public_key.clone(),
1347 private_key: wallet.keypair.private_key.clone(),
1348 };
1349
1350 log_print!(" Submitting batch of {} transfers...", num_proofs);
1351
1352 submit_transaction(
1353 quantus_client,
1354 &quantum_keypair,
1355 batch_tx,
1356 None,
1357 ExecutionMode { finalized: false, wait_for_transaction: true },
1358 )
1359 .await
1360 .map_err(|e| crate::error::QuantusError::Generic(format!("Batch transfer failed: {}", e)))?;
1361
1362 let client = quantus_client.client();
1364 let block = at_best_block(quantus_client)
1365 .await
1366 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?;
1367 let block_hash = block.hash();
1368
1369 let events_api =
1370 client.events().at(block_hash).await.map_err(|e| {
1371 crate::error::QuantusError::Generic(format!("Failed to get events: {}", e))
1372 })?;
1373
1374 let funding_account: SubxtAccountId = SubxtAccountId(wallet.keypair.to_account_id_32().into());
1376 let mut transfers = Vec::with_capacity(num_proofs);
1377
1378 for (i, secret) in secrets.iter().enumerate() {
1379 let event = events_api
1380 .find::<wormhole::events::NativeTransferred>()
1381 .find(|e| if let Ok(evt) = e { evt.to.0 == secret.address } else { false })
1382 .ok_or_else(|| {
1383 crate::error::QuantusError::Generic(format!(
1384 "No NativeTransferred event found for wormhole address {} (proof {})",
1385 hex::encode(secret.address),
1386 i + 1
1387 ))
1388 })?
1389 .map_err(|e| {
1390 crate::error::QuantusError::Generic(format!("Event decode error: {}", e))
1391 })?;
1392
1393 transfers.push(TransferInfo {
1394 block_hash,
1395 transfer_count: event.transfer_count,
1396 amount: partition_amounts[i],
1397 wormhole_address: SubxtAccountId(secret.address),
1398 funding_account: funding_account.clone(),
1399 });
1400 }
1401
1402 log_success!(
1403 " {} transfers submitted in a single batch (block {})",
1404 num_proofs,
1405 hex::encode(block_hash.0)
1406 );
1407
1408 Ok(transfers)
1409}
1410
1411async fn generate_round_proofs(
1413 quantus_client: &QuantusClient,
1414 secrets: &[WormholePair],
1415 transfers: &[TransferInfo],
1416 exit_accounts: &[SubxtAccountId],
1417 round_dir: &str,
1418 num_proofs: usize,
1419) -> crate::error::Result<Vec<String>> {
1420 use colored::Colorize;
1421
1422 log_print!("{}", "Step 2: Generating proofs...".bright_yellow());
1423
1424 let proof_block = at_best_block(quantus_client)
1426 .await
1427 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?;
1428 let proof_block_hash = proof_block.hash();
1429 log_print!(" Using block {} for all proofs", hex::encode(proof_block_hash.0));
1430
1431 let input_amounts: Vec<u128> = transfers.iter().map(|t| t.amount).collect();
1433 let exit_account_bytes: Vec<[u8; 32]> = exit_accounts.iter().map(|a| a.0).collect();
1434
1435 let output_assignments =
1437 compute_random_output_assignments(&input_amounts, &exit_account_bytes, VOLUME_FEE_BPS);
1438
1439 log_print!(" Random output partition:");
1441 for (i, assignment) in output_assignments.iter().enumerate() {
1442 let amt1_planck = (assignment.output_amount_1 as u128) * SCALE_DOWN_FACTOR;
1443 if assignment.output_amount_2 > 0 {
1444 let amt2_planck = (assignment.output_amount_2 as u128) * SCALE_DOWN_FACTOR;
1445 log_print!(
1446 " Proof {}: {} ({}) -> 0x{}..., {} ({}) -> 0x{}...",
1447 i + 1,
1448 assignment.output_amount_1,
1449 format_balance(amt1_planck),
1450 hex::encode(&assignment.exit_account_1[..4]),
1451 assignment.output_amount_2,
1452 format_balance(amt2_planck),
1453 hex::encode(&assignment.exit_account_2[..4])
1454 );
1455 } else {
1456 log_print!(
1457 " Proof {}: {} ({}) -> 0x{}...",
1458 i + 1,
1459 assignment.output_amount_1,
1460 format_balance(amt1_planck),
1461 hex::encode(&assignment.exit_account_1[..4])
1462 );
1463 }
1464 }
1465
1466 let pb = ProgressBar::new(num_proofs as u64);
1467 pb.set_style(
1468 ProgressStyle::default_bar()
1469 .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
1470 .unwrap()
1471 .progress_chars("#>-"),
1472 );
1473
1474 let proof_gen_start = std::time::Instant::now();
1475 let mut proof_files = Vec::new();
1476 for (i, (secret, transfer)) in secrets.iter().zip(transfers.iter()).enumerate() {
1477 pb.set_message(format!("Proof {}/{}", i + 1, num_proofs));
1478
1479 let proof_file = format!("{}/proof_{}.hex", round_dir, i + 1);
1480
1481 let funding_account_hex = format!("0x{}", hex::encode(transfer.funding_account.0));
1483
1484 let single_start = std::time::Instant::now();
1485
1486 generate_proof(
1488 &hex::encode(secret.secret),
1489 transfer.amount, &output_assignments[i],
1491 &format!("0x{}", hex::encode(proof_block_hash.0)),
1492 transfer.transfer_count,
1493 &funding_account_hex,
1494 &proof_file,
1495 quantus_client,
1496 )
1497 .await?;
1498
1499 let single_elapsed = single_start.elapsed();
1500 log_verbose!(" Proof {} generated in {:.2}s", i + 1, single_elapsed.as_secs_f64());
1501
1502 proof_files.push(proof_file);
1503 pb.inc(1);
1504 }
1505 pb.finish_with_message("Proofs generated");
1506 let proof_gen_elapsed = proof_gen_start.elapsed();
1507 log_print!(
1508 " Proof generation: {:.2}s ({} proofs, {:.2}s avg)",
1509 proof_gen_elapsed.as_secs_f64(),
1510 num_proofs,
1511 proof_gen_elapsed.as_secs_f64() / num_proofs as f64,
1512 );
1513
1514 Ok(proof_files)
1515}
1516
1517fn derive_round_secrets(
1519 mnemonic: &str,
1520 round: usize,
1521 num_proofs: usize,
1522) -> crate::error::Result<Vec<WormholePair>> {
1523 let pb = ProgressBar::new(num_proofs as u64);
1524 pb.set_style(
1525 ProgressStyle::default_bar()
1526 .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
1527 .unwrap()
1528 .progress_chars("#>-"),
1529 );
1530 pb.set_message("Deriving secrets...");
1531
1532 let mut secrets = Vec::new();
1533 for i in 1..=num_proofs {
1534 let secret = derive_wormhole_secret(mnemonic, round, i)?;
1535 secrets.push(secret);
1536 pb.inc(1);
1537 }
1538 pb.finish_with_message("Secrets derived");
1539
1540 Ok(secrets)
1541}
1542
1543fn verify_final_balance(
1545 initial_balance: u128,
1546 final_balance: u128,
1547 total_sent: u128,
1548 rounds: usize,
1549 num_proofs: usize,
1550) {
1551 use colored::Colorize;
1552
1553 log_print!("{}", "Balance Verification:".bright_cyan());
1554
1555 let total_received = calculate_round_amount(total_sent, rounds);
1557
1558 let expected_change = total_received as i128 - total_sent as i128;
1560 let actual_change = final_balance as i128 - initial_balance as i128;
1561
1562 log_print!(" Initial balance: {} ({})", initial_balance, format_balance(initial_balance));
1563 log_print!(" Final balance: {} ({})", final_balance, format_balance(final_balance));
1564 log_print!("");
1565 log_print!(" Total sent (round 1): {} ({})", total_sent, format_balance(total_sent));
1566 log_print!(
1567 " Total received (round {}): {} ({})",
1568 rounds,
1569 total_received,
1570 format_balance(total_received)
1571 );
1572 log_print!("");
1573
1574 let expected_change_str = if expected_change >= 0 {
1576 format!("+{}", expected_change)
1577 } else {
1578 format!("{}", expected_change)
1579 };
1580 let actual_change_str = if actual_change >= 0 {
1581 format!("+{}", actual_change)
1582 } else {
1583 format!("{}", actual_change)
1584 };
1585
1586 log_print!(" Expected change: {} planck", expected_change_str);
1587 log_print!(" Actual change: {} planck", actual_change_str);
1588 log_print!("");
1589
1590 let tolerance = (total_sent / 100).max(1_000_000_000_000); let diff = (actual_change - expected_change).unsigned_abs();
1594 if diff <= tolerance {
1595 log_success!(
1596 " {} Balance verification PASSED (within tolerance of {} planck)",
1597 "✓".bright_green(),
1598 tolerance
1599 );
1600 } else {
1601 log_print!(
1602 " {} Balance verification: difference of {} planck (tolerance: {} planck)",
1603 "!".bright_yellow(),
1604 diff,
1605 tolerance
1606 );
1607 log_print!(
1608 " Note: Transaction fees for {} initial transfers may account for the difference",
1609 num_proofs
1610 );
1611 }
1612 log_print!("");
1613}
1614
1615#[allow(clippy::too_many_arguments)]
1617async fn run_multiround(
1618 num_proofs: usize,
1619 rounds: usize,
1620 amount: u128,
1621 wallet_name: String,
1622 password: Option<String>,
1623 password_file: Option<String>,
1624 keep_files: bool,
1625 output_dir: String,
1626 dry_run: bool,
1627 node_url: &str,
1628) -> crate::error::Result<()> {
1629 use colored::Colorize;
1630
1631 log_print!("");
1632 log_print!("==================================================");
1633 log_print!(" Wormhole Multi-Round Flow Test");
1634 log_print!("==================================================");
1635 log_print!("");
1636
1637 let bins_dir = Path::new("generated-bins");
1639 let agg_config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
1640 crate::error::QuantusError::Generic(format!("Failed to load aggregation config: {}", e))
1641 })?;
1642
1643 validate_multiround_params(num_proofs, rounds, agg_config.num_leaf_proofs)?;
1645
1646 let wallet = load_multiround_wallet(&wallet_name, password, password_file)?;
1648
1649 let config =
1651 MultiroundConfig { num_proofs, rounds, amount, output_dir: output_dir.clone(), keep_files };
1652
1653 print_multiround_config(&config, &wallet, agg_config.num_leaf_proofs);
1655 log_print!(" Dry run: {}", dry_run);
1656 log_print!("");
1657
1658 std::fs::create_dir_all(&output_dir).map_err(|e| {
1660 crate::error::QuantusError::Generic(format!("Failed to create output directory: {}", e))
1661 })?;
1662
1663 if dry_run {
1664 return run_multiround_dry_run(
1665 &wallet.mnemonic,
1666 num_proofs,
1667 rounds,
1668 amount,
1669 &wallet.wallet_address,
1670 );
1671 }
1672
1673 let quantus_client = QuantusClient::new(node_url).await.map_err(|e| {
1675 crate::error::QuantusError::Generic(format!("Failed to connect to node: {}", e))
1676 })?;
1677 let client = quantus_client.client();
1678
1679 let minting_account = get_minting_account(client).await?;
1681 log_verbose!("Minting account: {:?}", minting_account);
1682
1683 let initial_balance = get_balance(&quantus_client, &wallet.wallet_address).await?;
1685 log_print!("{}", "Initial Balance:".bright_cyan());
1686 log_print!(" Wallet balance: {} ({})", initial_balance, format_balance(initial_balance));
1687 log_print!("");
1688
1689 let mut current_transfers: Vec<TransferInfo> = Vec::new();
1691
1692 for round in 1..=rounds {
1693 let is_final = round == rounds;
1694
1695 log_print!("");
1696 log_print!("--------------------------------------------------");
1697 log_print!(
1698 " {} Round {} of {} {}",
1699 ">>>".bright_blue(),
1700 round,
1701 rounds,
1702 "<<<".bright_blue()
1703 );
1704 log_print!("--------------------------------------------------");
1705 log_print!("");
1706
1707 let round_dir = format!("{}/round{}", output_dir, round);
1709 std::fs::create_dir_all(&round_dir).map_err(|e| {
1710 crate::error::QuantusError::Generic(format!("Failed to create round directory: {}", e))
1711 })?;
1712
1713 let secrets = derive_round_secrets(&wallet.mnemonic, round, num_proofs)?;
1715
1716 let exit_accounts: Vec<SubxtAccountId> = if is_final {
1718 log_print!("Final round - all proofs exit to wallet: {}", wallet.wallet_address);
1719 vec![wallet.wallet_account_id.clone(); num_proofs]
1720 } else {
1721 log_print!(
1722 "Intermediate round - proofs exit to round {} wormhole addresses",
1723 round + 1
1724 );
1725 let mut addrs = Vec::new();
1726 for i in 1..=num_proofs {
1727 let next_secret = derive_wormhole_secret(&wallet.mnemonic, round + 1, i)?;
1728 addrs.push(SubxtAccountId(next_secret.address));
1729 }
1730 addrs
1731 };
1732
1733 if round == 1 {
1736 current_transfers =
1737 execute_initial_transfers(&quantus_client, &wallet, &secrets, amount, num_proofs)
1738 .await?;
1739
1740 let balance_after_funding =
1742 get_balance(&quantus_client, &wallet.wallet_address).await?;
1743 let funding_deducted = initial_balance.saturating_sub(balance_after_funding);
1744 log_print!(
1745 " Balance after funding: {} ({}) [deducted: {} planck]",
1746 balance_after_funding,
1747 format_balance(balance_after_funding),
1748 funding_deducted
1749 );
1750 } else {
1751 log_print!("{}", "Step 1: Using transfer info from previous round...".bright_yellow());
1752 log_print!(" Found {} transfer(s) from previous round", current_transfers.len());
1753 }
1754
1755 let proof_files = generate_round_proofs(
1757 &quantus_client,
1758 &secrets,
1759 ¤t_transfers,
1760 &exit_accounts,
1761 &round_dir,
1762 num_proofs,
1763 )
1764 .await?;
1765
1766 log_print!("{}", "Step 3: Aggregating proofs...".bright_yellow());
1768
1769 let aggregated_file = format!("{}/aggregated.hex", round_dir);
1770 aggregate_proofs(proof_files, aggregated_file.clone()).await?;
1771
1772 log_print!(" Aggregated proof saved to {}", aggregated_file);
1773
1774 log_print!("{}", "Step 4: Submitting aggregated proof on-chain...".bright_yellow());
1776
1777 let (verification_block, extrinsic_hash, transfer_events) =
1778 verify_aggregated_and_get_events(&aggregated_file, &quantus_client).await?;
1779
1780 log_print!(
1781 " {} Proof verified in block {} (extrinsic: 0x{})",
1782 "✓".bright_green(),
1783 hex::encode(verification_block.0),
1784 hex::encode(extrinsic_hash.0)
1785 );
1786
1787 if !is_final {
1789 log_print!("{}", "Step 5: Capturing transfer info for next round...".bright_yellow());
1790
1791 let next_round_addresses: Vec<SubxtAccountId> = (1..=num_proofs)
1793 .map(|i| {
1794 let next_secret =
1795 derive_wormhole_secret(&wallet.mnemonic, round + 1, i).unwrap();
1796 SubxtAccountId(next_secret.address)
1797 })
1798 .collect();
1799
1800 current_transfers =
1801 parse_transfer_events(&transfer_events, &next_round_addresses, verification_block)?;
1802
1803 log_print!(
1804 " Captured {} transfer(s) for round {}",
1805 current_transfers.len(),
1806 round + 1
1807 );
1808 }
1809
1810 let balance_after_round = get_balance(&quantus_client, &wallet.wallet_address).await?;
1812 let change_from_initial = balance_after_round as i128 - initial_balance as i128;
1813 let change_str = if change_from_initial >= 0 {
1814 format!("+{}", change_from_initial)
1815 } else {
1816 format!("{}", change_from_initial)
1817 };
1818 log_print!("");
1819 log_print!(
1820 " Balance after round {}: {} ({}) [change: {} planck]",
1821 round,
1822 balance_after_round,
1823 format_balance(balance_after_round),
1824 change_str
1825 );
1826
1827 log_print!("");
1828 log_print!(" {} Round {} complete!", "✓".bright_green(), round);
1829 }
1830
1831 log_print!("");
1832 log_print!("==================================================");
1833 log_success!(" All {} rounds completed successfully!", rounds);
1834 log_print!("==================================================");
1835 log_print!("");
1836
1837 let final_balance = get_balance(&quantus_client, &wallet.wallet_address).await?;
1839 verify_final_balance(initial_balance, final_balance, amount, rounds, num_proofs);
1840
1841 if keep_files {
1842 log_print!("Proof files preserved in: {}", output_dir);
1843 } else {
1844 log_print!("Cleaning up proof files...");
1845 std::fs::remove_dir_all(&output_dir).ok();
1846 }
1847
1848 Ok(())
1849}
1850
1851async fn generate_proof(
1856 secret_hex: &str,
1857 funding_amount: u128,
1858 output_assignment: &ProofOutputAssignment,
1859 block_hash_str: &str,
1860 transfer_count: u64,
1861 funding_account_str: &str,
1862 output_file: &str,
1863 quantus_client: &QuantusClient,
1864) -> crate::error::Result<()> {
1865 let secret = parse_secret_hex(secret_hex).map_err(crate::error::QuantusError::Generic)?;
1867 let funding_account_bytes =
1868 parse_exit_account(funding_account_str).map_err(crate::error::QuantusError::Generic)?;
1869
1870 let block_hash_bytes: [u8; 32] = hex::decode(block_hash_str.trim_start_matches("0x"))
1871 .map_err(|e| crate::error::QuantusError::Generic(format!("Invalid block hash: {}", e)))?
1872 .try_into()
1873 .map_err(|_| {
1874 crate::error::QuantusError::Generic("Block hash must be 32 bytes".to_string())
1875 })?;
1876
1877 let wormhole_address = wormhole_lib::compute_wormhole_address(&secret)
1879 .map_err(|e| crate::error::QuantusError::Generic(e.message))?;
1880
1881 let storage_key = wormhole_lib::compute_storage_key(&wormhole_address, transfer_count);
1883
1884 let block_hash = subxt::utils::H256::from(block_hash_bytes);
1886 let client = quantus_client.client();
1887
1888 let blocks =
1890 client.blocks().at(block_hash).await.map_err(|e| {
1891 crate::error::QuantusError::Generic(format!("Failed to get block: {}", e))
1892 })?;
1893
1894 let storage_api = client.storage().at(block_hash);
1896 let val = storage_api
1897 .fetch_raw(storage_key.clone())
1898 .await
1899 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1900 if val.is_none() {
1901 return Err(crate::error::QuantusError::Generic(
1902 "Storage key not found - transfer may not exist in this block".to_string(),
1903 ));
1904 }
1905
1906 let proof_params = rpc_params![vec![to_hex(&storage_key)], block_hash];
1908 let read_proof: ReadProof<sp_core::H256> = quantus_client
1909 .rpc_client()
1910 .request("state_getReadProof", proof_params)
1911 .await
1912 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
1913
1914 let header = blocks.header();
1916 let parent_hash: [u8; 32] = header.parent_hash.0;
1917 let state_root: [u8; 32] = header.state_root.0;
1918 let extrinsics_root: [u8; 32] = header.extrinsics_root.0;
1919 let digest = header.digest.encode();
1920 let block_number = header.number;
1921
1922 let proof_nodes: Vec<Vec<u8>> = read_proof.proof.iter().map(|p| p.0.clone()).collect();
1924
1925 let input = wormhole_lib::ProofGenerationInput {
1927 secret,
1928 transfer_count,
1929 funding_account: funding_account_bytes,
1930 wormhole_address,
1931 funding_amount,
1932 block_hash: block_hash_bytes,
1933 block_number,
1934 parent_hash,
1935 state_root,
1936 extrinsics_root,
1937 digest,
1938 proof_nodes,
1939 exit_account_1: output_assignment.exit_account_1,
1940 exit_account_2: output_assignment.exit_account_2,
1941 output_amount_1: output_assignment.output_amount_1,
1942 output_amount_2: output_assignment.output_amount_2,
1943 volume_fee_bps: VOLUME_FEE_BPS,
1944 asset_id: NATIVE_ASSET_ID,
1945 };
1946
1947 let bins_dir = Path::new("generated-bins");
1949 let result = wormhole_lib::generate_proof(
1950 &input,
1951 &bins_dir.join("prover.bin"),
1952 &bins_dir.join("common.bin"),
1953 )
1954 .map_err(|e| crate::error::QuantusError::Generic(e.message))?;
1955
1956 let proof_hex = hex::encode(result.proof_bytes);
1958 std::fs::write(output_file, proof_hex).map_err(|e| {
1959 crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e))
1960 })?;
1961
1962 Ok(())
1963}
1964
1965async fn verify_aggregated_and_get_events(
1967 proof_file: &str,
1968 quantus_client: &QuantusClient,
1969) -> crate::error::Result<(
1970 subxt::utils::H256,
1971 subxt::utils::H256,
1972 Vec<wormhole::events::NativeTransferred>,
1973)> {
1974 use qp_wormhole_verifier::WormholeVerifier;
1975
1976 let proof_bytes = read_hex_proof_file_to_bytes(proof_file)?;
1977
1978 log_verbose!("Verifying aggregated proof locally before on-chain submission...");
1980 let bins_dir = Path::new("generated-bins");
1981 let verifier = WormholeVerifier::new_from_files(
1982 &bins_dir.join("aggregated_verifier.bin"),
1983 &bins_dir.join("aggregated_common.bin"),
1984 )
1985 .map_err(|e| {
1986 crate::error::QuantusError::Generic(format!("Failed to load aggregated verifier: {}", e))
1987 })?;
1988
1989 let proof = qp_wormhole_verifier::ProofWithPublicInputs::<
1990 qp_wormhole_verifier::F,
1991 qp_wormhole_verifier::C,
1992 { qp_wormhole_verifier::D },
1993 >::from_bytes(proof_bytes.clone(), &verifier.circuit_data.common)
1994 .map_err(|e| {
1995 crate::error::QuantusError::Generic(format!(
1996 "Failed to deserialize aggregated proof: {}",
1997 e
1998 ))
1999 })?;
2000
2001 verifier.verify(proof).map_err(|e| {
2002 crate::error::QuantusError::Generic(format!(
2003 "Local aggregated proof verification failed: {}",
2004 e
2005 ))
2006 })?;
2007 log_verbose!("Local verification passed!");
2008
2009 let (included_at, block_hash, tx_hash) =
2011 submit_unsigned_verify_aggregated_proof(quantus_client, proof_bytes).await?;
2012
2013 log_verbose!(
2014 "Submitted tx included in {}: block={:?}, tx={:?}",
2015 included_at.label(),
2016 block_hash,
2017 tx_hash
2018 );
2019
2020 let (found_proof_verified, transfer_events) =
2022 collect_wormhole_events_for_extrinsic(quantus_client, block_hash, tx_hash).await?;
2023
2024 if !found_proof_verified {
2025 return Err(crate::error::QuantusError::Generic(
2026 "Proof verification failed - no ProofVerified event".to_string(),
2027 ));
2028 }
2029
2030 log_print!(" Tokens minted (from NativeTransferred events):");
2032 for (idx, transfer) in transfer_events.iter().enumerate() {
2033 let to_hex = hex::encode(transfer.to.0);
2034 log_print!(
2035 " [{}] {} -> {} planck ({})",
2036 idx,
2037 to_hex,
2038 transfer.amount,
2039 format_balance(transfer.amount)
2040 );
2041 }
2042
2043 Ok((block_hash, tx_hash, transfer_events))
2044}
2045
2046fn run_multiround_dry_run(
2048 mnemonic: &str,
2049 num_proofs: usize,
2050 rounds: usize,
2051 amount: u128,
2052 wallet_address: &str,
2053) -> crate::error::Result<()> {
2054 use colored::Colorize;
2055
2056 log_print!("");
2057 log_print!("{}", "=== DRY RUN MODE ===".bright_yellow());
2058 log_print!("No transactions will be executed.");
2059 log_print!("");
2060
2061 for round in 1..=rounds {
2062 let is_final = round == rounds;
2063 let round_amount = calculate_round_amount(amount, round);
2064
2065 log_print!("");
2066 log_print!("{}", format!("Round {}", round).bright_cyan());
2067 log_print!(" Total amount: {} ({})", round_amount, format_balance(round_amount));
2068
2069 if round == 1 {
2071 let partition = random_partition(amount, num_proofs, 3 * SCALE_DOWN_FACTOR);
2072 log_print!(" Sample random partition (actual partition will differ):");
2073 for (i, &amt) in partition.iter().enumerate() {
2074 log_print!(" Proof {}: {} ({})", i + 1, amt, format_balance(amt));
2075 }
2076 }
2077 log_print!("");
2078
2079 log_print!(" Wormhole addresses (to be funded):");
2080 for i in 1..=num_proofs {
2081 let secret = derive_wormhole_secret(mnemonic, round, i)?;
2082 let address = sp_core::crypto::AccountId32::new(secret.address)
2083 .to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
2084 log_print!(" [{}] {}", i, address);
2085 log_verbose!(" secret: 0x{}", hex::encode(secret.secret));
2086 }
2087
2088 log_print!("");
2089 log_print!(" Exit accounts:");
2090 if is_final {
2091 log_print!(" All proofs exit to wallet: {}", wallet_address);
2092 } else {
2093 for i in 1..=num_proofs {
2094 let next_secret = derive_wormhole_secret(mnemonic, round + 1, i)?;
2095 let address = sp_core::crypto::AccountId32::new(next_secret.address)
2096 .to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
2097 log_print!(" [{}] {} (round {} wormhole)", i, address, round + 1);
2098 }
2099 }
2100 }
2101
2102 log_print!("");
2103 log_print!("{}", "=== END DRY RUN ===".bright_yellow());
2104 log_print!("");
2105
2106 Ok(())
2107}
2108
2109async fn parse_proof_file(
2111 proof_file: String,
2112 aggregated: bool,
2113 verify: bool,
2114) -> crate::error::Result<()> {
2115 use qp_wormhole_verifier::WormholeVerifier;
2116
2117 log_print!("Parsing proof file: {}", proof_file);
2118
2119 let proof_bytes = read_proof_file(&proof_file)
2121 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to read proof: {}", e)))?;
2122
2123 log_print!("Proof size: {} bytes", proof_bytes.len());
2124
2125 let bins_dir = Path::new("generated-bins");
2126
2127 if aggregated {
2128 let verifier = WormholeVerifier::new_from_files(
2130 &bins_dir.join("aggregated_verifier.bin"),
2131 &bins_dir.join("aggregated_common.bin"),
2132 )
2133 .map_err(|e| {
2134 crate::error::QuantusError::Generic(format!("Failed to load verifier: {}", e))
2135 })?;
2136
2137 let proof = qp_wormhole_verifier::ProofWithPublicInputs::<
2139 qp_wormhole_verifier::F,
2140 qp_wormhole_verifier::C,
2141 { qp_wormhole_verifier::D },
2142 >::from_bytes(proof_bytes.clone(), &verifier.circuit_data.common)
2143 .map_err(|e| {
2144 crate::error::QuantusError::Generic(format!(
2145 "Failed to deserialize aggregated proof: {:?}",
2146 e
2147 ))
2148 })?;
2149
2150 log_print!("\nPublic inputs count: {}", proof.public_inputs.len());
2151 log_verbose!("\nPublic inputs count: {}", proof.public_inputs.len());
2152
2153 match qp_wormhole_verifier::parse_aggregated_public_inputs(&proof) {
2155 Ok(agg_inputs) => {
2156 log_print!("\n=== Parsed Aggregated Public Inputs ===");
2157 log_print!("Asset ID: {}", agg_inputs.asset_id);
2158 log_print!("Volume Fee BPS: {}", agg_inputs.volume_fee_bps);
2159 log_print!(
2160 "Block Hash: 0x{}",
2161 hex::encode(agg_inputs.block_data.block_hash.as_ref())
2162 );
2163 log_print!("Block Number: {}", agg_inputs.block_data.block_number);
2164 log_print!("\nAccount Data ({} accounts):", agg_inputs.account_data.len());
2165 for (i, acct) in agg_inputs.account_data.iter().enumerate() {
2166 log_print!(
2167 " [{}] amount={}, exit=0x{}",
2168 i,
2169 acct.summed_output_amount,
2170 hex::encode(acct.exit_account.as_ref())
2171 );
2172 }
2173 log_print!("\nNullifiers ({} nullifiers):", agg_inputs.nullifiers.len());
2174 for (i, nullifier) in agg_inputs.nullifiers.iter().enumerate() {
2175 log_print!(" [{}] 0x{}", i, hex::encode(nullifier.as_ref()));
2176 }
2177 },
2178 Err(e) => {
2179 log_print!("Failed to parse as aggregated inputs: {}", e);
2180 },
2181 }
2182
2183 if verify {
2185 log_print!("\n=== Verifying Proof ===");
2186 match verifier.verify(proof) {
2187 Ok(()) => {
2188 log_success!("Proof verification PASSED");
2189 },
2190 Err(e) => {
2191 log_error!("Proof verification FAILED: {}", e);
2192 return Err(crate::error::QuantusError::Generic(format!(
2193 "Proof verification failed: {}",
2194 e
2195 )));
2196 },
2197 }
2198 }
2199 } else {
2200 let verifier = WormholeVerifier::new_from_files(
2202 &bins_dir.join("verifier.bin"),
2203 &bins_dir.join("common.bin"),
2204 )
2205 .map_err(|e| {
2206 crate::error::QuantusError::Generic(format!("Failed to load verifier: {}", e))
2207 })?;
2208
2209 let proof = qp_wormhole_verifier::ProofWithPublicInputs::<
2211 qp_wormhole_verifier::F,
2212 qp_wormhole_verifier::C,
2213 { qp_wormhole_verifier::D },
2214 >::from_bytes(proof_bytes, &verifier.circuit_data.common)
2215 .map_err(|e| {
2216 crate::error::QuantusError::Generic(format!("Failed to deserialize proof: {:?}", e))
2217 })?;
2218
2219 log_print!("\nPublic inputs count: {}", proof.public_inputs.len());
2220
2221 let pi = qp_wormhole_verifier::parse_public_inputs(&proof).map_err(|e| {
2222 crate::error::QuantusError::Generic(format!("Failed to parse public inputs: {}", e))
2223 })?;
2224
2225 log_print!("\n=== Parsed Leaf Public Inputs ===");
2226 log_print!("Asset ID: {}", pi.asset_id);
2227 log_print!("Output Amount 1: {}", pi.output_amount_1);
2228 log_print!("Output Amount 2: {}", pi.output_amount_2);
2229 log_print!("Volume Fee BPS: {}", pi.volume_fee_bps);
2230 log_print!("Nullifier: 0x{}", hex::encode(pi.nullifier.as_ref()));
2231 log_print!("Exit Account 1: 0x{}", hex::encode(pi.exit_account_1.as_ref()));
2232 log_print!("Exit Account 2: 0x{}", hex::encode(pi.exit_account_2.as_ref()));
2233 log_print!("Block Hash: 0x{}", hex::encode(pi.block_hash.as_ref()));
2234 log_print!("Block Number: {}", pi.block_number);
2235
2236 if verify {
2238 log_print!("\n=== Verifying Proof ===");
2239 match verifier.verify(proof) {
2240 Ok(()) => {
2241 log_success!("Proof verification PASSED");
2242 },
2243 Err(e) => {
2244 log_error!("Proof verification FAILED: {}", e);
2245 return Err(crate::error::QuantusError::Generic(format!(
2246 "Proof verification failed: {}",
2247 e
2248 )));
2249 },
2250 }
2251 }
2252 }
2253
2254 Ok(())
2255}
2256
2257#[derive(Debug, Clone)]
2259struct DissolveOutput {
2260 secret: [u8; 32],
2262 amount: u128,
2264 transfer_count: u64,
2266 funding_account: SubxtAccountId,
2268 proof_block_hash: subxt::utils::H256,
2270}
2271
2272#[allow(clippy::too_many_arguments)]
2288async fn run_dissolve(
2289 amount: u128,
2290 target_size: u128,
2291 wallet_name: String,
2292 password: Option<String>,
2293 password_file: Option<String>,
2294 keep_files: bool,
2295 output_dir: String,
2296 node_url: &str,
2297) -> crate::error::Result<()> {
2298 use colored::Colorize;
2299
2300 log_print!("");
2301 log_print!("==================================================");
2302 log_print!(" Wormhole Dissolve");
2303 log_print!("==================================================");
2304 log_print!("");
2305
2306 let mut num_outputs = 1u128;
2308 let mut layers = 0usize;
2309 while amount / num_outputs > target_size {
2310 num_outputs *= 2;
2311 layers += 1;
2312 }
2313 let final_output_count = num_outputs as usize;
2314
2315 log_print!(" Amount: {} ({})", amount, format_balance(amount));
2316 log_print!(" Target size: {} ({})", target_size, format_balance(target_size));
2317 log_print!(" Layers: {}", layers);
2318 log_print!(" Final outputs: {}", final_output_count);
2319 log_print!(
2320 " Approximate output size: {} ({})",
2321 amount / num_outputs,
2322 format_balance(amount / num_outputs)
2323 );
2324 log_print!("");
2325
2326 let wallet = load_multiround_wallet(&wallet_name, password, password_file)?;
2328 let funding_account = wallet.wallet_account_id.clone();
2329
2330 let quantus_client = QuantusClient::new(node_url)
2332 .await
2333 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to connect: {}", e)))?;
2334
2335 std::fs::create_dir_all(&output_dir).map_err(|e| {
2337 crate::error::QuantusError::Generic(format!("Failed to create output directory: {}", e))
2338 })?;
2339
2340 let bins_dir = std::path::Path::new("generated-bins");
2342 let agg_config = CircuitBinsConfig::load(bins_dir).map_err(|e| {
2343 crate::error::QuantusError::Generic(format!(
2344 "Failed to load aggregation circuit config: {}",
2345 e
2346 ))
2347 })?;
2348
2349 log_print!("{}", "Layer 0: Initial funding".bright_yellow());
2351
2352 let initial_secret = derive_wormhole_secret(&wallet.mnemonic, 0, 1)?;
2353 let wormhole_address = SubxtAccountId(initial_secret.address);
2354
2355 let transfer_tx = quantus_node::api::tx().balances().transfer_allow_death(
2357 subxt::ext::subxt_core::utils::MultiAddress::Id(wormhole_address.clone()),
2358 amount,
2359 );
2360
2361 let quantum_keypair = QuantumKeyPair {
2362 public_key: wallet.keypair.public_key.clone(),
2363 private_key: wallet.keypair.private_key.clone(),
2364 };
2365
2366 submit_transaction(
2367 &quantus_client,
2368 &quantum_keypair,
2369 transfer_tx,
2370 None,
2371 ExecutionMode { finalized: false, wait_for_transaction: true },
2372 )
2373 .await
2374 .map_err(|e| crate::error::QuantusError::Generic(format!("Initial transfer failed: {}", e)))?;
2375
2376 let block = at_best_block(&quantus_client)
2378 .await
2379 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?;
2380 let block_hash = block.hash();
2381 let events_api =
2382 quantus_client.client().events().at(block_hash).await.map_err(|e| {
2383 crate::error::QuantusError::Generic(format!("Failed to get events: {}", e))
2384 })?;
2385 let event = events_api
2386 .find::<wormhole::events::NativeTransferred>()
2387 .find(|e| if let Ok(evt) = e { evt.to.0 == initial_secret.address } else { false })
2388 .ok_or_else(|| crate::error::QuantusError::Generic("No transfer event found".to_string()))?
2389 .map_err(|e| crate::error::QuantusError::Generic(format!("Event decode error: {}", e)))?;
2390
2391 let mut current_outputs = vec![DissolveOutput {
2392 secret: initial_secret.secret,
2393 amount,
2394 transfer_count: event.transfer_count,
2395 funding_account: funding_account.clone(),
2396 proof_block_hash: block_hash,
2397 }];
2398
2399 log_success!(" Funded 1 wormhole address with {}", format_balance(amount));
2400
2401 for layer in 1..=layers {
2403 let num_inputs = current_outputs.len();
2404 let layer_dir = format!("{}/layer{}", output_dir, layer);
2405 std::fs::create_dir_all(&layer_dir).map_err(|e| {
2406 crate::error::QuantusError::Generic(format!("Failed to create layer directory: {}", e))
2407 })?;
2408
2409 log_print!("");
2410 log_print!(
2411 "{} Layer {}/{}: {} inputs → {} outputs {}",
2412 ">>>".bright_blue(),
2413 layer,
2414 layers,
2415 num_inputs,
2416 num_inputs * 2,
2417 "<<<".bright_blue()
2418 );
2419
2420 let mut next_secrets: Vec<WormholePair> = Vec::new();
2422 for i in 0..(num_inputs * 2) {
2423 next_secrets.push(derive_wormhole_secret(&wallet.mnemonic, layer, i + 1)?);
2424 }
2425
2426 let batch_size = agg_config.num_leaf_proofs;
2428 let mut all_next_outputs: Vec<DissolveOutput> = Vec::new();
2429 let num_batches = num_inputs.div_ceil(batch_size);
2430
2431 for batch_idx in 0..num_batches {
2432 let batch_start = batch_idx * batch_size;
2433 let batch_end = (batch_start + batch_size).min(num_inputs);
2434 let batch_inputs = ¤t_outputs[batch_start..batch_end];
2435 let batch_num_proofs = batch_inputs.len();
2436
2437 log_print!(" Batch {}/{}: {} proofs", batch_idx + 1, num_batches, batch_num_proofs);
2438
2439 let mut proof_files = Vec::new();
2441 let proof_gen_start = std::time::Instant::now();
2442
2443 let batch_proof_block_hash = batch_inputs[0].proof_block_hash;
2447
2448 for (i, input) in batch_inputs.iter().enumerate() {
2449 let global_idx = batch_start + i;
2450 let exit_1_idx = global_idx * 2;
2451 let exit_2_idx = global_idx * 2 + 1;
2452
2453 let input_quantized = quantize_funding_amount(input.amount)
2454 .map_err(crate::error::QuantusError::Generic)?;
2455 let total_output = compute_output_amount(input_quantized, VOLUME_FEE_BPS);
2456 let output_1 = total_output / 2;
2457 let output_2 = total_output - output_1;
2458
2459 let assignment = ProofOutputAssignment {
2460 output_amount_1: output_1.max(1),
2461 exit_account_1: next_secrets[exit_1_idx].address,
2462 output_amount_2: output_2.max(1),
2463 exit_account_2: next_secrets[exit_2_idx].address,
2464 };
2465
2466 let proof_file = format!("{}/batch{}_proof{}.hex", layer_dir, batch_idx, i);
2467
2468 generate_proof(
2469 &hex::encode(input.secret),
2470 input.amount,
2471 &assignment,
2472 &format!("0x{}", hex::encode(batch_proof_block_hash.0)),
2473 input.transfer_count,
2474 &format!("0x{}", hex::encode(input.funding_account.0)),
2475 &proof_file,
2476 &quantus_client,
2477 )
2478 .await?;
2479
2480 proof_files.push(proof_file);
2481 }
2482
2483 let proof_gen_elapsed = proof_gen_start.elapsed();
2484 log_print!(
2485 " Proof generation: {:.2}s ({} proofs)",
2486 proof_gen_elapsed.as_secs_f64(),
2487 batch_num_proofs
2488 );
2489
2490 log_print!(" Aggregating...");
2492 let aggregated_file = format!("{}/batch{}_aggregated.hex", layer_dir, batch_idx);
2493 aggregate_proofs_to_file(&proof_files, &aggregated_file)?;
2494
2495 log_print!(" Verifying on-chain...");
2497 let (verification_block, _extrinsic_hash, transfer_events) =
2498 verify_aggregated_and_get_events(&aggregated_file, &quantus_client).await?;
2499
2500 log_success!(" Verified in block 0x{}", hex::encode(verification_block.0));
2501
2502 for (i, _input) in batch_inputs.iter().enumerate() {
2505 let global_idx = batch_start + i;
2506 let exit_1_idx = global_idx * 2;
2507 let exit_2_idx = global_idx * 2 + 1;
2508
2509 for (secret_idx, target_address) in [
2510 (exit_1_idx, &next_secrets[exit_1_idx]),
2511 (exit_2_idx, &next_secrets[exit_2_idx]),
2512 ] {
2513 let event = transfer_events
2514 .iter()
2515 .find(|e| e.to.0 == target_address.address)
2516 .ok_or_else(|| {
2517 crate::error::QuantusError::Generic(format!(
2518 "No transfer event for output {} at layer {}",
2519 secret_idx, layer
2520 ))
2521 })?;
2522
2523 all_next_outputs.push(DissolveOutput {
2524 secret: target_address.secret,
2525 amount: event.amount,
2526 transfer_count: event.transfer_count,
2527 funding_account: event.from.clone(),
2528 proof_block_hash: verification_block,
2529 });
2530 }
2531 }
2532 }
2533
2534 log_print!(" Layer {} complete: {} outputs", layer, all_next_outputs.len());
2535
2536 current_outputs = all_next_outputs;
2537 }
2538
2539 log_print!("");
2541 log_print!("==================================================");
2542 log_success!(" Dissolve complete!");
2543 log_print!("==================================================");
2544 log_print!("");
2545 log_print!(" Final outputs: {}", current_outputs.len());
2546 let min_output = current_outputs.iter().map(|o| o.amount).min().unwrap_or(0);
2547 let max_output = current_outputs.iter().map(|o| o.amount).max().unwrap_or(0);
2548 let total_output: u128 = current_outputs.iter().map(|o| o.amount).sum();
2549 log_print!(
2550 " Output range: {} - {} ({})",
2551 format_balance(min_output),
2552 format_balance(max_output),
2553 format_balance(total_output)
2554 );
2555
2556 if keep_files {
2557 log_print!(" Proof files preserved in: {}", output_dir);
2558 } else {
2559 log_print!(" Cleaning up proof files...");
2560 std::fs::remove_dir_all(&output_dir).ok();
2561 }
2562
2563 Ok(())
2564}
2565
2566fn aggregate_proofs_to_file(proof_files: &[String], output_file: &str) -> crate::error::Result<()> {
2568 use qp_wormhole_aggregator::aggregator::Layer0Aggregator;
2569
2570 let bins_dir = std::path::Path::new("generated-bins");
2571 let mut aggregator = Layer0Aggregator::new(bins_dir).map_err(|e| {
2572 crate::error::QuantusError::Generic(format!("Failed to create aggregator: {}", e))
2573 })?;
2574
2575 let common_data = aggregator.load_common_data(CircuitType::Leaf).map_err(|e| {
2576 crate::error::QuantusError::Generic(format!("Failed to load common data: {}", e))
2577 })?;
2578
2579 for proof_file in proof_files {
2580 let proof_bytes = read_hex_proof_file_to_bytes(proof_file)?;
2581 let proof = ProofWithPublicInputs::<F, C, D>::from_bytes(proof_bytes, &common_data)
2582 .map_err(|e| {
2583 crate::error::QuantusError::Generic(format!(
2584 "Failed to deserialize proof from {}: {:?}",
2585 proof_file, e
2586 ))
2587 })?;
2588 aggregator.push_proof(proof).map_err(|e| {
2589 crate::error::QuantusError::Generic(format!("Failed to push proof: {}", e))
2590 })?;
2591 }
2592
2593 let agg_start = std::time::Instant::now();
2594 let proof = aggregator
2595 .aggregate()
2596 .map_err(|e| crate::error::QuantusError::Generic(format!("Aggregation failed: {}", e)))?;
2597 let agg_elapsed = agg_start.elapsed();
2598 log_print!(" Aggregation: {:.2}s", agg_elapsed.as_secs_f64());
2599
2600 let proof_hex = hex::encode(proof.to_bytes());
2601 std::fs::write(output_file, &proof_hex).map_err(|e| {
2602 crate::error::QuantusError::Generic(format!("Failed to write proof: {}", e))
2603 })?;
2604
2605 Ok(())
2606}
2607
2608const GOLDILOCKS_ORDER: u64 = 0xFFFFFFFF00000001;
2610
2611type FuzzInputs = (u128, [u8; 32], [u8; 32], u64, [u8; 32]);
2613
2614struct FuzzCase {
2616 name: &'static str,
2617 description: &'static str,
2618 expect_pass: bool,
2621}
2622
2623struct SeededRng {
2625 state: u64,
2626}
2627
2628impl SeededRng {
2629 fn new(seed: u64) -> Self {
2630 Self { state: seed }
2631 }
2632
2633 fn next_u64(&mut self) -> u64 {
2634 self.state ^= self.state << 13;
2636 self.state ^= self.state >> 7;
2637 self.state ^= self.state << 17;
2638 self.state
2639 }
2640
2641 fn next_u128(&mut self) -> u128 {
2642 let high = self.next_u64() as u128;
2643 let low = self.next_u64() as u128;
2644 (high << 64) | low
2645 }
2646}
2647
2648fn add_to_address_chunk(addr: &mut [u8; 32], chunk_idx: usize, value: u64) {
2650 let start = chunk_idx * 8;
2651 let end = start + 8;
2652 let current = u64::from_le_bytes(addr[start..end].try_into().unwrap());
2653 let new_value = current.wrapping_add(value);
2654 addr[start..end].copy_from_slice(&new_value.to_le_bytes());
2655}
2656
2657#[allow(clippy::vec_init_then_push)]
2659fn generate_fuzz_cases(
2660 amount: u128,
2661 from: [u8; 32],
2662 to: [u8; 32],
2663 count: u64,
2664 secret: [u8; 32],
2665 rng: &mut SeededRng,
2666) -> Vec<(FuzzCase, FuzzInputs)> {
2667 let random_u64 = rng.next_u64();
2668 let random_u128 = rng.next_u128();
2669 let mut random_addr = [0u8; 32];
2670 for chunk in random_addr.chunks_mut(8) {
2671 chunk.copy_from_slice(&rng.next_u64().to_le_bytes());
2672 }
2673 let mut random_secret = [0u8; 32];
2674 for chunk in random_secret.chunks_mut(8) {
2675 chunk.copy_from_slice(&rng.next_u64().to_le_bytes());
2676 }
2677
2678 let mut cases = Vec::new();
2679
2680 let amount_quantized = amount / SCALE_DOWN_FACTOR;
2686 let amount_plus_one_quantized = (amount + 1) / SCALE_DOWN_FACTOR;
2687 let amount_minus_one_quantized = amount.saturating_sub(1) / SCALE_DOWN_FACTOR;
2688
2689 let plus_one_same_bucket = amount_quantized == amount_plus_one_quantized;
2691 cases.push((
2692 FuzzCase {
2693 name: "amount_plus_one_planck",
2694 description: "Amount + 1 planck",
2695 expect_pass: plus_one_same_bucket,
2696 },
2697 (amount + 1, from, to, count, secret),
2698 ));
2699
2700 let minus_one_same_bucket = amount_quantized == amount_minus_one_quantized;
2702 cases.push((
2703 FuzzCase {
2704 name: "amount_minus_one_planck",
2705 description: "Amount - 1 planck",
2706 expect_pass: minus_one_same_bucket,
2707 },
2708 (amount.saturating_sub(1), from, to, count, secret),
2709 ));
2710
2711 cases.push((
2713 FuzzCase {
2714 name: "amount_plus_one_quant_unit",
2715 description: "Amount + 1 quantized unit",
2716 expect_pass: false,
2717 },
2718 (amount + SCALE_DOWN_FACTOR, from, to, count, secret),
2719 ));
2720
2721 cases.push((
2723 FuzzCase {
2724 name: "amount_minus_one_quant_unit",
2725 description: "Amount - 1 quantized unit",
2726 expect_pass: false,
2727 },
2728 (amount.saturating_sub(SCALE_DOWN_FACTOR), from, to, count, secret),
2729 ));
2730
2731 cases.push((
2733 FuzzCase { name: "amount_doubled", description: "Amount * 2", expect_pass: false },
2734 (amount * 2, from, to, count, secret),
2735 ));
2736
2737 cases.push((
2739 FuzzCase { name: "amount_zero", description: "Amount = 0", expect_pass: false },
2740 (0, from, to, count, secret),
2741 ));
2742
2743 cases.push((
2745 FuzzCase {
2746 name: "amount_plus_random",
2747 description: "Amount + random u128",
2748 expect_pass: false,
2749 },
2750 (amount.wrapping_add(random_u128), from, to, count, secret),
2751 ));
2752
2753 cases.push((
2755 FuzzCase {
2756 name: "amount_plus_goldilocks",
2757 description: "Amount + Goldilocks field order",
2758 expect_pass: false,
2759 },
2760 (amount.wrapping_add(GOLDILOCKS_ORDER as u128), from, to, count, secret),
2761 ));
2762
2763 cases.push((
2769 FuzzCase { name: "count_plus_one", description: "Transfer count + 1", expect_pass: false },
2770 (amount, from, to, count.wrapping_add(1), secret),
2771 ));
2772
2773 cases.push((
2775 FuzzCase {
2776 name: "count_minus_one_wrap",
2777 description: "Transfer count - 1 (wrapping)",
2778 expect_pass: false,
2779 },
2780 (amount, from, to, count.wrapping_sub(1), secret),
2781 ));
2782
2783 cases.push((
2785 FuzzCase {
2786 name: "count_max",
2787 description: "Transfer count = u64::MAX",
2788 expect_pass: false,
2789 },
2790 (amount, from, to, u64::MAX, secret),
2791 ));
2792
2793 cases.push((
2795 FuzzCase {
2796 name: "count_plus_random",
2797 description: "Transfer count + random u64",
2798 expect_pass: false,
2799 },
2800 (amount, from, to, count.wrapping_add(random_u64), secret),
2801 ));
2802
2803 cases.push((
2805 FuzzCase {
2806 name: "count_plus_goldilocks",
2807 description: "Transfer count + Goldilocks field order",
2808 expect_pass: false,
2809 },
2810 (amount, from, to, count.wrapping_add(GOLDILOCKS_ORDER), secret),
2811 ));
2812
2813 let mut from_bit_flip = from;
2819 from_bit_flip[0] ^= 0x01;
2820 cases.push((
2821 FuzzCase {
2822 name: "from_single_bit_flip",
2823 description: "From address single bit flip",
2824 expect_pass: false,
2825 },
2826 (amount, from_bit_flip, to, count, secret),
2827 ));
2828
2829 cases.push((
2831 FuzzCase { name: "from_zeroed", description: "From address zeroed", expect_pass: false },
2832 (amount, [0u8; 32], to, count, secret),
2833 ));
2834
2835 let mut from_plus_one = from;
2837 add_to_address_chunk(&mut from_plus_one, 0, 1);
2838 cases.push((
2839 FuzzCase {
2840 name: "from_plus_one",
2841 description: "From address + 1 (first chunk)",
2842 expect_pass: false,
2843 },
2844 (amount, from_plus_one, to, count, secret),
2845 ));
2846
2847 cases.push((
2849 FuzzCase { name: "from_random", description: "From address random", expect_pass: false },
2850 (amount, random_addr, to, count, secret),
2851 ));
2852
2853 for chunk_idx in 0..4 {
2855 let mut from_goldilocks = from;
2856 add_to_address_chunk(&mut from_goldilocks, chunk_idx, GOLDILOCKS_ORDER);
2857 cases.push((
2858 FuzzCase {
2859 name: match chunk_idx {
2860 0 => "from_goldilocks_chunk0",
2861 1 => "from_goldilocks_chunk1",
2862 2 => "from_goldilocks_chunk2",
2863 _ => "from_goldilocks_chunk3",
2864 },
2865 description: match chunk_idx {
2866 0 => "From + Goldilocks order (chunk 0)",
2867 1 => "From + Goldilocks order (chunk 1)",
2868 2 => "From + Goldilocks order (chunk 2)",
2869 _ => "From + Goldilocks order (chunk 3)",
2870 },
2871 expect_pass: false,
2872 },
2873 (amount, from_goldilocks, to, count, secret),
2874 ));
2875 }
2876
2877 let mut exit_bit_flip = to;
2894 exit_bit_flip[0] ^= 0x01;
2895 cases.push((
2896 FuzzCase {
2897 name: "exit_single_bit_flip",
2898 description: "Exit account single bit flip (not validated)",
2899 expect_pass: true,
2900 },
2901 (amount, from, exit_bit_flip, count, secret),
2902 ));
2903
2904 cases.push((
2906 FuzzCase {
2907 name: "exit_zeroed",
2908 description: "Exit account zeroed (not validated)",
2909 expect_pass: true,
2910 },
2911 (amount, from, [0u8; 32], count, secret),
2912 ));
2913
2914 let mut exit_plus_one = to;
2916 add_to_address_chunk(&mut exit_plus_one, 0, 1);
2917 cases.push((
2918 FuzzCase {
2919 name: "exit_plus_one",
2920 description: "Exit account + 1 (not validated)",
2921 expect_pass: true,
2922 },
2923 (amount, from, exit_plus_one, count, secret),
2924 ));
2925
2926 for chunk_idx in 0..4 {
2928 let mut exit_goldilocks = to;
2929 add_to_address_chunk(&mut exit_goldilocks, chunk_idx, GOLDILOCKS_ORDER);
2930 cases.push((
2931 FuzzCase {
2932 name: match chunk_idx {
2933 0 => "exit_goldilocks_chunk0",
2934 1 => "exit_goldilocks_chunk1",
2935 2 => "exit_goldilocks_chunk2",
2936 _ => "exit_goldilocks_chunk3",
2937 },
2938 description: match chunk_idx {
2939 0 => "Exit + Goldilocks (chunk 0, not validated)",
2940 1 => "Exit + Goldilocks (chunk 1, not validated)",
2941 2 => "Exit + Goldilocks (chunk 2, not validated)",
2942 _ => "Exit + Goldilocks (chunk 3, not validated)",
2943 },
2944 expect_pass: true,
2945 },
2946 (amount, from, exit_goldilocks, count, secret),
2947 ));
2948 }
2949
2950 cases.push((
2956 FuzzCase {
2957 name: "swapped_from_to",
2958 description: "From and To addresses swapped",
2959 expect_pass: false,
2960 },
2961 (amount, to, from, count, secret),
2962 ));
2963
2964 let mut all_fuzzed_from = from;
2966 let mut all_fuzzed_to = to;
2967 all_fuzzed_from[31] ^= 0xFF;
2968 all_fuzzed_to[31] ^= 0xFF;
2969 cases.push((
2970 FuzzCase {
2971 name: "all_random_fuzzed",
2972 description: "All inputs fuzzed with random values",
2973 expect_pass: false,
2974 },
2975 (
2976 amount.wrapping_add(random_u128),
2977 all_fuzzed_from,
2978 all_fuzzed_to,
2979 count.wrapping_add(random_u64),
2980 secret,
2981 ),
2982 ));
2983
2984 let mut all_gold_from = from;
2986 let mut all_gold_to = to;
2987 add_to_address_chunk(&mut all_gold_from, 0, GOLDILOCKS_ORDER);
2988 add_to_address_chunk(&mut all_gold_to, 0, GOLDILOCKS_ORDER);
2989 cases.push((
2990 FuzzCase {
2991 name: "all_goldilocks_fuzzed",
2992 description: "All inputs + Goldilocks order",
2993 expect_pass: false,
2994 },
2995 (
2996 amount.wrapping_add(GOLDILOCKS_ORDER as u128),
2997 all_gold_from,
2998 all_gold_to,
2999 count.wrapping_add(GOLDILOCKS_ORDER),
3000 secret,
3001 ),
3002 ));
3003
3004 let mut secret_bit_flip = secret;
3013 secret_bit_flip[0] ^= 0x01;
3014 cases.push((
3015 FuzzCase {
3016 name: "secret_single_bit_flip",
3017 description: "Secret single bit flip (wrong unspendable_account)",
3018 expect_pass: false,
3019 },
3020 (amount, from, to, count, secret_bit_flip),
3021 ));
3022
3023 cases.push((
3025 FuzzCase { name: "secret_zeroed", description: "Secret zeroed", expect_pass: false },
3026 (amount, from, to, count, [0u8; 32]),
3027 ));
3028
3029 cases.push((
3031 FuzzCase { name: "secret_random", description: "Secret random value", expect_pass: false },
3032 (amount, from, to, count, random_secret),
3033 ));
3034
3035 let mut secret_plus_one = secret;
3037 secret_plus_one[0] = secret_plus_one[0].wrapping_add(1);
3038 cases.push((
3039 FuzzCase {
3040 name: "secret_plus_one",
3041 description: "Secret + 1 (first byte)",
3042 expect_pass: false,
3043 },
3044 (amount, from, to, count, secret_plus_one),
3045 ));
3046
3047 let mut secret_goldilocks = secret;
3049 add_to_address_chunk(&mut secret_goldilocks, 0, GOLDILOCKS_ORDER);
3050 cases.push((
3051 FuzzCase {
3052 name: "secret_plus_goldilocks",
3053 description: "Secret + Goldilocks field order (chunk 0)",
3054 expect_pass: false,
3055 },
3056 (amount, from, to, count, secret_goldilocks),
3057 ));
3058
3059 cases
3060}
3061
3062async fn run_fuzz_test(
3070 wallet_name: String,
3071 password: Option<String>,
3072 password_file: Option<String>,
3073 amount: u128,
3074 node_url: &str,
3075) -> crate::error::Result<()> {
3076 use colored::Colorize;
3077
3078 log_print!("");
3079 log_print!("==================================================");
3080 log_print!(" Wormhole Fuzz Test");
3081 log_print!("==================================================");
3082 log_print!("");
3083
3084 let seed = std::time::SystemTime::now()
3086 .duration_since(std::time::UNIX_EPOCH)
3087 .unwrap()
3088 .as_secs();
3089 log_print!(" Random seed: {}", seed);
3090
3091 let wallet = load_multiround_wallet(&wallet_name, password, password_file)?;
3093
3094 let quantus_client = QuantusClient::new(node_url)
3096 .await
3097 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to connect: {}", e)))?;
3098
3099 log_print!("{}", "Step 1: Creating test transfer...".bright_yellow());
3101
3102 let mut secret_bytes = [0u8; 32];
3103 rand::rng().fill_bytes(&mut secret_bytes);
3104 let secret: BytesDigest = secret_bytes.try_into().map_err(|e| {
3105 crate::error::QuantusError::Generic(format!("Failed to convert secret: {:?}", e))
3106 })?;
3107
3108 let unspendable_account =
3109 qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
3110 .account_id;
3111 let unspendable_account_bytes_digest =
3112 qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account);
3113 let unspendable_account_bytes: [u8; 32] = *unspendable_account_bytes_digest;
3114
3115 let wormhole_address = SubxtAccountId(unspendable_account_bytes);
3116
3117 let transfer_tx = quantus_node::api::tx().balances().transfer_allow_death(
3119 subxt::ext::subxt_core::utils::MultiAddress::Id(wormhole_address.clone()),
3120 amount,
3121 );
3122
3123 let quantum_keypair = QuantumKeyPair {
3124 public_key: wallet.keypair.public_key.clone(),
3125 private_key: wallet.keypair.private_key.clone(),
3126 };
3127
3128 submit_transaction(
3129 &quantus_client,
3130 &quantum_keypair,
3131 transfer_tx,
3132 None,
3133 ExecutionMode { finalized: false, wait_for_transaction: true },
3134 )
3135 .await
3136 .map_err(|e| crate::error::QuantusError::Generic(format!("Transfer failed: {}", e)))?;
3137
3138 let block = at_best_block(&quantus_client)
3140 .await
3141 .map_err(|e| crate::error::QuantusError::Generic(format!("Failed to get block: {}", e)))?;
3142 let block_hash = block.hash();
3143 let events_api =
3144 quantus_client.client().events().at(block_hash).await.map_err(|e| {
3145 crate::error::QuantusError::Generic(format!("Failed to get events: {}", e))
3146 })?;
3147
3148 let event = events_api
3149 .find::<wormhole::events::NativeTransferred>()
3150 .find(|e| if let Ok(evt) = e { evt.to.0 == unspendable_account_bytes } else { false })
3151 .ok_or_else(|| crate::error::QuantusError::Generic("No transfer event found".to_string()))?
3152 .map_err(|e| crate::error::QuantusError::Generic(format!("Event decode error: {}", e)))?;
3153
3154 let transfer_count = event.transfer_count;
3155 let funding_account: [u8; 32] = wallet.keypair.to_account_id_32().into();
3156
3157 log_success!(
3158 " Transfer complete: {} to wormhole, transfer_count={}",
3159 format_balance(amount),
3160 transfer_count
3161 );
3162 log_print!(" Block: 0x{}", hex::encode(block_hash.0));
3163
3164 log_print!("");
3166 log_print!("{}", "Step 2: Verifying correct inputs work...".bright_yellow());
3167
3168 log_print!("{}", "Step 2: Fetching block header data...".bright_yellow());
3170
3171 let client = quantus_client.client();
3172 let blocks =
3173 client.blocks().at(block_hash).await.map_err(|e| {
3174 crate::error::QuantusError::Generic(format!("Failed to get block: {}", e))
3175 })?;
3176 let header = blocks.header();
3177
3178 let state_root = BytesDigest::try_from(header.state_root.as_bytes())
3179 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
3180 let parent_hash = BytesDigest::try_from(header.parent_hash.as_bytes())
3181 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
3182 let extrinsics_root = BytesDigest::try_from(header.extrinsics_root.as_bytes())
3183 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
3184 let digest: [u8; 110] =
3185 header.digest.encode().try_into().map_err(|_| {
3186 crate::error::QuantusError::Generic("Failed to encode digest".to_string())
3187 })?;
3188 let block_number = header.number;
3189 let state_root_hex = hex::encode(header.state_root.0);
3190
3191 let pallet_hash = sp_core::twox_128(b"Wormhole");
3193 let storage_hash = sp_core::twox_128(b"TransferProof");
3194 let mut final_key = Vec::with_capacity(32 + 32);
3195 final_key.extend_from_slice(&pallet_hash);
3196 final_key.extend_from_slice(&storage_hash);
3197 let key_tuple: TransferProofKey = (AccountId32::new(unspendable_account_bytes), transfer_count);
3198 let encoded_key = key_tuple.encode();
3199 let key_hash = sp_core::blake2_256(&encoded_key);
3200 final_key.extend_from_slice(&key_hash);
3201
3202 let proof_params = rpc_params![vec![to_hex(&final_key)], block_hash];
3204 let read_proof: ReadProof<sp_core::H256> = quantus_client
3205 .rpc_client()
3206 .request("state_getReadProof", proof_params)
3207 .await
3208 .map_err(|e| crate::error::QuantusError::Generic(e.to_string()))?;
3209
3210 log_success!(" Block header and storage proof fetched");
3211
3212 let bins_dir = Path::new("generated-bins");
3214
3215 log_print!("");
3217 log_print!("{}", "Step 3: Verifying correct inputs generate valid proof...".bright_yellow());
3218
3219 let correct_result = try_generate_fuzz_proof(
3220 bins_dir,
3221 &read_proof,
3222 &state_root_hex,
3223 block_hash,
3224 secret,
3225 amount,
3226 funding_account,
3227 unspendable_account_bytes,
3228 transfer_count,
3229 state_root,
3230 parent_hash,
3231 extrinsics_root,
3232 digest,
3233 block_number,
3234 );
3235
3236 match correct_result {
3237 Ok(_) => log_success!(" Correct inputs: PASSED (ZK proof generated successfully)"),
3238 Err(e) => {
3239 return Err(crate::error::QuantusError::Generic(format!(
3240 "FATAL: Correct inputs failed proof generation: {}",
3241 e
3242 )));
3243 },
3244 }
3245
3246 log_print!("");
3248 log_print!("{}", "Step 4: Running fuzz cases (generating ZK proofs)...".bright_yellow());
3249 log_print!(
3250 " NOTE: Each case attempts actual ZK proof generation - this tests circuit constraints"
3251 );
3252 log_print!("");
3253
3254 let mut rng = SeededRng::new(seed);
3255 let secret_bytes: [u8; 32] = secret.as_ref().try_into().expect("secret is 32 bytes");
3256 let fuzz_cases = generate_fuzz_cases(
3257 amount,
3258 funding_account,
3259 unspendable_account_bytes,
3260 transfer_count,
3261 secret_bytes,
3262 &mut rng,
3263 );
3264
3265 log_print!(" Total fuzz cases: {}", fuzz_cases.len());
3266 log_print!("");
3267
3268 let mut passed = 0;
3269 let mut failed = 0;
3270
3271 for (case, (fuzzed_amount, fuzzed_from, fuzzed_to, fuzzed_count, fuzzed_secret)) in &fuzz_cases
3272 {
3273 let fuzzed_secret_digest: BytesDigest =
3275 (*fuzzed_secret).try_into().expect("fuzzed_secret is 32 bytes");
3276
3277 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3279 try_generate_fuzz_proof(
3280 bins_dir,
3281 &read_proof,
3282 &state_root_hex,
3283 block_hash,
3284 fuzzed_secret_digest,
3285 *fuzzed_amount,
3286 *fuzzed_from,
3287 *fuzzed_to,
3288 *fuzzed_count,
3289 state_root,
3290 parent_hash,
3291 extrinsics_root,
3292 digest,
3293 block_number,
3294 )
3295 }));
3296
3297 let succeeded = match &result {
3298 Ok(Ok(_)) => true,
3299 Ok(Err(_)) => false,
3300 Err(_) => false, };
3302
3303 if succeeded == case.expect_pass {
3304 log_print!(" {} {}: {}", "OK".bright_green(), case.name, case.description);
3306 passed += 1;
3307 } else {
3308 let problem = if case.expect_pass {
3310 match result {
3311 Ok(Err(e)) => format!("Expected to pass but failed: {}", e),
3312 Err(_) => "Expected to pass but panicked".to_string(),
3313 _ => "Unknown error".to_string(),
3314 }
3315 } else {
3316 "Expected circuit to reject but proof succeeded!".to_string()
3317 };
3318 log_print!(
3319 " {} {}: {} - {}",
3320 "FAIL".bright_red(),
3321 case.name,
3322 case.description,
3323 problem.red()
3324 );
3325 failed += 1;
3326 }
3327 }
3328
3329 log_print!("");
3331 log_print!("==================================================");
3332 log_print!(" Fuzz Test Results");
3333 log_print!("==================================================");
3334 log_print!("");
3335 log_print!(" Total cases: {}", fuzz_cases.len());
3336 log_print!(" Passed: {} (correct behavior)", format!("{}", passed).bright_green());
3337
3338 if failed > 0 {
3339 log_print!(" Failed: {} (incorrect behavior)", format!("{}", failed).bright_red());
3340 log_print!("");
3341 return Err(crate::error::QuantusError::Generic(format!(
3342 "Fuzz test failed: {} cases had incorrect behavior",
3343 failed
3344 )));
3345 }
3346
3347 log_print!("");
3348 log_success!("All fuzz cases behaved correctly!");
3349
3350 Ok(())
3351}
3352
3353#[allow(clippy::too_many_arguments)]
3358fn try_generate_fuzz_proof(
3359 bins_dir: &Path,
3360 read_proof: &ReadProof<sp_core::H256>,
3361 state_root_hex: &str,
3362 block_hash: subxt::utils::H256,
3363 secret: BytesDigest,
3364 fuzzed_amount: u128,
3365 fuzzed_from: [u8; 32],
3366 fuzzed_to: [u8; 32],
3367 fuzzed_count: u64,
3368 state_root: BytesDigest,
3369 parent_hash: BytesDigest,
3370 extrinsics_root: BytesDigest,
3371 digest: [u8; 110],
3372 block_number: u32,
3373) -> Result<(), String> {
3374 use subxt::ext::codec::Encode;
3375
3376 let from_account = AccountId32::new(fuzzed_from);
3378 let to_account = AccountId32::new(fuzzed_to);
3379 let transfer_data_tuple =
3380 (NATIVE_ASSET_ID, fuzzed_count, from_account.clone(), to_account.clone(), fuzzed_amount);
3381 let encoded_data = transfer_data_tuple.encode();
3382 let fuzzed_leaf_hash =
3383 qp_poseidon::PoseidonHasher::hash_storage::<TransferProofData>(&encoded_data);
3384
3385 let processed_storage_proof = prepare_proof_for_circuit(
3387 read_proof.proof.iter().map(|proof| proof.0.clone()).collect(),
3388 state_root_hex.to_string(),
3389 fuzzed_leaf_hash,
3390 )
3391 .map_err(|e| e.to_string())?;
3392
3393 let input_amount_quantized: u32 = quantize_funding_amount(fuzzed_amount)?;
3395
3396 let output_amount = compute_output_amount(input_amount_quantized, VOLUME_FEE_BPS);
3398
3399 let unspendable_account =
3401 qp_wormhole_circuit::unspendable_account::UnspendableAccount::from_secret(secret)
3402 .account_id;
3403 let unspendable_account_bytes_digest =
3404 qp_zk_circuits_common::utils::digest_to_bytes(unspendable_account);
3405
3406 let inputs = CircuitInputs {
3408 private: PrivateCircuitInputs {
3409 secret,
3410 transfer_count: fuzzed_count,
3411 funding_account: BytesDigest::try_from(fuzzed_from.as_ref())
3412 .map_err(|e| e.to_string())?,
3413 storage_proof: processed_storage_proof,
3414 unspendable_account: unspendable_account_bytes_digest,
3415 parent_hash,
3416 state_root,
3417 extrinsics_root,
3418 digest,
3419 input_amount: input_amount_quantized,
3420 },
3421 public: PublicCircuitInputs {
3422 output_amount_1: output_amount,
3423 output_amount_2: 0,
3424 volume_fee_bps: VOLUME_FEE_BPS,
3425 nullifier: digest_to_bytes(Nullifier::from_preimage(secret, fuzzed_count).hash),
3426 exit_account_1: BytesDigest::try_from(fuzzed_to.as_ref()).map_err(|e| e.to_string())?,
3427 exit_account_2: BytesDigest::try_from([0u8; 32].as_ref()).map_err(|e| e.to_string())?,
3428 block_hash: BytesDigest::try_from(block_hash.as_ref()).map_err(|e| e.to_string())?,
3429 block_number,
3430 asset_id: NATIVE_ASSET_ID,
3431 },
3432 };
3433
3434 let prover =
3436 WormholeProver::new_from_files(&bins_dir.join("prover.bin"), &bins_dir.join("common.bin"))
3437 .map_err(|e| format!("Failed to load prover: {}", e))?;
3438
3439 let prover_next = prover.commit(&inputs).map_err(|e| e.to_string())?;
3441 let _proof: ProofWithPublicInputs<_, _, 2> =
3442 prover_next.prove().map_err(|e| format!("Circuit rejected: {}", e))?;
3443
3444 Ok(())
3445}
3446#[cfg(test)]
3447mod tests {
3448 use super::*;
3449 use std::collections::HashSet;
3450 use tempfile::NamedTempFile;
3451
3452 #[test]
3453 fn test_compute_output_amount() {
3454 assert_eq!(compute_output_amount(1000, 10), 999);
3456 assert_eq!(compute_output_amount(10000, 10), 9990);
3457
3458 assert_eq!(compute_output_amount(1000, 100), 990);
3460 assert_eq!(compute_output_amount(10000, 100), 9900);
3461
3462 assert_eq!(compute_output_amount(1000, 0), 1000);
3464
3465 assert_eq!(compute_output_amount(0, 10), 0);
3467 assert_eq!(compute_output_amount(1, 10), 0); assert_eq!(compute_output_amount(100, 10), 99);
3469 }
3470
3471 #[test]
3472 fn test_parse_secret_hex() {
3473 let secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
3475 assert!(parse_secret_hex(secret).is_ok());
3476 assert!(parse_secret_hex(&format!("0x{}", secret)).is_ok());
3477
3478 assert!(parse_secret_hex("0123456789abcdef").unwrap_err().contains("32 bytes"));
3480
3481 assert!(parse_secret_hex("ghij".repeat(16).as_str())
3483 .unwrap_err()
3484 .contains("Invalid secret hex"));
3485 }
3486
3487 #[test]
3488 fn test_parse_exit_account() {
3489 let hex = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
3491 assert!(parse_exit_account(hex).is_ok());
3492
3493 assert!(parse_exit_account("0x0123456789abcdef").unwrap_err().contains("32 bytes"));
3495
3496 assert!(parse_exit_account("not_valid").unwrap_err().contains("Invalid SS58"));
3498 }
3499
3500 #[test]
3501 fn test_quantize_funding_amount() {
3502 assert_eq!(quantize_funding_amount(1_000_000_000_000).unwrap(), 100);
3504
3505 assert_eq!(quantize_funding_amount(0).unwrap(), 0);
3507 assert_eq!(quantize_funding_amount(5_000_000_000).unwrap(), 0); let max_valid = (u32::MAX as u128) * SCALE_DOWN_FACTOR;
3511 assert_eq!(quantize_funding_amount(max_valid).unwrap(), u32::MAX);
3512 assert!(quantize_funding_amount(max_valid + SCALE_DOWN_FACTOR)
3513 .unwrap_err()
3514 .contains("exceeds u32::MAX"));
3515 }
3516
3517 #[test]
3518 fn test_proof_file_roundtrip() {
3519 let temp_file = NamedTempFile::new().unwrap();
3520 let path = temp_file.path().to_str().unwrap();
3521 let proof_bytes = vec![0x01, 0x02, 0x03, 0xaa, 0xbb, 0xcc];
3522
3523 write_proof_file(path, &proof_bytes).unwrap();
3524 assert_eq!(read_proof_file(path).unwrap(), proof_bytes);
3525 }
3526
3527 #[test]
3528 fn test_read_proof_file_errors() {
3529 assert!(read_proof_file("/nonexistent/path/proof.hex")
3531 .unwrap_err()
3532 .contains("Failed to read"));
3533
3534 let temp_file = NamedTempFile::new().unwrap();
3536 std::fs::write(temp_file.path(), "not valid hex!").unwrap();
3537 assert!(read_proof_file(temp_file.path().to_str().unwrap())
3538 .unwrap_err()
3539 .contains("Failed to decode"));
3540 }
3541
3542 #[test]
3543 fn test_fee_calculation_edge_cases() {
3544 let input_small: u32 = 100;
3549 let output_small = compute_output_amount(input_small, VOLUME_FEE_BPS);
3550 assert_eq!(output_small, 99);
3551 assert!(
3553 (output_small as u64) * 10000 <= (input_small as u64) * (10000 - VOLUME_FEE_BPS as u64)
3554 );
3555
3556 let input_medium: u32 = 10000;
3558 let output_medium = compute_output_amount(input_medium, VOLUME_FEE_BPS);
3559 assert_eq!(output_medium, 9990);
3560 assert!(
3561 (output_medium as u64) * 10000 <=
3562 (input_medium as u64) * (10000 - VOLUME_FEE_BPS as u64)
3563 );
3564
3565 let input_large: u32 = u32::MAX / 2;
3567 let output_large = compute_output_amount(input_large, VOLUME_FEE_BPS);
3568 assert!(
3569 (output_large as u64) * 10000 <= (input_large as u64) * (10000 - VOLUME_FEE_BPS as u64)
3570 );
3571
3572 for fee_bps in [0u32, 1, 10, 50, 100, 500, 1000] {
3574 let input: u32 = 100000;
3575 let output = compute_output_amount(input, fee_bps);
3576 assert!(
3577 (output as u64) * 10000 <= (input as u64) * (10000 - fee_bps as u64),
3578 "Fee constraint violated for fee_bps={}: {} * 10000 > {} * {}",
3579 fee_bps,
3580 output,
3581 input,
3582 10000 - fee_bps
3583 );
3584 }
3585 }
3586
3587 #[test]
3588 fn test_nullifier_determinism() {
3589 use qp_wormhole_circuit::nullifier::Nullifier;
3590 use qp_zk_circuits_common::utils::BytesDigest;
3591
3592 let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret");
3593 let transfer_count = 42u64;
3594
3595 let nullifier1 = Nullifier::from_preimage(secret, transfer_count);
3597 let nullifier2 = Nullifier::from_preimage(secret, transfer_count);
3598 let nullifier3 = Nullifier::from_preimage(secret, transfer_count);
3599
3600 assert_eq!(nullifier1.hash, nullifier2.hash);
3601 assert_eq!(nullifier2.hash, nullifier3.hash);
3602
3603 let nullifier_different = Nullifier::from_preimage(secret, transfer_count + 1);
3605 assert_ne!(nullifier1.hash, nullifier_different.hash);
3606
3607 let different_secret: BytesDigest = [2u8; 32].try_into().expect("valid secret");
3609 let nullifier_different_secret = Nullifier::from_preimage(different_secret, transfer_count);
3610 assert_ne!(nullifier1.hash, nullifier_different_secret.hash);
3611 }
3612
3613 #[test]
3614 fn test_unspendable_account_determinism() {
3615 use qp_wormhole_circuit::unspendable_account::UnspendableAccount;
3616 use qp_zk_circuits_common::utils::BytesDigest;
3617
3618 let secret: BytesDigest = [1u8; 32].try_into().expect("valid secret");
3619
3620 let account1 = UnspendableAccount::from_secret(secret);
3622 let account2 = UnspendableAccount::from_secret(secret);
3623
3624 assert_eq!(account1.account_id, account2.account_id);
3625
3626 let different_secret: BytesDigest = [2u8; 32].try_into().expect("valid secret");
3628 let account_different = UnspendableAccount::from_secret(different_secret);
3629 assert_ne!(account1.account_id, account_different.account_id);
3630 }
3631
3632 #[test]
3638 fn test_public_inputs_structure() {
3639 use qp_wormhole_inputs::{
3640 ASSET_ID_INDEX, BLOCK_HASH_END_INDEX, BLOCK_HASH_START_INDEX, BLOCK_NUMBER_INDEX,
3641 EXIT_ACCOUNT_1_END_INDEX, EXIT_ACCOUNT_1_START_INDEX, EXIT_ACCOUNT_2_END_INDEX,
3642 EXIT_ACCOUNT_2_START_INDEX, NULLIFIER_END_INDEX, NULLIFIER_START_INDEX,
3643 OUTPUT_AMOUNT_1_INDEX, OUTPUT_AMOUNT_2_INDEX, PUBLIC_INPUTS_FELTS_LEN,
3644 VOLUME_FEE_BPS_INDEX,
3645 };
3646
3647 assert_eq!(PUBLIC_INPUTS_FELTS_LEN, 21, "Public inputs should be 21 field elements");
3649 assert_eq!(ASSET_ID_INDEX, 0, "Asset ID should be first");
3650 assert_eq!(OUTPUT_AMOUNT_1_INDEX, 1, "Output amount 1 should be at index 1");
3651 assert_eq!(OUTPUT_AMOUNT_2_INDEX, 2, "Output amount 2 should be at index 2");
3652 assert_eq!(VOLUME_FEE_BPS_INDEX, 3, "Volume fee BPS should be at index 3");
3653 assert_eq!(NULLIFIER_START_INDEX, 4, "Nullifier should start at index 4");
3654 assert_eq!(NULLIFIER_END_INDEX, 8, "Nullifier should end at index 8");
3655 assert_eq!(EXIT_ACCOUNT_1_START_INDEX, 8, "Exit account 1 should start at index 8");
3656 assert_eq!(EXIT_ACCOUNT_1_END_INDEX, 12, "Exit account 1 should end at index 12");
3657 assert_eq!(EXIT_ACCOUNT_2_START_INDEX, 12, "Exit account 2 should start at index 12");
3658 assert_eq!(EXIT_ACCOUNT_2_END_INDEX, 16, "Exit account 2 should end at index 16");
3659 assert_eq!(BLOCK_HASH_START_INDEX, 16, "Block hash should start at index 16");
3660 assert_eq!(BLOCK_HASH_END_INDEX, 20, "Block hash should end at index 20");
3661 assert_eq!(BLOCK_NUMBER_INDEX, 20, "Block number should be at index 20");
3662 }
3663
3664 #[test]
3666 fn test_constants_match_chain_config() {
3667 assert_eq!(VOLUME_FEE_BPS, 10, "Volume fee should be 10 bps");
3669
3670 assert_eq!(NATIVE_ASSET_ID, 0, "Native asset ID should be 0");
3672
3673 assert_eq!(SCALE_DOWN_FACTOR, 10_000_000_000, "Scale down factor should be 10^10");
3675
3676 let one_token_12_decimals: u128 = 1_000_000_000_000;
3679 let quantized = quantize_funding_amount(one_token_12_decimals).unwrap();
3680 assert_eq!(quantized, 100, "1 token should quantize to 100 (1.00 with 2 decimals)");
3681 }
3682
3683 #[test]
3684 fn test_volume_fee_bps_constant() {
3685 assert_eq!(VOLUME_FEE_BPS, 10);
3687 }
3688
3689 #[test]
3690 fn test_aggregation_config_deserialization_matches_upstream_format() {
3691 let json = r#"{
3695 "num_leaf_proofs": 8,
3696 "num_layer0_proofs": null
3697 }"#;
3698
3699 let config: CircuitBinsConfig = serde_json::from_str(json).unwrap();
3700 assert_eq!(config.num_leaf_proofs, 8);
3701 assert_eq!(config.num_layer0_proofs, None);
3702 }
3703
3704 fn mk_accounts(n: usize) -> Vec<[u8; 32]> {
3705 (0..n)
3706 .map(|i| {
3707 let mut a = [0u8; 32];
3708 a[0] = (i as u8).wrapping_add(1); a
3710 })
3711 .collect()
3712 }
3713
3714 fn proof_outputs_for_inputs(input_amounts: &[u128], fee_bps: u32) -> Vec<u32> {
3715 input_amounts
3716 .iter()
3717 .map(|&input| {
3718 let input_quantized = quantize_funding_amount(input).unwrap_or(0);
3719 compute_output_amount(input_quantized, fee_bps)
3720 })
3721 .collect()
3722 }
3723
3724 fn total_output_for_inputs(input_amounts: &[u128], fee_bps: u32) -> u64 {
3725 proof_outputs_for_inputs(input_amounts, fee_bps)
3726 .into_iter()
3727 .map(|x| x as u64)
3728 .sum()
3729 }
3730
3731 fn find_input_for_min_output(fee_bps: u32, min_out: u32) -> u128 {
3734 let mut input: u128 = 1;
3735 for _ in 0..80 {
3736 let q = quantize_funding_amount(input).unwrap_or(0);
3737 let out = compute_output_amount(q, fee_bps);
3738 if out >= min_out {
3739 return input;
3740 }
3741 input = input.saturating_mul(10);
3743 }
3744 panic!("Could not find input producing output >= {}", min_out);
3745 }
3746
3747 #[test]
3752 fn random_partition_n0() {
3753 let parts = random_partition(100, 0, 1);
3754 assert!(parts.is_empty());
3755 }
3756
3757 #[test]
3758 fn random_partition_n1() {
3759 let total = 12345u128;
3760 let parts = random_partition(total, 1, 9999);
3761 assert_eq!(parts, vec![total]);
3762 }
3763
3764 #[test]
3765 fn random_partition_total_less_than_min_total_falls_back_to_equalish() {
3766 let total = 5u128;
3768 let n = 10usize;
3769 let min_per_part = 1u128;
3770
3771 let parts = random_partition(total, n, min_per_part);
3772
3773 assert_eq!(parts.len(), n);
3774 assert_eq!(parts.iter().sum::<u128>(), total);
3775
3776 for part in parts.iter().take(n - 1) {
3778 assert_eq!(*part, 0);
3779 }
3780 assert_eq!(parts[n - 1], 5);
3781 }
3782
3783 #[test]
3784 fn random_partition_min_achievable_invariants_hold() {
3785 let total = 100u128;
3786 let n = 10usize;
3787 let min_per_part = 3u128;
3788
3789 for _ in 0..200 {
3790 let parts = random_partition(total, n, min_per_part);
3791 assert_eq!(parts.len(), n);
3792 assert_eq!(parts.iter().sum::<u128>(), total);
3793 assert!(parts.iter().all(|&p| p >= min_per_part));
3794 }
3795 }
3796
3797 #[test]
3798 fn random_partition_distributable_zero_all_min() {
3799 let n = 10usize;
3800 let min_per_part = 3u128;
3801 let total = min_per_part * n as u128;
3802
3803 let parts = random_partition(total, n, min_per_part);
3804
3805 assert_eq!(parts.len(), n);
3806 assert_eq!(parts.iter().sum::<u128>(), total);
3807 assert!(parts.iter().all(|&p| p == min_per_part));
3808 }
3809
3810 #[test]
3815 fn compute_random_output_assignments_empty_inputs_or_targets() {
3816 let targets = mk_accounts(3);
3817 assert!(compute_random_output_assignments(&[], &targets, 0).is_empty());
3818
3819 let inputs = vec![1u128, 2u128, 3u128];
3820 assert!(compute_random_output_assignments(&inputs, &[], 0).is_empty());
3821 }
3822
3823 #[test]
3824 fn compute_random_output_assignments_basic_invariants() {
3825 let fee_bps = 0u32;
3826
3827 let input = find_input_for_min_output(fee_bps, 5);
3829 let input_amounts = vec![input, input, input, input, input];
3830 let targets = mk_accounts(4);
3831
3832 let assignments = compute_random_output_assignments(&input_amounts, &targets, fee_bps);
3833 assert_eq!(assignments.len(), input_amounts.len());
3834
3835 let proof_outputs = proof_outputs_for_inputs(&input_amounts, fee_bps);
3836
3837 for (i, a) in assignments.iter().enumerate() {
3839 let per_proof_sum = a.output_amount_1 as u64 + a.output_amount_2 as u64;
3840 assert_eq!(per_proof_sum, proof_outputs[i] as u64);
3841
3842 if a.output_amount_1 > 0 {
3844 assert!(targets.contains(&a.exit_account_1));
3845 } else {
3846 }
3849 if a.output_amount_2 > 0 {
3850 assert!(targets.contains(&a.exit_account_2));
3851 assert_ne!(a.exit_account_2, a.exit_account_1); } else {
3853 assert_eq!(a.exit_account_2, [0u8; 32]);
3855 }
3856 }
3857
3858 let total_assigned: u64 = assignments
3860 .iter()
3861 .map(|a| a.output_amount_1 as u64 + a.output_amount_2 as u64)
3862 .sum();
3863
3864 let total_expected = total_output_for_inputs(&input_amounts, fee_bps);
3865 assert_eq!(total_assigned, total_expected);
3866 }
3867
3868 #[test]
3869 fn compute_random_output_assignments_more_targets_than_capacity_still_conserves_funds() {
3870 let fee_bps = 0u32;
3874 let num_proofs = 1usize;
3875 let num_targets = 5usize;
3876
3877 let input = find_input_for_min_output(fee_bps, 10); let input_amounts = vec![input; num_proofs];
3879 let targets = mk_accounts(num_targets);
3880
3881 let assignments = compute_random_output_assignments(&input_amounts, &targets, fee_bps);
3882 assert_eq!(assignments.len(), num_proofs);
3883
3884 let total_assigned: u64 = assignments
3886 .iter()
3887 .map(|a| a.output_amount_1 as u64 + a.output_amount_2 as u64)
3888 .sum();
3889 let total_expected = total_output_for_inputs(&input_amounts, fee_bps);
3890 assert_eq!(total_assigned, total_expected);
3891
3892 let mut used = HashSet::new();
3894 for a in &assignments {
3895 if a.output_amount_1 > 0 {
3896 used.insert(a.exit_account_1);
3897 }
3898 if a.output_amount_2 > 0 {
3899 used.insert(a.exit_account_2);
3900 }
3901 }
3902 assert!(used.len() <= 2 * num_proofs);
3903 assert!(used.len() < num_targets);
3904 }
3905
3906 #[test]
3907 fn compute_random_output_assignments_total_output_less_than_num_targets_does_not_panic_and_conserves(
3908 ) {
3909 let fee_bps = 0u32;
3913
3914 let num_targets = 50usize;
3915 let targets = mk_accounts(num_targets);
3916
3917 let input = find_input_for_min_output(fee_bps, 1);
3920 let input_amounts = vec![input, input];
3921
3922 let assignments = compute_random_output_assignments(&input_amounts, &targets, fee_bps);
3923 assert_eq!(assignments.len(), input_amounts.len());
3924
3925 let total_assigned: u64 = assignments
3926 .iter()
3927 .map(|a| a.output_amount_1 as u64 + a.output_amount_2 as u64)
3928 .sum();
3929 let total_expected = total_output_for_inputs(&input_amounts, fee_bps);
3930 assert_eq!(total_assigned, total_expected);
3931
3932 for a in &assignments {
3934 if a.output_amount_1 > 0 {
3935 assert!(targets.contains(&a.exit_account_1));
3936 }
3937 if a.output_amount_2 > 0 {
3938 assert!(targets.contains(&a.exit_account_2));
3939 assert_ne!(a.exit_account_2, a.exit_account_1);
3940 }
3941 }
3942 }
3943}