From 5098562241cabefe42e92fb80cd5b0556f2c0f2f Mon Sep 17 00:00:00 2001 From: ganymedio <17599867+ganymedio@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:07:57 -0400 Subject: [PATCH 1/2] Assert that batch range-proof gas for confidential transfers is version gated The bulletproofs.verify.base_batch_* parameters only exist from RELEASE_V1_28. Below that version parameter resolution leaves them at zero rather than failing, so a confidential transfer is still accepted but pays nothing for verifying its two batched range proofs. Add a test that runs the same transfer at the current gas feature version and at RELEASE_V1_27, asserting the transfer succeeds either way and that the gap between the two is exactly the sum of the two parameters. RELEASE_V1_28 gates only those parameters, so the delta is fully attributable to them. Extract confidential_transfer_payload out of run_confidential_transfer so the test can submit the same payload through run_raw and read the gas charged. --- .../src/tests/confidential_asset.rs | 131 +++++++++++++++++- 1 file changed, 125 insertions(+), 6 deletions(-) diff --git a/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs b/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs index e0a769879b9..d2c8654a495 100644 --- a/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs +++ b/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs @@ -21,6 +21,10 @@ // injected `confidential_asset` module. use crate::{tests::common::framework_dir_path, MoveHarness}; +use aptos_gas_schedule::{ + gas_feature_versions, AptosGasParameters, InitialGasSchedule, ToOnChainGasSchedule, + LATEST_GAS_FEATURE_VERSION, +}; use aptos_language_e2e_tests::account::Account; use aptos_types::{ account_address::AccountAddress, @@ -607,14 +611,12 @@ fn pack_transfer_audited( std::array::from_fn(|i| ret.return_values[i].0.clone()) } -fn run_confidential_transfer( - h: &mut MoveHarness, - sender: &Account, +fn confidential_transfer_payload( recipient: AccountAddress, parts: &[Vec; 8], sender_auditor_hint: Vec, -) -> TransactionStatus { - let payload = TransactionPayload::EntryFunction(EntryFunction::new( +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( ca_module_id(), Identifier::new("confidential_transfer").unwrap(), vec![], @@ -631,7 +633,17 @@ fn run_confidential_transfer( parts[7].clone(), bcs::to_bytes(&sender_auditor_hint).unwrap(), ], - )); + )) +} + +fn run_confidential_transfer( + h: &mut MoveHarness, + sender: &Account, + recipient: AccountAddress, + parts: &[Vec; 8], + sender_auditor_hint: Vec, +) -> TransactionStatus { + let payload = confidential_transfer_payload(recipient, parts, sender_auditor_hint); let txn = h.create_transaction_payload(sender, payload); h.run(txn) } @@ -1313,3 +1325,110 @@ fn deposit_normalize_and_rollover_succeeds_when_not_normalized() { "deposit_normalize_and_rollover when not normalized", ); } + +// --- Gas accounting --- + +/// The gas feature version immediately below `RELEASE_V1_28`, where the +/// `bulletproofs.verify.base_batch_*` parameters are introduced. Below that version those +/// parameters are absent from the schedule, and parameter resolution leaves absent entries at +/// zero rather than failing, so batched range-proof verification is charged nothing. +const PRE_BATCH_RANGEPROOF_GAS_VERSION: u64 = gas_feature_versions::RELEASE_V1_27; + +fn pin_gas_feature_version(h: &mut MoveHarness, feature_version: u64) { + h.modify_gas_schedule_raw(|gas_schedule| { + gas_schedule.feature_version = feature_version; + gas_schedule.entries = + AptosGasParameters::initial().to_on_chain_gas_schedule(feature_version); + }); +} + +/// Runs one `confidential_transfer` end to end and returns the gas charged for it. The gas +/// schedule is pinned only after setup, so both feature versions measure the same transfer +/// against identical state. +fn confidential_transfer_gas_used(pinned_gas_feature_version: Option, idx: u8) -> u64 { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xA1, idx); + let bob_addr = confidential_e2e_addr(0xA2, idx); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + let bob = h.new_account_with_balance_at(bob_addr, 1_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let (bob_dk, bob_ek) = generate_elgamal_keypair(&mut h); + for (acct, addr, dk, ek) in [ + (&alice, alice_addr, &alice_dk, &alice_ek), + (&bob, bob_addr, &bob_dk, &bob_ek), + ] { + let pk = twisted_pubkey_bytes(&mut h, ek); + let (c, r) = prove_registration_parts(&mut h, chain, addr, dk, ek, MOVE_METADATA); + assert_kept_success(&run_register(&mut h, acct, &pk, &c, &r), "register"); + } + + assert_kept_success(&run_deposit(&mut h, &alice, 8_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + let parts = pack_transfer_simple( + &mut h, + chain, + alice_addr, + bob_addr, + &alice_dk, + 200, + 7_800, + vec![], + ); + + if let Some(feature_version) = pinned_gas_feature_version { + pin_gas_feature_version(&mut h, feature_version); + } + + let payload = confidential_transfer_payload(bob_addr, &parts, vec![]); + let txn = h.create_transaction_payload(&alice, payload); + let output = h.run_raw(txn); + assert_kept_success(output.status(), "transfer"); + output.gas_used() +} + +/// External-gas cost of the two batched range proofs a transfer verifies, over the 8 chunks +/// of the new balance and the 4 chunks of the transfer amount. +fn batch_rangeproof_gas_cost() -> u64 { + let params = AptosGasParameters::initial(); + let internal = u64::from( + params + .natives + .aptos_framework + .bulletproofs_verify_base_batch_8_bits_16, + ) + u64::from( + params + .natives + .aptos_framework + .bulletproofs_verify_base_batch_4_bits_16, + ); + internal / u64::from(params.vm.txn.scaling_factor()) +} + +/// A confidential transfer verifies two batched range proofs, over the 8 chunks of the new +/// balance and the 4 chunks of the transfer amount. Below `RELEASE_V1_28` their gas +/// parameters resolve to zero, so the transfer still succeeds but pays nothing for that +/// verification. This pins both halves of that behaviour: the transfer is accepted either +/// way, and the gap it leaves is exactly the two parameters. +#[test] +fn confidential_transfer_batch_rangeproof_gas_is_version_gated() { + let priced = confidential_transfer_gas_used(None, 1); + let unpriced = confidential_transfer_gas_used(Some(PRE_BATCH_RANGEPROOF_GAS_VERSION), 2); + let expected = batch_rangeproof_gas_cost(); + + assert_eq!( + priced - unpriced, + expected, + "a transfer costs {priced} at gas feature version {LATEST_GAS_FEATURE_VERSION} and \ + {unpriced} at {PRE_BATCH_RANGEPROOF_GAS_VERSION}; the gap should be exactly the two \ + batch range-proof parameters ({expected})" + ); + + println!( + "confidential_transfer gas: {priced} at gas feature version {}, {unpriced} at {}; \ + batch range-proof verification accounts for {expected}", + LATEST_GAS_FEATURE_VERSION, PRE_BATCH_RANGEPROOF_GAS_VERSION, + ); +} From dae2e3d350bb6c3466cd3ebeb9af65d76f231f90 Mon Sep 17 00:00:00 2001 From: ganymedio <17599867+ganymedio@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:03:48 -0400 Subject: [PATCH 2/2] Add a gas profile test for confidential transfers Breaks a transfer's gas down by operation using the gas profiler, asserting that the two batched range proofs are charged exactly their parameters and remain the largest single line item, and printing the rest so a shift in composition is visible. The breakdown shows range-proof verification at 52% of a transfer, other ristretto255 natives at 17%, and Move interpreter overhead at 22%, the last of which is driven by large instruction counts building and copying vectors of points and scalars. Intrinsic and storage costs are negligible. --- .../src/tests/confidential_asset.rs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs b/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs index d2c8654a495..38e38cdf289 100644 --- a/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs +++ b/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs @@ -1432,3 +1432,73 @@ fn confidential_transfer_batch_rangeproof_gas_is_version_gated() { LATEST_GAS_FEATURE_VERSION, PRE_BATCH_RANGEPROOF_GAS_VERSION, ); } + +/// Breaks a confidential transfer's gas down by operation. Asserts the two batched range +/// proofs are charged exactly what their parameters say and remain the single largest line +/// item, and prints the rest so the composition is visible when it shifts. +#[test] +fn confidential_transfer_gas_profile() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xA3, 1); + let bob_addr = confidential_e2e_addr(0xA4, 1); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + let bob = h.new_account_with_balance_at(bob_addr, 1_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let (bob_dk, bob_ek) = generate_elgamal_keypair(&mut h); + for (acct, addr, dk, ek) in [ + (&alice, alice_addr, &alice_dk, &alice_ek), + (&bob, bob_addr, &bob_dk, &bob_ek), + ] { + let pk = twisted_pubkey_bytes(&mut h, ek); + let (c, r) = prove_registration_parts(&mut h, chain, addr, dk, ek, MOVE_METADATA); + assert_kept_success(&run_register(&mut h, acct, &pk, &c, &r), "register"); + } + assert_kept_success(&run_deposit(&mut h, &alice, 8_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + let parts = pack_transfer_simple( + &mut h, + chain, + alice_addr, + bob_addr, + &alice_dk, + 200, + 7_800, + vec![], + ); + let payload = confidential_transfer_payload(bob_addr, &parts, vec![]); + + let (log, gas_used, _fee) = h.evaluate_gas_with_profiler(&alice, payload); + let io = &log.exec_io; + let scale = u64::from(io.gas_scaling_factor); + let aggregated = io.aggregate_gas_events(); + + println!("confidential_transfer: {gas_used} gas, intrinsic {}", u64::from(io.intrinsic_cost) / scale); + for (name, count, cost) in &aggregated.ops { + let cost = u64::from(*cost) / scale; + if cost > 0 { + println!(" {cost:>5} x{count:<6} {name}"); + } + } + + let (_, count, cost) = aggregated + .ops + .iter() + .find(|(name, _, _)| name.contains("verify_batch_range_proof")) + .expect("a transfer must verify batched range proofs"); + assert_eq!(*count, 2, "a transfer verifies two batched range proofs"); + assert_eq!( + u64::from(*cost) / scale, + batch_rangeproof_gas_cost(), + "range-proof verification should be charged exactly its two parameters" + ); + + let largest = u64::from(aggregated.ops[0].2) / scale; + assert_eq!( + largest, + batch_rangeproof_gas_cost(), + "range-proof verification should be the largest single cost in a transfer" + ); +}