From 33fb71085f2be3d444812b1fd8f5a57dd382d7ec Mon Sep 17 00:00:00 2001 From: Iamsohungrynow <261225237+Iamsohungrynow@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:42:31 +0800 Subject: [PATCH] fix(wormhole): parse --amount exactly, not via f64 The wormhole commands still read --amount (and dissolve's --target-size) as f64 and converted with (amount * 1_000_000_000_000.0) as u128, so `dissolve --amount 4.35` moved 4.34 DEV (2.01 and 16.4 lost 0.01 too), and nan, -1 and inf were accepted as 0, 0 and u128::MAX. They now go through send::parse_amount_with_decimals when clap parses them, like multisig since #179; the SCALE_DOWN_FACTOR alignment is unchanged. The exercise scenario now passes its multiround amount in raw units. --- src/cli/exercise/scenarios/wormhole.rs | 6 +- src/cli/wormhole.rs | 92 +++++++++++++++++++------- 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/cli/exercise/scenarios/wormhole.rs b/src/cli/exercise/scenarios/wormhole.rs index ea75aa8..533e8ee 100644 --- a/src/cli/exercise/scenarios/wormhole.rs +++ b/src/cli/exercise/scenarios/wormhole.rs @@ -12,7 +12,7 @@ use crate::{ /// fees, so a scaled-down amount rounds an output below the on-chain minimum in a later round /// and emits no transfer event. The amount exits back to the wallet and is swept to the root /// account afterwards, so the run only borrows it. -const MULTIROUND_AMOUNT_TOKENS: f64 = 50.0; +const MULTIROUND_AMOUNT_TOKENS: u128 = 50; pub async fn run(ctx: &mut ExerciseCtx, report: &mut Report, phase: &str) -> Result<()> { // Split the two runs across both signature schemes *and* both wormhole @@ -92,7 +92,7 @@ fn combine(primary: Result, cleanup: Result<()>, what: &str) -> Result u128 { - ((MULTIROUND_AMOUNT_TOKENS * unit as f64) as u128).saturating_add(2 * unit) + MULTIROUND_AMOUNT_TOKENS.saturating_mul(unit).saturating_add(2 * unit) } async fn run_multiround_command( @@ -104,7 +104,7 @@ async fn run_multiround_command( let command = crate::cli::wormhole::WormholeCommands::Multiround { num_proofs: 5, rounds: 5, - amount: MULTIROUND_AMOUNT_TOKENS, + amount: MULTIROUND_AMOUNT_TOKENS * ctx.unit, wallet: wallet_name.to_string(), password: None, password_file: None, diff --git a/src/cli/wormhole.rs b/src/cli/wormhole.rs index 9bc4434..f0cac88 100644 --- a/src/cli/wormhole.rs +++ b/src/cli/wormhole.rs @@ -876,6 +876,15 @@ fn format_dispatch_error( } } +/// Parse a DEV amount from the command line exactly (12 decimals), the same way +/// `send` and `multisig` do, instead of going through `f64`. +fn parse_dev_amount(amount: &str) -> Result { + crate::cli::send::parse_amount_with_decimals(amount, 12).map_err(|e| match e { + crate::error::QuantusError::Generic(msg) => msg, + other => other.to_string(), + }) +} + #[derive(Subcommand, Debug)] pub enum WormholeCommands { /// Derive the unspendable wormhole address from a secret file @@ -971,8 +980,8 @@ pub enum WormholeCommands { count: usize, /// DEV amount deposited per batch (partitioned across `--num-proofs`) - #[arg(short, long, default_value = "1.0")] - amount: f64, + #[arg(short, long, default_value = "1.0", value_parser = parse_dev_amount)] + amount: u128, /// Leaf proofs per private batch (padded to circuit size; default 1) #[arg(long, default_value_t = 1)] @@ -1024,8 +1033,8 @@ pub enum WormholeCommands { rounds: usize, /// Total amount in DEV to partition across all proofs (default: 100) - #[arg(short, long, default_value = "100")] - amount: f64, + #[arg(short, long, default_value = "100", value_parser = parse_dev_amount)] + amount: u128, /// Wallet name to use for funding and final exit #[arg(short, long)] @@ -1064,12 +1073,12 @@ pub enum WormholeCommands { /// low-amount bucket (many miner rewards, high privacy score). Dissolve { /// Amount in DEV to dissolve - #[arg(short, long)] - amount: f64, + #[arg(short, long, value_parser = parse_dev_amount)] + amount: u128, /// Target output size in DEV (stop splitting when all outputs are below this) - #[arg(short, long, default_value = "1.0")] - target_size: f64, + #[arg(short, long, default_value = "1.0", value_parser = parse_dev_amount)] + target_size: u128, /// Wallet name to use for funding #[arg(short, long)] @@ -1125,8 +1134,8 @@ pub enum WormholeCommands { password_file: Option, /// Amount in DEV to withdraw (default: withdraw all available) - #[arg(short, long)] - amount: Option, + #[arg(short, long, value_parser = parse_dev_amount)] + amount: Option, /// Destination address for withdrawn funds (required when using --mnemonic-file or /// --secret-file) @@ -1293,8 +1302,7 @@ pub async fn handle_wormhole_command( password_file, output_dir, } => { - let amount_planck = (amount * 1_000_000_000_000.0) as u128; - let amount_aligned = (amount_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; + let amount_aligned = (amount / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; let quantus_client = QuantusClient::new(node_url).await.map_err(|e| { crate::error::QuantusError::Generic(format!("Failed to connect: {e}")) })?; @@ -1330,9 +1338,8 @@ pub async fn handle_wormhole_command( dry_run, public, } => { - // Convert DEV to planck and align to SCALE_DOWN_FACTOR for clean quantization - let amount_planck = (amount * 1_000_000_000_000.0) as u128; - let amount_aligned = (amount_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; + // Align to SCALE_DOWN_FACTOR for clean quantization + let amount_aligned = (amount / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; run_multiround( num_proofs, rounds, @@ -1358,10 +1365,8 @@ pub async fn handle_wormhole_command( keep_files, output_dir, } => { - let amount_planck = (amount * 1_000_000_000_000.0) as u128; - let amount_aligned = (amount_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; - let target_planck = (target_size * 1_000_000_000_000.0) as u128; - let target_aligned = (target_planck / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; + let amount_aligned = (amount / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; + let target_aligned = (target_size / SCALE_DOWN_FACTOR) * SCALE_DOWN_FACTOR; run_dissolve( amount_aligned, target_aligned, @@ -4459,7 +4464,7 @@ async fn run_collect_rewards( secret_file_arg: Option, password: Option, password_file: Option, - amount: Option, + amount_planck: Option, destination: Option, subsquid_url: String, wormhole_index: usize, @@ -4510,9 +4515,6 @@ async fn run_collect_rewards( )); }; - // Convert amount from DEV to planck - let amount_planck = amount.map(|a| (a * 1_000_000_000_000.0) as u128); - // Print initial info if let Some(ref addr) = wallet_address { log_print!(" Wallet: {}", addr.bright_yellow()); @@ -5638,6 +5640,50 @@ mod tests { CollectRewardsTestCli::try_parse_from(args).map(|cli| cli.cmd) } + #[test] + fn amount_args_parse_exactly() { + use clap::Parser; + // The f64 path truncated ordinary amounts: 4.35 * 10^12 became + // 4_349_999_999_999 and then aligned down to 4.34 DEV. + let cmd = CollectRewardsTestCli::try_parse_from([ + "test", + "dissolve", + "--amount", + "4.35", + "--target-size", + "2.01", + "--wallet", + "w", + ]) + .unwrap() + .cmd; + match cmd { + WormholeCommands::Dissolve { amount, target_size, .. } => { + assert_eq!(amount, 4_350_000_000_000); + assert_eq!(target_size, 2_010_000_000_000); + }, + other => panic!("unexpected command: {other:?}"), + } + match try_parse_collect_rewards(&["--wallet", "w", "--amount", "16.4"]).unwrap() { + WormholeCommands::CollectRewards { amount, .. } => + assert_eq!(amount, Some(16_400_000_000_000)), + other => panic!("unexpected command: {other:?}"), + } + } + + #[test] + fn amount_args_reject_what_f64_accepted() { + // The f64 path turned nan and -1 into 0, inf into u128::MAX and read 1e3 as + // 1000 DEV; 13 decimals cannot be represented in planck. + for amount in ["nan", "inf", "-1", "1e3", "0.0000000000001"] { + let arg = format!("--amount={amount}"); + assert!( + try_parse_collect_rewards(&["--wallet", "w", &arg]).is_err(), + "--amount {amount} should be rejected" + ); + } + } + #[test] fn collect_rewards_requires_one_credential() { let err = try_parse_collect_rewards(&[]).unwrap_err();