diff --git a/.github/workflows/ca-e2e.yml b/.github/workflows/ca-e2e.yml index 6c7aeaa..4f41c1f 100644 --- a/.github/workflows/ca-e2e.yml +++ b/.github/workflows/ca-e2e.yml @@ -17,10 +17,12 @@ env: # branch so CI is reproducible and won't drift if the branch advances. # Bump when intentional changes to the localnet/module setup are needed. # - # f496b6c1: bind token_address into the FS transcript for transfer/withdraw/ - # rotation/normalization sigma proofs. Wire-format-breaking change; ships - # together with the corresponding SDK updates in this repo. - APTOS_CORE_COMMIT: f496b6c1679e5b1da5983569c909d9c9fccd5e9e + # c9cc7d2a: confidential_asset on confidential-asset-prod with the Movement + # bulletproofs DST rename (MovementConfidentialAsset/...), the chain/asset + # auditor split, the self-transfer ban (ESELF_TRANSFER), and the updated + # start-localnet-confidential-assets.sh. Matches the SDK in this repo: native + # Movement-DST prover (movement_rp_wasm) and two-account transfer tests. + APTOS_CORE_COMMIT: c9cc7d2a47c464296bb40e5d7532907e95ae888f jobs: ca-e2e: @@ -134,13 +136,12 @@ jobs: cat "$LOG" exit 1 fi - ADDR=$(grep -oE 'publish signer \(default profile\) 0x[0-9a-f]{64}' "$LOG" \ - | head -n1 | awk '{print $NF}') - if [[ -z "$ADDR" ]]; then - echo "Could not parse publish-signer address from script output." >&2 - cat "$LOG" - exit 1 - fi + # confidential_asset moved into the framework (aptos-core #366), so it's at the + # framework address 0x1 — not the experimental publish-signer account this step used to + # parse out of the script log. Use the full 32-byte form (the test helper requires a + # 64-hex-char address, matching TOKEN_ADDRESS). We still wait for "Done" above so the + # feature flag and chain-auditor bootstrap have completed before the tests run. + ADDR=0x0000000000000000000000000000000000000000000000000000000000000001 echo "CONFIDENTIAL_MODULE_ADDRESS=$ADDR" echo "CONFIDENTIAL_MODULE_ADDRESS=$ADDR" >> "$GITHUB_ENV" diff --git a/Cargo.lock b/Cargo.lock index d73e81c..0133b24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,13 +424,10 @@ name = "confidential-assets" version = "0.1.0" dependencies = [ "aptos-bcs", - "bulletproofs", "curve25519-dalek", "curve25519-dalek-ng", "hex", - "merlin", "movement-sdk", - "movement_pollard_kangaroo_wasm", "movement_rp_wasm", "num-bigint", "num-traits", @@ -1579,21 +1576,10 @@ dependencies = [ "trybuild", ] -[[package]] -name = "movement_pollard_kangaroo_wasm" -version = "0.0.0" -source = "git+https://github.com/moveindustries/confidential-asset-wasm-bindings?branch=main#8dc068a8146def3dc53221e3267e0233119f99d3" -dependencies = [ - "curve25519-dalek-ng", - "getrandom 0.2.17", - "pollard-kangaroo", - "wasm-bindgen", -] - [[package]] name = "movement_rp_wasm" version = "0.0.0" -source = "git+https://github.com/moveindustries/confidential-asset-wasm-bindings?branch=main#8dc068a8146def3dc53221e3267e0233119f99d3" +source = "git+https://github.com/moveindustries/confidential-asset-wasm-bindings?branch=main#527d22312a71c10c53344bdd802ce1e1ce684d80" dependencies = [ "bulletproofs", "curve25519-dalek-ng", diff --git a/crates/confidential-assets/Cargo.toml b/crates/confidential-assets/Cargo.toml index fc4ab55..b7ff11e 100644 --- a/crates/confidential-assets/Cargo.toml +++ b/crates/confidential-assets/Cargo.toml @@ -14,8 +14,12 @@ readme = "README.md" [features] e2e = [] +[[example]] +name = "simple_ca_transfer" +required-features = ["e2e"] + [dependencies] -movement-sdk = { workspace = true, features = ["twisted_ed25519"] } +movement-sdk = { workspace = true, features = ["twisted_ed25519", "ed25519", "faucet"] } aptos-bcs = { workspace = true } tokio = { workspace = true } @@ -32,13 +36,11 @@ hex = "0.4" num-bigint = "0.4" num-traits = "0.2" -# Upstream crypto shared with the TS SDK (built as WASM there, linked as rlib here). +# Upstream range-proof prover/verifier shared with the TS SDK (built as WASM there, linked as +# rlib here). Both prove and verify go through this crate's native `rp` module. movement_rp_wasm = { git = "https://github.com/moveindustries/confidential-asset-wasm-bindings", branch = "main" } -movement_pollard_kangaroo_wasm = { git = "https://github.com/moveindustries/confidential-asset-wasm-bindings", branch = "main" } -# Same versions the upstream wrappers pin to; needed for the verify path -# (the upstream `_batch_range_proof` only exposes the prover natively). -bulletproofs = "4.0.0" -merlin = "3" +# curve25519-dalek-ng: kangaroo-decryption point conversion in `twisted_el_gamal`. +# (Range-proof prove + verify both go through `movement_rp_wasm`'s native `rp` module.) curve25519-dalek-ng = "4" pollard-kangaroo = { git = "https://github.com/distributed-lab/pollard-kangaroo.git", branch = "main", features = ["table48", "table32", "presets"] } diff --git a/crates/confidential-assets/README.md b/crates/confidential-assets/README.md index 0427674..f1c8888 100644 --- a/crates/confidential-assets/README.md +++ b/crates/confidential-assets/README.md @@ -6,8 +6,9 @@ encryption, ZK proofs, and confidential transfers on MoveVM. Functional parity w TypeScript SDK; same on-chain wire format, validated end-to-end against a localnet. This crate is part of the [Movement Rust SDK workspace](../../README.md). It depends on -`movement-sdk` for HTTP / transaction building and on the upstream Rust source that the TS SDK -ships as WASM (`movement_rp_wasm`, `movement_pollard_kangaroo_wasm`) — same code path, no port. +`movement-sdk` for HTTP / transaction building and on `movement_rp_wasm` — the upstream Rust +range-proof source the TS SDK ships as WASM, consumed here as an `rlib` (same code path, no +port). Kangaroo decryption uses the upstream `pollard-kangaroo` crate directly. ## Status @@ -277,9 +278,10 @@ Cryptography: Movement-specific (shared with the TS SDK): - [`movement_rp_wasm`](https://github.com/moveindustries/confidential-asset-wasm-bindings) — - bulletproofs prover. Same Rust source the TS SDK builds as WASM; consumed here as an `rlib`. -- [`movement_pollard_kangaroo_wasm`](https://github.com/moveindustries/confidential-asset-wasm-bindings) — - Pollard kangaroo DLP solver. Same source as the TS SDK's WASM, `Kangaroo32` preset. + bulletproofs prover and verifier (native `rp` module). Same Rust source the TS SDK builds as + WASM; consumed here as an `rlib`. +- [`pollard-kangaroo`](https://github.com/distributed-lab/pollard-kangaroo) — Pollard kangaroo + DLP solver for decryption (`Kangaroo32` preset). Native Rust; the TS SDK uses a WASM solver. Movement-SDK plumbing: diff --git a/crates/confidential-assets/examples/simple_ca_transfer.rs b/crates/confidential-assets/examples/simple_ca_transfer.rs new file mode 100644 index 0000000..50ba3df --- /dev/null +++ b/crates/confidential-assets/examples/simple_ca_transfer.rs @@ -0,0 +1,257 @@ +// Example: Simple confidential asset transfer +// +// This example demonstrates how to: +// Init: +// create and fund 2 accounts +// CA transfer: +// 1. Initialize the confidential asset for the 2 accounts +// 2. Deposit some token on account 1 +// 3. Do a CA transfer from account 1 to account 2 +// 4. Verify the account balances +// +// Run with: +// 1. MOVEMENT_NETWORK=TESTNET|MAINNET|LOCAL — picks a preset +// 2. MOVEMENT_RPC_URL=http://... — custom endpoint (used when MOVEMENT_NETWORK is not set) +// 3. Falls back to http://127.0.0.1:8080/v1 (default local) +// 4. CONFIDENTIAL_MODULE_ADDRESS=0x (optional, defaults to 0x1) +// To run wih all defaul: +// cargo run --example simple_ca_transfer --features e2e +// cargo run -p confidential-assets --example simple_ca_transfer --features confidential-assets/e2e + +use confidential_assets::api::ConfidentialAsset; +use confidential_assets::crypto::twisted_ed25519::{ + DECRYPTION_KEY_DERIVATION_MESSAGE, TwistedEd25519PrivateKey, +}; +use movement_sdk::{ + Movement, MovementConfig, MovementError, MovementResult, + account::Ed25519Account, + transaction::{EntryFunction, TransactionPayload}, + types::{AccountAddress, Identifier, MoveModuleId, TypeTag}, +}; +use std::env; + +// 2 MOVE — enough for the default gas budget (max_gas × gas_price = 200_000_000 octas) +// plus headroom for subsequent transactions. +const FUND_AMOUNT: u64 = 1_000_000_000; +const DEPOSIT_AMOUNT: u64 = 10; +const TRANSFER_AMOUNT: u64 = 4; + +// The FA token used in the example. Defaults to the well-known testnet/localnet address +// (0x…0a, the MOVE coin FA metadata), but can be overridden via TOKEN_ADDRESS. +const DEFAULT_TOKEN_ADDRESS: &str = + "0x000000000000000000000000000000000000000000000000000000000000000a"; + +#[tokio::main] +async fn main() -> MovementResult<()> { + println!("=== Simple Confidential Asset Transfer Example ===\n"); + + // ── 1. Build the Movement client ───────────────────────────────────────── + let config = match env::var("MOVEMENT_NETWORK") { + Ok(network) => match network.to_uppercase().as_str() { + "TESTNET" => MovementConfig::testnet(), + "MAINNET" => MovementConfig::mainnet(), + _ => MovementConfig::local(), + }, + Err(_) => match env::var("MOVEMENT_RPC_URL") { + Ok(url) => MovementConfig::custom(&url)?, + Err(_) => MovementConfig::local(), + }, + }; + let movement = Movement::new(config)?; + println!("Connected to Movement network.\n"); + + // ── 2. Resolve addresses ────────────────────────────────────────────────── + let module_address = + env::var("CONFIDENTIAL_MODULE_ADDRESS").unwrap_or_else(|_| "0x1".to_string()); + + let token_raw = env::var("TOKEN_ADDRESS").unwrap_or_else(|_| DEFAULT_TOKEN_ADDRESS.to_string()); + let token = + AccountAddress::from_hex(&token_raw).expect("TOKEN_ADDRESS must be a valid hex address"); + + let ca = ConfidentialAsset::new(&movement, Some(&module_address))?; + + // ── 3. Create and fund two accounts ────────────────────────────────────── + let alice = Ed25519Account::generate(); + let bob = Ed25519Account::generate(); + + println!("Alice address: {}", alice.address()); + println!("Bob address: {}", bob.address()); + println!(); + + // Derive the CA decryption key from the account signing key (same approach as + // `get_test_confidential_account` in the e2e helpers). + let alice_dk = { + let sig = alice.sign_message(DECRYPTION_KEY_DERIVATION_MESSAGE); + let bytes = sig.to_bytes(); + TwistedEd25519PrivateKey::from_bytes(&bytes[..32]).expect("signature has at least 32 bytes") + }; + let bob_dk = { + let sig = bob.sign_message(DECRYPTION_KEY_DERIVATION_MESSAGE); + let bytes = sig.to_bytes(); + TwistedEd25519PrivateKey::from_bytes(&bytes[..32]).expect("signature has at least 32 bytes") + }; + + println!("Funding Alice …"); + movement.fund_account(alice.address(), FUND_AMOUNT).await?; + println!("Funding Bob …"); + movement.fund_account(bob.address(), FUND_AMOUNT).await?; + + // Migrate native coin → FA store (required before any FA operation). + println!("Migrating Alice's coins to fungible store …"); + let migrate = migrate_payload()?; + movement.sign_submit_and_wait(&alice, migrate, None).await?; + + println!("Migrating Bob's coins to fungible store …"); + let migrate = migrate_payload()?; + movement.sign_submit_and_wait(&bob, migrate, None).await?; + println!(); + + // ── 4. Register confidential balances for both accounts ────────────────── + println!("Registering Alice's confidential balance …"); + let payload = ca + .register_balance(&alice.address(), &token, &alice_dk) + .await?; + movement.sign_submit_and_wait(&alice, payload, None).await?; + + println!("Registering Bob's confidential balance …"); + let payload = ca.register_balance(&bob.address(), &token, &bob_dk).await?; + movement.sign_submit_and_wait(&bob, payload, None).await?; + println!(); + + // ── 5. Deposit public FA tokens into Alice's confidential balance ───────── + println!("Depositing {DEPOSIT_AMOUNT} tokens into Alice's confidential balance …"); + let payload = ca.deposit(&alice.address(), &token, DEPOSIT_AMOUNT, None)?; + movement.sign_submit_and_wait(&alice, payload, None).await?; + + let bal = ca.get_balance(&alice.address(), &token, &alice_dk).await?; + println!( + " Alice after deposit — available: {}, pending: {}", + bal.available_balance(), + bal.pending_balance() + ); + + // ── 6. Rollover Alice's pending balance → available ─────────────────────── + println!("Rolling over Alice's pending balance …"); + let payloads = ca + .rollover_pending_balance(&alice.address(), &token, None, false) + .await?; + for p in payloads { + movement.sign_submit_and_wait(&alice, p, None).await?; + } + + let bal = ca.get_balance(&alice.address(), &token, &alice_dk).await?; + println!( + " Alice after rollover — available: {}, pending: {}", + bal.available_balance(), + bal.pending_balance() + ); + println!(); + + // ── 7. Confidential transfer: Alice → Bob ──────────────────────────────── + println!("Transferring {TRANSFER_AMOUNT} tokens from Alice to Bob (confidential) …"); + let payload = ca + .transfer( + &alice.address(), + &bob.address(), + &token, + TRANSFER_AMOUNT, + &alice_dk, + &[], // no additional auditors + &[], // no auditor hint + ) + .await?; + movement.sign_submit_and_wait(&alice, payload, None).await?; + println!(); + + // ── 8. Verify balances after transfer ──────────────────────────────────── + let alice_bal = ca.get_balance(&alice.address(), &token, &alice_dk).await?; + let bob_bal = ca.get_balance(&bob.address(), &token, &bob_dk).await?; + + println!("=== Balances after transfer ==="); + println!( + " Alice — available: {}, pending: {}", + alice_bal.available_balance(), + alice_bal.pending_balance() + ); + println!( + " Bob — available: {}, pending: {}", + bob_bal.available_balance(), + bob_bal.pending_balance() + ); + println!(); + + assert_eq!( + alice_bal.available_balance(), + (DEPOSIT_AMOUNT - TRANSFER_AMOUNT) as u128, + "Alice's available balance should be DEPOSIT - TRANSFER" + ); + assert_eq!( + bob_bal.pending_balance(), + TRANSFER_AMOUNT as u128, + "Bob's pending balance should equal the transfer amount" + ); + + // ── 10. Rollover Bob's pending balance → available ──────────────────────── + println!("Rolling over Bob's pending balance …"); + let payloads = ca + .rollover_pending_balance(&bob.address(), &token, Some(&bob_dk), false) + .await?; + for p in payloads { + movement.sign_submit_and_wait(&bob, p, None).await?; + } + + let bob_bal = ca.get_balance(&bob.address(), &token, &bob_dk).await?; + println!( + " Bob after rollover — available: {}, pending: {}", + bob_bal.available_balance(), + bob_bal.pending_balance() + ); + assert_eq!( + bob_bal.available_balance(), + TRANSFER_AMOUNT as u128, + "Bob's available balance should equal the transfer amount after rollover" + ); + assert_eq!( + bob_bal.pending_balance(), + 0, + "Bob's pending balance should be zero after rollover" + ); + println!(); + + // ── 11. Normalize Bob's available balance ───────────────────────────────── + println!("Normalizing Bob's balance …"); + let payload = ca + .normalize_balance(&bob.address(), &bob_dk, &token) + .await?; + movement.sign_submit_and_wait(&bob, payload, None).await?; + + let bob_bal = ca.get_balance(&bob.address(), &token, &bob_dk).await?; + println!( + " Bob after normalize — available: {}, pending: {}", + bob_bal.available_balance(), + bob_bal.pending_balance() + ); + assert_eq!( + bob_bal.available_balance(), + TRANSFER_AMOUNT as u128, + "Bob's available balance should be unchanged after normalize" + ); + + println!("\nAll assertions passed. Example complete!"); + Ok(()) +} + +/// Build the `0x1::coin::migrate_to_fungible_store` payload. +fn migrate_payload() -> MovementResult { + let module = MoveModuleId::new( + AccountAddress::from_hex("0x1").map_err(|e| MovementError::Internal(e.to_string()))?, + Identifier::new("coin").map_err(|e| MovementError::Internal(e.to_string()))?, + ); + Ok(EntryFunction::new( + module, + "migrate_to_fungible_store", + vec![TypeTag::aptos_coin()], + vec![], + ) + .into()) +} diff --git a/crates/confidential-assets/src/api/confidential_asset.rs b/crates/confidential-assets/src/api/confidential_asset.rs index a02cc7d..f6e9b03 100644 --- a/crates/confidential-assets/src/api/confidential_asset.rs +++ b/crates/confidential-assets/src/api/confidential_asset.rs @@ -11,7 +11,7 @@ use crate::crypto::{TwistedEd25519PrivateKey, TwistedEd25519PublicKey}; use crate::internal::transaction_builder::ConfidentialAssetTransactionBuilder; use crate::internal::view_functions::{ - ConfidentialBalance, get_balance, get_encryption_key, get_global_auditor_encryption_key, + ConfidentialBalance, get_asset_auditor_encryption_key, get_balance, get_encryption_key, is_balance_normalized, is_pending_balance_frozen, }; use movement_sdk::account::Account; @@ -394,7 +394,7 @@ impl<'a> ConfidentialAsset<'a> { &self, token_address: &AccountAddress, ) -> Result, MovementError> { - get_global_auditor_encryption_key( + get_asset_auditor_encryption_key( self.transaction.client, token_address, Some(&self.transaction.confidential_asset_module_address), diff --git a/crates/confidential-assets/src/crypto/range_proof.rs b/crates/confidential-assets/src/crypto/range_proof.rs index 0a3ee97..58b8a6b 100644 --- a/crates/confidential-assets/src/crypto/range_proof.rs +++ b/crates/confidential-assets/src/crypto/range_proof.rs @@ -41,7 +41,11 @@ pub fn prove_range_batch( Ok((out.proof(), out.comms())) } -/// Verify a batch range proof. +/// Verify a batch range proof. Delegates to the prover crate (`movement_rp_wasm`) so the +/// Bulletproofs transcript DST lives in exactly one place — `BULLETPROOF_DST` in the +/// wasm-bindings `range-proofs` crate — and the Rust verifier is byte-identical to the prover +/// by construction (no DST is re-declared here). This mirrors how the TS SDK verifies via the +/// same crate's wasm build. pub fn verify_range_batch( proof: &[u8], commitments: &[Vec], @@ -49,34 +53,17 @@ pub fn verify_range_batch( rand_base: &RistrettoPoint, num_bits: usize, ) -> Result { - use bulletproofs::{BulletproofGens, PedersenGens, RangeProof}; - use curve25519_dalek_ng::ristretto::CompressedRistretto as NgCompressed; - use merlin::Transcript; - if commitments.is_empty() { return Err("empty commitments".into()); } - let proof = RangeProof::from_bytes(proof).map_err(|e| format!("rp deser: {:?}", e))?; - let val_base_b: [u8; 32] = val_base.compress().to_bytes(); - let rand_base_b: [u8; 32] = rand_base.compress().to_bytes(); - let pg = PedersenGens { - B: NgCompressed(val_base_b) - .decompress() - .ok_or("val_base decompress")?, - B_blinding: NgCompressed(rand_base_b) - .decompress() - .ok_or("rand_base decompress")?, - }; - let gens = BulletproofGens::new(64, 16); - let comms: Vec = commitments - .iter() - .map(|c| NgCompressed::from_slice(c.as_slice())) - .collect(); - let dst: &[u8] = b"AptosConfidentialAsset/BulletproofRangeProof"; - let ok = proof - .verify_multiple(&gens, &pg, &mut Transcript::new(dst), &comms, num_bits) - .is_ok(); - Ok(ok) + movement_rp_wasm::rp::_batch_verify_proof( + proof.to_vec(), + commitments.to_vec(), + val_base.compress().to_bytes().to_vec(), + rand_base.compress().to_bytes().to_vec(), + num_bits, + ) + .map_err(|e| format!("batch verify: {}", e)) } /// Convenience for sigma-protocol callers: prove the chunked plaintext values diff --git a/crates/confidential-assets/src/internal/transaction_builder.rs b/crates/confidential-assets/src/internal/transaction_builder.rs index c5829c1..346e936 100644 --- a/crates/confidential-assets/src/internal/transaction_builder.rs +++ b/crates/confidential-assets/src/internal/transaction_builder.rs @@ -8,8 +8,9 @@ //! use super::view_functions::{ - get_balance, get_chain_id_byte_for_proofs, get_encryption_key, - get_global_auditor_encryption_key, is_balance_normalized, is_pending_balance_frozen, + get_asset_auditor_encryption_key, get_balance, get_chain_auditor_encryption_key, + get_chain_id_byte_for_proofs, get_encryption_key, is_balance_normalized, + is_pending_balance_frozen, }; use crate::bcs::serialize_vector_u8; use crate::consts::{ @@ -262,8 +263,15 @@ impl<'a> ConfidentialAssetTransactionBuilder<'a> { let chain_id = get_chain_id_byte_for_proofs(self.client).await?; + // Get global chain auditor public key for the token + let chain_auditor_pub_key = get_chain_auditor_encryption_key( + self.client, + Some(&self.confidential_asset_module_address), + ) + .await?; + // Get auditor public key for the token - let global_auditor_pub_key = get_global_auditor_encryption_key( + let asset_auditor_pub_key = get_asset_auditor_encryption_key( self.client, token_address, Some(&self.confidential_asset_module_address), @@ -311,7 +319,10 @@ impl<'a> ConfidentialAssetTransactionBuilder<'a> { // Assemble auditor keys let mut auditor_keys: Vec = vec![]; - if let Some(auditor) = global_auditor_pub_key { + if let Some(auditor) = chain_auditor_pub_key { + auditor_keys.push(auditor); + } + if let Some(auditor) = asset_auditor_pub_key { auditor_keys.push(auditor); } auditor_keys.extend_from_slice(additional_auditor_encryption_keys); @@ -458,12 +469,20 @@ impl<'a> ConfidentialAssetTransactionBuilder<'a> { .into()) } + /// Get the global chain auditor encryption key for a token, if set. + pub async fn get_chain_auditor_encryption_key( + &self, + ) -> Result, MovementError> { + get_chain_auditor_encryption_key(self.client, Some(&self.confidential_asset_module_address)) + .await + } + /// Get the asset auditor encryption key for a token, if set. pub async fn get_asset_auditor_encryption_key( &self, token_address: &AccountAddress, ) -> Result, MovementError> { - get_global_auditor_encryption_key( + get_asset_auditor_encryption_key( self.client, token_address, Some(&self.confidential_asset_module_address), diff --git a/crates/confidential-assets/src/internal/view_functions.rs b/crates/confidential-assets/src/internal/view_functions.rs index 06fd31d..2719bcb 100644 --- a/crates/confidential-assets/src/internal/view_functions.rs +++ b/crates/confidential-assets/src/internal/view_functions.rs @@ -171,8 +171,37 @@ pub async fn get_encryption_key( .map_err(|e| MovementError::Internal(format!("invalid encryption key: {}", e))) } -/// Get the global auditor encryption key for a token, if set. -pub async fn get_global_auditor_encryption_key( +/// Get the global chain auditor encryption key for a token, if set. +pub async fn get_chain_auditor_encryption_key( + client: &Movement, + module_address: Option<&str>, +) -> Result, MovementError> { + let mod_addr = module_address.unwrap_or(DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS); + + // `get_auditor` returns `Option` — BCS-encoded + let result: Option> = client + .view_bcs(&func_path(mod_addr, "get_chain_auditor"), vec![], vec![]) + .await?; + + match result { + Some(bytes) if !bytes.is_empty() => { + let res = { + let arr: [u8; 32] = bytes + .try_into() + .map_err(|_| MovementError::Internal("auditor key not 32 bytes".to_string()))?; + TwistedEd25519PublicKey::from_bytes(&arr) + }; + match res { + Ok(key) => Ok(Some(key)), + Err(_) => Ok(None), + } + } + _ => Ok(None), + } +} + +/// Get the asset auditor encryption key for a token, if set. +pub async fn get_asset_auditor_encryption_key( client: &Movement, token_address: &AccountAddress, module_address: Option<&str>, @@ -182,7 +211,7 @@ pub async fn get_global_auditor_encryption_key( // `get_auditor` returns `Option` — BCS-encoded let result: Option> = client .view_bcs( - &func_path(mod_addr, "get_auditor"), + &func_path(mod_addr, "get_asset_auditor"), vec![], vec![bcs_address(token_address)], ) diff --git a/crates/confidential-assets/tests/README.md b/crates/confidential-assets/tests/README.md index 7a0e762..cec1210 100644 --- a/crates/confidential-assets/tests/README.md +++ b/crates/confidential-assets/tests/README.md @@ -87,7 +87,7 @@ accepted by the on-chain Move verifier, which is the same code path the TS SDK u exercises. Range proofs are delegated to the upstream `movement_rp_wasm` (same prover the TS SDK builds -as WASM); the verify path uses `bulletproofs::RangeProof::verify_multiple` against the -canonical DST `AptosConfidentialAsset/BulletproofRangeProof`. Pollard kangaroo decryption uses -the upstream `pollard-kangaroo` crate with the `Kangaroo32` preset, matching the TS WASM -module's secret-size assumption. +as WASM); both prove and verify go through its native `rp` module, so the Bulletproofs +transcript DST (`MovementConfidentialAsset/BulletproofRangeProof`) lives only in that crate. +Pollard kangaroo decryption uses the upstream `pollard-kangaroo` crate with the `Kangaroo32` +preset, matching the TS WASM module's secret-size assumption. diff --git a/crates/confidential-assets/tests/e2e/confidential_asset.rs b/crates/confidential-assets/tests/e2e/confidential_asset.rs index 960b76e..739c47d 100644 --- a/crates/confidential-assets/tests/e2e/confidential_asset.rs +++ b/crates/confidential-assets/tests/e2e/confidential_asset.rs @@ -19,15 +19,7 @@ async fn register_alice( alice: &Ed25519Account, alice_dk: &TwistedEd25519PrivateKey, ) { - let ca = make_confidential_asset(movement); - let token = token_address(); - let payload = ca - .register_balance(&alice.address(), &token, alice_dk) - .await - .expect("build register_balance"); - send_and_wait(movement, alice, payload) - .await - .expect("register_balance tx"); + register_account(movement, alice, alice_dk).await; } async fn deposit(movement: &movement_sdk::Movement, alice: &Ed25519Account, amount: u64) { @@ -41,6 +33,36 @@ async fn deposit(movement: &movement_sdk::Movement, alice: &Ed25519Account, amou .expect("deposit tx"); } +/// Register a confidential balance for any account (Alice, Bob, …). +async fn register_account( + movement: &movement_sdk::Movement, + account: &Ed25519Account, + dk: &TwistedEd25519PrivateKey, +) { + let ca = make_confidential_asset(movement); + let token = token_address(); + let payload = ca + .register_balance(&account.address(), &token, dk) + .await + .expect("build register_balance"); + send_and_wait(movement, account, payload) + .await + .expect("register_balance tx"); +} + +/// Create + fund + register a recipient account (e.g. Bob) so it can receive confidential +/// transfers. The contract rejects self-transfers (ESELF_TRANSFER), so transfer tests send to a +/// distinct registered account. Returns the account and its decryption key. +async fn setup_recipient( + movement: &movement_sdk::Movement, +) -> (Ed25519Account, TwistedEd25519PrivateKey) { + let bob = Ed25519Account::generate(); + let bob_dk = get_test_confidential_account(Some(&bob)); + fund_and_migrate(movement, &bob).await.expect("fund bob"); + register_account(movement, &bob, &bob_dk).await; + (bob, bob_dk) +} + /// Deposits a public-FA amount and checks it lands in `pending` (not `available`). #[tokio::test] #[ignore = "requires localnet — see tests/README.md"] @@ -302,11 +324,13 @@ async fn it_throws_when_transferring_more_than_available() { ); } -/// Self-transfer with no auditor: amount moves out of `available` and lands in the -/// sender's own `pending`. Smoke test for the basic transfer-σ + range-proof path. +/// The contract rejects confidential transfers where sender == recipient, aborting on-chain +/// with `ESELF_TRANSFER`. The SDK builds the payload fine (it special-cases the recipient key); +/// the abort happens at execution and leaves balances untouched. Mirrors the TS e2e test +/// "it should reject a self-transfer with ESELF_TRANSFER". #[tokio::test] #[ignore = "requires localnet — see tests/README.md"] -async fn it_transfers_alice_to_alice_pending_no_auditor() { +async fn it_rejects_self_transfer() { let movement = make_movement().expect("movement client"); let alice = get_test_account(); let alice_dk = get_test_confidential_account(Some(&alice)); @@ -324,15 +348,65 @@ async fn it_transfers_alice_to_alice_pending_no_auditor() { send_and_wait(&movement, &alice, p).await.expect("rollover"); } - let pre = ca + // A valid amount, so the build's balance/range checks pass — the only reason this fails is + // the self-transfer ban, not insufficient funds. + let payload = ca + .transfer( + &alice.address(), + &alice.address(), + &token_address(), + TRANSFER_AMOUNT, + &alice_dk, + &[], + &[], + ) + .await + .expect("self-transfer builds; the ban is enforced on-chain"); + let res = send_and_wait(&movement, &alice, payload).await; + assert!( + res.is_err(), + "self-transfer must be rejected on-chain with ESELF_TRANSFER" + ); +} + +/// Transfer Alice → Bob with no auditor: the amount leaves Alice's `available` and lands in +/// Bob's `pending`. Smoke test for the basic transfer-σ + range-proof path. (Self-transfers are +/// rejected on-chain with ESELF_TRANSFER, so the recipient is a distinct registered account.) +#[tokio::test] +#[ignore = "requires localnet — see tests/README.md"] +async fn it_transfers_alice_to_bob_pending_no_auditor() { + let movement = make_movement().expect("movement client"); + let alice = get_test_account(); + let alice_dk = get_test_confidential_account(Some(&alice)); + + fund_and_migrate(&movement, &alice).await.expect("fund"); + register_alice(&movement, &alice, &alice_dk).await; + deposit(&movement, &alice, DEPOSIT_AMOUNT).await; + + let (bob, bob_dk) = setup_recipient(&movement).await; + + let ca = make_confidential_asset(&movement); + let rollovers = ca + .rollover_pending_balance(&alice.address(), &token_address(), None, false) + .await + .expect("rollover build"); + for p in rollovers { + send_and_wait(&movement, &alice, p).await.expect("rollover"); + } + + let alice_pre = ca .get_balance(&alice.address(), &token_address(), &alice_dk) .await - .expect("get_balance"); + .expect("get_balance alice"); + let bob_pre = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); let payload = ca .transfer( &alice.address(), - &alice.address(), + &bob.address(), &token_address(), TRANSFER_AMOUNT, &alice_dk, @@ -345,17 +419,22 @@ async fn it_transfers_alice_to_alice_pending_no_auditor() { .await .expect("transfer tx"); - let post = ca + let alice_post = ca .get_balance(&alice.address(), &token_address(), &alice_dk) .await - .expect("get_balance"); + .expect("get_balance alice"); + let bob_post = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); + // Amount leaves Alice's available and lands in Bob's pending. assert_eq!( - post.available_balance(), - pre.available_balance() - TRANSFER_AMOUNT as u128 + alice_post.available_balance(), + alice_pre.available_balance() - TRANSFER_AMOUNT as u128 ); assert_eq!( - post.pending_balance(), - pre.pending_balance() + TRANSFER_AMOUNT as u128 + bob_post.pending_balance(), + bob_pre.pending_balance() + TRANSFER_AMOUNT as u128 ); } @@ -377,6 +456,8 @@ async fn it_transfers_twice_without_intervening_rollover() { register_alice(&movement, &alice, &alice_dk).await; deposit(&movement, &alice, DEPOSIT_AMOUNT).await; + let (bob, bob_dk) = setup_recipient(&movement).await; + let ca = make_confidential_asset(&movement); let rollovers = ca .rollover_pending_balance(&alice.address(), &token_address(), None, false) @@ -386,15 +467,19 @@ async fn it_transfers_twice_without_intervening_rollover() { send_and_wait(&movement, &alice, p).await.expect("rollover"); } - let pre = ca + let alice_pre = ca .get_balance(&alice.address(), &token_address(), &alice_dk) .await - .expect("get_balance"); + .expect("get_balance alice"); + let bob_pre = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); let payload1 = ca .transfer( &alice.address(), - &alice.address(), + &bob.address(), &token_address(), TRANSFER_AMOUNT, &alice_dk, @@ -411,7 +496,7 @@ async fn it_transfers_twice_without_intervening_rollover() { let payload2 = ca .transfer( &alice.address(), - &alice.address(), + &bob.address(), &token_address(), TRANSFER_AMOUNT, &alice_dk, @@ -424,25 +509,29 @@ async fn it_transfers_twice_without_intervening_rollover() { .await .expect("transfer 2 tx"); - let post = ca + let alice_post = ca .get_balance(&alice.address(), &token_address(), &alice_dk) .await - .expect("get_balance"); + .expect("get_balance alice"); + let bob_post = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); assert_eq!( - post.available_balance(), - pre.available_balance() - 2 * TRANSFER_AMOUNT as u128 + alice_post.available_balance(), + alice_pre.available_balance() - 2 * TRANSFER_AMOUNT as u128 ); assert_eq!( - post.pending_balance(), - pre.pending_balance() + 2 * TRANSFER_AMOUNT as u128 + bob_post.pending_balance(), + bob_pre.pending_balance() + 2 * TRANSFER_AMOUNT as u128 ); } -/// Transfer with one extra auditor key: the σ-proof's `x7` rows and the auditor-amount -/// ciphertexts must verify in the same MSM Move's verifier runs. +/// Transfer Alice → Bob with one extra auditor key: the σ-proof's `x7` rows and the +/// auditor-amount ciphertexts must verify in the same MSM Move's verifier runs. #[tokio::test] #[ignore = "requires localnet — see tests/README.md"] -async fn it_transfers_alice_to_alice_with_auditor() { +async fn it_transfers_alice_to_bob_with_auditor() { let movement = make_movement().expect("movement client"); let alice = get_test_account(); let alice_dk = get_test_confidential_account(Some(&alice)); @@ -452,6 +541,8 @@ async fn it_transfers_alice_to_alice_with_auditor() { register_alice(&movement, &alice, &alice_dk).await; deposit(&movement, &alice, DEPOSIT_AMOUNT).await; + let (bob, bob_dk) = setup_recipient(&movement).await; + let ca = make_confidential_asset(&movement); let rollovers = ca .rollover_pending_balance(&alice.address(), &token_address(), None, false) @@ -461,10 +552,15 @@ async fn it_transfers_alice_to_alice_with_auditor() { send_and_wait(&movement, &alice, p).await.expect("rollover"); } + let bob_pre = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); + let payload = ca .transfer( &alice.address(), - &alice.address(), + &bob.address(), &token_address(), TRANSFER_AMOUNT, &alice_dk, @@ -476,6 +572,15 @@ async fn it_transfers_alice_to_alice_with_auditor() { send_and_wait(&movement, &alice, payload) .await .expect("transfer tx"); + + let bob_post = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); + assert_eq!( + bob_post.pending_balance(), + bob_pre.pending_balance() + TRANSFER_AMOUNT as u128 + ); } /// A freshly-registered account's pending balance is not frozen. @@ -726,17 +831,24 @@ async fn it_transfers_with_total_balance_after_deposit() { register_alice(&movement, &alice, &alice_dk).await; deposit(&movement, &alice, DEPOSIT_AMOUNT).await; + let (bob, bob_dk) = setup_recipient(&movement).await; + let ca = make_confidential_asset(&movement); - let pre = ca + let alice_pre = ca .get_balance(&alice.address(), &token_address(), &alice_dk) .await - .expect("get_balance"); - let transfer_amount = pre.available_balance() as u64 + 1; + .expect("get_balance alice"); + let bob_pre = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); + // Force the auto-rollover path: spend more than `available`, drawing from pending too. + let transfer_amount = alice_pre.available_balance() as u64 + 1; let transfer_payload = ca .transfer_with_total_balance( &alice, - &alice.address(), + &bob.address(), &token_address(), transfer_amount, &alice_dk, @@ -749,10 +861,22 @@ async fn it_transfers_with_total_balance_after_deposit() { .await .expect("transfer-with-total tx"); - let post = ca + let alice_post = ca .get_balance(&alice.address(), &token_address(), &alice_dk) .await - .expect("get_balance"); - let pre_total = pre.available_balance() + pre.pending_balance(); - assert_eq!(post.available_balance() + post.pending_balance(), pre_total); + .expect("get_balance alice"); + let bob_post = ca + .get_balance(&bob.address(), &token_address(), &bob_dk) + .await + .expect("get_balance bob"); + // Alice's total (available + pending) drops by the transfer amount; it lands in Bob's pending. + let alice_pre_total = alice_pre.available_balance() + alice_pre.pending_balance(); + assert_eq!( + alice_post.available_balance() + alice_post.pending_balance(), + alice_pre_total - transfer_amount as u128 + ); + assert_eq!( + bob_post.pending_balance(), + bob_pre.pending_balance() + transfer_amount as u128 + ); } diff --git a/crates/confidential-assets/tests/e2e/helpers.rs b/crates/confidential-assets/tests/e2e/helpers.rs index 072cd57..048e8bd 100644 --- a/crates/confidential-assets/tests/e2e/helpers.rs +++ b/crates/confidential-assets/tests/e2e/helpers.rs @@ -19,8 +19,10 @@ pub const FUND_AMOUNT: u64 = 1_000_000_000; pub fn module_address() -> String { let raw = env::var("CONFIDENTIAL_MODULE_ADDRESS").expect( - "CONFIDENTIAL_MODULE_ADDRESS env var is required for e2e tests \ - (run scripts/start-localnet-confidential-assets.sh and export the printed module address)", + "CONFIDENTIAL_MODULE_ADDRESS env var is required for e2e tests (run \ + scripts/start-localnet-confidential-assets.sh). confidential_asset now ships in the \ + framework, so this is 0x1 in full 32-byte form: \ + 0x0000000000000000000000000000000000000000000000000000000000000001", ); let hex = raw.trim_start_matches("0x"); assert_eq!( diff --git a/crates/confidential-assets/tests/e2e/txn_builder.rs b/crates/confidential-assets/tests/e2e/txn_builder.rs index c1163fa..c0fe0d6 100644 --- a/crates/confidential-assets/tests/e2e/txn_builder.rs +++ b/crates/confidential-assets/tests/e2e/txn_builder.rs @@ -178,26 +178,37 @@ async fn builder_withdraws_alices_balance() { .expect("withdraw tx"); } -/// Builder-level self-transfer: produces a transfer payload accepted by Move's -/// `verify_transfer_sigma_proof` (basic positive case for the raw builder API). +/// Builder-level transfer Alice → Bob: produces a transfer payload accepted by Move's +/// `verify_transfer_sigma_proof` (basic positive case for the raw builder API). A distinct +/// recipient is required — the contract rejects self-transfers with ESELF_TRANSFER. #[tokio::test] #[ignore = "requires localnet — see tests/README.md"] -async fn builder_transfers_to_self() { +async fn builder_transfers_to_bob() { let movement = make_movement().expect("movement client"); let alice = get_test_account(); let alice_dk = get_test_confidential_account(Some(&alice)); + let bob = Ed25519Account::generate(); + let bob_dk = get_test_confidential_account(Some(&bob)); let module = module_address(); let builder = ConfidentialAssetTransactionBuilder::new(&movement, Some(&module)) .expect("valid module address"); - fund_and_migrate(&movement, &alice).await.expect("fund"); - let r = builder - .register_balance(&alice.address(), &token_address(), &alice_dk) - .await - .expect("build register"); - send_and_wait(&movement, &alice, r) + fund_and_migrate(&movement, &alice) .await - .expect("register tx"); + .expect("fund alice"); + fund_and_migrate(&movement, &bob).await.expect("fund bob"); + + // Register both Alice (sender) and Bob (recipient) confidential balances. + for (acct, dk) in [(&alice, &alice_dk), (&bob, &bob_dk)] { + let r = builder + .register_balance(&acct.address(), &token_address(), dk) + .await + .expect("build register"); + send_and_wait(&movement, acct, r) + .await + .expect("register tx"); + } + let dep = builder .deposit(&alice.address(), &token_address(), DEPOSIT_AMOUNT, None) .expect("build deposit"); @@ -215,7 +226,7 @@ async fn builder_transfers_to_self() { let transfer = builder .transfer( &alice.address(), - &alice.address(), + &bob.address(), &token_address(), TRANSFER_AMOUNT, &alice_dk,