diff --git a/Cargo.lock b/Cargo.lock index aa5da02afb8..4fceac9bead 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7935,11 +7935,13 @@ dependencies = [ "bcs 0.1.4", "claims", "hex", + "legacy-move-compiler", "move-binary-format", "move-core-types", "move-model", "move-package", "move-symbol-pool", + "move-vm-runtime", "once_cell", "project-root", "proptest", diff --git a/aptos-move/e2e-move-tests/Cargo.toml b/aptos-move/e2e-move-tests/Cargo.toml index b7f49974f51..c3ed364f96b 100644 --- a/aptos-move/e2e-move-tests/Cargo.toml +++ b/aptos-move/e2e-move-tests/Cargo.toml @@ -32,11 +32,13 @@ aptos-vm-environment = { workspace = true } bcs = { workspace = true } claims = { workspace = true } hex = { workspace = true } +legacy-move-compiler = { workspace = true } move-binary-format = { workspace = true } move-core-types = { workspace = true } move-model = { workspace = true } move-package = { workspace = true } move-symbol-pool = { workspace = true } +move-vm-runtime = { workspace = true } once_cell = { workspace = true } project-root = { workspace = true } proptest = { workspace = true } diff --git a/aptos-move/e2e-move-tests/README.md b/aptos-move/e2e-move-tests/README.md index f025e0ca31a..f1786ea8133 100644 --- a/aptos-move/e2e-move-tests/README.md +++ b/aptos-move/e2e-move-tests/README.md @@ -1,5 +1,14 @@ # e2e-move-tests +## Confidential assets (`confidential_asset_e2e`) + +These tests compile large Move bundles; if you see a **stack overflow** on the test thread, raise the stack before running, for example: + +```bash +export RUST_MIN_STACK=8388608 # 8 MiB; same order as MSVC `/STACK:8000000` for Windows hosts +cargo test -p e2e-move-tests confidential_asset +``` + ## Keyless To run the keyless VM tests: diff --git a/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs b/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs new file mode 100644 index 00000000000..e0a769879b9 --- /dev/null +++ b/aptos-move/e2e-move-tests/src/tests/confidential_asset.rs @@ -0,0 +1,1315 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 +// +// If `cargo test` fails with a stack overflow on this module, set `RUST_MIN_STACK` (see +// `aptos-move/e2e-move-tests/README.md`) and re-run. +// +// VM-level confidential-asset checks for this fork. Scenarios are written against the behavior +// documented in `aptos_framework::confidential_asset` (e.g. `validate_auditors`, entry +// signatures)—not transcribed from other repositories' test code. +// +// The harness hot-swaps all `0x1` modules from a test-mode compile of `aptos-stdlib` (MoveStdlib + +// AptosStdlib, so `ristretto255::random_scalar` and friends resolve consistently), then overlays the +// confidential-asset module family (`confidential_asset`, `confidential_balance`, +// `confidential_proof`, `ristretto255_twisted_elgamal`, `confidential_gas_e2e_helpers`) plus +// `event` from a test-mode compile of `aptos-framework`, so the bytecode of those modules matches +// what `confidential_asset` was compiled against and `event::emitted_events` resolves. We deliberately +// do NOT replace other 0x1 framework modules — doing so swaps account/fungible-store/transaction- +// validation layouts out from under state that genesis already published, breaking gas-fee +// prologue reads. Genesis already publishes `GlobalConfig` for an older bytecode revision; we +// delete that resource and re-run `init_module_for_testing` so on-disk layout matches the +// injected `confidential_asset` module. + +use crate::{tests::common::framework_dir_path, MoveHarness}; +use aptos_language_e2e_tests::account::Account; +use aptos_types::{ + account_address::AccountAddress, + on_chain_config::FeatureFlag, + state_store::state_key::StateKey, + transaction::{ + EntryFunction, ExecutionStatus, TransactionPayload, TransactionStatus, + }, + write_set::{WriteOp, WriteSetMut}, +}; +use legacy_move_compiler::compiled_unit::{CompiledUnit, NamedCompiledModule}; +use move_binary_format::file_format_common::VERSION_MAX; +use move_core_types::{ + identifier::Identifier, + language_storage::{ModuleId, StructTag, TypeTag}, + value::MoveValue, +}; +use move_model::metadata::{CompilerVersion, LanguageVersion}; +use move_package::BuildConfig; +use move_vm_runtime::move_vm::SerializedReturnValues; +use once_cell::sync::OnceCell; +use std::collections::BTreeMap; + +const APTOS_FRAMEWORK: AccountAddress = AccountAddress::ONE; +/// Published fungible metadata object for gas/APT in test genesis. +const MOVE_METADATA: AccountAddress = AccountAddress::new({ + let mut b = [0u8; AccountAddress::LENGTH]; + b[31] = 0x0a; + b +}); + +static CONFIDENTIAL_E2E_INJECT_MODULES: OnceCell)>> = OnceCell::new(); + +/// `generate_twisted_elgamal_keypair` is `#[test_only]` and needs `ristretto255::random_scalar` (also +/// `#[test_only]`). Injecting only `ristretto255` breaks verification (imports into other `0x1` deps); +/// we replace every `0x1` module produced by compiling `aptos-stdlib` + its dependency tree. +fn move_test_build_config() -> BuildConfig { + let mut build_config = BuildConfig::default(); + build_config.test_mode = true; + build_config.dev_mode = false; + build_config.skip_fetch_latest_git_deps = true; + build_config.additional_named_addresses.insert( + "aptos_framework".to_string(), + APTOS_FRAMEWORK, + ); + build_config.compiler_config.bytecode_version = Some(VERSION_MAX); + build_config.compiler_config.language_version = Some(LanguageVersion::latest()); + build_config.compiler_config.compiler_version = Some(CompilerVersion::latest()); + build_config.compiler_config.skip_attribute_checks = true; + build_config +} + +fn ca_module_id() -> ModuleId { + ModuleId::new( + APTOS_FRAMEWORK, + Identifier::new("confidential_asset").unwrap(), + ) +} + +fn compile_stdlib_inject_modules() -> Vec<(ModuleId, Vec)> { + let pkg = framework_dir_path("aptos-stdlib"); + let build_config = move_test_build_config(); + let mut stderr = Vec::::new(); + let resolved_graph = build_config + .clone() + .resolution_graph_for_package(&pkg, &mut stderr) + .unwrap_or_else(|e| { + panic!( + "resolve aptos-stdlib: {:?}\n{}", + e, + String::from_utf8_lossy(&stderr) + ) + }); + let (compiled, _) = build_config + .compile_package_no_exit(resolved_graph, vec![], &mut stderr) + .unwrap_or_else(|e| { + panic!( + "compile aptos-stdlib: {:?}\n{}", + e, + String::from_utf8_lossy(&stderr) + ) + }); + + let mut out = Vec::new(); + for unit in compiled.all_modules() { + if let CompiledUnit::Module(NamedCompiledModule { module, .. }) = &unit.unit { + let id = module.self_id(); + if id.address() != &AccountAddress::ONE { + continue; + } + let bytes = unit.unit.serialize(Some(module.version)); + out.push((id, bytes)); + } + } + out.sort_by(|a, b| a.0.name().as_str().cmp(b.0.name().as_str())); + assert!( + !out.is_empty(), + "expected at least one 0x1 module from aptos-stdlib test build" + ); + out +} + +/// Confidential-asset module family (the modules that moved from `aptos-experimental` 0x7 into +/// `aptos-framework` 0x1). Only these — plus `event` — get overlaid from the framework test build; +/// replacing other framework modules would invalidate state genesis already published. +const CONFIDENTIAL_FRAMEWORK_MODULES: &[&str] = &[ + "confidential_asset", + "confidential_balance", + "confidential_gas_e2e_helpers", + "confidential_proof", + "event", + "ristretto255_twisted_elgamal", +]; + +fn compile_framework_inject_modules() -> Vec<(ModuleId, Vec)> { + let pkg = framework_dir_path("aptos-framework"); + let build_config = move_test_build_config(); + + let mut stderr = Vec::::new(); + let resolved_graph = build_config + .clone() + .resolution_graph_for_package(&pkg, &mut stderr) + .unwrap_or_else(|e| { + panic!( + "resolve aptos-framework: {:?}\n{}", + e, + String::from_utf8_lossy(&stderr) + ) + }); + let (compiled, _) = build_config + .compile_package_no_exit(resolved_graph, vec![], &mut stderr) + .unwrap_or_else(|e| { + panic!( + "compile aptos-framework: {:?}\n{}", + e, + String::from_utf8_lossy(&stderr) + ) + }); + + let mut out = Vec::new(); + for unit in compiled.all_modules() { + if let CompiledUnit::Module(NamedCompiledModule { module, .. }) = &unit.unit { + let id = module.self_id(); + if id.address() == &APTOS_FRAMEWORK + && CONFIDENTIAL_FRAMEWORK_MODULES.contains(&id.name().as_str()) + { + let bytes = unit.unit.serialize(Some(module.version)); + out.push((id, bytes)); + } + } + } + out.sort_by(|a, b| a.0.name().as_str().cmp(b.0.name().as_str())); + for required in CONFIDENTIAL_FRAMEWORK_MODULES { + assert!( + out.iter().any(|(id, _)| id.name().as_str() == *required), + "aptos-framework compile graph missing 0x1::{required}" + ); + } + out +} + +fn compile_confidential_e2e_inject_modules() -> Vec<(ModuleId, Vec)> { + let mut by_id: BTreeMap> = + compile_stdlib_inject_modules().into_iter().collect(); + for (id, bytes) in compile_framework_inject_modules() { + by_id.insert(id, bytes); + } + let mut v: Vec<(ModuleId, Vec)> = by_id.into_iter().collect(); + v.sort_by(|a, b| a.0.name().as_str().cmp(b.0.name().as_str())); + v +} + +fn inject_confidential_e2e_modules(h: &mut MoveHarness) { + let blobs = CONFIDENTIAL_E2E_INJECT_MODULES.get_or_init(compile_confidential_e2e_inject_modules); + for (id, bytes) in blobs { + h.executor.add_module(&id, bytes.clone()); + } +} + +fn enable_confidential_features(h: &mut MoveHarness) { + h.enable_features( + vec![ + FeatureFlag::BULLETPROOFS_NATIVES, + FeatureFlag::BULLETPROOFS_BATCH_NATIVES, + FeatureFlag::NEW_ACCOUNTS_DEFAULT_TO_FA_APT_STORE, + ], + vec![], + ); +} + +/// Decode a `vector` value as returned by the VM (`bcs::to_bytes` of `Vec`). +fn raw_bytes_from_move_vector_u8(blob: &[u8]) -> Vec { + bcs::from_bytes::>(blob).expect("decode move vector") +} + +fn assert_kept_success(status: &TransactionStatus, ctx: &str) { + assert!( + matches!( + status, + TransactionStatus::Keep(ExecutionStatus::Success) + ), + "{ctx}: unexpected status {status:?}" + ); +} + +fn assert_kept_failure(status: &TransactionStatus, ctx: &str) { + match status { + TransactionStatus::Keep(ExecutionStatus::Success) => { + panic!("{ctx}: expected kept failure, got success") + } + TransactionStatus::Keep(_) => {} + other => panic!("{ctx}: expected kept failure, got {other:?}"), + } +} + +/// Deterministic addresses for matrix cases (avoid reusing state across scenarios). +fn confidential_e2e_addr(tag: u8, idx: u8) -> AccountAddress { + let mut b = [0u8; AccountAddress::LENGTH]; + b[30] = tag; + b[31] = idx; + AccountAddress::new(b) +} + +fn bcs_auditor_pubkeys_from_ek_structs(h: &mut MoveHarness, ek_structs: &[Vec]) -> Vec> { + ek_structs + .iter() + .map(|ek| twisted_pubkey_bytes(h, ek)) + .collect() +} + +fn bypass_at( + h: &mut MoveHarness, + module: &str, + fun: &str, + ty_args: Vec, + args: Vec>, +) -> SerializedReturnValues { + h.executor + .try_exec_function_bypass_at( + APTOS_FRAMEWORK, + module, + fun, + ty_args, + args, + ) + .unwrap_or_else(|e| panic!("bypass {module}::{fun}: {e:?}")) +} + +/// Genesis `head` publishes `GlobalConfig` for bytecode that may differ from the injected module; +/// remove it so `init_module` can republish with a matching layout. +fn delete_genesis_global_config_if_present(h: &mut MoveHarness) { + let tag = StructTag { + address: APTOS_FRAMEWORK, + module: Identifier::new("confidential_asset").unwrap(), + name: Identifier::new("GlobalConfig").unwrap(), + type_args: vec![], + }; + let key = StateKey::resource(&APTOS_FRAMEWORK, &tag).unwrap(); + if h.executor.read_state_value(&key).is_none() { + return; + } + let mut w = WriteSetMut::default(); + w.insert((key, WriteOp::legacy_deletion())); + let ws = w.freeze().expect("writeset freeze"); + h.executor.apply_write_set(&ws); +} + +fn reinit_confidential_asset_module(h: &mut MoveHarness) { + let signer_arg = MoveValue::Signer(APTOS_FRAMEWORK) + .simple_serialize() + .expect("signer arg"); + let _ = bypass_at( + h, + "confidential_asset", + "init_module_for_testing", + vec![], + vec![signer_arg], + ); +} + +fn generate_elgamal_keypair(h: &mut MoveHarness) -> (Vec, Vec) { + let ret = bypass_at(h, "ristretto255_twisted_elgamal", "generate_twisted_elgamal_keypair", vec![], vec![]); + assert_eq!(ret.return_values.len(), 2, "keypair return arity"); + ( + ret.return_values[0].0.clone(), + ret.return_values[1].0.clone(), + ) +} + +/// `register` / auditors expect the 32-byte compressed point; keygen returns full `CompressedPubkey` BCS. +fn twisted_pubkey_bytes(h: &mut MoveHarness, compressed_pubkey_struct: &[u8]) -> Vec { + let ret = bypass_at( + h, + "ristretto255_twisted_elgamal", + "pubkey_to_bytes", + vec![], + vec![compressed_pubkey_struct.to_vec()], + ); + assert_eq!(ret.return_values.len(), 1); + ret.return_values[0].0.clone() +} + +fn prove_registration_parts( + h: &mut MoveHarness, + chain_byte: u8, + user: AccountAddress, + dk: &[u8], + ek: &[u8], + token: AccountAddress, +) -> (Vec, Vec) { + let args = vec![ + bcs::to_bytes(&chain_byte).unwrap(), + bcs::to_bytes(&user).unwrap(), + bcs::to_bytes(&APTOS_FRAMEWORK).unwrap(), + dk.to_vec(), + ek.to_vec(), + bcs::to_bytes(&token).unwrap(), + ]; + let ret = bypass_at(h, "confidential_proof", "prove_registration", vec![], args); + assert_eq!(ret.return_values.len(), 2); + ( + ret.return_values[0].0.clone(), + ret.return_values[1].0.clone(), + ) +} + +fn run_register( + h: &mut MoveHarness, + account: &Account, + ek_pubkey_32: &[u8], + comm: &[u8], + resp: &[u8], +) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("register").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + // `vector` returns from the VM are already BCS (ULEB length + bytes); do not wrap again. + ek_pubkey_32.to_vec(), + comm.to_vec(), + resp.to_vec(), + ], + )); + let txn = h.create_transaction_payload(account, payload); + h.run(txn) +} + +fn run_deposit(h: &mut MoveHarness, account: &Account, amount: u64) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("deposit").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&amount).unwrap(), + ], + )); + let txn = h.create_transaction_payload(account, payload); + h.run(txn) +} + +fn run_deposit_to( + h: &mut MoveHarness, + sender: &Account, + to: AccountAddress, + amount: u64, +) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("deposit_to").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&to).unwrap(), + bcs::to_bytes(&amount).unwrap(), + ], + )); + let txn = h.create_transaction_payload(sender, payload); + h.run(txn) +} + +fn run_rollover(h: &mut MoveHarness, account: &Account) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("rollover_pending_balance").unwrap(), + vec![], + vec![bcs::to_bytes(&MOVE_METADATA).unwrap()], + )); + let txn = h.create_transaction_payload(account, payload); + h.run(txn) +} + +fn run_normalize_and_rollover( + h: &mut MoveHarness, + account: &Account, + new_bal: &[u8], + zkrp: &[u8], + sigma: &[u8], +) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("normalize_and_rollover_pending_balance").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + new_bal.to_vec(), + zkrp.to_vec(), + sigma.to_vec(), + ], + )); + let txn = h.create_transaction_payload(account, payload); + h.run(txn) +} + +fn set_asset_auditor(h: &mut MoveHarness, auditor_pubkey_32: &[u8]) { + let args = vec![ + MoveValue::Signer(AccountAddress::ONE) + .simple_serialize() + .unwrap(), + bcs::to_bytes(&MOVE_METADATA).unwrap(), + auditor_pubkey_32.to_vec(), + ]; + bypass_at( + h, + "confidential_asset", + "set_asset_auditor", + vec![], + args, + ); +} + +fn set_chain_auditor(h: &mut MoveHarness, auditor_pubkey_32: &[u8]) { + let args = vec![ + MoveValue::Signer(AccountAddress::ONE) + .simple_serialize() + .unwrap(), + auditor_pubkey_32.to_vec(), + ]; + bypass_at( + h, + "confidential_asset", + "set_chain_auditor", + vec![], + args, + ); +} + +/// Designates `admin_addr` as the chain-auditor admin (governance-only path). Required +/// before `set_chain_auditor` will accept the corresponding signer; governance no longer +/// holds chain-auditor authority directly. +fn set_chain_auditor_admin(h: &mut MoveHarness, admin_addr: AccountAddress) { + let args = vec![ + MoveValue::Signer(AccountAddress::ONE) + .simple_serialize() + .unwrap(), + bcs::to_bytes(&admin_addr).unwrap(), + ]; + bypass_at( + h, + "confidential_asset", + "set_chain_auditor_admin", + vec![], + args, + ); +} + +/// Generates a fresh chain auditor keypair and installs it via `set_chain_auditor`. +/// Used by `fresh_harness` so every confidential transfer in the test suite has a +/// valid `auditor_eks[0]` available; tests that need to exercise rotation can call +/// this again to install a successor. Designates `@0x1` as the chain-auditor admin so +/// the bypass path can subsequently invoke `set_chain_auditor` with the framework +/// signer; production deployments would point this at a dedicated admin account. +fn install_default_chain_auditor(h: &mut MoveHarness) { + set_chain_auditor_admin(h, AccountAddress::ONE); + let (_chain_dk, chain_ek) = generate_elgamal_keypair(h); + let chain_pk = twisted_pubkey_bytes(h, &chain_ek); + set_chain_auditor(h, &chain_pk); +} + +fn pack_transfer_simple( + h: &mut MoveHarness, + chain_byte: u8, + sender: AccountAddress, + recipient: AccountAddress, + dk: &[u8], + amount: u64, + new_balance: u128, + sender_auditor_hint: Vec, +) -> [Vec; 8] { + let args = vec![ + bcs::to_bytes(&chain_byte).unwrap(), + bcs::to_bytes(&sender).unwrap(), + bcs::to_bytes(&recipient).unwrap(), + dk.to_vec(), + bcs::to_bytes(&amount).unwrap(), + bcs::to_bytes(&new_balance).unwrap(), + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&sender_auditor_hint).unwrap(), + ]; + let ret = bypass_at( + h, + "confidential_gas_e2e_helpers", + "pack_confidential_transfer_proof_simple", + vec![], + args, + ); + assert_eq!(ret.return_values.len(), 8); + std::array::from_fn(|i| ret.return_values[i].0.clone()) +} + +fn pack_transfer_audited_verbatim( + h: &mut MoveHarness, + chain_byte: u8, + sender: AccountAddress, + recipient: AccountAddress, + dk: &[u8], + amount: u64, + new_balance: u128, + auditor_eks: Vec>, + sender_auditor_hint: Vec, +) -> [Vec; 8] { + let auditor_inner: Vec> = auditor_eks + .iter() + .map(|b| raw_bytes_from_move_vector_u8(b)) + .collect(); + let args = vec![ + bcs::to_bytes(&chain_byte).unwrap(), + bcs::to_bytes(&sender).unwrap(), + bcs::to_bytes(&recipient).unwrap(), + dk.to_vec(), + bcs::to_bytes(&amount).unwrap(), + bcs::to_bytes(&new_balance).unwrap(), + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&auditor_inner).unwrap(), + bcs::to_bytes(&sender_auditor_hint).unwrap(), + ]; + let ret = bypass_at( + h, + "confidential_gas_e2e_helpers", + "pack_confidential_transfer_proof_verbatim", + vec![], + args, + ); + assert_eq!(ret.return_values.len(), 8); + std::array::from_fn(|i| ret.return_values[i].0.clone()) +} + +fn pack_transfer_audited( + h: &mut MoveHarness, + chain_byte: u8, + sender: AccountAddress, + recipient: AccountAddress, + dk: &[u8], + amount: u64, + new_balance: u128, + auditor_eks: Vec>, + sender_auditor_hint: Vec, +) -> [Vec; 8] { + let auditor_inner: Vec> = auditor_eks + .iter() + .map(|b| raw_bytes_from_move_vector_u8(b)) + .collect(); + let args = vec![ + bcs::to_bytes(&chain_byte).unwrap(), + bcs::to_bytes(&sender).unwrap(), + bcs::to_bytes(&recipient).unwrap(), + dk.to_vec(), + bcs::to_bytes(&amount).unwrap(), + bcs::to_bytes(&new_balance).unwrap(), + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&auditor_inner).unwrap(), + bcs::to_bytes(&sender_auditor_hint).unwrap(), + ]; + let ret = bypass_at( + h, + "confidential_gas_e2e_helpers", + "pack_confidential_transfer_proof_with_auditors", + vec![], + args, + ); + assert_eq!(ret.return_values.len(), 8); + std::array::from_fn(|i| ret.return_values[i].0.clone()) +} + +fn run_confidential_transfer( + h: &mut MoveHarness, + sender: &Account, + recipient: AccountAddress, + parts: &[Vec; 8], + sender_auditor_hint: Vec, +) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("confidential_transfer").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&recipient).unwrap(), + parts[0].clone(), + parts[1].clone(), + parts[2].clone(), + parts[3].clone(), + parts[4].clone(), + parts[5].clone(), + parts[6].clone(), + parts[7].clone(), + bcs::to_bytes(&sender_auditor_hint).unwrap(), + ], + )); + let txn = h.create_transaction_payload(sender, payload); + h.run(txn) +} + +fn pack_withdraw( + h: &mut MoveHarness, + chain_byte: u8, + sender: AccountAddress, + dk: &[u8], + ek_struct: &[u8], + withdraw_amt: u64, + new_balance: u128, +) -> (Vec, Vec, Vec) { + let args = vec![ + bcs::to_bytes(&chain_byte).unwrap(), + bcs::to_bytes(&sender).unwrap(), + dk.to_vec(), + ek_struct.to_vec(), + bcs::to_bytes(&withdraw_amt).unwrap(), + bcs::to_bytes(&new_balance).unwrap(), + bcs::to_bytes(&MOVE_METADATA).unwrap(), + ]; + let ret = bypass_at( + h, + "confidential_gas_e2e_helpers", + "pack_withdraw_to_proof", + vec![], + args, + ); + assert_eq!(ret.return_values.len(), 3); + ( + ret.return_values[0].0.clone(), + ret.return_values[1].0.clone(), + ret.return_values[2].0.clone(), + ) +} + +fn run_withdraw( + h: &mut MoveHarness, + sender: &Account, + amount: u64, + new_bal: &[u8], + zkrp: &[u8], + sigma: &[u8], +) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("withdraw").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&amount).unwrap(), + new_bal.to_vec(), + zkrp.to_vec(), + sigma.to_vec(), + ], + )); + let txn = h.create_transaction_payload(sender, payload); + h.run(txn) +} + +fn pack_normalize( + h: &mut MoveHarness, + chain_byte: u8, + sender: AccountAddress, + dk: &[u8], + amount: u128, +) -> (Vec, Vec, Vec) { + let args = vec![ + bcs::to_bytes(&chain_byte).unwrap(), + bcs::to_bytes(&sender).unwrap(), + dk.to_vec(), + bcs::to_bytes(&amount).unwrap(), + bcs::to_bytes(&MOVE_METADATA).unwrap(), + ]; + let ret = bypass_at( + h, + "confidential_gas_e2e_helpers", + "pack_normalization_proof", + vec![], + args, + ); + assert_eq!(ret.return_values.len(), 3); + ( + ret.return_values[0].0.clone(), + ret.return_values[1].0.clone(), + ret.return_values[2].0.clone(), + ) +} + +fn fresh_harness() -> MoveHarness { + let mut h = MoveHarness::new(); + enable_confidential_features(&mut h); + delete_genesis_global_config_if_present(&mut h); + inject_confidential_e2e_modules(&mut h); + reinit_confidential_asset_module(&mut h); + // Every confidential transfer requires a chain-level auditor; install a deterministic + // throwaway one here so individual tests don't need to know about it. Tests that + // exercise the unset state should call `delete_genesis_global_config_if_present` + + // `reinit_confidential_asset_module` themselves to get a clean slate. + install_default_chain_auditor(&mut h); + h +} + +// --- Comprehensive scenarios (auditors, withdrawals, validation errors) --- + +#[test] +fn confidential_transfer_with_voluntary_auditors_only() { + for num_voluntary in 1u8..=3 { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xE1, num_voluntary); + let bob_addr = confidential_e2e_addr(0xE2, num_voluntary); + 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"); + } + + let mut vol_eks = Vec::>::new(); + for _ in 0..num_voluntary { + let (_dk, ek) = generate_elgamal_keypair(&mut h); + vol_eks.push(ek); + } + let vol_pks = bcs_auditor_pubkeys_from_ek_structs(&mut h, &vol_eks); + + assert_kept_success(&run_deposit(&mut h, &alice, 8_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + let xfer = 200u64; + let mut remaining: u128 = 8_000 - xfer as u128; + let parts = pack_transfer_audited( + &mut h, + chain, + alice_addr, + bob_addr, + &alice_dk, + xfer, + remaining, + vol_pks, + vec![], + ); + assert_kept_success( + &run_confidential_transfer(&mut h, &alice, bob_addr, &parts, vec![]), + &format!("transfer {num_voluntary} voluntary auditors"), + ); + + remaining -= xfer as u128; + let vol_eks2: Vec> = (0..num_voluntary) + .map(|_| generate_elgamal_keypair(&mut h).1) + .collect(); + let vol_pks2 = bcs_auditor_pubkeys_from_ek_structs(&mut h, &vol_eks2); + let parts2 = pack_transfer_audited( + &mut h, + chain, + alice_addr, + bob_addr, + &alice_dk, + xfer, + remaining, + vol_pks2, + vec![], + ); + assert_kept_success( + &run_confidential_transfer(&mut h, &alice, bob_addr, &parts2, vec![]), + "second transfer (new voluntary auditor set)", + ); + } +} + +#[test] +fn confidential_transfer_asset_auditor_plus_voluntary_auditors() { + for num_voluntary in 0u8..=3 { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xE3, num_voluntary); + let bob_addr = confidential_e2e_addr(0xE4, num_voluntary); + let alice = h.new_account_with_balance_at(alice_addr, 60_000_000_000_000); + let bob = h.new_account_with_balance_at(bob_addr, 1_000_000_000); + + let (_asset_dk, asset_ek) = generate_elgamal_keypair(&mut h); + let asset_pk = twisted_pubkey_bytes(&mut h, &asset_ek); + set_asset_auditor(&mut h, &asset_pk); + + 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"); + } + + let mut auditor_keys = vec![asset_pk.clone()]; + let mut vol_structs = Vec::new(); + for _ in 0..num_voluntary { + vol_structs.push(generate_elgamal_keypair(&mut h).1); + } + auditor_keys.extend(bcs_auditor_pubkeys_from_ek_structs(&mut h, &vol_structs)); + + assert_kept_success(&run_deposit(&mut h, &alice, 9_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + let xfer = 300u64; + let remaining: u128 = 9_000 - xfer as u128; + let parts = pack_transfer_audited( + &mut h, + chain, + alice_addr, + bob_addr, + &alice_dk, + xfer, + remaining, + auditor_keys, + vec![], + ); + assert_kept_success( + &run_confidential_transfer(&mut h, &alice, bob_addr, &parts, vec![]), + &format!("audited transfer asset auditor + {num_voluntary} voluntary"), + ); + } +} + +#[test] +fn confidential_transfer_rejects_empty_auditors_when_asset_auditor_set() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xE7, 1); + let bob_addr = confidential_e2e_addr(0xE7, 2); + 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 (_aud_dk, aud_ek) = generate_elgamal_keypair(&mut h); + let aud_pk = twisted_pubkey_bytes(&mut h, &aud_ek); + set_asset_auditor(&mut h, &aud_pk); + + 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, 2_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, + 100, + 1900, + vec![], + ); + let st = run_confidential_transfer(&mut h, &alice, bob_addr, &parts, vec![]); + assert_kept_failure(&st, "transfer with zero auditors in proof when asset auditor required"); +} + +#[test] +fn confidential_transfer_rejects_non_matching_asset_auditor_pubkey() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xE8, 1); + let bob_addr = confidential_e2e_addr(0xE8, 2); + 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 (_real_aud_dk, real_aud_ek) = generate_elgamal_keypair(&mut h); + let _real_aud_pk = twisted_pubkey_bytes(&mut h, &real_aud_ek); + set_asset_auditor(&mut h, &_real_aud_pk); + + let (_wrong_dk, wrong_ek) = generate_elgamal_keypair(&mut h); + let wrong_pk = twisted_pubkey_bytes(&mut h, &wrong_ek); + + 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, 2_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + let parts = pack_transfer_audited( + &mut h, + chain, + alice_addr, + bob_addr, + &alice_dk, + 100, + 1900, + vec![wrong_pk], + vec![], + ); + let st = run_confidential_transfer(&mut h, &alice, bob_addr, &parts, vec![]); + assert_kept_failure(&st, "first auditor EK must match asset auditor"); +} + +// --- Chain-level auditor scenarios --- + +/// Harness variant that *omits* the default chain-auditor install. Tests use this when they +/// need to exercise either the unset state or installation timing. +fn fresh_harness_no_chain_auditor() -> MoveHarness { + let mut h = MoveHarness::new(); + enable_confidential_features(&mut h); + delete_genesis_global_config_if_present(&mut h); + inject_confidential_e2e_modules(&mut h); + reinit_confidential_asset_module(&mut h); + h +} + +#[test] +fn confidential_transfer_rejects_when_chain_auditor_unset() { + let mut h = fresh_harness_no_chain_auditor(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xE9, 1); + let bob_addr = confidential_e2e_addr(0xE9, 2); + 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, 2_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + // Empty auditor list — chain auditor isn't set on-chain, so any transfer must abort + // at the `ECHAIN_AUDITOR_NOT_SET` precondition before slot-matching even runs. + let parts = pack_transfer_audited_verbatim( + &mut h, chain, alice_addr, bob_addr, &alice_dk, 100, 1900, vec![], vec![]); + let st = run_confidential_transfer(&mut h, &alice, bob_addr, &parts, vec![]); + assert_kept_failure(&st, "transfer must abort when chain auditor is unset"); +} + +#[test] +fn confidential_transfer_rejects_when_slot0_not_chain_auditor() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xEA, 1); + let bob_addr = confidential_e2e_addr(0xEA, 2); + 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, 2_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + // Use a fresh keypair as slot 0 — proof is internally consistent (FS transcript binds + // this key) but `validate_auditors` rejects because slot 0 ≠ on-chain chain auditor. + let (_dk, wrong_ek) = generate_elgamal_keypair(&mut h); + let wrong_pk = twisted_pubkey_bytes(&mut h, &wrong_ek); + let parts = pack_transfer_audited_verbatim( + &mut h, chain, alice_addr, bob_addr, &alice_dk, 100, 1900, vec![wrong_pk], vec![]); + let st = run_confidential_transfer(&mut h, &alice, bob_addr, &parts, vec![]); + assert_kept_failure(&st, "slot 0 must equal active chain auditor"); +} + +#[test] +fn confidential_transfer_rejects_after_chain_auditor_rotation() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xEB, 1); + let bob_addr = confidential_e2e_addr(0xEB, 2); + 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, 2_000), "deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover"); + + // Snapshot the chain auditor key in force at proof-generation time, then rotate. + // The pre-rotation proof (slot 0 = old chain key) becomes unsubmittable. + let old_chain_pk = view_chain_auditor_pubkey(&mut h); + let parts = pack_transfer_audited_verbatim( + &mut h, chain, alice_addr, bob_addr, &alice_dk, 100, 1900, vec![old_chain_pk], vec![]); + + install_default_chain_auditor(&mut h); // bumps to a new chain auditor key + + let st = run_confidential_transfer(&mut h, &alice, bob_addr, &parts, vec![]); + assert_kept_failure(&st, "post-rotation old-key proof must be rejected"); +} + +/// `normalize_and_rollover_pending_balance` does both steps in one tx. The proof is +/// generated against the *current* (unnormalized) actual balance; success implies both +/// `normalize_internal` and `rollover_pending_balance_internal` ran (their state asserts +/// are mutually exclusive — `normalize` requires `!normalized`, `rollover` requires +/// `normalized`, so a wrong composition would abort one of them). +#[test] +fn normalize_and_rollover_combined_entry_succeeds() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xEC, 1); + let bob_addr = confidential_e2e_addr(0xEC, 2); + 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, 50_000_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"); + } + + // Stack two max-chunk deposits into the available balance to leave it unnormalized. + let max_chunk: u64 = (1u64 << 16) - 1; + assert_kept_success(&run_deposit(&mut h, &alice, max_chunk), "alice deposit 1"); + assert_kept_success(&run_deposit_to(&mut h, &bob, alice_addr, max_chunk), "bob → alice deposit"); + assert_kept_success(&run_rollover(&mut h, &alice), "rollover (now unnormalized)"); + + // A fresh deposit lands in pending; the combined entry must roll it in. + assert_kept_success(&run_deposit(&mut h, &alice, 50), "alice deposit 2"); + + // Proof normalizes against the *current* (unnormalized, pre-rollover) actual balance. + let cur: u128 = 2u128 * max_chunk as u128; + let (new_bal, zkrp, sigma) = pack_normalize(&mut h, chain, alice_addr, &alice_dk, cur); + assert_kept_success( + &run_normalize_and_rollover(&mut h, &alice, &new_bal, &zkrp, &sigma), + "normalize_and_rollover_pending_balance", + ); +} + +fn view_chain_auditor_pubkey(h: &mut MoveHarness) -> Vec { + let ret = bypass_at(h, "confidential_asset", "get_chain_auditor", vec![], vec![]); + assert_eq!(ret.return_values.len(), 1); + let opt_struct = ret.return_values[0].0.clone(); + // `Option` is BCS `0x01 || pubkey_bytes` when Some. + assert!(!opt_struct.is_empty() && opt_struct[0] == 1, "chain auditor must be Some"); + let inner = opt_struct[1..].to_vec(); + twisted_pubkey_bytes(h, &inner) +} + +// ---- Combined "lands spendable" entrypoints (single-tx make-private flows) ---- +// +// These three Move entrypoints collapse a deposit into a spendable confidential balance in one +// transaction. All three end with `rollover_pending_balance_internal`, which writes the new +// actual balance and zeros pending. The wallet picks based on on-chain state: +// +// - unregistered → register_and_deposit_and_rollover_pending_balance +// - registered, normalized=true → deposit_and_rollover_pending_balance +// - registered, normalized=false (post-rollover) → deposit_and_normalize_and_rollover_pending_balance + +fn run_register_and_deposit_and_rollover( + h: &mut MoveHarness, + sender: &Account, + amount: u64, + ek_pubkey_32: &[u8], + comm: &[u8], + resp: &[u8], +) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("register_and_deposit_and_rollover_pending_balance").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&amount).unwrap(), + ek_pubkey_32.to_vec(), + comm.to_vec(), + resp.to_vec(), + ], + )); + let txn = h.create_transaction_payload(sender, payload); + h.run(txn) +} + +fn run_deposit_and_rollover(h: &mut MoveHarness, sender: &Account, amount: u64) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("deposit_and_rollover_pending_balance").unwrap(), + vec![], + vec![bcs::to_bytes(&MOVE_METADATA).unwrap(), bcs::to_bytes(&amount).unwrap()], + )); + let txn = h.create_transaction_payload(sender, payload); + h.run(txn) +} + +fn run_deposit_normalize_and_rollover( + h: &mut MoveHarness, + sender: &Account, + amount: u64, + new_balance: &[u8], + zkrp: &[u8], + sigma: &[u8], +) -> TransactionStatus { + let payload = TransactionPayload::EntryFunction(EntryFunction::new( + ca_module_id(), + Identifier::new("deposit_and_normalize_and_rollover_pending_balance").unwrap(), + vec![], + vec![ + bcs::to_bytes(&MOVE_METADATA).unwrap(), + bcs::to_bytes(&amount).unwrap(), + new_balance.to_vec(), + zkrp.to_vec(), + sigma.to_vec(), + ], + )); + let txn = h.create_transaction_payload(sender, payload); + h.run(txn) +} + +/// First-time atomic register + deposit + rollover. Verifies (a) the entry succeeds, (b) the +/// store is genuinely registered (a follow-up plain deposit works), and (c) the funds landed in +/// actual (spendable), since the test exercises the same path the wallet's "Make private" UX +/// uses for unregistered users. +#[test] +fn register_and_deposit_and_rollover_succeeds() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xCD, 3); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let alice_pk = twisted_pubkey_bytes(&mut h, &alice_ek); + let (c, r) = prove_registration_parts(&mut h, chain, alice_addr, &alice_dk, &alice_ek, MOVE_METADATA); + + assert_kept_success( + &run_register_and_deposit_and_rollover(&mut h, &alice, 100, &alice_pk, &c, &r), + "register_and_deposit_and_rollover", + ); + // Registration genuinely persisted: subsequent plain deposit (which requires an existing + // store) must succeed. + assert_kept_success(&run_deposit(&mut h, &alice, 25), "post-deposit"); +} + +/// Bad registration proof must reject before any state mutates. +#[test] +fn register_and_deposit_and_rollover_rejects_bad_proof() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xCD, 4); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let (_other_dk, other_ek) = generate_elgamal_keypair(&mut h); + let other_pk = twisted_pubkey_bytes(&mut h, &other_ek); + let (c, r) = prove_registration_parts(&mut h, chain, alice_addr, &alice_dk, &alice_ek, MOVE_METADATA); + + assert_kept_failure( + &run_register_and_deposit_and_rollover(&mut h, &alice, 50, &other_pk, &c, &r), + "bad registration proof must reject combined call", + ); +} + +/// Combined entry aborts when the sender is already registered. +#[test] +fn register_and_deposit_and_rollover_aborts_when_already_registered() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xCD, 8); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let alice_pk = twisted_pubkey_bytes(&mut h, &alice_ek); + let (c, r) = prove_registration_parts(&mut h, chain, alice_addr, &alice_dk, &alice_ek, MOVE_METADATA); + + assert_kept_success( + &run_register(&mut h, &alice, &alice_pk, &c, &r), + "alice plain register", + ); + assert_kept_failure( + &run_register_and_deposit_and_rollover(&mut h, &alice, 5, &alice_pk, &c, &r), + "register_and_deposit_and_rollover on already-registered must abort", + ); +} + +/// Subsequent combined entry on a normalized state: deposit + rollover, no normalize required. +/// Pre-state (normalized=true) is established by registering, depositing, rolling over, then +/// withdrawing — the withdraw path sets normalized=true. +#[test] +fn deposit_and_rollover_succeeds_when_normalized() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xCD, 10); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let alice_pk = twisted_pubkey_bytes(&mut h, &alice_ek); + let (c, r) = prove_registration_parts(&mut h, chain, alice_addr, &alice_dk, &alice_ek, MOVE_METADATA); + + // First-time path leaves normalized=false (rollover side effect). + assert_kept_success( + &run_register_and_deposit_and_rollover(&mut h, &alice, 100, &alice_pk, &c, &r), + "register_and_deposit_and_rollover", + ); + + // Withdraw any amount: normalized is set true on the sender's store. + let (new_bal, zkrp, sigma) = pack_withdraw(&mut h, chain, alice_addr, &alice_dk, &alice_ek, 1, 99); + assert_kept_success( + &run_withdraw(&mut h, &alice, 1, &new_bal, &zkrp, &sigma), + "withdraw to set normalized=true", + ); + + // Now the deposit_and_rollover path must succeed. + assert_kept_success( + &run_deposit_and_rollover(&mut h, &alice, 50), + "deposit_and_rollover when normalized", + ); +} + +/// Subsequent combined entry on a NOT-normalized state must abort with ENORMALIZATION_REQUIRED +/// (3 << 16 | 10 = 196618). The wallet detects this state via `is_normalized` view and routes +/// to `deposit_and_normalize_and_rollover_pending_balance` instead. +#[test] +fn deposit_and_rollover_aborts_when_not_normalized() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xCD, 11); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let alice_pk = twisted_pubkey_bytes(&mut h, &alice_ek); + let (c, r) = prove_registration_parts(&mut h, chain, alice_addr, &alice_dk, &alice_ek, MOVE_METADATA); + + // After register_and_deposit_and_rollover, normalized=false. + assert_kept_success( + &run_register_and_deposit_and_rollover(&mut h, &alice, 100, &alice_pk, &c, &r), + "register_and_deposit_and_rollover", + ); + + // No withdraw / transfer / normalize in between → normalized still false. + assert_kept_failure( + &run_deposit_and_rollover(&mut h, &alice, 50), + "deposit_and_rollover when not normalized must abort", + ); +} + +/// Subsequent combined entry on a NOT-normalized state, with normalize proof. Lands funds +/// spendable in one tx. +#[test] +fn deposit_normalize_and_rollover_succeeds_when_not_normalized() { + let mut h = fresh_harness(); + let chain = h.executor.get_chain_id().id(); + let alice_addr = confidential_e2e_addr(0xCD, 12); + let alice = h.new_account_with_balance_at(alice_addr, 50_000_000_000_000); + + let (alice_dk, alice_ek) = generate_elgamal_keypair(&mut h); + let alice_pk = twisted_pubkey_bytes(&mut h, &alice_ek); + let (c, r) = prove_registration_parts(&mut h, chain, alice_addr, &alice_dk, &alice_ek, MOVE_METADATA); + + // First-time → normalized=false, actual=100. + assert_kept_success( + &run_register_and_deposit_and_rollover(&mut h, &alice, 100, &alice_pk, &c, &r), + "register_and_deposit_and_rollover", + ); + + // Build the normalize proof against the *current* (pre-second-deposit) actual balance = + // 100. `deposit_to_internal` only mutates pending, so the actual the proof binds to matches + // the on-chain actual at normalize_internal time. + let (new_bal, zkrp, sigma) = pack_normalize(&mut h, chain, alice_addr, &alice_dk, 100u128); + + assert_kept_success( + &run_deposit_normalize_and_rollover(&mut h, &alice, 50, &new_bal, &zkrp, &sigma), + "deposit_normalize_and_rollover when not normalized", + ); +} diff --git a/aptos-move/e2e-move-tests/src/tests/mod.rs b/aptos-move/e2e-move-tests/src/tests/mod.rs index c432ce73def..40d1a3204ad 100644 --- a/aptos-move/e2e-move-tests/src/tests/mod.rs +++ b/aptos-move/e2e-move-tests/src/tests/mod.rs @@ -14,6 +14,7 @@ mod attributes; mod chain_id; mod code_publishing; mod common; +mod confidential_asset; mod constructor_args; mod cryptoalgebra; mod dependencies; diff --git a/aptos-move/e2e-tests/src/executor.rs b/aptos-move/e2e-tests/src/executor.rs index e847ff0c49a..0f22a54bbaa 100644 --- a/aptos-move/e2e-tests/src/executor.rs +++ b/aptos-move/e2e-tests/src/executor.rs @@ -87,6 +87,7 @@ use move_vm_runtime::{ module_traversal::{TraversalContext, TraversalStorage}, ModuleStorage, }; +use move_vm_runtime::move_vm::SerializedReturnValues; use move_vm_types::gas::UnmeteredGasMeter; use serde::Serialize; use std::{ @@ -1332,6 +1333,51 @@ impl FakeExecutor { self.event_store.extend(events); } + /// Like [`Self::try_exec`], but targets an arbitrary published address (e.g. `0x7` experimental) + /// and returns serialized Move return values (for test-only / internal callees). + pub fn try_exec_function_bypass_at( + &mut self, + module_addr: AccountAddress, + module_name: &str, + function_name: &str, + type_params: Vec, + args: Vec>, + ) -> Result { + let env = AptosEnvironment::new(&self.state_store); + let resolver = self.state_store.as_move_resolver(); + let vm = MoveVmExt::new(&env); + + let module_storage = self.state_store.as_aptos_code_storage(&env); + + let mut session = vm.new_session(&resolver, SessionId::void(), None); + let traversal_storage = TraversalStorage::new(); + let module_id = ModuleId::new( + module_addr, + Identifier::new(module_name).expect("valid module name"), + ); + let ret = session + .execute_function_bypass_visibility( + &module_id, + Identifier::new(function_name) + .expect("valid function name") + .as_ref(), + type_params, + args, + &mut UnmeteredGasMeter, + &mut TraversalContext::new(&traversal_storage), + &module_storage, + ) + .map_err(|e| e.into_vm_status())?; + let (write_set, events) = finish_session_assert_no_modules( + session, + &module_storage, + &ChangeSetConfigs::unlimited_at_gas_feature_version(env.gas_feature_version()), + ); + self.state_store.apply_write_set(&write_set).unwrap(); + self.event_store.extend(events); + Ok(ret) + } + pub fn try_exec( &mut self, module_name: &str, diff --git a/aptos-move/framework/aptos-experimental/Move.toml b/aptos-move/framework/aptos-experimental/Move.toml index 1d5a6845b0d..badb012204a 100644 --- a/aptos-move/framework/aptos-experimental/Move.toml +++ b/aptos-move/framework/aptos-experimental/Move.toml @@ -3,7 +3,9 @@ name = "AptosExperimental" version = "1.0.0" [addresses] -aptos_experimental = "0x7" +# Placeholder: resolved to `0x7` in Rust unit tests (`framework/tests/move_unit_test.rs`), release +# builds (`framework/src/aptos.rs`), and `e2e-move-tests` — not fixed until publish. +aptos_experimental = "_" [dependencies] AptosFramework = { local = "../aptos-framework" } diff --git a/aptos-move/framework/aptos-experimental/README.md b/aptos-move/framework/aptos-experimental/README.md new file mode 100644 index 00000000000..a38b93c54fc --- /dev/null +++ b/aptos-move/framework/aptos-experimental/README.md @@ -0,0 +1,31 @@ +# aptos-experimental + +Move packages that are **experimental**: APIs may change. The largest surface area here is **Confidential Assets** — private fungible balances with homomorphic encryption and on-chain zero-knowledge verification. + +## Confidential Assets — where to read + +| Document | Purpose | +| -------- | ------- | +| [`whitepaper.md`](./whitepaper.md) | End-to-end protocol, Fiat–Shamir, security discussion, and **full `Transferred` event field reference** (§5). | +| [`doc/`](./doc/) | Generated Move API reference (`confidential_asset`, `confidential_proof`, `confidential_balance`, …). | +| [`sources/confidential_asset/`](./sources/confidential_asset/) | Modules: `confidential_asset`, `confidential_proof`, `confidential_balance`, `ristretto255_twisted_elgamal`, gas e2e helpers (`#[test_only]`). | +| [`tests/confidential_asset/`](./tests/confidential_asset/) | Move unit tests (`confidential_asset_tests`, `confidential_proof_tests`). | + +Rust **`e2e-move-tests`** (repo root `aptos-move/e2e-move-tests`) calls into `confidential_gas_e2e_helpers` to pack proofs for real transactions; they complement but do not replace Move tests for event shape. If those Rust tests overflow the stack on your machine, set **`RUST_MIN_STACK`** as documented in `e2e-move-tests/README.md`. + +## `Transferred` event (indexers & integrators) + +Emitted on each successful **`confidential_transfer`**. There is **no cleartext amount** in the payload; observers still see cryptographic material suitable for correlation and auditor workflows. + +| Field | Summary | +| ----- | ------- | +| **`from`**, **`to`**, **`asset_type`** | Sender and recipient addresses; **`asset_type`** is the FA metadata object address for the token. | +| **`amount`** | Compressed ciphertext for the transferred amount (recipient key, pending-balance layout). | +| **`ek_volun_auds`** | Flattened **`sigma_proof.xs.x7s`**: per auditor row in the proof, **four** compressed Ristretto points (32 bytes each), row-major order. Length **`128 × n`** bytes (`n` = auditor rows; **`n = 0`** ⇒ empty `vector`). Produced by `confidential_proof::transfer_proof_ek_volun_auds_flat_bytes`. | +| **`sender_auditor_hint`** | Opaque bytes (≤ **256**); must be identical when proving and when submitting the entry; bound into the transfer sigma Fiat–Shamir hash (BCS). | +| **`new_sender_available_balance`**, **`new_recip_pending_balance`** | Sender’s new **actual** balance and recipient’s new **pending** balance (compressed ciphertexts). | +| **`memo`** | Reserved; **empty** `vector` in the current implementation. | + +**Integrator rule:** whatever bytes you pass as **`sender_auditor_hint`** to the on-chain entry must be the same bytes you included when generating the transfer proof (Fiat–Shamir binds them). + +**Test coverage:** `confidential_asset_tests` uses `assert_last_transferred_event_matches_state` to check addresses, `asset_type`, hint, `ek_volun_auds` **length** vs auditor count, and both post-transfer ciphertexts against on-chain store. `confidential_proof_tests` cover proof verification (including wrong-hint failure) without going through the event path. diff --git a/aptos-move/framework/aptos-experimental/doc/confidential_asset.md b/aptos-move/framework/aptos-experimental/doc/confidential_asset.md deleted file mode 100644 index 3d0b0c0c233..00000000000 --- a/aptos-move/framework/aptos-experimental/doc/confidential_asset.md +++ /dev/null @@ -1,2488 +0,0 @@ - - - -# Module `0x7::confidential_asset` - -This module implements the Confidential Asset (CA) Standard, a privacy-focused protocol for managing fungible assets (FA). -It enables private transfers by obfuscating token amounts while keeping sender and recipient addresses visible. - - -- [Resource `ConfidentialAssetStore`](#0x7_confidential_asset_ConfidentialAssetStore) -- [Resource `FAController`](#0x7_confidential_asset_FAController) -- [Resource `FAConfig`](#0x7_confidential_asset_FAConfig) -- [Struct `Deposited`](#0x7_confidential_asset_Deposited) -- [Struct `Withdrawn`](#0x7_confidential_asset_Withdrawn) -- [Struct `Transferred`](#0x7_confidential_asset_Transferred) -- [Constants](#@Constants_0) -- [Function `init_module`](#0x7_confidential_asset_init_module) -- [Function `register`](#0x7_confidential_asset_register) -- [Function `deposit_to`](#0x7_confidential_asset_deposit_to) -- [Function `deposit`](#0x7_confidential_asset_deposit) -- [Function `deposit_coins_to`](#0x7_confidential_asset_deposit_coins_to) -- [Function `deposit_coins`](#0x7_confidential_asset_deposit_coins) -- [Function `withdraw_to`](#0x7_confidential_asset_withdraw_to) -- [Function `withdraw`](#0x7_confidential_asset_withdraw) -- [Function `confidential_transfer`](#0x7_confidential_asset_confidential_transfer) -- [Function `rotate_encryption_key`](#0x7_confidential_asset_rotate_encryption_key) -- [Function `normalize`](#0x7_confidential_asset_normalize) -- [Function `freeze_token`](#0x7_confidential_asset_freeze_token) -- [Function `unfreeze_token`](#0x7_confidential_asset_unfreeze_token) -- [Function `rollover_pending_balance`](#0x7_confidential_asset_rollover_pending_balance) -- [Function `rollover_pending_balance_and_freeze`](#0x7_confidential_asset_rollover_pending_balance_and_freeze) -- [Function `rotate_encryption_key_and_unfreeze`](#0x7_confidential_asset_rotate_encryption_key_and_unfreeze) -- [Function `enable_allow_list`](#0x7_confidential_asset_enable_allow_list) -- [Function `disable_allow_list`](#0x7_confidential_asset_disable_allow_list) -- [Function `enable_token`](#0x7_confidential_asset_enable_token) -- [Function `disable_token`](#0x7_confidential_asset_disable_token) -- [Function `set_auditor`](#0x7_confidential_asset_set_auditor) -- [Function `has_confidential_asset_store`](#0x7_confidential_asset_has_confidential_asset_store) -- [Function `is_token_allowed`](#0x7_confidential_asset_is_token_allowed) -- [Function `is_allow_list_enabled`](#0x7_confidential_asset_is_allow_list_enabled) -- [Function `pending_balance`](#0x7_confidential_asset_pending_balance) -- [Function `actual_balance`](#0x7_confidential_asset_actual_balance) -- [Function `encryption_key`](#0x7_confidential_asset_encryption_key) -- [Function `is_normalized`](#0x7_confidential_asset_is_normalized) -- [Function `is_frozen`](#0x7_confidential_asset_is_frozen) -- [Function `get_auditor`](#0x7_confidential_asset_get_auditor) -- [Function `confidential_asset_balance`](#0x7_confidential_asset_confidential_asset_balance) -- [Function `register_internal`](#0x7_confidential_asset_register_internal) -- [Function `deposit_to_internal`](#0x7_confidential_asset_deposit_to_internal) -- [Function `withdraw_to_internal`](#0x7_confidential_asset_withdraw_to_internal) -- [Function `confidential_transfer_internal`](#0x7_confidential_asset_confidential_transfer_internal) -- [Function `rotate_encryption_key_internal`](#0x7_confidential_asset_rotate_encryption_key_internal) -- [Function `normalize_internal`](#0x7_confidential_asset_normalize_internal) -- [Function `rollover_pending_balance_internal`](#0x7_confidential_asset_rollover_pending_balance_internal) -- [Function `freeze_token_internal`](#0x7_confidential_asset_freeze_token_internal) -- [Function `unfreeze_token_internal`](#0x7_confidential_asset_unfreeze_token_internal) -- [Function `ensure_fa_config_exists`](#0x7_confidential_asset_ensure_fa_config_exists) -- [Function `get_fa_store_signer`](#0x7_confidential_asset_get_fa_store_signer) -- [Function `get_fa_store_address`](#0x7_confidential_asset_get_fa_store_address) -- [Function `get_user_signer`](#0x7_confidential_asset_get_user_signer) -- [Function `get_user_address`](#0x7_confidential_asset_get_user_address) -- [Function `get_fa_config_signer`](#0x7_confidential_asset_get_fa_config_signer) -- [Function `get_fa_config_address`](#0x7_confidential_asset_get_fa_config_address) -- [Function `construct_user_seed`](#0x7_confidential_asset_construct_user_seed) -- [Function `construct_fa_seed`](#0x7_confidential_asset_construct_fa_seed) -- [Function `validate_auditors`](#0x7_confidential_asset_validate_auditors) -- [Function `deserialize_auditor_eks`](#0x7_confidential_asset_deserialize_auditor_eks) -- [Function `deserialize_auditor_amounts`](#0x7_confidential_asset_deserialize_auditor_amounts) -- [Function `ensure_sufficient_fa`](#0x7_confidential_asset_ensure_sufficient_fa) - - -
use 0x1::bcs;
-use 0x1::chain_id;
-use 0x1::coin;
-use 0x1::dispatchable_fungible_asset;
-use 0x1::error;
-use 0x1::event;
-use 0x1::fungible_asset;
-use 0x1::object;
-use 0x1::option;
-use 0x1::primary_fungible_store;
-use 0x1::ristretto255;
-use 0x1::ristretto255_bulletproofs;
-use 0x1::signer;
-use 0x1::string;
-use 0x1::string_utils;
-use 0x1::system_addresses;
-use 0x1::vector;
-use 0x7::confidential_balance;
-use 0x7::confidential_proof;
-use 0x7::ristretto255_twisted_elgamal;
-
- - - - - -## Resource `ConfidentialAssetStore` - -The confidential_asset module stores a ConfidentialAssetStore object for each user-token pair. - - -
struct ConfidentialAssetStore has key
-
- - - -
-Fields - - -
-
-frozen: bool -
-
- Indicates if the account is frozen. If true, transactions are temporarily disabled - for this account. This is particularly useful during key rotations, which require - two transactions: rolling over the pending balance to the actual balance and rotating - the encryption key. Freezing prevents the user from accepting additional payments - between these two transactions. -
-
-normalized: bool -
-
- A flag indicating whether the actual balance is normalized. A normalized balance - ensures that all chunks fit within the defined 16-bit bounds, preventing overflows. -
-
-pending_counter: u64 -
-
- Tracks the maximum number of transactions the user can accept before normalization - is required. For example, if the user can accept up to 2^16 transactions and each - chunk has a 16-bit limit, the maximum chunk value before normalization would be - 2^16 * 2^16 = 2^32. Maintaining this counter is crucial because users must solve - a discrete logarithm problem of this size to decrypt their balances. -
-
-pending_balance: confidential_balance::CompressedConfidentialBalance -
-
- Stores the user's pending balance, which is used for accepting incoming payments. - Represented as four 16-bit chunks (p0 + 2^16 * p1 + 2^32 * p2 + 2^48 * p3), that can grow up to 32 bits. - All payments are accepted into this pending balance, which users must roll over into the actual balance - to perform transactions like withdrawals or transfers. - This separation helps protect against front-running attacks, where small incoming transfers could force - frequent regenerating of zk-proofs. -
-
-actual_balance: confidential_balance::CompressedConfidentialBalance -
-
- Represents the actual user balance, which is available for sending payments. - It consists of eight 16-bit chunks (p0 + 2^16 * p1 + ... + 2^112 * p8), supporting a 128-bit balance. - Users can decrypt this balance with their decryption keys and by solving a discrete logarithm problem. -
-
-ek: ristretto255_twisted_elgamal::CompressedPubkey -
-
- The encryption key associated with the user's confidential asset account, different for each token. -
-
- - -
- - - -## Resource `FAController` - -Represents the controller for the primary FA stores and FAConfig objects. - - -
struct FAController has key
-
- - - -
-Fields - - -
-
-allow_list_enabled: bool -
-
- Indicates whether the allow list is enabled. If true, only tokens from the allow list can be transferred. - This flag is managed by the governance module. -
-
-extend_ref: object::ExtendRef -
-
- Used to derive a signer that owns all the FAs' primary stores and FAConfig objects. -
-
- - -
- - - -## Resource `FAConfig` - -Represents the configuration of a token. - - -
struct FAConfig has key
-
- - - -
-Fields - - -
-
-allowed: bool -
-
- Indicates whether the token is allowed for confidential transfers. - If allow list is disabled, all tokens are allowed. - Can be toggled by the governance module. The withdrawals are always allowed. -
-
-auditor_ek: option::Option<ristretto255_twisted_elgamal::CompressedPubkey> -
-
- The auditor's public key for the token. If the auditor is not set, this field is None. - Otherwise, each confidential transfer must include the auditor as an additional party, - alongside the recipient, who has access to the decrypted transferred amount. -
-
- - -
- - - -## Struct `Deposited` - -Emitted when tokens are brought into the protocol. - - -
#[event]
-struct Deposited has drop, store
-
- - - -
-Fields - - -
-
-from: address -
-
- -
-
-to: address -
-
- -
-
-amount: u64 -
-
- -
-
- - -
- - - -## Struct `Withdrawn` - -Emitted when tokens are brought out of the protocol. - - -
#[event]
-struct Withdrawn has drop, store
-
- - - -
-Fields - - -
-
-from: address -
-
- -
-
-to: address -
-
- -
-
-amount: u64 -
-
- -
-
- - -
- - - -## Struct `Transferred` - -Emitted when tokens are transferred within the protocol between users' confidential balances. -Note that a numeric amount is not included, as it is hidden. - - -
#[event]
-struct Transferred has drop, store
-
- - - -
-Fields - - -
-
-from: address -
-
- -
-
-to: address -
-
- -
-
- - -
- - - -## Constants - - - - -An internal error occurred, indicating unexpected behavior. - - -
const EINTERNAL_ERROR: u64 = 16;
-
- - - - - -The allow list is already disabled. - - -
const EALLOW_LIST_DISABLED: u64 = 15;
-
- - - - - -The allow list is already enabled. - - -
const EALLOW_LIST_ENABLED: u64 = 14;
-
- - - - - -The confidential asset account is already frozen. - - -
const EALREADY_FROZEN: u64 = 7;
-
- - - - - -The balance is already normalized and cannot be normalized again. - - -
const EALREADY_NORMALIZED: u64 = 11;
-
- - - - - -The deserialization of the auditor EK failed. - - -
const EAUDITOR_EK_DESERIALIZATION_FAILED: u64 = 4;
-
- - - - - -The confidential asset store has already been published for the given user-token pair. - - -
const ECA_STORE_ALREADY_PUBLISHED: u64 = 2;
-
- - - - - -The confidential asset store has not been published for the given user-token pair. - - -
const ECA_STORE_NOT_PUBLISHED: u64 = 3;
-
- - - - - -The provided auditors or auditor proofs are invalid. - - -
const EINVALID_AUDITORS: u64 = 6;
-
- - - - - -Sender and recipient amounts encrypt different transfer amounts - - -
const EINVALID_SENDER_AMOUNT: u64 = 17;
-
- - - - - -The operation requires the actual balance to be normalized. - - -
const ENORMALIZATION_REQUIRED: u64 = 10;
-
- - - - - -The sender is not the registered auditor. - - -
const ENOT_AUDITOR: u64 = 5;
-
- - - - - -The confidential asset account is not frozen. - - -
const ENOT_FROZEN: u64 = 8;
-
- - - - - -The pending balance must be zero for this operation. - - -
const ENOT_ZERO_BALANCE: u64 = 9;
-
- - - - - -The range proof system does not support sufficient range. - - -
const ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE: u64 = 1;
-
- - - - - -The token is not allowed for confidential transfers. - - -
const ETOKEN_DISABLED: u64 = 13;
-
- - - - - -The token is already allowed for confidential transfers. - - -
const ETOKEN_ENABLED: u64 = 12;
-
- - - - - -The mainnet chain ID. If the chain ID is 1, the allow list is enabled. - - -
const MAINNET_CHAIN_ID: u8 = 1;
-
- - - - - -The maximum number of transactions can be aggregated on the pending balance before rollover is required. - - -
const MAX_TRANSFERS_BEFORE_ROLLOVER: u64 = 65534;
-
- - - - - -## Function `init_module` - - - -
fun init_module(deployer: &signer)
-
- - - -
-Implementation - - -
fun init_module(deployer: &signer) {
-    assert!(
-        bulletproofs::get_max_range_bits() >= confidential_proof::get_bulletproofs_num_bits(),
-        error::internal(ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE)
-    );
-
-    let deployer_address = signer::address_of(deployer);
-
-    let fa_controller_ctor_ref = &object::create_object(deployer_address);
-
-    move_to(deployer, FAController {
-        allow_list_enabled: chain_id::get() == MAINNET_CHAIN_ID,
-        extend_ref: object::generate_extend_ref(fa_controller_ctor_ref),
-    });
-}
-
- - - -
- - - -## Function `register` - -Registers an account for a specified token. Users must register an account for each token they -intend to transact with. - -Users are also responsible for generating a Twisted ElGamal key pair on their side. - - -
public entry fun register(sender: &signer, token: object::Object<fungible_asset::Metadata>, ek: vector<u8>)
-
- - - -
-Implementation - - -
public entry fun register(
-    sender: &signer,
-    token: Object<Metadata>,
-    ek: vector<u8>) acquires FAController, FAConfig
-{
-    let ek = twisted_elgamal::new_pubkey_from_bytes(ek).extract();
-
-    register_internal(sender, token, ek);
-}
-
- - - -
- - - -## Function `deposit_to` - -Brings tokens into the protocol, transferring the passed amount from the sender's primary FA store -to the pending balance of the recipient. -The initial confidential balance is publicly visible, as entering the protocol requires a normal transfer. -However, tokens within the protocol become obfuscated through confidential transfers, ensuring privacy in -subsequent transactions. - - -
public entry fun deposit_to(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64)
-
- - - -
-Implementation - - -
public entry fun deposit_to(
-    sender: &signer,
-    token: Object<Metadata>,
-    to: address,
-    amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig
-{
-    deposit_to_internal(sender, token, to, amount)
-}
-
- - - -
- - - -## Function `deposit` - -The same as deposit_to, but the recipient is the sender. - - -
public entry fun deposit(sender: &signer, token: object::Object<fungible_asset::Metadata>, amount: u64)
-
- - - -
-Implementation - - -
public entry fun deposit(
-    sender: &signer,
-    token: Object<Metadata>,
-    amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig
-{
-    deposit_to_internal(sender, token, signer::address_of(sender), amount)
-}
-
- - - -
- - - -## Function `deposit_coins_to` - -The same as deposit_to, but converts coins to missing FA first. - - -
public entry fun deposit_coins_to<CoinType>(sender: &signer, to: address, amount: u64)
-
- - - -
-Implementation - - -
public entry fun deposit_coins_to<CoinType>(
-    sender: &signer,
-    to: address,
-    amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig
-{
-    let token = ensure_sufficient_fa<CoinType>(sender, amount).extract();
-
-    deposit_to_internal(sender, token, to, amount)
-}
-
- - - -
- - - -## Function `deposit_coins` - -The same as deposit, but converts coins to missing FA first. - - -
public entry fun deposit_coins<CoinType>(sender: &signer, amount: u64)
-
- - - -
-Implementation - - -
public entry fun deposit_coins<CoinType>(
-    sender: &signer,
-    amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig
-{
-    let token = ensure_sufficient_fa<CoinType>(sender, amount).extract();
-
-    deposit_to_internal(sender, token, signer::address_of(sender), amount)
-}
-
- - - -
- - - -## Function `withdraw_to` - -Brings tokens out of the protocol by transferring the specified amount from the sender's actual balance to -the recipient's primary FA store. -The withdrawn amount is publicly visible, as this process requires a normal transfer. -The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. - - -
public entry fun withdraw_to(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
-
- - - -
-Implementation - - -
public entry fun withdraw_to(
-    sender: &signer,
-    token: Object<Metadata>,
-    to: address,
-    amount: u64,
-    new_balance: vector<u8>,
-    zkrp_new_balance: vector<u8>,
-    sigma_proof: vector<u8>) acquires ConfidentialAssetStore, FAController
-{
-    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
-    let proof = confidential_proof::deserialize_withdrawal_proof(sigma_proof, zkrp_new_balance).extract();
-
-    withdraw_to_internal(sender, token, to, amount, new_balance, proof);
-
-    event::emit(Withdrawn { from: signer::address_of(sender), to, amount });
-}
-
- - - -
- - - -## Function `withdraw` - -The same as withdraw_to, but the recipient is the sender. - - -
public entry fun withdraw(sender: &signer, token: object::Object<fungible_asset::Metadata>, amount: u64, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
-
- - - -
-Implementation - - -
public entry fun withdraw(
-    sender: &signer,
-    token: Object<Metadata>,
-    amount: u64,
-    new_balance: vector<u8>,
-    zkrp_new_balance: vector<u8>,
-    sigma_proof: vector<u8>) acquires ConfidentialAssetStore, FAController
-{
-    withdraw_to(
-        sender,
-        token,
-        signer::address_of(sender),
-        amount,
-        new_balance,
-        zkrp_new_balance,
-        sigma_proof
-    )
-}
-
- - - -
- - - -## Function `confidential_transfer` - -Transfers tokens from the sender's actual balance to the recipient's pending balance. -The function hides the transferred amount while keeping the sender and recipient addresses visible. -The sender encrypts the transferred amount with the recipient's encryption key and the function updates the -recipient's confidential balance homomorphically. -Additionally, the sender encrypts the transferred amount with the auditors' EKs, allowing auditors to decrypt -the it on their side. -The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. -Warning: If the auditor feature is enabled, the sender must include the auditor as the first element in the -auditor_eks vector. - - -
public entry fun confidential_transfer(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, new_balance: vector<u8>, sender_amount: vector<u8>, recipient_amount: vector<u8>, auditor_eks: vector<u8>, auditor_amounts: vector<u8>, zkrp_new_balance: vector<u8>, zkrp_transfer_amount: vector<u8>, sigma_proof: vector<u8>)
-
- - - -
-Implementation - - -
public entry fun confidential_transfer(
-    sender: &signer,
-    token: Object<Metadata>,
-    to: address,
-    new_balance: vector<u8>,
-    sender_amount: vector<u8>,
-    recipient_amount: vector<u8>,
-    auditor_eks: vector<u8>,
-    auditor_amounts: vector<u8>,
-    zkrp_new_balance: vector<u8>,
-    zkrp_transfer_amount: vector<u8>,
-    sigma_proof: vector<u8>) acquires ConfidentialAssetStore, FAConfig, FAController
-{
-    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
-    let sender_amount = confidential_balance::new_pending_balance_from_bytes(sender_amount).extract();
-    let recipient_amount = confidential_balance::new_pending_balance_from_bytes(recipient_amount).extract();
-    let auditor_eks = deserialize_auditor_eks(auditor_eks).extract();
-    let auditor_amounts = deserialize_auditor_amounts(auditor_amounts).extract();
-    let proof = confidential_proof::deserialize_transfer_proof(
-        sigma_proof,
-        zkrp_new_balance,
-        zkrp_transfer_amount
-    ).extract();
-
-    confidential_transfer_internal(
-        sender,
-        token,
-        to,
-        new_balance,
-        sender_amount,
-        recipient_amount,
-        auditor_eks,
-        auditor_amounts,
-        proof
-    )
-}
-
- - - -
- - - -## Function `rotate_encryption_key` - -Rotates the encryption key for the user's confidential balance, updating it to a new encryption key. -The function ensures that the pending balance is zero before the key rotation, requiring the sender to -call rollover_pending_balance_and_freeze beforehand if necessary. -The sender provides their new normalized confidential balance, encrypted with the new encryption key and fresh randomness -to preserve privacy. - - -
public entry fun rotate_encryption_key(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_ek: vector<u8>, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
-
- - - -
-Implementation - - -
public entry fun rotate_encryption_key(
-    sender: &signer,
-    token: Object<Metadata>,
-    new_ek: vector<u8>,
-    new_balance: vector<u8>,
-    zkrp_new_balance: vector<u8>,
-    sigma_proof: vector<u8>) acquires ConfidentialAssetStore
-{
-    let new_ek = twisted_elgamal::new_pubkey_from_bytes(new_ek).extract();
-    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
-    let proof = confidential_proof::deserialize_rotation_proof(sigma_proof, zkrp_new_balance).extract();
-
-    rotate_encryption_key_internal(sender, token, new_ek, new_balance, proof);
-}
-
- - - -
- - - -## Function `normalize` - -Adjusts each chunk to fit into defined 16-bit bounds to prevent overflows. -Most functions perform implicit normalization by accepting a new normalized confidential balance as a parameter. -However, explicit normalization is required before rolling over the pending balance, as multiple rolls may cause -chunk overflows. -The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. - - -
public entry fun normalize(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
-
- - - -
-Implementation - - -
public entry fun normalize(
-    sender: &signer,
-    token: Object<Metadata>,
-    new_balance: vector<u8>,
-    zkrp_new_balance: vector<u8>,
-    sigma_proof: vector<u8>) acquires ConfidentialAssetStore
-{
-    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
-    let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract();
-
-    normalize_internal(sender, token, new_balance, proof);
-}
-
- - - -
- - - -## Function `freeze_token` - -Freezes the confidential account for the specified token, disabling all incoming transactions. - - -
public entry fun freeze_token(sender: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public entry fun freeze_token(sender: &signer, token: Object<Metadata>) acquires ConfidentialAssetStore {
-    freeze_token_internal(sender, token);
-}
-
- - - -
- - - -## Function `unfreeze_token` - -Unfreezes the confidential account for the specified token, re-enabling incoming transactions. - - -
public entry fun unfreeze_token(sender: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public entry fun unfreeze_token(sender: &signer, token: Object<Metadata>) acquires ConfidentialAssetStore {
-    unfreeze_token_internal(sender, token);
-}
-
- - - -
- - - -## Function `rollover_pending_balance` - -Adds the pending balance to the actual balance for the specified token, resetting the pending balance to zero. -This operation is necessary to use tokens from the pending balance for outgoing transactions. - - -
public entry fun rollover_pending_balance(sender: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public entry fun rollover_pending_balance(
-    sender: &signer,
-    token: Object<Metadata>) acquires ConfidentialAssetStore
-{
-    rollover_pending_balance_internal(sender, token);
-}
-
- - - -
- - - -## Function `rollover_pending_balance_and_freeze` - -Before calling rotate_encryption_key, we need to rollover the pending balance and freeze the token to prevent -any new payments being come. - - -
public entry fun rollover_pending_balance_and_freeze(sender: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public entry fun rollover_pending_balance_and_freeze(
-    sender: &signer,
-    token: Object<Metadata>) acquires ConfidentialAssetStore
-{
-    rollover_pending_balance(sender, token);
-    freeze_token(sender, token);
-}
-
- - - -
- - - -## Function `rotate_encryption_key_and_unfreeze` - -After rotating the encryption key, we may want to unfreeze the token to allow payments. -This function facilitates making both calls in a single transaction. - - -
public entry fun rotate_encryption_key_and_unfreeze(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_ek: vector<u8>, new_confidential_balance: vector<u8>, zkrp_new_balance: vector<u8>, rotate_proof: vector<u8>)
-
- - - -
-Implementation - - -
public entry fun rotate_encryption_key_and_unfreeze(
-    sender: &signer,
-    token: Object<Metadata>,
-    new_ek: vector<u8>,
-    new_confidential_balance: vector<u8>,
-    zkrp_new_balance: vector<u8>,
-    rotate_proof: vector<u8>) acquires ConfidentialAssetStore
-{
-    rotate_encryption_key(sender, token, new_ek, new_confidential_balance, zkrp_new_balance, rotate_proof);
-    unfreeze_token(sender, token);
-}
-
- - - -
- - - -## Function `enable_allow_list` - -Enables the allow list, restricting confidential transfers to tokens on the allow list. - - -
public fun enable_allow_list(aptos_framework: &signer)
-
- - - -
-Implementation - - -
public fun enable_allow_list(aptos_framework: &signer) acquires FAController {
-    system_addresses::assert_aptos_framework(aptos_framework);
-
-    let fa_controller = borrow_global_mut<FAController>(@aptos_experimental);
-
-    assert!(!fa_controller.allow_list_enabled, error::invalid_state(EALLOW_LIST_ENABLED));
-
-    fa_controller.allow_list_enabled = true;
-}
-
- - - -
- - - -## Function `disable_allow_list` - -Disables the allow list, allowing confidential transfers for all tokens. - - -
public fun disable_allow_list(aptos_framework: &signer)
-
- - - -
-Implementation - - -
public fun disable_allow_list(aptos_framework: &signer) acquires FAController {
-    system_addresses::assert_aptos_framework(aptos_framework);
-
-    let fa_controller = borrow_global_mut<FAController>(@aptos_experimental);
-
-    assert!(fa_controller.allow_list_enabled, error::invalid_state(EALLOW_LIST_DISABLED));
-
-    fa_controller.allow_list_enabled = false;
-}
-
- - - -
- - - -## Function `enable_token` - -Enables confidential transfers for the specified token. - - -
public fun enable_token(aptos_framework: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public fun enable_token(aptos_framework: &signer, token: Object<Metadata>) acquires FAConfig, FAController {
-    system_addresses::assert_aptos_framework(aptos_framework);
-
-    let fa_config = borrow_global_mut<FAConfig>(ensure_fa_config_exists(token));
-
-    assert!(!fa_config.allowed, error::invalid_state(ETOKEN_ENABLED));
-
-    fa_config.allowed = true;
-}
-
- - - -
- - - -## Function `disable_token` - -Disables confidential transfers for the specified token. - - -
public fun disable_token(aptos_framework: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public fun disable_token(aptos_framework: &signer, token: Object<Metadata>) acquires FAConfig, FAController {
-    system_addresses::assert_aptos_framework(aptos_framework);
-
-    let fa_config = borrow_global_mut<FAConfig>(ensure_fa_config_exists(token));
-
-    assert!(fa_config.allowed, error::invalid_state(ETOKEN_DISABLED));
-
-    fa_config.allowed = false;
-}
-
- - - -
- - - -## Function `set_auditor` - -Sets the auditor's public key for the specified token. - - -
public fun set_auditor(aptos_framework: &signer, token: object::Object<fungible_asset::Metadata>, new_auditor_ek: vector<u8>)
-
- - - -
-Implementation - - -
public fun set_auditor(
-    aptos_framework: &signer,
-    token: Object<Metadata>,
-    new_auditor_ek: vector<u8>) acquires FAConfig, FAController
-{
-    system_addresses::assert_aptos_framework(aptos_framework);
-
-    let fa_config = borrow_global_mut<FAConfig>(ensure_fa_config_exists(token));
-
-    fa_config.auditor_ek = if (new_auditor_ek.length() == 0) {
-        std::option::none()
-    } else {
-        let new_auditor_ek = twisted_elgamal::new_pubkey_from_bytes(new_auditor_ek);
-        assert!(new_auditor_ek.is_some(), error::invalid_argument(EAUDITOR_EK_DESERIALIZATION_FAILED));
-        new_auditor_ek
-    };
-}
-
- - - -
- - - -## Function `has_confidential_asset_store` - -Checks if the user has a confidential asset store for the specified token. - - -
#[view]
-public fun has_confidential_asset_store(user: address, token: object::Object<fungible_asset::Metadata>): bool
-
- - - -
-Implementation - - -
public fun has_confidential_asset_store(user: address, token: Object<Metadata>): bool {
-    exists<ConfidentialAssetStore>(get_user_address(user, token))
-}
-
- - - -
- - - -## Function `is_token_allowed` - -Checks if the token is allowed for confidential transfers. - - -
#[view]
-public fun is_token_allowed(token: object::Object<fungible_asset::Metadata>): bool
-
- - - -
-Implementation - - -
public fun is_token_allowed(token: Object<Metadata>): bool acquires FAController, FAConfig {
-    if (!is_allow_list_enabled()) {
-        return true
-    };
-
-    let fa_config_address = get_fa_config_address(token);
-
-    if (!exists<FAConfig>(fa_config_address)) {
-        return false
-    };
-
-    borrow_global<FAConfig>(fa_config_address).allowed
-}
-
- - - -
- - - -## Function `is_allow_list_enabled` - -Checks if the allow list is enabled. -If the allow list is enabled, only tokens from the allow list can be transferred. -Otherwise, all tokens are allowed. - - -
#[view]
-public fun is_allow_list_enabled(): bool
-
- - - -
-Implementation - - -
public fun is_allow_list_enabled(): bool acquires FAController {
-    borrow_global<FAController>(@aptos_experimental).allow_list_enabled
-}
-
- - - -
- - - -## Function `pending_balance` - -Returns the pending balance of the user for the specified token. - - -
#[view]
-public fun pending_balance(owner: address, token: object::Object<fungible_asset::Metadata>): confidential_balance::CompressedConfidentialBalance
-
- - - -
-Implementation - - -
public fun pending_balance(
-    owner: address,
-    token: Object<Metadata>): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore
-{
-    assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    let ca_store = borrow_global<ConfidentialAssetStore>(get_user_address(owner, token));
-
-    ca_store.pending_balance
-}
-
- - - -
- - - -## Function `actual_balance` - -Returns the actual balance of the user for the specified token. - - -
#[view]
-public fun actual_balance(owner: address, token: object::Object<fungible_asset::Metadata>): confidential_balance::CompressedConfidentialBalance
-
- - - -
-Implementation - - -
public fun actual_balance(
-    owner: address,
-    token: Object<Metadata>): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore
-{
-    assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    let ca_store = borrow_global<ConfidentialAssetStore>(get_user_address(owner, token));
-
-    ca_store.actual_balance
-}
-
- - - -
- - - -## Function `encryption_key` - -Returns the encryption key (EK) of the user for the specified token. - - -
#[view]
-public fun encryption_key(user: address, token: object::Object<fungible_asset::Metadata>): ristretto255_twisted_elgamal::CompressedPubkey
-
- - - -
-Implementation - - -
public fun encryption_key(
-    user: address,
-    token: Object<Metadata>): twisted_elgamal::CompressedPubkey acquires ConfidentialAssetStore
-{
-    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token)).ek
-}
-
- - - -
- - - -## Function `is_normalized` - -Checks if the user's actual balance is normalized for the specified token. - - -
#[view]
-public fun is_normalized(user: address, token: object::Object<fungible_asset::Metadata>): bool
-
- - - -
-Implementation - - -
public fun is_normalized(user: address, token: Object<Metadata>): bool acquires ConfidentialAssetStore {
-    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    borrow_global<ConfidentialAssetStore>(get_user_address(user, token)).normalized
-}
-
- - - -
- - - -## Function `is_frozen` - -Checks if the user's confidential asset store is frozen for the specified token. - - -
#[view]
-public fun is_frozen(user: address, token: object::Object<fungible_asset::Metadata>): bool
-
- - - -
-Implementation - - -
public fun is_frozen(user: address, token: Object<Metadata>): bool acquires ConfidentialAssetStore {
-    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    borrow_global<ConfidentialAssetStore>(get_user_address(user, token)).frozen
-}
-
- - - -
- - - -## Function `get_auditor` - -Returns the asset-specific auditor's encryption key. -If the auditing feature is disabled for the token, the encryption key is set to None. - - -
#[view]
-public fun get_auditor(token: object::Object<fungible_asset::Metadata>): option::Option<ristretto255_twisted_elgamal::CompressedPubkey>
-
- - - -
-Implementation - - -
public fun get_auditor(
-    token: Object<Metadata>): Option<twisted_elgamal::CompressedPubkey> acquires FAConfig, FAController
-{
-    let fa_config_address = get_fa_config_address(token);
-
-    if (!is_allow_list_enabled() && !exists<FAConfig>(fa_config_address)) {
-        return std::option::none();
-    };
-
-    borrow_global<FAConfig>(fa_config_address).auditor_ek
-}
-
- - - -
- - - -## Function `confidential_asset_balance` - -Returns the circulating supply of the confidential asset. - - -
#[view]
-public fun confidential_asset_balance(token: object::Object<fungible_asset::Metadata>): u64
-
- - - -
-Implementation - - -
public fun confidential_asset_balance(token: Object<Metadata>): u64 acquires FAController {
-    let fa_store_address = get_fa_store_address();
-    assert!(primary_fungible_store::primary_store_exists(fa_store_address, token), EINTERNAL_ERROR);
-
-    primary_fungible_store::balance(fa_store_address, token)
-}
-
- - - -
- - - -## Function `register_internal` - -Implementation of the register entry function. - - -
public fun register_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, ek: ristretto255_twisted_elgamal::CompressedPubkey)
-
- - - -
-Implementation - - -
public fun register_internal(
-    sender: &signer,
-    token: Object<Metadata>,
-    ek: twisted_elgamal::CompressedPubkey) acquires FAController, FAConfig
-{
-    assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED));
-
-    let user = signer::address_of(sender);
-
-    assert!(!has_confidential_asset_store(user, token), error::already_exists(ECA_STORE_ALREADY_PUBLISHED));
-
-    let ca_store = ConfidentialAssetStore {
-        frozen: false,
-        normalized: true,
-        pending_counter: 0,
-        pending_balance: confidential_balance::new_compressed_pending_balance_no_randomness(),
-        actual_balance: confidential_balance::new_compressed_actual_balance_no_randomness(),
-        ek,
-    };
-
-    move_to(&get_user_signer(sender, token), ca_store);
-}
-
- - - -
- - - -## Function `deposit_to_internal` - -Implementation of the deposit_to entry function. - - -
public fun deposit_to_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64)
-
- - - -
-Implementation - - -
public fun deposit_to_internal(
-    sender: &signer,
-    token: Object<Metadata>,
-    to: address,
-    amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig
-{
-    assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED));
-    assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN));
-
-    let from = signer::address_of(sender);
-
-    let sender_fa_store = primary_fungible_store::ensure_primary_store_exists(from, token);
-    let ca_fa_store = primary_fungible_store::ensure_primary_store_exists(get_fa_store_address(), token);
-
-    dispatchable_fungible_asset::transfer(sender, sender_fa_store, ca_fa_store, amount);
-
-    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(to, token));
-    let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance);
-
-    confidential_balance::add_balances_mut(
-        &mut pending_balance,
-        &confidential_balance::new_pending_balance_u64_no_randonmess(amount)
-    );
-
-    ca_store.pending_balance = confidential_balance::compress_balance(&pending_balance);
-
-    assert!(
-        ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER,
-        error::invalid_argument(EINTERNAL_ERROR)
-    );
-
-    ca_store.pending_counter += 1;
-
-    event::emit(Deposited { from, to, amount });
-}
-
- - - -
- - - -## Function `withdraw_to_internal` - -Implementation of the withdraw_to entry function. -Withdrawals are always allowed, regardless of the token allow status. - - -
public fun withdraw_to_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64, new_balance: confidential_balance::ConfidentialBalance, proof: confidential_proof::WithdrawalProof)
-
- - - -
-Implementation - - -
public fun withdraw_to_internal(
-    sender: &signer,
-    token: Object<Metadata>,
-    to: address,
-    amount: u64,
-    new_balance: confidential_balance::ConfidentialBalance,
-    proof: WithdrawalProof) acquires ConfidentialAssetStore, FAController
-{
-    let from = signer::address_of(sender);
-
-    let sender_ek = encryption_key(from, token);
-
-    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(from, token));
-    let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
-
-    confidential_proof::verify_withdrawal_proof(&sender_ek, amount, ¤t_balance, &new_balance, &proof);
-
-    ca_store.normalized = true;
-    ca_store.actual_balance = confidential_balance::compress_balance(&new_balance);
-
-    primary_fungible_store::transfer(&get_fa_store_signer(), token, to, amount);
-}
-
- - - -
- - - -## Function `confidential_transfer_internal` - -Implementation of the confidential_transfer entry function. - - -
public fun confidential_transfer_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, new_balance: confidential_balance::ConfidentialBalance, sender_amount: confidential_balance::ConfidentialBalance, recipient_amount: confidential_balance::ConfidentialBalance, auditor_eks: vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: vector<confidential_balance::ConfidentialBalance>, proof: confidential_proof::TransferProof)
-
- - - -
-Implementation - - -
public fun confidential_transfer_internal(
-    sender: &signer,
-    token: Object<Metadata>,
-    to: address,
-    new_balance: confidential_balance::ConfidentialBalance,
-    sender_amount: confidential_balance::ConfidentialBalance,
-    recipient_amount: confidential_balance::ConfidentialBalance,
-    auditor_eks: vector<twisted_elgamal::CompressedPubkey>,
-    auditor_amounts: vector<confidential_balance::ConfidentialBalance>,
-    proof: TransferProof) acquires ConfidentialAssetStore, FAConfig, FAController
-{
-    assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED));
-    assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN));
-    assert!(
-        validate_auditors(token, &recipient_amount, &auditor_eks, &auditor_amounts, &proof),
-        error::invalid_argument(EINVALID_AUDITORS)
-    );
-    assert!(
-        confidential_balance::balance_c_equals(&sender_amount, &recipient_amount),
-        error::invalid_argument(EINVALID_SENDER_AMOUNT)
-    );
-
-    let from = signer::address_of(sender);
-
-    let sender_ek = encryption_key(from, token);
-    let recipient_ek = encryption_key(to, token);
-
-    let sender_ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(from, token));
-
-    let sender_current_actual_balance = confidential_balance::decompress_balance(
-        &sender_ca_store.actual_balance
-    );
-
-    confidential_proof::verify_transfer_proof(
-        &sender_ek,
-        &recipient_ek,
-        &sender_current_actual_balance,
-        &new_balance,
-        &sender_amount,
-        &recipient_amount,
-        &auditor_eks,
-        &auditor_amounts,
-        &proof);
-
-    sender_ca_store.normalized = true;
-    sender_ca_store.actual_balance = confidential_balance::compress_balance(&new_balance);
-
-    // Cannot create multiple mutable references to the same type, so we need to drop it
-    let ConfidentialAssetStore { .. } = sender_ca_store;
-
-    let recipient_ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(to, token));
-
-    assert!(
-        recipient_ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER,
-        error::invalid_argument(EINTERNAL_ERROR)
-    );
-
-    let recipient_pending_balance = confidential_balance::decompress_balance(
-        &recipient_ca_store.pending_balance
-    );
-    confidential_balance::add_balances_mut(&mut recipient_pending_balance, &recipient_amount);
-
-    recipient_ca_store.pending_counter += 1;
-    recipient_ca_store.pending_balance = confidential_balance::compress_balance(&recipient_pending_balance);
-
-    event::emit(Transferred { from, to });
-}
-
- - - -
- - - -## Function `rotate_encryption_key_internal` - -Implementation of the rotate_encryption_key entry function. - - -
public fun rotate_encryption_key_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_ek: ristretto255_twisted_elgamal::CompressedPubkey, new_balance: confidential_balance::ConfidentialBalance, proof: confidential_proof::RotationProof)
-
- - - -
-Implementation - - -
public fun rotate_encryption_key_internal(
-    sender: &signer,
-    token: Object<Metadata>,
-    new_ek: twisted_elgamal::CompressedPubkey,
-    new_balance: confidential_balance::ConfidentialBalance,
-    proof: RotationProof) acquires ConfidentialAssetStore
-{
-    let user = signer::address_of(sender);
-    let current_ek = encryption_key(user, token);
-
-    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
-
-    let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance);
-
-    // We need to ensure that the pending balance is zero before rotating the key.
-    // To guarantee this, the user must call `rollover_pending_balance_and_freeze` beforehand.
-    assert!(confidential_balance::is_zero_balance(&pending_balance), error::invalid_state(ENOT_ZERO_BALANCE));
-
-    let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
-
-    confidential_proof::verify_rotation_proof(¤t_ek, &new_ek, ¤t_balance, &new_balance, &proof);
-
-    ca_store.ek = new_ek;
-    // We don't need to update the pending balance here, as it has been asserted to be zero.
-    ca_store.actual_balance = confidential_balance::compress_balance(&new_balance);
-    ca_store.normalized = true;
-}
-
- - - -
- - - -## Function `normalize_internal` - -Implementation of the normalize entry function. - - -
public fun normalize_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_balance: confidential_balance::ConfidentialBalance, proof: confidential_proof::NormalizationProof)
-
- - - -
-Implementation - - -
public fun normalize_internal(
-    sender: &signer,
-    token: Object<Metadata>,
-    new_balance: confidential_balance::ConfidentialBalance,
-    proof: NormalizationProof) acquires ConfidentialAssetStore
-{
-    let user = signer::address_of(sender);
-    let sender_ek = encryption_key(user, token);
-
-    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
-
-    assert!(!ca_store.normalized, error::invalid_state(EALREADY_NORMALIZED));
-
-    let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
-
-    confidential_proof::verify_normalization_proof(&sender_ek, ¤t_balance, &new_balance, &proof);
-
-    ca_store.actual_balance = confidential_balance::compress_balance(&new_balance);
-    ca_store.normalized = true;
-}
-
- - - -
- - - -## Function `rollover_pending_balance_internal` - -Implementation of the rollover_pending_balance entry function. - - -
public fun rollover_pending_balance_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public fun rollover_pending_balance_internal(
-    sender: &signer,
-    token: Object<Metadata>) acquires ConfidentialAssetStore
-{
-    let user = signer::address_of(sender);
-
-    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
-
-    assert!(ca_store.normalized, error::invalid_state(ENORMALIZATION_REQUIRED));
-
-    let actual_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
-    let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance);
-
-    confidential_balance::add_balances_mut(&mut actual_balance, &pending_balance);
-
-    ca_store.normalized = false;
-    ca_store.pending_counter = 0;
-    ca_store.actual_balance = confidential_balance::compress_balance(&actual_balance);
-    ca_store.pending_balance = confidential_balance::new_compressed_pending_balance_no_randomness();
-}
-
- - - -
- - - -## Function `freeze_token_internal` - -Implementation of the freeze_token entry function. - - -
public fun freeze_token_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public fun freeze_token_internal(
-    sender: &signer,
-    token: Object<Metadata>) acquires ConfidentialAssetStore
-{
-    let user = signer::address_of(sender);
-
-    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
-
-    assert!(!ca_store.frozen, error::invalid_state(EALREADY_FROZEN));
-
-    ca_store.frozen = true;
-}
-
- - - -
- - - -## Function `unfreeze_token_internal` - -Implementation of the unfreeze_token entry function. - - -
public fun unfreeze_token_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>)
-
- - - -
-Implementation - - -
public fun unfreeze_token_internal(
-    sender: &signer,
-    token: Object<Metadata>) acquires ConfidentialAssetStore
-{
-    let user = signer::address_of(sender);
-
-    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
-
-    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
-
-    assert!(ca_store.frozen, error::invalid_state(ENOT_FROZEN));
-
-    ca_store.frozen = false;
-}
-
- - - -
- - - -## Function `ensure_fa_config_exists` - -Ensures that the FAConfig object exists for the specified token. -If the object does not exist, creates it. -Used only for internal purposes. - - -
fun ensure_fa_config_exists(token: object::Object<fungible_asset::Metadata>): address
-
- - - -
-Implementation - - -
fun ensure_fa_config_exists(token: Object<Metadata>): address acquires FAController {
-    let fa_config_address = get_fa_config_address(token);
-
-    if (!exists<FAConfig>(fa_config_address)) {
-        let fa_config_singer = get_fa_config_signer(token);
-
-        move_to(&fa_config_singer, FAConfig {
-            allowed: false,
-            auditor_ek: std::option::none(),
-        });
-    };
-
-    fa_config_address
-}
-
- - - -
- - - -## Function `get_fa_store_signer` - -Returns an object for handling all the FA primary stores, and returns a signer for it. - - -
fun get_fa_store_signer(): signer
-
- - - -
-Implementation - - -
fun get_fa_store_signer(): signer acquires FAController {
-    object::generate_signer_for_extending(&borrow_global<FAController>(@aptos_experimental).extend_ref)
-}
-
- - - -
- - - -## Function `get_fa_store_address` - -Returns the address that handles all the FA primary stores. - - -
fun get_fa_store_address(): address
-
- - - -
-Implementation - - -
fun get_fa_store_address(): address acquires FAController {
-    object::address_from_extend_ref(&borrow_global<FAController>(@aptos_experimental).extend_ref)
-}
-
- - - -
- - - -## Function `get_user_signer` - -Returns an object for handling the ConfidentialAssetStore and returns a signer for it. - - -
fun get_user_signer(user: &signer, token: object::Object<fungible_asset::Metadata>): signer
-
- - - -
-Implementation - - -
fun get_user_signer(user: &signer, token: Object<Metadata>): signer {
-    let user_ctor = &object::create_named_object(user, construct_user_seed(token));
-
-    object::generate_signer(user_ctor)
-}
-
- - - -
- - - -## Function `get_user_address` - -Returns the address that handles the user's ConfidentialAssetStore object for the specified user and token. - - -
fun get_user_address(user: address, token: object::Object<fungible_asset::Metadata>): address
-
- - - -
-Implementation - - -
fun get_user_address(user: address, token: Object<Metadata>): address {
-    object::create_object_address(&user, construct_user_seed(token))
-}
-
- - - -
- - - -## Function `get_fa_config_signer` - -Returns an object for handling the FAConfig, and returns a signer for it. - - -
fun get_fa_config_signer(token: object::Object<fungible_asset::Metadata>): signer
-
- - - -
-Implementation - - -
fun get_fa_config_signer(token: Object<Metadata>): signer acquires FAController {
-    let fa_ext = &borrow_global<FAController>(@aptos_experimental).extend_ref;
-    let fa_ext_signer = object::generate_signer_for_extending(fa_ext);
-
-    let fa_ctor = &object::create_named_object(&fa_ext_signer, construct_fa_seed(token));
-
-    object::generate_signer(fa_ctor)
-}
-
- - - -
- - - -## Function `get_fa_config_address` - -Returns the address that handles primary FA store and FAConfig objects for the specified token. - - -
fun get_fa_config_address(token: object::Object<fungible_asset::Metadata>): address
-
- - - -
-Implementation - - -
fun get_fa_config_address(token: Object<Metadata>): address acquires FAController {
-    let fa_ext = &borrow_global<FAController>(@aptos_experimental).extend_ref;
-    let fa_ext_address = object::address_from_extend_ref(fa_ext);
-
-    object::create_object_address(&fa_ext_address, construct_fa_seed(token))
-}
-
- - - -
- - - -## Function `construct_user_seed` - -Constructs a unique seed for the user's ConfidentialAssetStore object. -As all the ConfidentialAssetStore's have the same type, we need to differentiate them by the seed. - - -
fun construct_user_seed(token: object::Object<fungible_asset::Metadata>): vector<u8>
-
- - - -
-Implementation - - -
fun construct_user_seed(token: Object<Metadata>): vector<u8> {
-    bcs::to_bytes(
-        &string_utils::format2(
-            &b"confidential_asset::{}::token::{}::user",
-            @aptos_experimental,
-            object::object_address(&token)
-        )
-    )
-}
-
- - - -
- - - -## Function `construct_fa_seed` - -Constructs a unique seed for the FA's FAConfig object. -As all the FAConfig's have the same type, we need to differentiate them by the seed. - - -
fun construct_fa_seed(token: object::Object<fungible_asset::Metadata>): vector<u8>
-
- - - -
-Implementation - - -
fun construct_fa_seed(token: Object<Metadata>): vector<u8> {
-    bcs::to_bytes(
-        &string_utils::format2(
-            &b"confidential_asset::{}::token::{}::fa",
-            @aptos_experimental,
-            object::object_address(&token)
-        )
-    )
-}
-
- - - -
- - - -## Function `validate_auditors` - -Validates that the auditor-related fields in the confidential transfer are correct. -Returns false if the transfer amount is not the same as the auditor amounts. -Returns false if the number of auditors in the transfer proof and auditor lists do not match. -Returns false if the first auditor in the list and the asset-specific auditor do not match. -Note: If the asset-specific auditor is not set, the validation is successful for any list of auditors. -Otherwise, returns true. - - -
fun validate_auditors(token: object::Object<fungible_asset::Metadata>, transfer_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, proof: &confidential_proof::TransferProof): bool
-
- - - -
-Implementation - - -
fun validate_auditors(
-    token: Object<Metadata>,
-    transfer_amount: &confidential_balance::ConfidentialBalance,
-    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
-    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
-    proof: &TransferProof): bool acquires FAConfig, FAController
-{
-    if (
-        !auditor_amounts.all(|auditor_amount| {
-            confidential_balance::balance_c_equals(transfer_amount, auditor_amount)
-        })
-    ) {
-        return false
-    };
-
-    if (
-        auditor_eks.length() != auditor_amounts.length() ||
-            auditor_eks.length() != confidential_proof::auditors_count_in_transfer_proof(proof)
-    ) {
-        return false
-    };
-
-    let asset_auditor_ek = get_auditor(token);
-    if (asset_auditor_ek.is_none()) {
-        return true
-    };
-
-    if (auditor_eks.length() == 0) {
-        return false
-    };
-
-    let asset_auditor_ek = twisted_elgamal::pubkey_to_point(&asset_auditor_ek.extract());
-    let first_auditor_ek = twisted_elgamal::pubkey_to_point(&auditor_eks[0]);
-
-    ristretto255::point_equals(&asset_auditor_ek, &first_auditor_ek)
-}
-
- - - -
- - - -## Function `deserialize_auditor_eks` - -Deserializes the auditor EKs from a byte array. -Returns Some(vector<twisted_elgamal::CompressedPubkey>) if the deserialization is successful, otherwise None. - - -
fun deserialize_auditor_eks(auditor_eks_bytes: vector<u8>): option::Option<vector<ristretto255_twisted_elgamal::CompressedPubkey>>
-
- - - -
-Implementation - - -
fun deserialize_auditor_eks(
-    auditor_eks_bytes: vector<u8>): Option<vector<twisted_elgamal::CompressedPubkey>>
-{
-    if (auditor_eks_bytes.length() % 32 != 0) {
-        return std::option::none()
-    };
-
-    let auditors_count = auditor_eks_bytes.length() / 32;
-
-    let auditor_eks = vector::range(0, auditors_count).map(|i| {
-        twisted_elgamal::new_pubkey_from_bytes(auditor_eks_bytes.slice(i * 32, (i + 1) * 32))
-    });
-
-    if (auditor_eks.any(|ek| ek.is_none())) {
-        return std::option::none()
-    };
-
-    std::option::some(auditor_eks.map(|ek| ek.extract()))
-}
-
- - - -
- - - -## Function `deserialize_auditor_amounts` - -Deserializes the auditor amounts from a byte array. -Returns Some(vector<confidential_balance::ConfidentialBalance>) if the deserialization is successful, otherwise None. - - -
fun deserialize_auditor_amounts(auditor_amounts_bytes: vector<u8>): option::Option<vector<confidential_balance::ConfidentialBalance>>
-
- - - -
-Implementation - - -
fun deserialize_auditor_amounts(
-    auditor_amounts_bytes: vector<u8>): Option<vector<confidential_balance::ConfidentialBalance>>
-{
-    if (auditor_amounts_bytes.length() % 256 != 0) {
-        return std::option::none()
-    };
-
-    let auditors_count = auditor_amounts_bytes.length() / 256;
-
-    let auditor_amounts = vector::range(0, auditors_count).map(|i| {
-        confidential_balance::new_pending_balance_from_bytes(auditor_amounts_bytes.slice(i * 256, (i + 1) * 256))
-    });
-
-    if (auditor_amounts.any(|ek| ek.is_none())) {
-        return std::option::none()
-    };
-
-    std::option::some(auditor_amounts.map(|balance| balance.extract()))
-}
-
- - - -
- - - -## Function `ensure_sufficient_fa` - -Converts coins to missing FA. -Returns Some(Object<Metadata>) if user has a suffucient amount of FA to proceed, otherwise None. - - -
fun ensure_sufficient_fa<CoinType>(sender: &signer, amount: u64): option::Option<object::Object<fungible_asset::Metadata>>
-
- - - -
-Implementation - - -
fun ensure_sufficient_fa<CoinType>(sender: &signer, amount: u64): Option<Object<Metadata>> {
-    let user = signer::address_of(sender);
-    let fa = coin::paired_metadata<CoinType>();
-
-    if (fa.is_none()) {
-        return fa;
-    };
-
-    let fa_balance = primary_fungible_store::balance(user, *fa.borrow());
-
-    if (fa_balance >= amount) {
-        return fa;
-    };
-
-    if (coin::balance<CoinType>(user) < amount) {
-        return std::option::none();
-    };
-
-    let coin_amount = coin::withdraw<CoinType>(sender, amount - fa_balance);
-    let fa_amount = coin::coin_to_fungible_asset(coin_amount);
-
-    primary_fungible_store::deposit(user, fa_amount);
-
-    fa
-}
-
- - - -
- - -[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-experimental/doc/confidential_balance.md b/aptos-move/framework/aptos-experimental/doc/confidential_balance.md deleted file mode 100644 index 30bf118e097..00000000000 --- a/aptos-move/framework/aptos-experimental/doc/confidential_balance.md +++ /dev/null @@ -1,823 +0,0 @@ - - - -# Module `0x7::confidential_balance` - -This module implements a Confidential Balance abstraction, built on top of Twisted ElGamal encryption, -over the Ristretto255 curve. - -The Confidential Balance encapsulates encrypted representations of a balance, split into chunks and stored as pairs of -ciphertext components (C_i, D_i) under basepoints G and H and an encryption key P = dk^(-1) * H, where dk -is the corresponding decryption key. Each pair represents an encrypted value a_i - the i-th 16-bit portion of -the total encrypted amount - and its associated randomness r_i, such that C_i = a_i * G + r_i * H and D_i = r_i * P. - -The module supports two types of balances: -- Pending balances are represented by four ciphertext pairs (C_i, D_i), i = 1..4, suitable for 64-bit values. -- Actual balances are represented by eight ciphertext pairs (C_i, D_i), i = 1..8, capable of handling 128-bit values. - -This implementation leverages the homomorphic properties of Twisted ElGamal encryption to allow arithmetic operations -directly on encrypted data. - - -- [Struct `CompressedConfidentialBalance`](#0x7_confidential_balance_CompressedConfidentialBalance) -- [Struct `ConfidentialBalance`](#0x7_confidential_balance_ConfidentialBalance) -- [Constants](#@Constants_0) -- [Function `new_pending_balance_no_randomness`](#0x7_confidential_balance_new_pending_balance_no_randomness) -- [Function `new_actual_balance_no_randomness`](#0x7_confidential_balance_new_actual_balance_no_randomness) -- [Function `new_compressed_pending_balance_no_randomness`](#0x7_confidential_balance_new_compressed_pending_balance_no_randomness) -- [Function `new_compressed_actual_balance_no_randomness`](#0x7_confidential_balance_new_compressed_actual_balance_no_randomness) -- [Function `new_pending_balance_u64_no_randonmess`](#0x7_confidential_balance_new_pending_balance_u64_no_randonmess) -- [Function `new_pending_balance_from_bytes`](#0x7_confidential_balance_new_pending_balance_from_bytes) -- [Function `new_actual_balance_from_bytes`](#0x7_confidential_balance_new_actual_balance_from_bytes) -- [Function `compress_balance`](#0x7_confidential_balance_compress_balance) -- [Function `decompress_balance`](#0x7_confidential_balance_decompress_balance) -- [Function `balance_to_bytes`](#0x7_confidential_balance_balance_to_bytes) -- [Function `balance_to_points_c`](#0x7_confidential_balance_balance_to_points_c) -- [Function `balance_to_points_d`](#0x7_confidential_balance_balance_to_points_d) -- [Function `add_balances_mut`](#0x7_confidential_balance_add_balances_mut) -- [Function `sub_balances_mut`](#0x7_confidential_balance_sub_balances_mut) -- [Function `balance_equals`](#0x7_confidential_balance_balance_equals) -- [Function `balance_c_equals`](#0x7_confidential_balance_balance_c_equals) -- [Function `is_zero_balance`](#0x7_confidential_balance_is_zero_balance) -- [Function `split_into_chunks_u64`](#0x7_confidential_balance_split_into_chunks_u64) -- [Function `split_into_chunks_u128`](#0x7_confidential_balance_split_into_chunks_u128) -- [Function `get_pending_balance_chunks`](#0x7_confidential_balance_get_pending_balance_chunks) -- [Function `get_actual_balance_chunks`](#0x7_confidential_balance_get_actual_balance_chunks) -- [Function `get_chunk_size_bits`](#0x7_confidential_balance_get_chunk_size_bits) - - -
use 0x1::error;
-use 0x1::option;
-use 0x1::ristretto255;
-use 0x1::vector;
-use 0x7::ristretto255_twisted_elgamal;
-
- - - - - -## Struct `CompressedConfidentialBalance` - -Represents a compressed confidential balance, where each chunk is a compressed Twisted ElGamal ciphertext. - - -
struct CompressedConfidentialBalance has copy, drop, store
-
- - - -
-Fields - - -
-
-chunks: vector<ristretto255_twisted_elgamal::CompressedCiphertext> -
-
- -
-
- - -
- - - -## Struct `ConfidentialBalance` - -Represents a confidential balance, where each chunk is a Twisted ElGamal ciphertext. - - -
struct ConfidentialBalance has drop
-
- - - -
-Fields - - -
-
-chunks: vector<ristretto255_twisted_elgamal::Ciphertext> -
-
- -
-
- - -
- - - -## Constants - - - - -The number of chunks in an actual balance. - - -
const ACTUAL_BALANCE_CHUNKS: u64 = 8;
-
- - - - - -The number of bits in a single chunk. - - -
const CHUNK_SIZE_BITS: u64 = 16;
-
- - - - - -An internal error occurred, indicating unexpected behavior. - - -
const EINTERNAL_ERROR: u64 = 1;
-
- - - - - -The number of chunks in a pending balance. - - -
const PENDING_BALANCE_CHUNKS: u64 = 4;
-
- - - - - -## Function `new_pending_balance_no_randomness` - -Creates a new zero pending balance, where each chunk is set to zero Twisted ElGamal ciphertext. - - -
public fun new_pending_balance_no_randomness(): confidential_balance::ConfidentialBalance
-
- - - -
-Implementation - - -
public fun new_pending_balance_no_randomness(): ConfidentialBalance {
-    ConfidentialBalance {
-        chunks: vector::range(0, PENDING_BALANCE_CHUNKS).map(|_| {
-            twisted_elgamal::ciphertext_from_points(ristretto255::point_identity(), ristretto255::point_identity())
-        })
-    }
-}
-
- - - -
- - - -## Function `new_actual_balance_no_randomness` - -Creates a new zero actual balance, where each chunk is set to zero Twisted ElGamal ciphertext. - - -
public fun new_actual_balance_no_randomness(): confidential_balance::ConfidentialBalance
-
- - - -
-Implementation - - -
public fun new_actual_balance_no_randomness(): ConfidentialBalance {
-    ConfidentialBalance {
-        chunks: vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|_| {
-            twisted_elgamal::ciphertext_from_points(ristretto255::point_identity(), ristretto255::point_identity())
-        })
-    }
-}
-
- - - -
- - - -## Function `new_compressed_pending_balance_no_randomness` - -Creates a new compressed zero pending balance, where each chunk is set to compressed zero Twisted ElGamal ciphertext. - - -
public fun new_compressed_pending_balance_no_randomness(): confidential_balance::CompressedConfidentialBalance
-
- - - -
-Implementation - - -
public fun new_compressed_pending_balance_no_randomness(): CompressedConfidentialBalance {
-    CompressedConfidentialBalance {
-        chunks: vector::range(0, PENDING_BALANCE_CHUNKS).map(|_| {
-            twisted_elgamal::ciphertext_from_compressed_points(
-                ristretto255::point_identity_compressed(), ristretto255::point_identity_compressed())
-        })
-    }
-}
-
- - - -
- - - -## Function `new_compressed_actual_balance_no_randomness` - -Creates a new compressed zero actual balance, where each chunk is set to compressed zero Twisted ElGamal ciphertext. - - -
public fun new_compressed_actual_balance_no_randomness(): confidential_balance::CompressedConfidentialBalance
-
- - - -
-Implementation - - -
public fun new_compressed_actual_balance_no_randomness(): CompressedConfidentialBalance {
-    CompressedConfidentialBalance {
-        chunks: vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|_| {
-            twisted_elgamal::ciphertext_from_compressed_points(
-                ristretto255::point_identity_compressed(), ristretto255::point_identity_compressed())
-        })
-    }
-}
-
- - - -
- - - -## Function `new_pending_balance_u64_no_randonmess` - -Creates a new pending balance from a 64-bit amount with no randomness, splitting the amount into four 16-bit chunks. - - -
public fun new_pending_balance_u64_no_randonmess(amount: u64): confidential_balance::ConfidentialBalance
-
- - - -
-Implementation - - -
public fun new_pending_balance_u64_no_randonmess(amount: u64): ConfidentialBalance {
-    ConfidentialBalance {
-        chunks: split_into_chunks_u64(amount).map(|chunk| {
-            twisted_elgamal::new_ciphertext_no_randomness(&chunk)
-        })
-    }
-}
-
- - - -
- - - -## Function `new_pending_balance_from_bytes` - -Creates a new pending balance from a serialized byte array representation. -Returns Some(ConfidentialBalance) if deserialization succeeds, otherwise None. - - -
public fun new_pending_balance_from_bytes(bytes: vector<u8>): option::Option<confidential_balance::ConfidentialBalance>
-
- - - -
-Implementation - - -
public fun new_pending_balance_from_bytes(bytes: vector<u8>): Option<ConfidentialBalance> {
-    if (bytes.length() != 64 * PENDING_BALANCE_CHUNKS) {
-        return std::option::none()
-    };
-
-    let chunks = vector::range(0, PENDING_BALANCE_CHUNKS).map(|i| {
-        twisted_elgamal::new_ciphertext_from_bytes(bytes.slice(i * 64, (i + 1) * 64))
-    });
-
-    if (chunks.any(|chunk| chunk.is_none())) {
-        return std::option::none()
-    };
-
-    option::some(ConfidentialBalance {
-        chunks: chunks.map(|chunk| chunk.extract())
-    })
-}
-
- - - -
- - - -## Function `new_actual_balance_from_bytes` - -Creates a new actual balance from a serialized byte array representation. -Returns Some(ConfidentialBalance) if deserialization succeeds, otherwise None. - - -
public fun new_actual_balance_from_bytes(bytes: vector<u8>): option::Option<confidential_balance::ConfidentialBalance>
-
- - - -
-Implementation - - -
public fun new_actual_balance_from_bytes(bytes: vector<u8>): Option<ConfidentialBalance> {
-    if (bytes.length() != 64 * ACTUAL_BALANCE_CHUNKS) {
-        return std::option::none()
-    };
-
-    let chunks = vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|i| {
-        twisted_elgamal::new_ciphertext_from_bytes(bytes.slice(i * 64, (i + 1) * 64))
-    });
-
-    if (chunks.any(|chunk| chunk.is_none())) {
-        return std::option::none()
-    };
-
-    option::some(ConfidentialBalance {
-        chunks: chunks.map(|chunk| chunk.extract())
-    })
-}
-
- - - -
- - - -## Function `compress_balance` - -Compresses a confidential balance into its CompressedConfidentialBalance representation. - - -
public fun compress_balance(balance: &confidential_balance::ConfidentialBalance): confidential_balance::CompressedConfidentialBalance
-
- - - -
-Implementation - - -
public fun compress_balance(balance: &ConfidentialBalance): CompressedConfidentialBalance {
-    CompressedConfidentialBalance {
-        chunks: balance.chunks.map_ref(|ciphertext| twisted_elgamal::compress_ciphertext(ciphertext))
-    }
-}
-
- - - -
- - - -## Function `decompress_balance` - -Decompresses a compressed confidential balance into its ConfidentialBalance representation. - - -
public fun decompress_balance(balance: &confidential_balance::CompressedConfidentialBalance): confidential_balance::ConfidentialBalance
-
- - - -
-Implementation - - -
public fun decompress_balance(balance: &CompressedConfidentialBalance): ConfidentialBalance {
-    ConfidentialBalance {
-        chunks: balance.chunks.map_ref(|ciphertext| twisted_elgamal::decompress_ciphertext(ciphertext))
-    }
-}
-
- - - -
- - - -## Function `balance_to_bytes` - -Serializes a confidential balance into a byte array representation. - - -
public fun balance_to_bytes(balance: &confidential_balance::ConfidentialBalance): vector<u8>
-
- - - -
-Implementation - - -
public fun balance_to_bytes(balance: &ConfidentialBalance): vector<u8> {
-    let bytes = vector<u8>[];
-
-    balance.chunks.for_each_ref(|ciphertext| {
-        bytes.append(twisted_elgamal::ciphertext_to_bytes(ciphertext));
-    });
-
-    bytes
-}
-
- - - -
- - - -## Function `balance_to_points_c` - -Extracts the C value component (a * H + r * G) of each chunk in a confidential balance as a vector of RistrettoPoints. - - -
public fun balance_to_points_c(balance: &confidential_balance::ConfidentialBalance): vector<ristretto255::RistrettoPoint>
-
- - - -
-Implementation - - -
public fun balance_to_points_c(balance: &ConfidentialBalance): vector<RistrettoPoint> {
-    balance.chunks.map_ref(|chunk| {
-        let (c, _) = twisted_elgamal::ciphertext_as_points(chunk);
-        ristretto255::point_clone(c)
-    })
-}
-
- - - -
- - - -## Function `balance_to_points_d` - -Extracts the D randomness component (r * Y) of each chunk in a confidential balance as a vector of RistrettoPoints. - - -
public fun balance_to_points_d(balance: &confidential_balance::ConfidentialBalance): vector<ristretto255::RistrettoPoint>
-
- - - -
-Implementation - - -
public fun balance_to_points_d(balance: &ConfidentialBalance): vector<RistrettoPoint> {
-    balance.chunks.map_ref(|chunk| {
-        let (_, d) = twisted_elgamal::ciphertext_as_points(chunk);
-        ristretto255::point_clone(d)
-    })
-}
-
- - - -
- - - -## Function `add_balances_mut` - -Adds two confidential balances homomorphically, mutating the first balance in place. -The second balance must have fewer or equal chunks compared to the first. - - -
public fun add_balances_mut(lhs: &mut confidential_balance::ConfidentialBalance, rhs: &confidential_balance::ConfidentialBalance)
-
- - - -
-Implementation - - -
public fun add_balances_mut(lhs: &mut ConfidentialBalance, rhs: &ConfidentialBalance) {
-    assert!(lhs.chunks.length() >= rhs.chunks.length(), error::internal(EINTERNAL_ERROR));
-
-    lhs.chunks.enumerate_mut(|i, chunk| {
-        if (i < rhs.chunks.length()) {
-            twisted_elgamal::ciphertext_add_assign(chunk, &rhs.chunks[i])
-        }
-    })
-}
-
- - - -
- - - -## Function `sub_balances_mut` - -Subtracts one confidential balance from another homomorphically, mutating the first balance in place. -The second balance must have fewer or equal chunks compared to the first. - - -
public fun sub_balances_mut(lhs: &mut confidential_balance::ConfidentialBalance, rhs: &confidential_balance::ConfidentialBalance)
-
- - - -
-Implementation - - -
public fun sub_balances_mut(lhs: &mut ConfidentialBalance, rhs: &ConfidentialBalance) {
-    assert!(lhs.chunks.length() >= rhs.chunks.length(), error::internal(EINTERNAL_ERROR));
-
-    lhs.chunks.enumerate_mut(|i, chunk| {
-        if (i < rhs.chunks.length()) {
-            twisted_elgamal::ciphertext_add_assign(chunk, &rhs.chunks[i])
-        }
-    })
-}
-
- - - -
- - - -## Function `balance_equals` - -Checks if two confidential balances are equivalent, including both value and randomness components. - - -
public fun balance_equals(lhs: &confidential_balance::ConfidentialBalance, rhs: &confidential_balance::ConfidentialBalance): bool
-
- - - -
-Implementation - - -
public fun balance_equals(lhs: &ConfidentialBalance, rhs: &ConfidentialBalance): bool {
-    assert!(lhs.chunks.length() == rhs.chunks.length(), error::internal(EINTERNAL_ERROR));
-
-    let ok = true;
-
-    lhs.chunks.zip_ref(&rhs.chunks, |l, r| {
-        ok = ok && twisted_elgamal::ciphertext_equals(l, r);
-    });
-
-    ok
-}
-
- - - -
- - - -## Function `balance_c_equals` - -Checks if the corresponding value components (C) of two confidential balances are equivalent. - - -
public fun balance_c_equals(lhs: &confidential_balance::ConfidentialBalance, rhs: &confidential_balance::ConfidentialBalance): bool
-
- - - -
-Implementation - - -
public fun balance_c_equals(lhs: &ConfidentialBalance, rhs: &ConfidentialBalance): bool {
-    assert!(lhs.chunks.length() == rhs.chunks.length(), error::internal(EINTERNAL_ERROR));
-
-    let ok = true;
-
-    lhs.chunks.zip_ref(&rhs.chunks, |l, r| {
-        let (lc, _) = twisted_elgamal::ciphertext_as_points(l);
-        let (rc, _) = twisted_elgamal::ciphertext_as_points(r);
-
-        ok = ok && ristretto255::point_equals(lc, rc);
-    });
-
-    ok
-}
-
- - - -
- - - -## Function `is_zero_balance` - -Checks if a confidential balance is equivalent to zero, where all chunks are the identity element. - - -
public fun is_zero_balance(balance: &confidential_balance::ConfidentialBalance): bool
-
- - - -
-Implementation - - -
public fun is_zero_balance(balance: &ConfidentialBalance): bool {
-    balance.chunks.all(|chunk| {
-        twisted_elgamal::ciphertext_equals(
-            chunk,
-            &twisted_elgamal::ciphertext_from_points(ristretto255::point_identity(), ristretto255::point_identity())
-        )
-    })
-}
-
- - - -
- - - -## Function `split_into_chunks_u64` - -Splits a 64-bit integer amount into four 16-bit chunks, represented as Scalar values. - - -
public fun split_into_chunks_u64(amount: u64): vector<ristretto255::Scalar>
-
- - - -
-Implementation - - -
public fun split_into_chunks_u64(amount: u64): vector<Scalar> {
-    vector::range(0, PENDING_BALANCE_CHUNKS).map(|i| {
-        ristretto255::new_scalar_from_u64(amount >> (i * CHUNK_SIZE_BITS as u8) & 0xffff)
-    })
-}
-
- - - -
- - - -## Function `split_into_chunks_u128` - -Splits a 128-bit integer amount into eight 16-bit chunks, represented as Scalar values. - - -
public fun split_into_chunks_u128(amount: u128): vector<ristretto255::Scalar>
-
- - - -
-Implementation - - -
public fun split_into_chunks_u128(amount: u128): vector<Scalar> {
-    vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|i| {
-        ristretto255::new_scalar_from_u128(amount >> (i * CHUNK_SIZE_BITS as u8) & 0xffff)
-    })
-}
-
- - - -
- - - -## Function `get_pending_balance_chunks` - -Returns the number of chunks in a pending balance. - - -
#[view]
-public fun get_pending_balance_chunks(): u64
-
- - - -
-Implementation - - -
public fun get_pending_balance_chunks(): u64 {
-    PENDING_BALANCE_CHUNKS
-}
-
- - - -
- - - -## Function `get_actual_balance_chunks` - -Returns the number of chunks in an actual balance. - - -
#[view]
-public fun get_actual_balance_chunks(): u64
-
- - - -
-Implementation - - -
public fun get_actual_balance_chunks(): u64 {
-    ACTUAL_BALANCE_CHUNKS
-}
-
- - - -
- - - -## Function `get_chunk_size_bits` - -Returns the number of bits in a single chunk. - - -
#[view]
-public fun get_chunk_size_bits(): u64
-
- - - -
-Implementation - - -
public fun get_chunk_size_bits(): u64 {
-    CHUNK_SIZE_BITS
-}
-
- - - -
- - -[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-experimental/doc/confidential_proof.md b/aptos-move/framework/aptos-experimental/doc/confidential_proof.md deleted file mode 100644 index 9ed1df41021..00000000000 --- a/aptos-move/framework/aptos-experimental/doc/confidential_proof.md +++ /dev/null @@ -1,2982 +0,0 @@ - - - -# Module `0x7::confidential_proof` - -The confidential_proof module provides the infrastructure for verifying zero-knowledge proofs used in the Confidential Asset protocol. -These proofs ensure correctness for operations such as confidential_transfer, withdraw, rotate_encryption_key, and normalize. - - -- [Struct `WithdrawalProof`](#0x7_confidential_proof_WithdrawalProof) -- [Struct `TransferProof`](#0x7_confidential_proof_TransferProof) -- [Struct `NormalizationProof`](#0x7_confidential_proof_NormalizationProof) -- [Struct `RotationProof`](#0x7_confidential_proof_RotationProof) -- [Struct `WithdrawalSigmaProofXs`](#0x7_confidential_proof_WithdrawalSigmaProofXs) -- [Struct `WithdrawalSigmaProofAlphas`](#0x7_confidential_proof_WithdrawalSigmaProofAlphas) -- [Struct `WithdrawalSigmaProofGammas`](#0x7_confidential_proof_WithdrawalSigmaProofGammas) -- [Struct `WithdrawalSigmaProof`](#0x7_confidential_proof_WithdrawalSigmaProof) -- [Struct `TransferSigmaProofXs`](#0x7_confidential_proof_TransferSigmaProofXs) -- [Struct `TransferSigmaProofAlphas`](#0x7_confidential_proof_TransferSigmaProofAlphas) -- [Struct `TransferSigmaProofGammas`](#0x7_confidential_proof_TransferSigmaProofGammas) -- [Struct `TransferSigmaProof`](#0x7_confidential_proof_TransferSigmaProof) -- [Struct `NormalizationSigmaProofXs`](#0x7_confidential_proof_NormalizationSigmaProofXs) -- [Struct `NormalizationSigmaProofAlphas`](#0x7_confidential_proof_NormalizationSigmaProofAlphas) -- [Struct `NormalizationSigmaProofGammas`](#0x7_confidential_proof_NormalizationSigmaProofGammas) -- [Struct `NormalizationSigmaProof`](#0x7_confidential_proof_NormalizationSigmaProof) -- [Struct `RotationSigmaProofXs`](#0x7_confidential_proof_RotationSigmaProofXs) -- [Struct `RotationSigmaProofAlphas`](#0x7_confidential_proof_RotationSigmaProofAlphas) -- [Struct `RotationSigmaProofGammas`](#0x7_confidential_proof_RotationSigmaProofGammas) -- [Struct `RotationSigmaProof`](#0x7_confidential_proof_RotationSigmaProof) -- [Constants](#@Constants_0) -- [Function `verify_withdrawal_proof`](#0x7_confidential_proof_verify_withdrawal_proof) -- [Function `verify_transfer_proof`](#0x7_confidential_proof_verify_transfer_proof) -- [Function `verify_normalization_proof`](#0x7_confidential_proof_verify_normalization_proof) -- [Function `verify_rotation_proof`](#0x7_confidential_proof_verify_rotation_proof) -- [Function `verify_withdrawal_sigma_proof`](#0x7_confidential_proof_verify_withdrawal_sigma_proof) -- [Function `verify_transfer_sigma_proof`](#0x7_confidential_proof_verify_transfer_sigma_proof) -- [Function `verify_normalization_sigma_proof`](#0x7_confidential_proof_verify_normalization_sigma_proof) -- [Function `verify_rotation_sigma_proof`](#0x7_confidential_proof_verify_rotation_sigma_proof) -- [Function `verify_new_balance_range_proof`](#0x7_confidential_proof_verify_new_balance_range_proof) -- [Function `verify_transfer_amount_range_proof`](#0x7_confidential_proof_verify_transfer_amount_range_proof) -- [Function `auditors_count_in_transfer_proof`](#0x7_confidential_proof_auditors_count_in_transfer_proof) -- [Function `deserialize_withdrawal_proof`](#0x7_confidential_proof_deserialize_withdrawal_proof) -- [Function `deserialize_transfer_proof`](#0x7_confidential_proof_deserialize_transfer_proof) -- [Function `deserialize_normalization_proof`](#0x7_confidential_proof_deserialize_normalization_proof) -- [Function `deserialize_rotation_proof`](#0x7_confidential_proof_deserialize_rotation_proof) -- [Function `deserialize_withdrawal_sigma_proof`](#0x7_confidential_proof_deserialize_withdrawal_sigma_proof) -- [Function `deserialize_transfer_sigma_proof`](#0x7_confidential_proof_deserialize_transfer_sigma_proof) -- [Function `deserialize_normalization_sigma_proof`](#0x7_confidential_proof_deserialize_normalization_sigma_proof) -- [Function `deserialize_rotation_sigma_proof`](#0x7_confidential_proof_deserialize_rotation_sigma_proof) -- [Function `get_fiat_shamir_withdrawal_sigma_dst`](#0x7_confidential_proof_get_fiat_shamir_withdrawal_sigma_dst) -- [Function `get_fiat_shamir_transfer_sigma_dst`](#0x7_confidential_proof_get_fiat_shamir_transfer_sigma_dst) -- [Function `get_fiat_shamir_normalization_sigma_dst`](#0x7_confidential_proof_get_fiat_shamir_normalization_sigma_dst) -- [Function `get_fiat_shamir_rotation_sigma_dst`](#0x7_confidential_proof_get_fiat_shamir_rotation_sigma_dst) -- [Function `get_bulletproofs_dst`](#0x7_confidential_proof_get_bulletproofs_dst) -- [Function `get_bulletproofs_num_bits`](#0x7_confidential_proof_get_bulletproofs_num_bits) -- [Function `fiat_shamir_withdrawal_sigma_proof_challenge`](#0x7_confidential_proof_fiat_shamir_withdrawal_sigma_proof_challenge) -- [Function `fiat_shamir_transfer_sigma_proof_challenge`](#0x7_confidential_proof_fiat_shamir_transfer_sigma_proof_challenge) -- [Function `fiat_shamir_normalization_sigma_proof_challenge`](#0x7_confidential_proof_fiat_shamir_normalization_sigma_proof_challenge) -- [Function `fiat_shamir_rotation_sigma_proof_challenge`](#0x7_confidential_proof_fiat_shamir_rotation_sigma_proof_challenge) -- [Function `msm_withdrawal_gammas`](#0x7_confidential_proof_msm_withdrawal_gammas) -- [Function `msm_transfer_gammas`](#0x7_confidential_proof_msm_transfer_gammas) -- [Function `msm_normalization_gammas`](#0x7_confidential_proof_msm_normalization_gammas) -- [Function `msm_rotation_gammas`](#0x7_confidential_proof_msm_rotation_gammas) -- [Function `msm_gamma_1`](#0x7_confidential_proof_msm_gamma_1) -- [Function `msm_gamma_2`](#0x7_confidential_proof_msm_gamma_2) -- [Function `scalar_mul_3`](#0x7_confidential_proof_scalar_mul_3) -- [Function `scalar_linear_combination`](#0x7_confidential_proof_scalar_linear_combination) -- [Function `new_scalar_from_pow2`](#0x7_confidential_proof_new_scalar_from_pow2) - - -
use 0x1::error;
-use 0x1::option;
-use 0x1::ristretto255;
-use 0x1::ristretto255_bulletproofs;
-use 0x1::vector;
-use 0x7::confidential_balance;
-use 0x7::ristretto255_twisted_elgamal;
-
- - - - - -## Struct `WithdrawalProof` - -Represents the proof structure for validating a withdrawal operation. - - -
struct WithdrawalProof has drop
-
- - - -
-Fields - - -
-
-sigma_proof: confidential_proof::WithdrawalSigmaProof -
-
- Sigma proof ensuring that the withdrawal operation maintains balance integrity. -
-
-zkrp_new_balance: ristretto255_bulletproofs::RangeProof -
-
- Range proof ensuring that the resulting balance chunks are normalized (i.e., within the 16-bit limit). -
-
- - -
- - - -## Struct `TransferProof` - -Represents the proof structure for validating a transfer operation. - - -
struct TransferProof has drop
-
- - - -
-Fields - - -
-
-sigma_proof: confidential_proof::TransferSigmaProof -
-
- Sigma proof ensuring that the transfer operation maintains balance integrity and correctness. -
-
-zkrp_new_balance: ristretto255_bulletproofs::RangeProof -
-
- Range proof ensuring that the resulting balance chunks for the sender are normalized (i.e., within the 16-bit limit). -
-
-zkrp_transfer_amount: ristretto255_bulletproofs::RangeProof -
-
- Range proof ensuring that the transferred amount chunks are normalized (i.e., within the 16-bit limit). -
-
- - -
- - - -## Struct `NormalizationProof` - -Represents the proof structure for validating a normalization operation. - - -
struct NormalizationProof has drop
-
- - - -
-Fields - - -
-
-sigma_proof: confidential_proof::NormalizationSigmaProof -
-
- Sigma proof ensuring that the normalization operation maintains balance integrity. -
-
-zkrp_new_balance: ristretto255_bulletproofs::RangeProof -
-
- Range proof ensuring that the resulting balance chunks are normalized (i.e., within the 16-bit limit). -
-
- - -
- - - -## Struct `RotationProof` - -Represents the proof structure for validating a key rotation operation. - - -
struct RotationProof has drop
-
- - - -
-Fields - - -
-
-sigma_proof: confidential_proof::RotationSigmaProof -
-
- Sigma proof ensuring that the key rotation operation preserves balance integrity. -
-
-zkrp_new_balance: ristretto255_bulletproofs::RangeProof -
-
- Range proof ensuring that the resulting balance chunks after key rotation are normalized (i.e., within the 16-bit limit). -
-
- - -
- - - -## Struct `WithdrawalSigmaProofXs` - - - -
struct WithdrawalSigmaProofXs has drop
-
- - - -
-Fields - - -
-
-x1: ristretto255::CompressedRistretto -
-
- -
-
-x2: ristretto255::CompressedRistretto -
-
- -
-
-x3s: vector<ristretto255::CompressedRistretto> -
-
- -
-
-x4s: vector<ristretto255::CompressedRistretto> -
-
- -
-
- - -
- - - -## Struct `WithdrawalSigmaProofAlphas` - - - -
struct WithdrawalSigmaProofAlphas has drop
-
- - - -
-Fields - - -
-
-a1s: vector<ristretto255::Scalar> -
-
- -
-
-a2: ristretto255::Scalar -
-
- -
-
-a3: ristretto255::Scalar -
-
- -
-
-a4s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `WithdrawalSigmaProofGammas` - - - -
struct WithdrawalSigmaProofGammas has drop
-
- - - -
-Fields - - -
-
-g1: ristretto255::Scalar -
-
- -
-
-g2: ristretto255::Scalar -
-
- -
-
-g3s: vector<ristretto255::Scalar> -
-
- -
-
-g4s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `WithdrawalSigmaProof` - - - -
struct WithdrawalSigmaProof has drop
-
- - - -
-Fields - - -
-
-alphas: confidential_proof::WithdrawalSigmaProofAlphas -
-
- -
-
-xs: confidential_proof::WithdrawalSigmaProofXs -
-
- -
-
- - -
- - - -## Struct `TransferSigmaProofXs` - - - -
struct TransferSigmaProofXs has drop
-
- - - -
-Fields - - -
-
-x1: ristretto255::CompressedRistretto -
-
- -
-
-x2s: vector<ristretto255::CompressedRistretto> -
-
- -
-
-x3s: vector<ristretto255::CompressedRistretto> -
-
- -
-
-x4s: vector<ristretto255::CompressedRistretto> -
-
- -
-
-x5: ristretto255::CompressedRistretto -
-
- -
-
-x6s: vector<ristretto255::CompressedRistretto> -
-
- -
-
-x7s: vector<vector<ristretto255::CompressedRistretto>> -
-
- -
-
-x8s: vector<ristretto255::CompressedRistretto> -
-
- -
-
- - -
- - - -## Struct `TransferSigmaProofAlphas` - - - -
struct TransferSigmaProofAlphas has drop
-
- - - -
-Fields - - -
-
-a1s: vector<ristretto255::Scalar> -
-
- -
-
-a2: ristretto255::Scalar -
-
- -
-
-a3s: vector<ristretto255::Scalar> -
-
- -
-
-a4s: vector<ristretto255::Scalar> -
-
- -
-
-a5: ristretto255::Scalar -
-
- -
-
-a6s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `TransferSigmaProofGammas` - - - -
struct TransferSigmaProofGammas has drop
-
- - - -
-Fields - - -
-
-g1: ristretto255::Scalar -
-
- -
-
-g2s: vector<ristretto255::Scalar> -
-
- -
-
-g3s: vector<ristretto255::Scalar> -
-
- -
-
-g4s: vector<ristretto255::Scalar> -
-
- -
-
-g5: ristretto255::Scalar -
-
- -
-
-g6s: vector<ristretto255::Scalar> -
-
- -
-
-g7s: vector<vector<ristretto255::Scalar>> -
-
- -
-
-g8s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `TransferSigmaProof` - - - -
struct TransferSigmaProof has drop
-
- - - -
-Fields - - -
-
-alphas: confidential_proof::TransferSigmaProofAlphas -
-
- -
-
-xs: confidential_proof::TransferSigmaProofXs -
-
- -
-
- - -
- - - -## Struct `NormalizationSigmaProofXs` - - - -
struct NormalizationSigmaProofXs has drop
-
- - - -
-Fields - - -
-
-x1: ristretto255::CompressedRistretto -
-
- -
-
-x2: ristretto255::CompressedRistretto -
-
- -
-
-x3s: vector<ristretto255::CompressedRistretto> -
-
- -
-
-x4s: vector<ristretto255::CompressedRistretto> -
-
- -
-
- - -
- - - -## Struct `NormalizationSigmaProofAlphas` - - - -
struct NormalizationSigmaProofAlphas has drop
-
- - - -
-Fields - - -
-
-a1s: vector<ristretto255::Scalar> -
-
- -
-
-a2: ristretto255::Scalar -
-
- -
-
-a3: ristretto255::Scalar -
-
- -
-
-a4s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `NormalizationSigmaProofGammas` - - - -
struct NormalizationSigmaProofGammas has drop
-
- - - -
-Fields - - -
-
-g1: ristretto255::Scalar -
-
- -
-
-g2: ristretto255::Scalar -
-
- -
-
-g3s: vector<ristretto255::Scalar> -
-
- -
-
-g4s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `NormalizationSigmaProof` - - - -
struct NormalizationSigmaProof has drop
-
- - - -
-Fields - - -
-
-alphas: confidential_proof::NormalizationSigmaProofAlphas -
-
- -
-
-xs: confidential_proof::NormalizationSigmaProofXs -
-
- -
-
- - -
- - - -## Struct `RotationSigmaProofXs` - - - -
struct RotationSigmaProofXs has drop
-
- - - -
-Fields - - -
-
-x1: ristretto255::CompressedRistretto -
-
- -
-
-x2: ristretto255::CompressedRistretto -
-
- -
-
-x3: ristretto255::CompressedRistretto -
-
- -
-
-x4s: vector<ristretto255::CompressedRistretto> -
-
- -
-
-x5s: vector<ristretto255::CompressedRistretto> -
-
- -
-
- - -
- - - -## Struct `RotationSigmaProofAlphas` - - - -
struct RotationSigmaProofAlphas has drop
-
- - - -
-Fields - - -
-
-a1s: vector<ristretto255::Scalar> -
-
- -
-
-a2: ristretto255::Scalar -
-
- -
-
-a3: ristretto255::Scalar -
-
- -
-
-a4: ristretto255::Scalar -
-
- -
-
-a5s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `RotationSigmaProofGammas` - - - -
struct RotationSigmaProofGammas has drop
-
- - - -
-Fields - - -
-
-g1: ristretto255::Scalar -
-
- -
-
-g2: ristretto255::Scalar -
-
- -
-
-g3: ristretto255::Scalar -
-
- -
-
-g4s: vector<ristretto255::Scalar> -
-
- -
-
-g5s: vector<ristretto255::Scalar> -
-
- -
-
- - -
- - - -## Struct `RotationSigmaProof` - - - -
struct RotationSigmaProof has drop
-
- - - -
-Fields - - -
-
-alphas: confidential_proof::RotationSigmaProofAlphas -
-
- -
-
-xs: confidential_proof::RotationSigmaProofXs -
-
- -
-
- - -
- - - -## Constants - - - - - - -
const BULLETPROOFS_DST: vector<u8> = [65, 112, 116, 111, 115, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 66, 117, 108, 108, 101, 116, 112, 114, 111, 111, 102, 82, 97, 110, 103, 101, 80, 114, 111, 111, 102];
-
- - - - - - - -
const BULLETPROOFS_NUM_BITS: u64 = 16;
-
- - - - - - - -
const ERANGE_PROOF_VERIFICATION_FAILED: u64 = 2;
-
- - - - - - - -
const ESIGMA_PROTOCOL_VERIFY_FAILED: u64 = 1;
-
- - - - - - - -
const FIAT_SHAMIR_NORMALIZATION_SIGMA_DST: vector<u8> = [65, 112, 116, 111, 115, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 78, 111, 114, 109, 97, 108, 105, 122, 97, 116, 105, 111, 110, 80, 114, 111, 111, 102, 70, 105, 97, 116, 83, 104, 97, 109, 105, 114];
-
- - - - - - - -
const FIAT_SHAMIR_ROTATION_SIGMA_DST: vector<u8> = [65, 112, 116, 111, 115, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 82, 111, 116, 97, 116, 105, 111, 110, 80, 114, 111, 111, 102, 70, 105, 97, 116, 83, 104, 97, 109, 105, 114];
-
- - - - - - - -
const FIAT_SHAMIR_TRANSFER_SIGMA_DST: vector<u8> = [65, 112, 116, 111, 115, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 84, 114, 97, 110, 115, 102, 101, 114, 80, 114, 111, 111, 102, 70, 105, 97, 116, 83, 104, 97, 109, 105, 114];
-
- - - - - - - -
const FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST: vector<u8> = [65, 112, 116, 111, 115, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 87, 105, 116, 104, 100, 114, 97, 119, 97, 108, 80, 114, 111, 111, 102, 70, 105, 97, 116, 83, 104, 97, 109, 105, 114];
-
- - - - - -## Function `verify_withdrawal_proof` - -Verifies the validity of the withdraw operation. - -This function ensures that the provided proof (WithdrawalProof) meets the following conditions: -1. The current balance (current_balance) and new balance (new_balance) encrypt the corresponding values -under the same encryption key (ek) before and after the withdrawal of the specified amount (amount), respectively. -2. The relationship new_balance = current_balance - amount holds, verifying that the withdrawal amount is deducted correctly. -3. The new balance (new_balance) is normalized, with each chunk adhering to the range [0, 2^16). - -If all conditions are satisfied, the proof validates the withdrawal; otherwise, the function causes an error. - - -
public fun verify_withdrawal_proof(ek: &ristretto255_twisted_elgamal::CompressedPubkey, amount: u64, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::WithdrawalProof)
-
- - - -
-Implementation - - -
public fun verify_withdrawal_proof(
-    ek: &twisted_elgamal::CompressedPubkey,
-    amount: u64,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof: &WithdrawalProof)
-{
-    verify_withdrawal_sigma_proof(ek, amount, current_balance, new_balance, &proof.sigma_proof);
-    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
-}
-
- - - -
- - - -## Function `verify_transfer_proof` - -Verifies the validity of the confidential_transfer operation. - -This function ensures that the provided proof (TransferProof) meets the following conditions: -1. The transferred amount (recipient_amount and sender_amount) and the auditors' amounts -(auditor_amounts), if provided, encrypt the transfer value using the recipient's, sender's, -and auditors' encryption keys, repectively. -2. The sender's current balance (current_balance) and new balance (new_balance) encrypt the corresponding values -under the sender's encryption key (sender_ek) before and after the transfer, respectively. -3. The relationship new_balance = current_balance - transfer_amount is maintained, ensuring balance integrity. -4. The transferred value (recipient_amount) is properly normalized, with each chunk adhering to the range [0, 2^16). -5. The sender's new balance is normalized, with each chunk in new_balance also adhering to the range [0, 2^16). - -If all conditions are satisfied, the proof validates the transfer; otherwise, the function causes an error. - - -
public fun verify_transfer_proof(sender_ek: &ristretto255_twisted_elgamal::CompressedPubkey, recipient_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, sender_amount: &confidential_balance::ConfidentialBalance, recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, proof: &confidential_proof::TransferProof)
-
- - - -
-Implementation - - -
public fun verify_transfer_proof(
-    sender_ek: &twisted_elgamal::CompressedPubkey,
-    recipient_ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    sender_amount: &confidential_balance::ConfidentialBalance,
-    recipient_amount: &confidential_balance::ConfidentialBalance,
-    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
-    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
-    proof: &TransferProof)
-{
-    verify_transfer_sigma_proof(
-        sender_ek,
-        recipient_ek,
-        current_balance,
-        new_balance,
-        sender_amount,
-        recipient_amount,
-        auditor_eks,
-        auditor_amounts,
-        &proof.sigma_proof
-    );
-    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
-    verify_transfer_amount_range_proof(recipient_amount, &proof.zkrp_transfer_amount);
-}
-
- - - -
- - - -## Function `verify_normalization_proof` - -Verifies the validity of the normalize operation. - -This function ensures that the provided proof (NormalizationProof) meets the following conditions: -1. The current balance (current_balance) and new balance (new_balance) encrypt the same value -under the same provided encryption key (ek), verifying that the normalization process preserves the balance value. -2. The new balance (new_balance) is properly normalized, with each chunk adhering to the range [0, 2^16), -as verified through the range proof in the normalization process. - -If all conditions are satisfied, the proof validates the normalization; otherwise, the function causes an error. - - -
public fun verify_normalization_proof(ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::NormalizationProof)
-
- - - -
-Implementation - - -
public fun verify_normalization_proof(
-    ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof: &NormalizationProof)
-{
-    verify_normalization_sigma_proof(ek, current_balance, new_balance, &proof.sigma_proof);
-    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
-}
-
- - - -
- - - -## Function `verify_rotation_proof` - -Verifies the validity of the rotate_encryption_key operation. - -This function ensures that the provided proof (RotationProof) meets the following conditions: -1. The current balance (current_balance) and new balance (new_balance) encrypt the same value under the -current encryption key (current_ek) and the new encryption key (new_ek), respectively, verifying -that the key rotation preserves the balance value. -2. The new balance (new_balance) is properly normalized, with each chunk adhering to the range [0, 2^16), -ensuring balance integrity after the key rotation. - -If all conditions are satisfied, the proof validates the key rotation; otherwise, the function causes an error. - - -
public fun verify_rotation_proof(current_ek: &ristretto255_twisted_elgamal::CompressedPubkey, new_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::RotationProof)
-
- - - -
-Implementation - - -
public fun verify_rotation_proof(
-    current_ek: &twisted_elgamal::CompressedPubkey,
-    new_ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof: &RotationProof)
-{
-    verify_rotation_sigma_proof(current_ek, new_ek, current_balance, new_balance, &proof.sigma_proof);
-    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
-}
-
- - - -
- - - -## Function `verify_withdrawal_sigma_proof` - -Verifies the validity of the WithdrawalSigmaProof. - - -
fun verify_withdrawal_sigma_proof(ek: &ristretto255_twisted_elgamal::CompressedPubkey, amount: u64, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::WithdrawalSigmaProof)
-
- - - -
-Implementation - - -
fun verify_withdrawal_sigma_proof(
-    ek: &twisted_elgamal::CompressedPubkey,
-    amount: u64,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof: &WithdrawalSigmaProof)
-{
-    let amount_chunks = confidential_balance::split_into_chunks_u64(amount);
-    let amount = ristretto255::new_scalar_from_u64(amount);
-
-    let rho = fiat_shamir_withdrawal_sigma_proof_challenge(ek, &amount_chunks, current_balance, &proof.xs);
-
-    let gammas = msm_withdrawal_gammas(&rho);
-
-    let scalars_lhs = vector[gammas.g1, gammas.g2];
-    scalars_lhs.append(gammas.g3s);
-    scalars_lhs.append(gammas.g4s);
-
-    let points_lhs = vector[
-        ristretto255::point_decompress(&proof.xs.x1),
-        ristretto255::point_decompress(&proof.xs.x2)
-    ];
-    points_lhs.append(proof.xs.x3s.map_ref(|x| ristretto255::point_decompress(x)));
-    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
-
-    let scalar_g = scalar_linear_combination(
-        &proof.alphas.a1s,
-        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
-    );
-    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
-    ristretto255::scalar_add_assign(
-        &mut scalar_g,
-        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a1s)
-    );
-    ristretto255::scalar_sub_assign(&mut scalar_g, &scalar_mul_3(&gammas.g1, &rho, &amount));
-
-    let scalar_h = ristretto255::scalar_mul(&gammas.g2, &proof.alphas.a3);
-    ristretto255::scalar_add_assign(
-        &mut scalar_h,
-        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a4s)
-    );
-
-    let scalar_ek = ristretto255::scalar_mul(&gammas.g2, &rho);
-    ristretto255::scalar_add_assign(
-        &mut scalar_ek,
-        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a4s)
-    );
-
-    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
-        ristretto255::scalar_mul(&gammas.g4s[i], &rho)
-    });
-
-    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
-        ristretto255::scalar_mul(&gammas.g3s[i], &rho)
-    });
-
-    let scalars_rhs = vector[scalar_g, scalar_h, scalar_ek];
-    scalars_rhs.append(scalars_current_balance_d);
-    scalars_rhs.append(scalars_new_balance_d);
-    scalars_rhs.append(scalars_current_balance_c);
-    scalars_rhs.append(scalars_new_balance_c);
-
-    let points_rhs = vector[
-        ristretto255::basepoint(),
-        ristretto255::hash_to_point_base(),
-        twisted_elgamal::pubkey_to_point(ek)
-    ];
-    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
-    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
-    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
-    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
-
-    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
-    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
-
-    assert!(
-        ristretto255::point_equals(&lhs, &rhs),
-        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
-    );
-}
-
- - - -
- - - -## Function `verify_transfer_sigma_proof` - -Verifies the validity of the TransferSigmaProof. - - -
fun verify_transfer_sigma_proof(sender_ek: &ristretto255_twisted_elgamal::CompressedPubkey, recipient_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, sender_amount: &confidential_balance::ConfidentialBalance, recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, proof: &confidential_proof::TransferSigmaProof)
-
- - - -
-Implementation - - -
fun verify_transfer_sigma_proof(
-    sender_ek: &twisted_elgamal::CompressedPubkey,
-    recipient_ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    sender_amount: &confidential_balance::ConfidentialBalance,
-    recipient_amount: &confidential_balance::ConfidentialBalance,
-    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
-    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
-    proof: &TransferSigmaProof)
-{
-    let rho = fiat_shamir_transfer_sigma_proof_challenge(
-        sender_ek,
-        recipient_ek,
-        current_balance,
-        new_balance,
-        sender_amount,
-        recipient_amount,
-        auditor_eks,
-        auditor_amounts,
-        &proof.xs
-    );
-
-    let gammas = msm_transfer_gammas(&rho, proof.xs.x7s.length());
-
-    let scalars_lhs = vector[gammas.g1];
-    scalars_lhs.append(gammas.g2s);
-    scalars_lhs.append(gammas.g3s);
-    scalars_lhs.append(gammas.g4s);
-    scalars_lhs.push_back(gammas.g5);
-    scalars_lhs.append(gammas.g6s);
-    gammas.g7s.for_each(|gamma| scalars_lhs.append(gamma));
-    scalars_lhs.append(gammas.g8s);
-
-    let points_lhs = vector[
-        ristretto255::point_decompress(&proof.xs.x1),
-    ];
-    points_lhs.append(proof.xs.x2s.map_ref(|x| ristretto255::point_decompress(x)));
-    points_lhs.append(proof.xs.x3s.map_ref(|x| ristretto255::point_decompress(x)));
-    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
-    points_lhs.push_back(ristretto255::point_decompress(&proof.xs.x5));
-    points_lhs.append(proof.xs.x6s.map_ref(|x| ristretto255::point_decompress(x)));
-    proof.xs.x7s.for_each_ref(|xs| {
-        points_lhs.append(xs.map_ref(|x| ristretto255::point_decompress(x)));
-    });
-    points_lhs.append(proof.xs.x8s.map_ref(|x| ristretto255::point_decompress(x)));
-
-    let scalar_g = scalar_linear_combination(
-        &proof.alphas.a1s,
-        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
-    );
-    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
-    vector::range(0, 4).for_each(|i| {
-        ristretto255::scalar_add_assign(
-            &mut scalar_g,
-            &ristretto255::scalar_mul(&gammas.g4s[i], &proof.alphas.a4s[i])
-        );
-    });
-    ristretto255::scalar_add_assign(
-        &mut scalar_g,
-        &scalar_linear_combination(&gammas.g6s, &proof.alphas.a1s)
-    );
-
-    let scalar_h = ristretto255::scalar_mul(&gammas.g5, &proof.alphas.a5);
-    vector::range(0, 8).for_each(|i| {
-        ristretto255::scalar_add_assign(
-            &mut scalar_h,
-            &scalar_mul_3(&gammas.g1, &proof.alphas.a6s[i], &new_scalar_from_pow2(i * 16))
-        );
-    });
-    vector::range(0, 4).for_each(|i| {
-        ristretto255::scalar_sub_assign(
-            &mut scalar_h,
-            &scalar_mul_3(&gammas.g1, &proof.alphas.a3s[i], &new_scalar_from_pow2(i * 16))
-        );
-    });
-    ristretto255::scalar_add_assign(
-        &mut scalar_h,
-        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a3s)
-    );
-    ristretto255::scalar_add_assign(
-        &mut scalar_h,
-        &scalar_linear_combination(&gammas.g6s, &proof.alphas.a6s)
-    );
-
-    let scalar_sender_ek = scalar_linear_combination(&gammas.g2s, &proof.alphas.a6s);
-    ristretto255::scalar_add_assign(&mut scalar_sender_ek, &ristretto255::scalar_mul(&gammas.g5, &rho));
-    ristretto255::scalar_add_assign(
-        &mut scalar_sender_ek,
-        &scalar_linear_combination(&gammas.g8s, &proof.alphas.a3s)
-    );
-
-    let scalar_recipient_ek = ristretto255::scalar_zero();
-    vector::range(0, 4).for_each(|i| {
-        ristretto255::scalar_add_assign(
-            &mut scalar_recipient_ek,
-            &ristretto255::scalar_mul(&gammas.g3s[i], &proof.alphas.a3s[i])
-        );
-    });
-
-    let scalar_ek_auditors = gammas.g7s.map_ref(|gamma: &vector<Scalar>| {
-        let scalar_auditor_ek = ristretto255::scalar_zero();
-        vector::range(0, 4).for_each(|i| {
-            ristretto255::scalar_add_assign(
-                &mut scalar_auditor_ek,
-                &ristretto255::scalar_mul(&gamma[i], &proof.alphas.a3s[i])
-            );
-        });
-        scalar_auditor_ek
-    });
-
-    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
-        let scalar = ristretto255::scalar_mul(&gammas.g2s[i], &rho);
-        ristretto255::scalar_sub_assign(
-            &mut scalar,
-            &scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
-        );
-        scalar
-    });
-
-    let scalars_recipient_amount_d = vector::range(0, 4).map(|i| {
-        ristretto255::scalar_mul(&gammas.g3s[i], &rho)
-    });
-
-    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_auditor_amount_d = gammas.g7s.map_ref(|gamma| {
-        gamma.map_ref(|gamma| ristretto255::scalar_mul(gamma, &rho))
-    });
-
-    let scalars_sender_amount_d = vector::range(0, 4).map(|i| {
-        ristretto255::scalar_mul(&gammas.g8s[i], &rho)
-    });
-
-    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_transfer_amount_c = vector::range(0, 4).map(|i| {
-        let scalar = ristretto255::scalar_mul(&gammas.g4s[i], &rho);
-        ristretto255::scalar_sub_assign(
-            &mut scalar,
-            &scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
-        );
-        scalar
-    });
-
-    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
-        ristretto255::scalar_mul(&gammas.g6s[i], &rho)
-    });
-
-    let scalars_rhs = vector[scalar_g, scalar_h, scalar_sender_ek, scalar_recipient_ek];
-    scalars_rhs.append(scalar_ek_auditors);
-    scalars_rhs.append(scalars_new_balance_d);
-    scalars_rhs.append(scalars_recipient_amount_d);
-    scalars_rhs.append(scalars_current_balance_d);
-    scalars_auditor_amount_d.for_each(|scalars| scalars_rhs.append(scalars));
-    scalars_rhs.append(scalars_sender_amount_d);
-    scalars_rhs.append(scalars_current_balance_c);
-    scalars_rhs.append(scalars_transfer_amount_c);
-    scalars_rhs.append(scalars_new_balance_c);
-
-    let points_rhs = vector[
-        ristretto255::basepoint(),
-        ristretto255::hash_to_point_base(),
-        twisted_elgamal::pubkey_to_point(sender_ek),
-        twisted_elgamal::pubkey_to_point(recipient_ek)
-    ];
-    points_rhs.append(auditor_eks.map_ref(|ek| twisted_elgamal::pubkey_to_point(ek)));
-    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
-    points_rhs.append(confidential_balance::balance_to_points_d(recipient_amount));
-    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
-    auditor_amounts.for_each_ref(|balance| {
-        points_rhs.append(confidential_balance::balance_to_points_d(balance));
-    });
-    points_rhs.append(confidential_balance::balance_to_points_d(sender_amount));
-    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
-    points_rhs.append(confidential_balance::balance_to_points_c(recipient_amount));
-    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
-
-    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
-    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
-
-    assert!(
-        ristretto255::point_equals(&lhs, &rhs),
-        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
-    );
-}
-
- - - -
- - - -## Function `verify_normalization_sigma_proof` - -Verifies the validity of the NormalizationSigmaProof. - - -
fun verify_normalization_sigma_proof(ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::NormalizationSigmaProof)
-
- - - -
-Implementation - - -
fun verify_normalization_sigma_proof(
-    ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof: &NormalizationSigmaProof)
-{
-    let rho = fiat_shamir_normalization_sigma_proof_challenge(ek, current_balance, new_balance, &proof.xs);
-    let gammas = msm_normalization_gammas(&rho);
-
-    let scalars_lhs = vector[gammas.g1, gammas.g2];
-    scalars_lhs.append(gammas.g3s);
-    scalars_lhs.append(gammas.g4s);
-
-    let points_lhs = vector[
-        ristretto255::point_decompress(&proof.xs.x1),
-        ristretto255::point_decompress(&proof.xs.x2)
-    ];
-    points_lhs.append(proof.xs.x3s.map_ref(|x| ristretto255::point_decompress(x)));
-    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
-
-    let scalar_g = scalar_linear_combination(
-        &proof.alphas.a1s,
-        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
-    );
-    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
-    ristretto255::scalar_add_assign(
-        &mut scalar_g,
-        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a1s)
-    );
-
-    let scalar_h = ristretto255::scalar_mul(&gammas.g2, &proof.alphas.a3);
-    ristretto255::scalar_add_assign(
-        &mut scalar_h,
-        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a4s)
-    );
-
-    let scalar_ek = ristretto255::scalar_mul(&gammas.g2, &rho);
-    ristretto255::scalar_add_assign(
-        &mut scalar_ek,
-        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a4s)
-    );
-
-    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
-        ristretto255::scalar_mul(&gammas.g4s[i], &rho)
-    });
-
-    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
-        ristretto255::scalar_mul(&gammas.g3s[i], &rho)
-    });
-
-    let scalars_rhs = vector[scalar_g, scalar_h, scalar_ek];
-    scalars_rhs.append(scalars_current_balance_d);
-    scalars_rhs.append(scalars_new_balance_d);
-    scalars_rhs.append(scalars_current_balance_c);
-    scalars_rhs.append(scalars_new_balance_c);
-
-    let points_rhs = vector[
-        ristretto255::basepoint(),
-        ristretto255::hash_to_point_base(),
-        twisted_elgamal::pubkey_to_point(ek)
-    ];
-    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
-    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
-    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
-    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
-
-    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
-    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
-
-    assert!(
-        ristretto255::point_equals(&lhs, &rhs),
-        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
-    );
-}
-
- - - -
- - - -## Function `verify_rotation_sigma_proof` - -Verifies the validity of the RotationSigmaProof. - - -
fun verify_rotation_sigma_proof(current_ek: &ristretto255_twisted_elgamal::CompressedPubkey, new_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::RotationSigmaProof)
-
- - - -
-Implementation - - -
fun verify_rotation_sigma_proof(
-    current_ek: &twisted_elgamal::CompressedPubkey,
-    new_ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof: &RotationSigmaProof)
-{
-    let rho = fiat_shamir_rotation_sigma_proof_challenge(
-        current_ek,
-        new_ek,
-        current_balance,
-        new_balance,
-        &proof.xs
-    );
-    let gammas = msm_rotation_gammas(&rho);
-
-    let scalars_lhs = vector[gammas.g1, gammas.g2, gammas.g3];
-    scalars_lhs.append(gammas.g4s);
-    scalars_lhs.append(gammas.g5s);
-
-    let points_lhs = vector[
-        ristretto255::point_decompress(&proof.xs.x1),
-        ristretto255::point_decompress(&proof.xs.x2),
-        ristretto255::point_decompress(&proof.xs.x3)
-    ];
-    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
-    points_lhs.append(proof.xs.x5s.map_ref(|x| ristretto255::point_decompress(x)));
-
-    let scalar_g = scalar_linear_combination(
-        &proof.alphas.a1s,
-        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
-    );
-    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
-    ristretto255::scalar_add_assign(
-        &mut scalar_g,
-        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a1s)
-    );
-
-    let scalar_h = ristretto255::scalar_mul(&gammas.g2, &proof.alphas.a3);
-    ristretto255::scalar_add_assign(&mut scalar_h, &ristretto255::scalar_mul(&gammas.g3, &proof.alphas.a4));
-    ristretto255::scalar_add_assign(
-        &mut scalar_h,
-        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a5s)
-    );
-
-    let scalar_ek_cur = ristretto255::scalar_mul(&gammas.g2, &rho);
-
-    let scalar_ek_new = ristretto255::scalar_mul(&gammas.g3, &rho);
-    ristretto255::scalar_add_assign(
-        &mut scalar_ek_new,
-        &scalar_linear_combination(&gammas.g5s, &proof.alphas.a5s)
-    );
-
-    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
-        ristretto255::scalar_mul(&gammas.g5s[i], &rho)
-    });
-
-    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
-        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
-    });
-
-    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
-        ristretto255::scalar_mul(&gammas.g4s[i], &rho)
-    });
-
-    let scalars_rhs = vector[scalar_g, scalar_h, scalar_ek_cur, scalar_ek_new];
-    scalars_rhs.append(scalars_current_balance_d);
-    scalars_rhs.append(scalars_new_balance_d);
-    scalars_rhs.append(scalars_current_balance_c);
-    scalars_rhs.append(scalars_new_balance_c);
-
-    let points_rhs = vector[
-        ristretto255::basepoint(),
-        ristretto255::hash_to_point_base(),
-        twisted_elgamal::pubkey_to_point(current_ek),
-        twisted_elgamal::pubkey_to_point(new_ek)
-    ];
-    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
-    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
-    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
-    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
-
-    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
-    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
-
-    assert!(
-        ristretto255::point_equals(&lhs, &rhs),
-        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
-    );
-}
-
- - - -
- - - -## Function `verify_new_balance_range_proof` - -Verifies the validity of the NewBalanceRangeProof. - - -
fun verify_new_balance_range_proof(new_balance: &confidential_balance::ConfidentialBalance, zkrp_new_balance: &ristretto255_bulletproofs::RangeProof)
-
- - - -
-Implementation - - -
fun verify_new_balance_range_proof(
-    new_balance: &confidential_balance::ConfidentialBalance,
-    zkrp_new_balance: &RangeProof)
-{
-    let balance_c = confidential_balance::balance_to_points_c(new_balance);
-
-    assert!(
-        bulletproofs::verify_batch_range_proof(
-            &balance_c,
-            &ristretto255::basepoint(),
-            &ristretto255::hash_to_point_base(),
-            zkrp_new_balance,
-            BULLETPROOFS_NUM_BITS,
-            BULLETPROOFS_DST
-        ),
-        error::out_of_range(ERANGE_PROOF_VERIFICATION_FAILED)
-    );
-}
-
- - - -
- - - -## Function `verify_transfer_amount_range_proof` - -Verifies the validity of the TransferBalanceRangeProof. - - -
fun verify_transfer_amount_range_proof(transfer_amount: &confidential_balance::ConfidentialBalance, zkrp_transfer_amount: &ristretto255_bulletproofs::RangeProof)
-
- - - -
-Implementation - - -
fun verify_transfer_amount_range_proof(
-    transfer_amount: &confidential_balance::ConfidentialBalance,
-    zkrp_transfer_amount: &RangeProof)
-{
-    let balance_c = confidential_balance::balance_to_points_c(transfer_amount);
-
-    assert!(
-        bulletproofs::verify_batch_range_proof(
-            &balance_c,
-            &ristretto255::basepoint(),
-            &ristretto255::hash_to_point_base(),
-            zkrp_transfer_amount,
-            BULLETPROOFS_NUM_BITS,
-            BULLETPROOFS_DST
-        ),
-        error::out_of_range(ERANGE_PROOF_VERIFICATION_FAILED)
-    );
-}
-
- - - -
- - - -## Function `auditors_count_in_transfer_proof` - -Returns the number of range proofs in the provided WithdrawalProof. -Used in the confidential_asset module to validate input parameters of the confidential_transfer function. - - -
public(friend) fun auditors_count_in_transfer_proof(proof: &confidential_proof::TransferProof): u64
-
- - - -
-Implementation - - -
public(friend) fun auditors_count_in_transfer_proof(proof: &TransferProof): u64 {
-    proof.sigma_proof.xs.x7s.length()
-}
-
- - - -
- - - -## Function `deserialize_withdrawal_proof` - -Deserializes the WithdrawalProof from the byte array. -Returns Some(WithdrawalProof) if the deserialization is successful; otherwise, returns None. - - -
public fun deserialize_withdrawal_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>): option::Option<confidential_proof::WithdrawalProof>
-
- - - -
-Implementation - - -
public fun deserialize_withdrawal_proof(
-    sigma_proof_bytes: vector<u8>,
-    zkrp_new_balance_bytes: vector<u8>): Option<WithdrawalProof>
-{
-    let sigma_proof = deserialize_withdrawal_sigma_proof(sigma_proof_bytes);
-    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
-
-    if (sigma_proof.is_none()) {
-        return option::none()
-    };
-
-    option::some(
-        WithdrawalProof {
-            sigma_proof: sigma_proof.extract(),
-            zkrp_new_balance,
-        }
-    )
-}
-
- - - -
- - - -## Function `deserialize_transfer_proof` - -Deserializes the TransferProof from the byte array. -Returns Some(TransferProof) if the deserialization is successful; otherwise, returns None. - - -
public fun deserialize_transfer_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>, zkrp_transfer_amount_bytes: vector<u8>): option::Option<confidential_proof::TransferProof>
-
- - - -
-Implementation - - -
public fun deserialize_transfer_proof(
-    sigma_proof_bytes: vector<u8>,
-    zkrp_new_balance_bytes: vector<u8>,
-    zkrp_transfer_amount_bytes: vector<u8>): Option<TransferProof>
-{
-    let sigma_proof = deserialize_transfer_sigma_proof(sigma_proof_bytes);
-    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
-    let zkrp_transfer_amount = bulletproofs::range_proof_from_bytes(zkrp_transfer_amount_bytes);
-
-    if (sigma_proof.is_none()) {
-        return option::none()
-    };
-
-    option::some(
-        TransferProof {
-            sigma_proof: sigma_proof.extract(),
-            zkrp_new_balance,
-            zkrp_transfer_amount,
-        }
-    )
-}
-
- - - -
- - - -## Function `deserialize_normalization_proof` - -Deserializes the NormalizationProof from the byte array. -Returns Some(NormalizationProof) if the deserialization is successful; otherwise, returns None. - - -
public fun deserialize_normalization_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>): option::Option<confidential_proof::NormalizationProof>
-
- - - -
-Implementation - - -
public fun deserialize_normalization_proof(
-    sigma_proof_bytes: vector<u8>,
-    zkrp_new_balance_bytes: vector<u8>): Option<NormalizationProof>
-{
-    let sigma_proof = deserialize_normalization_sigma_proof(sigma_proof_bytes);
-    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
-
-    if (sigma_proof.is_none()) {
-        return option::none()
-    };
-
-    option::some(
-        NormalizationProof {
-            sigma_proof: sigma_proof.extract(),
-            zkrp_new_balance,
-        }
-    )
-}
-
- - - -
- - - -## Function `deserialize_rotation_proof` - -Deserializes the RotationProof from the byte array. -Returns Some(RotationProof) if the deserialization is successful; otherwise, returns None. - - -
public fun deserialize_rotation_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>): option::Option<confidential_proof::RotationProof>
-
- - - -
-Implementation - - -
public fun deserialize_rotation_proof(
-    sigma_proof_bytes: vector<u8>,
-    zkrp_new_balance_bytes: vector<u8>): Option<RotationProof>
-{
-    let sigma_proof = deserialize_rotation_sigma_proof(sigma_proof_bytes);
-    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
-
-    if (sigma_proof.is_none()) {
-        return option::none()
-    };
-
-    option::some(
-        RotationProof {
-            sigma_proof: sigma_proof.extract(),
-            zkrp_new_balance,
-        }
-    )
-}
-
- - - -
- - - -## Function `deserialize_withdrawal_sigma_proof` - -Deserializes the WithdrawalSigmaProof from the byte array. -Returns Some(WithdrawalSigmaProof) if the deserialization is successful; otherwise, returns None. - - -
fun deserialize_withdrawal_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::WithdrawalSigmaProof>
-
- - - -
-Implementation - - -
fun deserialize_withdrawal_sigma_proof(proof_bytes: vector<u8>): Option<WithdrawalSigmaProof> {
-    let alphas_count = 18;
-    let xs_count = 18;
-
-    if (proof_bytes.length() != 32 * xs_count + 32 * alphas_count) {
-        return option::none()
-    };
-
-    let alphas = vector::range(0, alphas_count).map(|i| {
-        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
-        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-
-    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
-        return option::none()
-    };
-
-    option::some(
-        WithdrawalSigmaProof {
-            alphas: WithdrawalSigmaProofAlphas {
-                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
-                a2: alphas[8].extract(),
-                a3: alphas[9].extract(),
-                a4s: alphas.slice(10, 18).map(|alpha| alpha.extract()),
-            },
-            xs: WithdrawalSigmaProofXs {
-                x1: xs[0].extract(),
-                x2: xs[1].extract(),
-                x3s: xs.slice(2, 10).map(|x| x.extract()),
-                x4s: xs.slice(10, 18).map(|x| x.extract()),
-            },
-        }
-    )
-}
-
- - - -
- - - -## Function `deserialize_transfer_sigma_proof` - -Deserializes the TransferSigmaProof from the byte array. -Returns Some(TransferSigmaProof) if the deserialization is successful; otherwise, returns None. - - -
fun deserialize_transfer_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::TransferSigmaProof>
-
- - - -
-Implementation - - -
fun deserialize_transfer_sigma_proof(proof_bytes: vector<u8>): Option<TransferSigmaProof> {
-    let alphas_count = 26;
-    let xs_count = 30;
-
-    if (proof_bytes.length() < 32 * xs_count + 32 * alphas_count) {
-        return option::none()
-    };
-
-    // Transfer proof may contain additional four Xs for each auditor.
-    let auditor_xs = proof_bytes.length() - (32 * xs_count + 32 * alphas_count);
-
-    if (auditor_xs % 128 != 0) {
-        return option::none()
-    };
-
-    xs_count += auditor_xs / 32;
-
-    let alphas = vector::range(0, alphas_count).map(|i| {
-        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
-        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-
-    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
-        return option::none()
-    };
-
-    option::some(
-        TransferSigmaProof {
-            alphas: TransferSigmaProofAlphas {
-                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
-                a2: alphas[8].extract(),
-                a3s: alphas.slice(9, 13).map(|alpha| alpha.extract()),
-                a4s: alphas.slice(13, 17).map(|alpha| alpha.extract()),
-                a5: alphas[17].extract(),
-                a6s: alphas.slice(18, 26).map(|alpha| alpha.extract()),
-            },
-            xs: TransferSigmaProofXs {
-                x1: xs[0].extract(),
-                x2s: xs.slice(1, 9).map(|x| x.extract()),
-                x3s: xs.slice(9, 13).map(|x| x.extract()),
-                x4s: xs.slice(13, 17).map(|x| x.extract()),
-                x5: xs[17].extract(),
-                x6s: xs.slice(18, 26).map(|x| x.extract()),
-                x7s: vector::range_with_step(26, xs_count - 4, 4).map(|i| {
-                    vector::range(i, i + 4).map(|j| xs[j].extract())
-                }),
-                x8s: xs.slice(xs_count - 4, xs_count).map(|x| x.extract()),
-            },
-        }
-    )
-}
-
- - - -
- - - -## Function `deserialize_normalization_sigma_proof` - -Deserializes the NormalizationSigmaProof from the byte array. -Returns Some(NormalizationSigmaProof) if the deserialization is successful; otherwise, returns None. - - -
fun deserialize_normalization_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::NormalizationSigmaProof>
-
- - - -
-Implementation - - -
fun deserialize_normalization_sigma_proof(proof_bytes: vector<u8>): Option<NormalizationSigmaProof> {
-    let alphas_count = 18;
-    let xs_count = 18;
-
-    if (proof_bytes.length() != 32 * xs_count + 32 * alphas_count) {
-        return option::none()
-    };
-
-    let alphas = vector::range(0, alphas_count).map(|i| {
-        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
-        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-
-    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
-        return option::none()
-    };
-
-    option::some(
-        NormalizationSigmaProof {
-            alphas: NormalizationSigmaProofAlphas {
-                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
-                a2: alphas[8].extract(),
-                a3: alphas[9].extract(),
-                a4s: alphas.slice(10, 18).map(|alpha| alpha.extract()),
-            },
-            xs: NormalizationSigmaProofXs {
-                x1: xs[0].extract(),
-                x2: xs[1].extract(),
-                x3s: xs.slice(2, 10).map(|x| x.extract()),
-                x4s: xs.slice(10, 18).map(|x| x.extract()),
-            },
-        }
-    )
-}
-
- - - -
- - - -## Function `deserialize_rotation_sigma_proof` - -Deserializes the RotationSigmaProof from the byte array. -Returns Some(RotationSigmaProof) if the deserialization is successful; otherwise, returns None. - - -
fun deserialize_rotation_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::RotationSigmaProof>
-
- - - -
-Implementation - - -
fun deserialize_rotation_sigma_proof(proof_bytes: vector<u8>): Option<RotationSigmaProof> {
-    let alphas_count = 19;
-    let xs_count = 19;
-
-    if (proof_bytes.length() != 32 * xs_count + 32 * alphas_count) {
-        return option::none()
-    };
-
-    let alphas = vector::range(0, alphas_count).map(|i| {
-        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
-        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
-    });
-
-    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
-        return option::none()
-    };
-
-    option::some(
-        RotationSigmaProof {
-            alphas: RotationSigmaProofAlphas {
-                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
-                a2: alphas[8].extract(),
-                a3: alphas[9].extract(),
-                a4: alphas[10].extract(),
-                a5s: alphas.slice(11, 19).map(|alpha| alpha.extract()),
-            },
-            xs: RotationSigmaProofXs {
-                x1: xs[0].extract(),
-                x2: xs[1].extract(),
-                x3: xs[2].extract(),
-                x4s: xs.slice(3, 11).map(|x| x.extract()),
-                x5s: xs.slice(11, 19).map(|x| x.extract()),
-            },
-        }
-    )
-}
-
- - - -
- - - -## Function `get_fiat_shamir_withdrawal_sigma_dst` - -Returns the Fiat Shamir DST for the WithdrawalSigmaProof. - - -
#[view]
-public fun get_fiat_shamir_withdrawal_sigma_dst(): vector<u8>
-
- - - -
-Implementation - - -
public fun get_fiat_shamir_withdrawal_sigma_dst(): vector<u8> {
-    FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST
-}
-
- - - -
- - - -## Function `get_fiat_shamir_transfer_sigma_dst` - -Returns the Fiat Shamir DST for the TransferSigmaProof. - - -
#[view]
-public fun get_fiat_shamir_transfer_sigma_dst(): vector<u8>
-
- - - -
-Implementation - - -
public fun get_fiat_shamir_transfer_sigma_dst(): vector<u8> {
-    FIAT_SHAMIR_TRANSFER_SIGMA_DST
-}
-
- - - -
- - - -## Function `get_fiat_shamir_normalization_sigma_dst` - -Returns the Fiat Shamir DST for the NormalizationSigmaProof. - - -
#[view]
-public fun get_fiat_shamir_normalization_sigma_dst(): vector<u8>
-
- - - -
-Implementation - - -
public fun get_fiat_shamir_normalization_sigma_dst(): vector<u8> {
-    FIAT_SHAMIR_NORMALIZATION_SIGMA_DST
-}
-
- - - -
- - - -## Function `get_fiat_shamir_rotation_sigma_dst` - -Returns the Fiat Shamir DST for the RotationSigmaProof. - - -
#[view]
-public fun get_fiat_shamir_rotation_sigma_dst(): vector<u8>
-
- - - -
-Implementation - - -
public fun get_fiat_shamir_rotation_sigma_dst(): vector<u8> {
-    FIAT_SHAMIR_ROTATION_SIGMA_DST
-}
-
- - - -
- - - -## Function `get_bulletproofs_dst` - -Returns the DST for the range proofs. - - -
#[view]
-public fun get_bulletproofs_dst(): vector<u8>
-
- - - -
-Implementation - - -
public fun get_bulletproofs_dst(): vector<u8> {
-    BULLETPROOFS_DST
-}
-
- - - -
- - - -## Function `get_bulletproofs_num_bits` - -Returns the maximum number of bits of the normalized chunk for the range proofs. - - -
#[view]
-public fun get_bulletproofs_num_bits(): u64
-
- - - -
-Implementation - - -
public fun get_bulletproofs_num_bits(): u64 {
-    BULLETPROOFS_NUM_BITS
-}
-
- - - -
- - - -## Function `fiat_shamir_withdrawal_sigma_proof_challenge` - -Derives the Fiat-Shamir challenge for the WithdrawalSigmaProof. - - -
fun fiat_shamir_withdrawal_sigma_proof_challenge(ek: &ristretto255_twisted_elgamal::CompressedPubkey, amount_chunks: &vector<ristretto255::Scalar>, current_balance: &confidential_balance::ConfidentialBalance, proof_xs: &confidential_proof::WithdrawalSigmaProofXs): ristretto255::Scalar
-
- - - -
-Implementation - - -
fun fiat_shamir_withdrawal_sigma_proof_challenge(
-    ek: &twisted_elgamal::CompressedPubkey,
-    amount_chunks: &vector<Scalar>,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    proof_xs: &WithdrawalSigmaProofXs): Scalar
-{
-    // rho = H(DST, G, H, P, v_{1..4}, (C_cur, D_cur)_{1..8}, X_{1..18})
-    let bytes = FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST;
-
-    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
-    bytes.append(
-        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
-    );
-    bytes.append(twisted_elgamal::pubkey_to_bytes(ek));
-    amount_chunks.for_each_ref(|chunk| {
-        bytes.append(ristretto255::scalar_to_bytes(chunk));
-    });
-    bytes.append(confidential_balance::balance_to_bytes(current_balance));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x2));
-    proof_xs.x3s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-    proof_xs.x4s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-
-    ristretto255::new_scalar_from_sha2_512(bytes)
-}
-
- - - -
- - - -## Function `fiat_shamir_transfer_sigma_proof_challenge` - -Derives the Fiat-Shamir challenge for the TransferSigmaProof. - - -
fun fiat_shamir_transfer_sigma_proof_challenge(sender_ek: &ristretto255_twisted_elgamal::CompressedPubkey, recipient_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, sender_amount: &confidential_balance::ConfidentialBalance, recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, proof_xs: &confidential_proof::TransferSigmaProofXs): ristretto255::Scalar
-
- - - -
-Implementation - - -
fun fiat_shamir_transfer_sigma_proof_challenge(
-    sender_ek: &twisted_elgamal::CompressedPubkey,
-    recipient_ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    sender_amount: &confidential_balance::ConfidentialBalance,
-    recipient_amount: &confidential_balance::ConfidentialBalance,
-    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
-    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
-    proof_xs: &TransferSigmaProofXs): Scalar
-{
-    // rho = H(DST, G, H, P_s, P_r, P_a_{1..n}, (C_cur, D_cur)_{1..8}, (C_v, D_v)_{1..4}, D_a_{1..4n}, D_s_{1..4}, (C_new, D_new)_{1..8}, X_{1..30 + 4n})
-    let bytes = FIAT_SHAMIR_TRANSFER_SIGMA_DST;
-
-    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
-    bytes.append(
-        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
-    );
-    bytes.append(twisted_elgamal::pubkey_to_bytes(sender_ek));
-    bytes.append(twisted_elgamal::pubkey_to_bytes(recipient_ek));
-    auditor_eks.for_each_ref(|ek| {
-        bytes.append(twisted_elgamal::pubkey_to_bytes(ek));
-    });
-    bytes.append(confidential_balance::balance_to_bytes(current_balance));
-    bytes.append(confidential_balance::balance_to_bytes(recipient_amount));
-    auditor_amounts.for_each_ref(|balance| {
-        confidential_balance::balance_to_points_d(balance).for_each_ref(|d| {
-            bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::point_compress(d)));
-        });
-    });
-    confidential_balance::balance_to_points_d(sender_amount).for_each_ref(|d| {
-        bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::point_compress(d)));
-    });
-    bytes.append(confidential_balance::balance_to_bytes(new_balance));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
-    proof_xs.x2s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-    proof_xs.x3s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-    proof_xs.x4s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x5));
-    proof_xs.x6s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-    proof_xs.x7s.for_each_ref(|xs| {
-        xs.for_each_ref(|x| {
-            bytes.append(ristretto255::point_to_bytes(x));
-        });
-    });
-    proof_xs.x8s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-
-    ristretto255::new_scalar_from_sha2_512(bytes)
-}
-
- - - -
- - - -## Function `fiat_shamir_normalization_sigma_proof_challenge` - -Derives the Fiat-Shamir challenge for the NormalizationSigmaProof. - - -
fun fiat_shamir_normalization_sigma_proof_challenge(ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof_xs: &confidential_proof::NormalizationSigmaProofXs): ristretto255::Scalar
-
- - - -
-Implementation - - -
fun fiat_shamir_normalization_sigma_proof_challenge(
-    ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof_xs: &NormalizationSigmaProofXs): Scalar
-{
-    // rho = H(DST, G, H, P, (C_cur, D_cur)_{1..8}, (C_new, D_new)_{1..8}, X_{1..18})
-    let bytes = FIAT_SHAMIR_NORMALIZATION_SIGMA_DST;
-
-    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
-    bytes.append(
-        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
-    );
-    bytes.append(twisted_elgamal::pubkey_to_bytes(ek));
-    bytes.append(confidential_balance::balance_to_bytes(current_balance));
-    bytes.append(confidential_balance::balance_to_bytes(new_balance));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x2));
-    proof_xs.x3s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-    proof_xs.x4s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-
-    ristretto255::new_scalar_from_sha2_512(bytes)
-}
-
- - - -
- - - -## Function `fiat_shamir_rotation_sigma_proof_challenge` - -Derives the Fiat-Shamir challenge for the RotationSigmaProof. - - -
fun fiat_shamir_rotation_sigma_proof_challenge(current_ek: &ristretto255_twisted_elgamal::CompressedPubkey, new_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof_xs: &confidential_proof::RotationSigmaProofXs): ristretto255::Scalar
-
- - - -
-Implementation - - -
fun fiat_shamir_rotation_sigma_proof_challenge(
-    current_ek: &twisted_elgamal::CompressedPubkey,
-    new_ek: &twisted_elgamal::CompressedPubkey,
-    current_balance: &confidential_balance::ConfidentialBalance,
-    new_balance: &confidential_balance::ConfidentialBalance,
-    proof_xs: &RotationSigmaProofXs): Scalar
-{
-    // rho = H(DST, G, H, P_cur, P_new, (C_cur, D_cur)_{1..8}, (C_new, D_new)_{1..8}, X_{1..19})
-    let bytes = FIAT_SHAMIR_ROTATION_SIGMA_DST;
-
-    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
-    bytes.append(
-        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
-    );
-    bytes.append(twisted_elgamal::pubkey_to_bytes(current_ek));
-    bytes.append(twisted_elgamal::pubkey_to_bytes(new_ek));
-    bytes.append(confidential_balance::balance_to_bytes(current_balance));
-    bytes.append(confidential_balance::balance_to_bytes(new_balance));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x2));
-    bytes.append(ristretto255::point_to_bytes(&proof_xs.x3));
-    proof_xs.x4s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-    proof_xs.x5s.for_each_ref(|x| {
-        bytes.append(ristretto255::point_to_bytes(x));
-    });
-
-    ristretto255::new_scalar_from_sha2_512(bytes)
-}
-
- - - -
- - - -## Function `msm_withdrawal_gammas` - -Returns the scalar multipliers for the WithdrawalSigmaProof. - - -
fun msm_withdrawal_gammas(rho: &ristretto255::Scalar): confidential_proof::WithdrawalSigmaProofGammas
-
- - - -
-Implementation - - -
fun msm_withdrawal_gammas(rho: &Scalar): WithdrawalSigmaProofGammas {
-    WithdrawalSigmaProofGammas {
-        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
-        g2: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 2)),
-        g3s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 3, (i as u8)))
-        }),
-        g4s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
-        }),
-    }
-}
-
- - - -
- - - -## Function `msm_transfer_gammas` - -Returns the scalar multipliers for the TransferSigmaProof. - - -
fun msm_transfer_gammas(rho: &ristretto255::Scalar, auditors_count: u64): confidential_proof::TransferSigmaProofGammas
-
- - - -
-Implementation - - -
fun msm_transfer_gammas(rho: &Scalar, auditors_count: u64): TransferSigmaProofGammas {
-    TransferSigmaProofGammas {
-        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
-        g2s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 2, (i as u8)))
-        }),
-        g3s: vector::range(0, 4).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 3, (i as u8)))
-        }),
-        g4s: vector::range(0, 4).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
-        }),
-        g5: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 5)),
-        g6s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 6, (i as u8)))
-        }),
-        g7s: vector::range(0, auditors_count).map(|i| {
-            vector::range(0, 4).map(|j| {
-                ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, (i + 7 as u8), (j as u8)))
-            })
-        }),
-        g8s: vector::range(0, 4).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 8, (i as u8)))
-        }),
-    }
-}
-
- - - -
- - - -## Function `msm_normalization_gammas` - -Returns the scalar multipliers for the NormalizationSigmaProof. - - -
fun msm_normalization_gammas(rho: &ristretto255::Scalar): confidential_proof::NormalizationSigmaProofGammas
-
- - - -
-Implementation - - -
fun msm_normalization_gammas(rho: &Scalar): NormalizationSigmaProofGammas {
-    NormalizationSigmaProofGammas {
-        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
-        g2: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 2)),
-        g3s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 3, (i as u8)))
-        }),
-        g4s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
-        }),
-    }
-}
-
- - - -
- - - -## Function `msm_rotation_gammas` - -Returns the scalar multipliers for the RotationSigmaProof. - - -
fun msm_rotation_gammas(rho: &ristretto255::Scalar): confidential_proof::RotationSigmaProofGammas
-
- - - -
-Implementation - - -
fun msm_rotation_gammas(rho: &Scalar): RotationSigmaProofGammas {
-    RotationSigmaProofGammas {
-        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
-        g2: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 2)),
-        g3: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 3)),
-        g4s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
-        }),
-        g5s: vector::range(0, 8).map(|i| {
-            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 5, (i as u8)))
-        }),
-    }
-}
-
- - - -
- - - -## Function `msm_gamma_1` - -Returns the scalar multiplier computed as a hash of the provided rho and corresponding gamma index. - - -
fun msm_gamma_1(rho: &ristretto255::Scalar, i: u8): vector<u8>
-
- - - -
-Implementation - - -
fun msm_gamma_1(rho: &Scalar, i: u8): vector<u8> {
-    let bytes = ristretto255::scalar_to_bytes(rho);
-    bytes.push_back(i);
-    bytes
-}
-
- - - -
- - - -## Function `msm_gamma_2` - -Returns the scalar multiplier computed as a hash of the provided rho and corresponding gamma indices. - - -
fun msm_gamma_2(rho: &ristretto255::Scalar, i: u8, j: u8): vector<u8>
-
- - - -
-Implementation - - -
fun msm_gamma_2(rho: &Scalar, i: u8, j: u8): vector<u8> {
-    let bytes = ristretto255::scalar_to_bytes(rho);
-    bytes.push_back(i);
-    bytes.push_back(j);
-    bytes
-}
-
- - - -
- - - -## Function `scalar_mul_3` - -Calculates the product of the provided scalars. - - -
fun scalar_mul_3(scalar1: &ristretto255::Scalar, scalar2: &ristretto255::Scalar, scalar3: &ristretto255::Scalar): ristretto255::Scalar
-
- - - -
-Implementation - - -
fun scalar_mul_3(scalar1: &Scalar, scalar2: &Scalar, scalar3: &Scalar): Scalar {
-    let result = *scalar1;
-
-    ristretto255::scalar_mul_assign(&mut result, scalar2);
-    ristretto255::scalar_mul_assign(&mut result, scalar3);
-
-    result
-}
-
- - - -
- - - -## Function `scalar_linear_combination` - -Calculates the linear combination of the provided scalars. - - -
fun scalar_linear_combination(lhs: &vector<ristretto255::Scalar>, rhs: &vector<ristretto255::Scalar>): ristretto255::Scalar
-
- - - -
-Implementation - - -
fun scalar_linear_combination(lhs: &vector<Scalar>, rhs: &vector<Scalar>): Scalar {
-    let result = ristretto255::scalar_zero();
-
-    lhs.zip_ref(rhs, |l, r| {
-        ristretto255::scalar_add_assign(&mut result, &ristretto255::scalar_mul(l, r));
-    });
-
-    result
-}
-
- - - -
- - - -## Function `new_scalar_from_pow2` - -Raises 2 to the power of the provided exponent and returns the result as a scalar. - - -
fun new_scalar_from_pow2(exp: u64): ristretto255::Scalar
-
- - - -
-Implementation - - -
fun new_scalar_from_pow2(exp: u64): Scalar {
-    ristretto255::new_scalar_from_u128(1 << (exp as u8))
-}
-
- - - -
- - -[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-experimental/doc/overview.md b/aptos-move/framework/aptos-experimental/doc/overview.md index d2b794233d4..2371d4495be 100644 --- a/aptos-move/framework/aptos-experimental/doc/overview.md +++ b/aptos-move/framework/aptos-experimental/doc/overview.md @@ -12,22 +12,11 @@ This is the reference documentation of the Aptos experimental framework. ## Index -- [`0x7::active_order_book`](active_order_book.md#0x7_active_order_book) - [`0x7::benchmark_utils`](benchmark_utils.md#0x7_benchmark_utils) -- [`0x7::confidential_asset`](confidential_asset.md#0x7_confidential_asset) -- [`0x7::confidential_balance`](confidential_balance.md#0x7_confidential_balance) -- [`0x7::confidential_proof`](confidential_proof.md#0x7_confidential_proof) - [`0x7::helpers`](helpers.md#0x7_helpers) - [`0x7::large_packages`](large_packages.md#0x7_large_packages) -- [`0x7::market`](market.md#0x7_market) -- [`0x7::market_types`](market_types.md#0x7_market_types) -- [`0x7::order_book`](order_book.md#0x7_order_book) -- [`0x7::order_book_types`](order_book_types.md#0x7_order_book_types) -- [`0x7::pending_order_book_index`](pending_order_book_index.md#0x7_pending_order_book_index) -- [`0x7::ristretto255_twisted_elgamal`](ristretto255_twisted_elgamal.md#0x7_ristretto255_twisted_elgamal) - [`0x7::sigma_protos`](sigma_protos.md#0x7_sigma_protos) - [`0x7::test_derivable_account_abstraction_ed25519_hex`](test_derivable_account_abstraction_ed25519_hex.md#0x7_test_derivable_account_abstraction_ed25519_hex) -- [`0x7::test_function_values`](test_function_values.md#0x7_test_function_values) - [`0x7::veiled_coin`](veiled_coin.md#0x7_veiled_coin) diff --git a/aptos-move/framework/aptos-experimental/doc/ristretto255_twisted_elgamal.md b/aptos-move/framework/aptos-experimental/doc/ristretto255_twisted_elgamal.md deleted file mode 100644 index 45308f08067..00000000000 --- a/aptos-move/framework/aptos-experimental/doc/ristretto255_twisted_elgamal.md +++ /dev/null @@ -1,707 +0,0 @@ - - - -# Module `0x7::ristretto255_twisted_elgamal` - -This module implements a Twisted ElGamal encryption API, over the Ristretto255 curve, designed to work with -additional cryptographic constructs such as Bulletproofs. - -A Twisted ElGamal *ciphertext* encrypts a value v under a basepoint G and a secondary point H, -alongside a public key Y = sk^(-1) * H, where sk is the corresponding secret key. The ciphertext is of the form: -(v * G + r * H, r * Y), where r is a random scalar. - -The Twisted ElGamal scheme differs from standard ElGamal by introducing a secondary point H to enhance -flexibility and functionality in cryptographic protocols. This design still maintains the homomorphic property: -Enc_Y(v, r) + Enc_Y(v', r') = Enc_Y(v + v', r + r'), where v, v' are plaintexts, Y is the public key, -and r, r' are random scalars. - - -- [Struct `Ciphertext`](#0x7_ristretto255_twisted_elgamal_Ciphertext) -- [Struct `CompressedCiphertext`](#0x7_ristretto255_twisted_elgamal_CompressedCiphertext) -- [Struct `CompressedPubkey`](#0x7_ristretto255_twisted_elgamal_CompressedPubkey) -- [Function `new_pubkey_from_bytes`](#0x7_ristretto255_twisted_elgamal_new_pubkey_from_bytes) -- [Function `pubkey_to_bytes`](#0x7_ristretto255_twisted_elgamal_pubkey_to_bytes) -- [Function `pubkey_to_point`](#0x7_ristretto255_twisted_elgamal_pubkey_to_point) -- [Function `pubkey_to_compressed_point`](#0x7_ristretto255_twisted_elgamal_pubkey_to_compressed_point) -- [Function `new_ciphertext_from_bytes`](#0x7_ristretto255_twisted_elgamal_new_ciphertext_from_bytes) -- [Function `new_ciphertext_no_randomness`](#0x7_ristretto255_twisted_elgamal_new_ciphertext_no_randomness) -- [Function `ciphertext_from_points`](#0x7_ristretto255_twisted_elgamal_ciphertext_from_points) -- [Function `ciphertext_from_compressed_points`](#0x7_ristretto255_twisted_elgamal_ciphertext_from_compressed_points) -- [Function `ciphertext_to_bytes`](#0x7_ristretto255_twisted_elgamal_ciphertext_to_bytes) -- [Function `ciphertext_into_points`](#0x7_ristretto255_twisted_elgamal_ciphertext_into_points) -- [Function `ciphertext_as_points`](#0x7_ristretto255_twisted_elgamal_ciphertext_as_points) -- [Function `compress_ciphertext`](#0x7_ristretto255_twisted_elgamal_compress_ciphertext) -- [Function `decompress_ciphertext`](#0x7_ristretto255_twisted_elgamal_decompress_ciphertext) -- [Function `ciphertext_add`](#0x7_ristretto255_twisted_elgamal_ciphertext_add) -- [Function `ciphertext_add_assign`](#0x7_ristretto255_twisted_elgamal_ciphertext_add_assign) -- [Function `ciphertext_sub`](#0x7_ristretto255_twisted_elgamal_ciphertext_sub) -- [Function `ciphertext_sub_assign`](#0x7_ristretto255_twisted_elgamal_ciphertext_sub_assign) -- [Function `ciphertext_clone`](#0x7_ristretto255_twisted_elgamal_ciphertext_clone) -- [Function `ciphertext_equals`](#0x7_ristretto255_twisted_elgamal_ciphertext_equals) -- [Function `get_value_component`](#0x7_ristretto255_twisted_elgamal_get_value_component) - - -
use 0x1::option;
-use 0x1::ristretto255;
-use 0x1::vector;
-
- - - - - -## Struct `Ciphertext` - -A Twisted ElGamal ciphertext, consisting of two Ristretto255 points. - - -
struct Ciphertext has drop
-
- - - -
-Fields - - -
-
-left: ristretto255::RistrettoPoint -
-
- -
-
-right: ristretto255::RistrettoPoint -
-
- -
-
- - -
- - - -## Struct `CompressedCiphertext` - -A compressed Twisted ElGamal ciphertext, consisting of two compressed Ristretto255 points. - - -
struct CompressedCiphertext has copy, drop, store
-
- - - -
-Fields - - -
-
-left: ristretto255::CompressedRistretto -
-
- -
-
-right: ristretto255::CompressedRistretto -
-
- -
-
- - -
- - - -## Struct `CompressedPubkey` - -A Twisted ElGamal public key, represented as a compressed Ristretto255 point. - - -
struct CompressedPubkey has copy, drop, store
-
- - - -
-Fields - - -
-
-point: ristretto255::CompressedRistretto -
-
- -
-
- - -
- - - -## Function `new_pubkey_from_bytes` - -Creates a new public key from a serialized Ristretto255 point. -Returns Some(CompressedPubkey) if the deserialization is successful, otherwise None. - - -
public fun new_pubkey_from_bytes(bytes: vector<u8>): option::Option<ristretto255_twisted_elgamal::CompressedPubkey>
-
- - - -
-Implementation - - -
public fun new_pubkey_from_bytes(bytes: vector<u8>): Option<CompressedPubkey> {
-    let point = ristretto255::new_compressed_point_from_bytes(bytes);
-    if (point.is_some()) {
-        let pk = CompressedPubkey {
-            point: point.extract()
-        };
-        std::option::some(pk)
-    } else {
-        std::option::none()
-    }
-}
-
- - - -
- - - -## Function `pubkey_to_bytes` - -Serializes a Twisted ElGamal public key into its byte representation. - - -
public fun pubkey_to_bytes(pubkey: &ristretto255_twisted_elgamal::CompressedPubkey): vector<u8>
-
- - - -
-Implementation - - -
public fun pubkey_to_bytes(pubkey: &CompressedPubkey): vector<u8> {
-    ristretto255::compressed_point_to_bytes(pubkey.point)
-}
-
- - - -
- - - -## Function `pubkey_to_point` - -Converts a public key into its corresponding RistrettoPoint. - - -
public fun pubkey_to_point(pubkey: &ristretto255_twisted_elgamal::CompressedPubkey): ristretto255::RistrettoPoint
-
- - - -
-Implementation - - -
public fun pubkey_to_point(pubkey: &CompressedPubkey): RistrettoPoint {
-    ristretto255::point_decompress(&pubkey.point)
-}
-
- - - -
- - - -## Function `pubkey_to_compressed_point` - -Converts a public key into its corresponding CompressedRistretto representation. - - -
public fun pubkey_to_compressed_point(pubkey: &ristretto255_twisted_elgamal::CompressedPubkey): ristretto255::CompressedRistretto
-
- - - -
-Implementation - - -
public fun pubkey_to_compressed_point(pubkey: &CompressedPubkey): CompressedRistretto {
-    pubkey.point
-}
-
- - - -
- - - -## Function `new_ciphertext_from_bytes` - -Creates a new ciphertext from a serialized representation, consisting of two 32-byte Ristretto255 points. -Returns Some(Ciphertext) if the deserialization succeeds, otherwise None. - - -
public fun new_ciphertext_from_bytes(bytes: vector<u8>): option::Option<ristretto255_twisted_elgamal::Ciphertext>
-
- - - -
-Implementation - - -
public fun new_ciphertext_from_bytes(bytes: vector<u8>): Option<Ciphertext> {
-    if (bytes.length() != 64) {
-        return std::option::none()
-    };
-
-    let bytes_right = bytes.trim(32);
-
-    let left_point = ristretto255::new_point_from_bytes(bytes);
-    let right_point = ristretto255::new_point_from_bytes(bytes_right);
-
-    if (left_point.is_some() && right_point.is_some()) {
-        std::option::some(Ciphertext {
-            left: left_point.extract(),
-            right: right_point.extract()
-        })
-    } else {
-        std::option::none()
-    }
-}
-
- - - -
- - - -## Function `new_ciphertext_no_randomness` - -Creates a ciphertext (val * G, 0 * G) where val is the plaintext, and the randomness is set to zero. - - -
public fun new_ciphertext_no_randomness(val: &ristretto255::Scalar): ristretto255_twisted_elgamal::Ciphertext
-
- - - -
-Implementation - - -
public fun new_ciphertext_no_randomness(val: &Scalar): Ciphertext {
-    Ciphertext {
-        left: ristretto255::basepoint_mul(val),
-        right: ristretto255::point_identity(),
-    }
-}
-
- - - -
- - - -## Function `ciphertext_from_points` - -Constructs a Twisted ElGamal ciphertext from two RistrettoPoints. - - -
public fun ciphertext_from_points(left: ristretto255::RistrettoPoint, right: ristretto255::RistrettoPoint): ristretto255_twisted_elgamal::Ciphertext
-
- - - -
-Implementation - - -
public fun ciphertext_from_points(left: RistrettoPoint, right: RistrettoPoint): Ciphertext {
-    Ciphertext {
-        left,
-        right,
-    }
-}
-
- - - -
- - - -## Function `ciphertext_from_compressed_points` - -Constructs a Twisted ElGamal ciphertext from two compressed Ristretto255 points. - - -
public fun ciphertext_from_compressed_points(left: ristretto255::CompressedRistretto, right: ristretto255::CompressedRistretto): ristretto255_twisted_elgamal::CompressedCiphertext
-
- - - -
-Implementation - - -
public fun ciphertext_from_compressed_points(
-    left: CompressedRistretto,
-    right: CompressedRistretto
-): CompressedCiphertext {
-    CompressedCiphertext {
-        left,
-        right,
-    }
-}
-
- - - -
- - - -## Function `ciphertext_to_bytes` - -Serializes a Twisted ElGamal ciphertext into its byte representation. - - -
public fun ciphertext_to_bytes(ct: &ristretto255_twisted_elgamal::Ciphertext): vector<u8>
-
- - - -
-Implementation - - -
public fun ciphertext_to_bytes(ct: &Ciphertext): vector<u8> {
-    let bytes = ristretto255::point_to_bytes(&ristretto255::point_compress(&ct.left));
-    bytes.append(ristretto255::point_to_bytes(&ristretto255::point_compress(&ct.right)));
-    bytes
-}
-
- - - -
- - - -## Function `ciphertext_into_points` - -Converts a ciphertext into a pair of RistrettoPoints. - - -
public fun ciphertext_into_points(c: ristretto255_twisted_elgamal::Ciphertext): (ristretto255::RistrettoPoint, ristretto255::RistrettoPoint)
-
- - - -
-Implementation - - -
public fun ciphertext_into_points(c: Ciphertext): (RistrettoPoint, RistrettoPoint) {
-    let Ciphertext { left, right } = c;
-    (left, right)
-}
-
- - - -
- - - -## Function `ciphertext_as_points` - -Returns the two RistrettoPoints representing the ciphertext. - - -
public fun ciphertext_as_points(c: &ristretto255_twisted_elgamal::Ciphertext): (&ristretto255::RistrettoPoint, &ristretto255::RistrettoPoint)
-
- - - -
-Implementation - - -
public fun ciphertext_as_points(c: &Ciphertext): (&RistrettoPoint, &RistrettoPoint) {
-    (&c.left, &c.right)
-}
-
- - - -
- - - -## Function `compress_ciphertext` - -Compresses a Twisted ElGamal ciphertext into its CompressedCiphertext representation. - - -
public fun compress_ciphertext(ct: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::CompressedCiphertext
-
- - - -
-Implementation - - -
public fun compress_ciphertext(ct: &Ciphertext): CompressedCiphertext {
-    CompressedCiphertext {
-        left: ristretto255::point_compress(&ct.left),
-        right: ristretto255::point_compress(&ct.right),
-    }
-}
-
- - - -
- - - -## Function `decompress_ciphertext` - -Decompresses a CompressedCiphertext back into its Ciphertext representation. - - -
public fun decompress_ciphertext(ct: &ristretto255_twisted_elgamal::CompressedCiphertext): ristretto255_twisted_elgamal::Ciphertext
-
- - - -
-Implementation - - -
public fun decompress_ciphertext(ct: &CompressedCiphertext): Ciphertext {
-    Ciphertext {
-        left: ristretto255::point_decompress(&ct.left),
-        right: ristretto255::point_decompress(&ct.right),
-    }
-}
-
- - - -
- - - -## Function `ciphertext_add` - -Adds two ciphertexts homomorphically, producing a new ciphertext representing the sum of the two. - - -
public fun ciphertext_add(lhs: &ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::Ciphertext
-
- - - -
-Implementation - - -
public fun ciphertext_add(lhs: &Ciphertext, rhs: &Ciphertext): Ciphertext {
-    Ciphertext {
-        left: ristretto255::point_add(&lhs.left, &rhs.left),
-        right: ristretto255::point_add(&lhs.right, &rhs.right),
-    }
-}
-
- - - -
- - - -## Function `ciphertext_add_assign` - -Adds two ciphertexts homomorphically, updating the first ciphertext in place. - - -
public fun ciphertext_add_assign(lhs: &mut ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext)
-
- - - -
-Implementation - - -
public fun ciphertext_add_assign(lhs: &mut Ciphertext, rhs: &Ciphertext) {
-    ristretto255::point_add_assign(&mut lhs.left, &rhs.left);
-    ristretto255::point_add_assign(&mut lhs.right, &rhs.right);
-}
-
- - - -
- - - -## Function `ciphertext_sub` - -Subtracts one ciphertext from another homomorphically, producing a new ciphertext representing the difference. - - -
public fun ciphertext_sub(lhs: &ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::Ciphertext
-
- - - -
-Implementation - - -
public fun ciphertext_sub(lhs: &Ciphertext, rhs: &Ciphertext): Ciphertext {
-    Ciphertext {
-        left: ristretto255::point_sub(&lhs.left, &rhs.left),
-        right: ristretto255::point_sub(&lhs.right, &rhs.right),
-    }
-}
-
- - - -
- - - -## Function `ciphertext_sub_assign` - -Subtracts one ciphertext from another homomorphically, updating the first ciphertext in place. - - -
public fun ciphertext_sub_assign(lhs: &mut ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext)
-
- - - -
-Implementation - - -
public fun ciphertext_sub_assign(lhs: &mut Ciphertext, rhs: &Ciphertext) {
-    ristretto255::point_sub_assign(&mut lhs.left, &rhs.left);
-    ristretto255::point_sub_assign(&mut lhs.right, &rhs.right);
-}
-
- - - -
- - - -## Function `ciphertext_clone` - -Creates a copy of the provided ciphertext. - - -
public fun ciphertext_clone(c: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::Ciphertext
-
- - - -
-Implementation - - -
public fun ciphertext_clone(c: &Ciphertext): Ciphertext {
-    Ciphertext {
-        left: ristretto255::point_clone(&c.left),
-        right: ristretto255::point_clone(&c.right),
-    }
-}
-
- - - -
- - - -## Function `ciphertext_equals` - -Compares two ciphertexts for equality, returning true if they encrypt the same value and randomness. - - -
public fun ciphertext_equals(lhs: &ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext): bool
-
- - - -
-Implementation - - -
public fun ciphertext_equals(lhs: &Ciphertext, rhs: &Ciphertext): bool {
-    ristretto255::point_equals(&lhs.left, &rhs.left) &&
-        ristretto255::point_equals(&lhs.right, &rhs.right)
-}
-
- - - -
- - - -## Function `get_value_component` - -Returns the RistrettoPoint in the ciphertext that contains the encrypted value in the exponent. - - -
public fun get_value_component(ct: &ristretto255_twisted_elgamal::Ciphertext): &ristretto255::RistrettoPoint
-
- - - -
-Implementation - - -
public fun get_value_component(ct: &Ciphertext): &RistrettoPoint {
-    &ct.left
-}
-
- - - -
- - -[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_asset.move b/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_asset.move deleted file mode 100644 index 32c07f8c04f..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_asset.move +++ /dev/null @@ -1,1141 +0,0 @@ -/// This module implements the Confidential Asset (CA) Standard, a privacy-focused protocol for managing fungible assets (FA). -/// It enables private transfers by obfuscating token amounts while keeping sender and recipient addresses visible. -module aptos_experimental::confidential_asset { - use std::bcs; - use std::error; - use std::option::Option; - use std::signer; - use std::vector; - use aptos_std::ristretto255::Self; - use aptos_std::ristretto255_bulletproofs::Self as bulletproofs; - use aptos_std::string_utils; - use aptos_framework::chain_id; - use aptos_framework::coin; - use aptos_framework::event; - use aptos_framework::dispatchable_fungible_asset; - use aptos_framework::fungible_asset::{Metadata}; - use aptos_framework::object::{Self, ExtendRef, Object}; - use aptos_framework::primary_fungible_store; - use aptos_framework::system_addresses; - - use aptos_experimental::confidential_balance; - use aptos_experimental::confidential_proof::{ - Self, NormalizationProof, RotationProof, TransferProof, WithdrawalProof - }; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; - - #[test_only] - use aptos_std::ristretto255::Scalar; - - // - // Errors - // - - /// The range proof system does not support sufficient range. - const ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE: u64 = 1; - - /// The confidential asset store has already been published for the given user-token pair. - const ECA_STORE_ALREADY_PUBLISHED: u64 = 2; - - /// The confidential asset store has not been published for the given user-token pair. - const ECA_STORE_NOT_PUBLISHED: u64 = 3; - - /// The deserialization of the auditor EK failed. - const EAUDITOR_EK_DESERIALIZATION_FAILED: u64 = 4; - - /// The sender is not the registered auditor. - const ENOT_AUDITOR: u64 = 5; - - /// The provided auditors or auditor proofs are invalid. - const EINVALID_AUDITORS: u64 = 6; - - /// The confidential asset account is already frozen. - const EALREADY_FROZEN: u64 = 7; - - /// The confidential asset account is not frozen. - const ENOT_FROZEN: u64 = 8; - - /// The pending balance must be zero for this operation. - const ENOT_ZERO_BALANCE: u64 = 9; - - /// The operation requires the actual balance to be normalized. - const ENORMALIZATION_REQUIRED: u64 = 10; - - /// The balance is already normalized and cannot be normalized again. - const EALREADY_NORMALIZED: u64 = 11; - - /// The token is already allowed for confidential transfers. - const ETOKEN_ENABLED: u64 = 12; - - /// The token is not allowed for confidential transfers. - const ETOKEN_DISABLED: u64 = 13; - - /// The allow list is already enabled. - const EALLOW_LIST_ENABLED: u64 = 14; - - /// The allow list is already disabled. - const EALLOW_LIST_DISABLED: u64 = 15; - - /// An internal error occurred, indicating unexpected behavior. - const EINTERNAL_ERROR: u64 = 16; - - /// Sender and recipient amounts encrypt different transfer amounts - const EINVALID_SENDER_AMOUNT: u64 = 17; - - // - // Constants - // - - /// The maximum number of transactions can be aggregated on the pending balance before rollover is required. - const MAX_TRANSFERS_BEFORE_ROLLOVER: u64 = 65534; - - /// The mainnet chain ID. If the chain ID is 1, the allow list is enabled. - const MAINNET_CHAIN_ID: u8 = 1; - - // - // Structs - // - - /// The `confidential_asset` module stores a `ConfidentialAssetStore` object for each user-token pair. - struct ConfidentialAssetStore has key { - /// Indicates if the account is frozen. If `true`, transactions are temporarily disabled - /// for this account. This is particularly useful during key rotations, which require - /// two transactions: rolling over the pending balance to the actual balance and rotating - /// the encryption key. Freezing prevents the user from accepting additional payments - /// between these two transactions. - frozen: bool, - - /// A flag indicating whether the actual balance is normalized. A normalized balance - /// ensures that all chunks fit within the defined 16-bit bounds, preventing overflows. - normalized: bool, - - /// Tracks the maximum number of transactions the user can accept before normalization - /// is required. For example, if the user can accept up to 2^16 transactions and each - /// chunk has a 16-bit limit, the maximum chunk value before normalization would be - /// 2^16 * 2^16 = 2^32. Maintaining this counter is crucial because users must solve - /// a discrete logarithm problem of this size to decrypt their balances. - pending_counter: u64, - - /// Stores the user's pending balance, which is used for accepting incoming payments. - /// Represented as four 16-bit chunks (p0 + 2^16 * p1 + 2^32 * p2 + 2^48 * p3), that can grow up to 32 bits. - /// All payments are accepted into this pending balance, which users must roll over into the actual balance - /// to perform transactions like withdrawals or transfers. - /// This separation helps protect against front-running attacks, where small incoming transfers could force - /// frequent regenerating of zk-proofs. - pending_balance: confidential_balance::CompressedConfidentialBalance, - - /// Represents the actual user balance, which is available for sending payments. - /// It consists of eight 16-bit chunks (p0 + 2^16 * p1 + ... + 2^112 * p8), supporting a 128-bit balance. - /// Users can decrypt this balance with their decryption keys and by solving a discrete logarithm problem. - actual_balance: confidential_balance::CompressedConfidentialBalance, - - /// The encryption key associated with the user's confidential asset account, different for each token. - ek: twisted_elgamal::CompressedPubkey, - } - - /// Represents the controller for the primary FA stores and `FAConfig` objects. - struct FAController has key { - /// Indicates whether the allow list is enabled. If `true`, only tokens from the allow list can be transferred. - /// This flag is managed by the governance module. - allow_list_enabled: bool, - - /// Used to derive a signer that owns all the FAs' primary stores and `FAConfig` objects. - extend_ref: ExtendRef - } - - /// Represents the configuration of a token. - struct FAConfig has key { - /// Indicates whether the token is allowed for confidential transfers. - /// If allow list is disabled, all tokens are allowed. - /// Can be toggled by the governance module. The withdrawals are always allowed. - allowed: bool, - - /// The auditor's public key for the token. If the auditor is not set, this field is `None`. - /// Otherwise, each confidential transfer must include the auditor as an additional party, - /// alongside the recipient, who has access to the decrypted transferred amount. - auditor_ek: Option, - } - - // - // Events - // - - #[event] - /// Emitted when tokens are brought into the protocol. - struct Deposited has drop, store { - from: address, - to: address, - amount: u64 - } - - #[event] - /// Emitted when tokens are brought out of the protocol. - struct Withdrawn has drop, store { - from: address, - to: address, - amount: u64 - } - - #[event] - /// Emitted when tokens are transferred within the protocol between users' confidential balances. - /// Note that a numeric amount is not included, as it is hidden. - struct Transferred has drop, store { - from: address, - to: address - } - - // - // Module initialization, done only once when this module is first published on the blockchain - // - - fun init_module(deployer: &signer) { - assert!( - bulletproofs::get_max_range_bits() >= confidential_proof::get_bulletproofs_num_bits(), - error::internal(ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE) - ); - - let deployer_address = signer::address_of(deployer); - - let fa_controller_ctor_ref = &object::create_object(deployer_address); - - move_to(deployer, FAController { - allow_list_enabled: chain_id::get() == MAINNET_CHAIN_ID, - extend_ref: object::generate_extend_ref(fa_controller_ctor_ref), - }); - } - - // - // Entry functions - // - - /// Registers an account for a specified token. Users must register an account for each token they - /// intend to transact with. - /// - /// Users are also responsible for generating a Twisted ElGamal key pair on their side. - public entry fun register( - sender: &signer, - token: Object, - ek: vector) acquires FAController, FAConfig - { - let ek = twisted_elgamal::new_pubkey_from_bytes(ek).extract(); - - register_internal(sender, token, ek); - } - - /// Brings tokens into the protocol, transferring the passed amount from the sender's primary FA store - /// to the pending balance of the recipient. - /// The initial confidential balance is publicly visible, as entering the protocol requires a normal transfer. - /// However, tokens within the protocol become obfuscated through confidential transfers, ensuring privacy in - /// subsequent transactions. - public entry fun deposit_to( - sender: &signer, - token: Object, - to: address, - amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig - { - deposit_to_internal(sender, token, to, amount) - } - - /// The same as `deposit_to`, but the recipient is the sender. - public entry fun deposit( - sender: &signer, - token: Object, - amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig - { - deposit_to_internal(sender, token, signer::address_of(sender), amount) - } - - /// The same as `deposit_to`, but converts coins to missing FA first. - public entry fun deposit_coins_to( - sender: &signer, - to: address, - amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig - { - let token = ensure_sufficient_fa(sender, amount).extract(); - - deposit_to_internal(sender, token, to, amount) - } - - /// The same as `deposit`, but converts coins to missing FA first. - public entry fun deposit_coins( - sender: &signer, - amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig - { - let token = ensure_sufficient_fa(sender, amount).extract(); - - deposit_to_internal(sender, token, signer::address_of(sender), amount) - } - - /// Brings tokens out of the protocol by transferring the specified amount from the sender's actual balance to - /// the recipient's primary FA store. - /// The withdrawn amount is publicly visible, as this process requires a normal transfer. - /// The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. - public entry fun withdraw_to( - sender: &signer, - token: Object, - to: address, - amount: u64, - new_balance: vector, - zkrp_new_balance: vector, - sigma_proof: vector) acquires ConfidentialAssetStore, FAController - { - let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); - let proof = confidential_proof::deserialize_withdrawal_proof(sigma_proof, zkrp_new_balance).extract(); - - withdraw_to_internal(sender, token, to, amount, new_balance, proof); - - event::emit(Withdrawn { from: signer::address_of(sender), to, amount }); - } - - /// The same as `withdraw_to`, but the recipient is the sender. - public entry fun withdraw( - sender: &signer, - token: Object, - amount: u64, - new_balance: vector, - zkrp_new_balance: vector, - sigma_proof: vector) acquires ConfidentialAssetStore, FAController - { - withdraw_to( - sender, - token, - signer::address_of(sender), - amount, - new_balance, - zkrp_new_balance, - sigma_proof - ) - } - - /// Transfers tokens from the sender's actual balance to the recipient's pending balance. - /// The function hides the transferred amount while keeping the sender and recipient addresses visible. - /// The sender encrypts the transferred amount with the recipient's encryption key and the function updates the - /// recipient's confidential balance homomorphically. - /// Additionally, the sender encrypts the transferred amount with the auditors' EKs, allowing auditors to decrypt - /// the it on their side. - /// The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. - /// Warning: If the auditor feature is enabled, the sender must include the auditor as the first element in the - /// `auditor_eks` vector. - public entry fun confidential_transfer( - sender: &signer, - token: Object, - to: address, - new_balance: vector, - sender_amount: vector, - recipient_amount: vector, - auditor_eks: vector, - auditor_amounts: vector, - zkrp_new_balance: vector, - zkrp_transfer_amount: vector, - sigma_proof: vector) acquires ConfidentialAssetStore, FAConfig, FAController - { - let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); - let sender_amount = confidential_balance::new_pending_balance_from_bytes(sender_amount).extract(); - let recipient_amount = confidential_balance::new_pending_balance_from_bytes(recipient_amount).extract(); - let auditor_eks = deserialize_auditor_eks(auditor_eks).extract(); - let auditor_amounts = deserialize_auditor_amounts(auditor_amounts).extract(); - let proof = confidential_proof::deserialize_transfer_proof( - sigma_proof, - zkrp_new_balance, - zkrp_transfer_amount - ).extract(); - - confidential_transfer_internal( - sender, - token, - to, - new_balance, - sender_amount, - recipient_amount, - auditor_eks, - auditor_amounts, - proof - ) - } - - /// Rotates the encryption key for the user's confidential balance, updating it to a new encryption key. - /// The function ensures that the pending balance is zero before the key rotation, requiring the sender to - /// call `rollover_pending_balance_and_freeze` beforehand if necessary. - /// The sender provides their new normalized confidential balance, encrypted with the new encryption key and fresh randomness - /// to preserve privacy. - public entry fun rotate_encryption_key( - sender: &signer, - token: Object, - new_ek: vector, - new_balance: vector, - zkrp_new_balance: vector, - sigma_proof: vector) acquires ConfidentialAssetStore - { - let new_ek = twisted_elgamal::new_pubkey_from_bytes(new_ek).extract(); - let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); - let proof = confidential_proof::deserialize_rotation_proof(sigma_proof, zkrp_new_balance).extract(); - - rotate_encryption_key_internal(sender, token, new_ek, new_balance, proof); - } - - /// Adjusts each chunk to fit into defined 16-bit bounds to prevent overflows. - /// Most functions perform implicit normalization by accepting a new normalized confidential balance as a parameter. - /// However, explicit normalization is required before rolling over the pending balance, as multiple rolls may cause - /// chunk overflows. - /// The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. - public entry fun normalize( - sender: &signer, - token: Object, - new_balance: vector, - zkrp_new_balance: vector, - sigma_proof: vector) acquires ConfidentialAssetStore - { - let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); - let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract(); - - normalize_internal(sender, token, new_balance, proof); - } - - /// Freezes the confidential account for the specified token, disabling all incoming transactions. - public entry fun freeze_token(sender: &signer, token: Object) acquires ConfidentialAssetStore { - freeze_token_internal(sender, token); - } - - /// Unfreezes the confidential account for the specified token, re-enabling incoming transactions. - public entry fun unfreeze_token(sender: &signer, token: Object) acquires ConfidentialAssetStore { - unfreeze_token_internal(sender, token); - } - - /// Adds the pending balance to the actual balance for the specified token, resetting the pending balance to zero. - /// This operation is necessary to use tokens from the pending balance for outgoing transactions. - public entry fun rollover_pending_balance( - sender: &signer, - token: Object) acquires ConfidentialAssetStore - { - rollover_pending_balance_internal(sender, token); - } - - /// Before calling `rotate_encryption_key`, we need to rollover the pending balance and freeze the token to prevent - /// any new payments being come. - public entry fun rollover_pending_balance_and_freeze( - sender: &signer, - token: Object) acquires ConfidentialAssetStore - { - rollover_pending_balance(sender, token); - freeze_token(sender, token); - } - - /// After rotating the encryption key, we may want to unfreeze the token to allow payments. - /// This function facilitates making both calls in a single transaction. - public entry fun rotate_encryption_key_and_unfreeze( - sender: &signer, - token: Object, - new_ek: vector, - new_confidential_balance: vector, - zkrp_new_balance: vector, - rotate_proof: vector) acquires ConfidentialAssetStore - { - rotate_encryption_key(sender, token, new_ek, new_confidential_balance, zkrp_new_balance, rotate_proof); - unfreeze_token(sender, token); - } - - // - // Public governance functions - // - - /// Enables the allow list, restricting confidential transfers to tokens on the allow list. - public fun enable_allow_list(aptos_framework: &signer) acquires FAController { - system_addresses::assert_aptos_framework(aptos_framework); - - let fa_controller = borrow_global_mut(@aptos_experimental); - - assert!(!fa_controller.allow_list_enabled, error::invalid_state(EALLOW_LIST_ENABLED)); - - fa_controller.allow_list_enabled = true; - } - - /// Disables the allow list, allowing confidential transfers for all tokens. - public fun disable_allow_list(aptos_framework: &signer) acquires FAController { - system_addresses::assert_aptos_framework(aptos_framework); - - let fa_controller = borrow_global_mut(@aptos_experimental); - - assert!(fa_controller.allow_list_enabled, error::invalid_state(EALLOW_LIST_DISABLED)); - - fa_controller.allow_list_enabled = false; - } - - /// Enables confidential transfers for the specified token. - public fun enable_token(aptos_framework: &signer, token: Object) acquires FAConfig, FAController { - system_addresses::assert_aptos_framework(aptos_framework); - - let fa_config = borrow_global_mut(ensure_fa_config_exists(token)); - - assert!(!fa_config.allowed, error::invalid_state(ETOKEN_ENABLED)); - - fa_config.allowed = true; - } - - /// Disables confidential transfers for the specified token. - public fun disable_token(aptos_framework: &signer, token: Object) acquires FAConfig, FAController { - system_addresses::assert_aptos_framework(aptos_framework); - - let fa_config = borrow_global_mut(ensure_fa_config_exists(token)); - - assert!(fa_config.allowed, error::invalid_state(ETOKEN_DISABLED)); - - fa_config.allowed = false; - } - - /// Sets the auditor's public key for the specified token. - public fun set_auditor( - aptos_framework: &signer, - token: Object, - new_auditor_ek: vector) acquires FAConfig, FAController - { - system_addresses::assert_aptos_framework(aptos_framework); - - let fa_config = borrow_global_mut(ensure_fa_config_exists(token)); - - fa_config.auditor_ek = if (new_auditor_ek.length() == 0) { - std::option::none() - } else { - let new_auditor_ek = twisted_elgamal::new_pubkey_from_bytes(new_auditor_ek); - assert!(new_auditor_ek.is_some(), error::invalid_argument(EAUDITOR_EK_DESERIALIZATION_FAILED)); - new_auditor_ek - }; - } - - // - // Public view functions - // - - #[view] - /// Checks if the user has a confidential asset store for the specified token. - public fun has_confidential_asset_store(user: address, token: Object): bool { - exists(get_user_address(user, token)) - } - - #[view] - /// Checks if the token is allowed for confidential transfers. - public fun is_token_allowed(token: Object): bool acquires FAController, FAConfig { - if (!is_allow_list_enabled()) { - return true - }; - - let fa_config_address = get_fa_config_address(token); - - if (!exists(fa_config_address)) { - return false - }; - - borrow_global(fa_config_address).allowed - } - - #[view] - /// Checks if the allow list is enabled. - /// If the allow list is enabled, only tokens from the allow list can be transferred. - /// Otherwise, all tokens are allowed. - public fun is_allow_list_enabled(): bool acquires FAController { - borrow_global(@aptos_experimental).allow_list_enabled - } - - #[view] - /// Returns the pending balance of the user for the specified token. - public fun pending_balance( - owner: address, - token: Object): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore - { - assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - let ca_store = borrow_global(get_user_address(owner, token)); - - ca_store.pending_balance - } - - #[view] - /// Returns the actual balance of the user for the specified token. - public fun actual_balance( - owner: address, - token: Object): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore - { - assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - let ca_store = borrow_global(get_user_address(owner, token)); - - ca_store.actual_balance - } - - #[view] - /// Returns the encryption key (EK) of the user for the specified token. - public fun encryption_key( - user: address, - token: Object): twisted_elgamal::CompressedPubkey acquires ConfidentialAssetStore - { - assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - borrow_global_mut(get_user_address(user, token)).ek - } - - #[view] - /// Checks if the user's actual balance is normalized for the specified token. - public fun is_normalized(user: address, token: Object): bool acquires ConfidentialAssetStore { - assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - borrow_global(get_user_address(user, token)).normalized - } - - #[view] - /// Checks if the user's confidential asset store is frozen for the specified token. - public fun is_frozen(user: address, token: Object): bool acquires ConfidentialAssetStore { - assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - borrow_global(get_user_address(user, token)).frozen - } - - #[view] - /// Returns the asset-specific auditor's encryption key. - /// If the auditing feature is disabled for the token, the encryption key is set to `None`. - public fun get_auditor( - token: Object): Option acquires FAConfig, FAController - { - let fa_config_address = get_fa_config_address(token); - - if (!is_allow_list_enabled() && !exists(fa_config_address)) { - return std::option::none(); - }; - - borrow_global(fa_config_address).auditor_ek - } - - #[view] - /// Returns the circulating supply of the confidential asset. - public fun confidential_asset_balance(token: Object): u64 acquires FAController { - let fa_store_address = get_fa_store_address(); - assert!(primary_fungible_store::primary_store_exists(fa_store_address, token), EINTERNAL_ERROR); - - primary_fungible_store::balance(fa_store_address, token) - } - - // - // Public functions that correspond to the entry functions and don't require serializtion of the input data. - // These function can be useful for external contracts that want to integrate with the Confidential Asset protocol. - // - - /// Implementation of the `register` entry function. - public fun register_internal( - sender: &signer, - token: Object, - ek: twisted_elgamal::CompressedPubkey) acquires FAController, FAConfig - { - assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED)); - - let user = signer::address_of(sender); - - assert!(!has_confidential_asset_store(user, token), error::already_exists(ECA_STORE_ALREADY_PUBLISHED)); - - let ca_store = ConfidentialAssetStore { - frozen: false, - normalized: true, - pending_counter: 0, - pending_balance: confidential_balance::new_compressed_pending_balance_no_randomness(), - actual_balance: confidential_balance::new_compressed_actual_balance_no_randomness(), - ek, - }; - - move_to(&get_user_signer(sender, token), ca_store); - } - - /// Implementation of the `deposit_to` entry function. - public fun deposit_to_internal( - sender: &signer, - token: Object, - to: address, - amount: u64) acquires ConfidentialAssetStore, FAController, FAConfig - { - assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED)); - assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN)); - - let from = signer::address_of(sender); - - let sender_fa_store = primary_fungible_store::ensure_primary_store_exists(from, token); - let ca_fa_store = primary_fungible_store::ensure_primary_store_exists(get_fa_store_address(), token); - - dispatchable_fungible_asset::transfer(sender, sender_fa_store, ca_fa_store, amount); - - let ca_store = borrow_global_mut(get_user_address(to, token)); - let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); - - confidential_balance::add_balances_mut( - &mut pending_balance, - &confidential_balance::new_pending_balance_u64_no_randonmess(amount) - ); - - ca_store.pending_balance = confidential_balance::compress_balance(&pending_balance); - - assert!( - ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER, - error::invalid_argument(EINTERNAL_ERROR) - ); - - ca_store.pending_counter += 1; - - event::emit(Deposited { from, to, amount }); - } - - /// Implementation of the `withdraw_to` entry function. - /// Withdrawals are always allowed, regardless of the token allow status. - public fun withdraw_to_internal( - sender: &signer, - token: Object, - to: address, - amount: u64, - new_balance: confidential_balance::ConfidentialBalance, - proof: WithdrawalProof) acquires ConfidentialAssetStore, FAController - { - let from = signer::address_of(sender); - - let sender_ek = encryption_key(from, token); - - let ca_store = borrow_global_mut(get_user_address(from, token)); - let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); - - confidential_proof::verify_withdrawal_proof(&sender_ek, amount, ¤t_balance, &new_balance, &proof); - - ca_store.normalized = true; - ca_store.actual_balance = confidential_balance::compress_balance(&new_balance); - - primary_fungible_store::transfer(&get_fa_store_signer(), token, to, amount); - } - - /// Implementation of the `confidential_transfer` entry function. - public fun confidential_transfer_internal( - sender: &signer, - token: Object, - to: address, - new_balance: confidential_balance::ConfidentialBalance, - sender_amount: confidential_balance::ConfidentialBalance, - recipient_amount: confidential_balance::ConfidentialBalance, - auditor_eks: vector, - auditor_amounts: vector, - proof: TransferProof) acquires ConfidentialAssetStore, FAConfig, FAController - { - assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED)); - assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN)); - assert!( - validate_auditors(token, &recipient_amount, &auditor_eks, &auditor_amounts, &proof), - error::invalid_argument(EINVALID_AUDITORS) - ); - assert!( - confidential_balance::balance_c_equals(&sender_amount, &recipient_amount), - error::invalid_argument(EINVALID_SENDER_AMOUNT) - ); - - let from = signer::address_of(sender); - - let sender_ek = encryption_key(from, token); - let recipient_ek = encryption_key(to, token); - - let sender_ca_store = borrow_global_mut(get_user_address(from, token)); - - let sender_current_actual_balance = confidential_balance::decompress_balance( - &sender_ca_store.actual_balance - ); - - confidential_proof::verify_transfer_proof( - &sender_ek, - &recipient_ek, - &sender_current_actual_balance, - &new_balance, - &sender_amount, - &recipient_amount, - &auditor_eks, - &auditor_amounts, - &proof); - - sender_ca_store.normalized = true; - sender_ca_store.actual_balance = confidential_balance::compress_balance(&new_balance); - - // Cannot create multiple mutable references to the same type, so we need to drop it - let ConfidentialAssetStore { .. } = sender_ca_store; - - let recipient_ca_store = borrow_global_mut(get_user_address(to, token)); - - assert!( - recipient_ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER, - error::invalid_argument(EINTERNAL_ERROR) - ); - - let recipient_pending_balance = confidential_balance::decompress_balance( - &recipient_ca_store.pending_balance - ); - confidential_balance::add_balances_mut(&mut recipient_pending_balance, &recipient_amount); - - recipient_ca_store.pending_counter += 1; - recipient_ca_store.pending_balance = confidential_balance::compress_balance(&recipient_pending_balance); - - event::emit(Transferred { from, to }); - } - - /// Implementation of the `rotate_encryption_key` entry function. - public fun rotate_encryption_key_internal( - sender: &signer, - token: Object, - new_ek: twisted_elgamal::CompressedPubkey, - new_balance: confidential_balance::ConfidentialBalance, - proof: RotationProof) acquires ConfidentialAssetStore - { - let user = signer::address_of(sender); - let current_ek = encryption_key(user, token); - - let ca_store = borrow_global_mut(get_user_address(user, token)); - - let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); - - // We need to ensure that the pending balance is zero before rotating the key. - // To guarantee this, the user must call `rollover_pending_balance_and_freeze` beforehand. - assert!(confidential_balance::is_zero_balance(&pending_balance), error::invalid_state(ENOT_ZERO_BALANCE)); - - let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); - - confidential_proof::verify_rotation_proof(¤t_ek, &new_ek, ¤t_balance, &new_balance, &proof); - - ca_store.ek = new_ek; - // We don't need to update the pending balance here, as it has been asserted to be zero. - ca_store.actual_balance = confidential_balance::compress_balance(&new_balance); - ca_store.normalized = true; - } - - /// Implementation of the `normalize` entry function. - public fun normalize_internal( - sender: &signer, - token: Object, - new_balance: confidential_balance::ConfidentialBalance, - proof: NormalizationProof) acquires ConfidentialAssetStore - { - let user = signer::address_of(sender); - let sender_ek = encryption_key(user, token); - - let ca_store = borrow_global_mut(get_user_address(user, token)); - - assert!(!ca_store.normalized, error::invalid_state(EALREADY_NORMALIZED)); - - let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); - - confidential_proof::verify_normalization_proof(&sender_ek, ¤t_balance, &new_balance, &proof); - - ca_store.actual_balance = confidential_balance::compress_balance(&new_balance); - ca_store.normalized = true; - } - - /// Implementation of the `rollover_pending_balance` entry function. - public fun rollover_pending_balance_internal( - sender: &signer, - token: Object) acquires ConfidentialAssetStore - { - let user = signer::address_of(sender); - - assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - let ca_store = borrow_global_mut(get_user_address(user, token)); - - assert!(ca_store.normalized, error::invalid_state(ENORMALIZATION_REQUIRED)); - - let actual_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); - let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); - - confidential_balance::add_balances_mut(&mut actual_balance, &pending_balance); - - ca_store.normalized = false; - ca_store.pending_counter = 0; - ca_store.actual_balance = confidential_balance::compress_balance(&actual_balance); - ca_store.pending_balance = confidential_balance::new_compressed_pending_balance_no_randomness(); - } - - /// Implementation of the `freeze_token` entry function. - public fun freeze_token_internal( - sender: &signer, - token: Object) acquires ConfidentialAssetStore - { - let user = signer::address_of(sender); - - assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - let ca_store = borrow_global_mut(get_user_address(user, token)); - - assert!(!ca_store.frozen, error::invalid_state(EALREADY_FROZEN)); - - ca_store.frozen = true; - } - - /// Implementation of the `unfreeze_token` entry function. - public fun unfreeze_token_internal( - sender: &signer, - token: Object) acquires ConfidentialAssetStore - { - let user = signer::address_of(sender); - - assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); - - let ca_store = borrow_global_mut(get_user_address(user, token)); - - assert!(ca_store.frozen, error::invalid_state(ENOT_FROZEN)); - - ca_store.frozen = false; - } - - // - // Private functions. - // - - /// Ensures that the `FAConfig` object exists for the specified token. - /// If the object does not exist, creates it. - /// Used only for internal purposes. - fun ensure_fa_config_exists(token: Object): address acquires FAController { - let fa_config_address = get_fa_config_address(token); - - if (!exists(fa_config_address)) { - let fa_config_singer = get_fa_config_signer(token); - - move_to(&fa_config_singer, FAConfig { - allowed: false, - auditor_ek: std::option::none(), - }); - }; - - fa_config_address - } - - /// Returns an object for handling all the FA primary stores, and returns a signer for it. - fun get_fa_store_signer(): signer acquires FAController { - object::generate_signer_for_extending(&borrow_global(@aptos_experimental).extend_ref) - } - - /// Returns the address that handles all the FA primary stores. - fun get_fa_store_address(): address acquires FAController { - object::address_from_extend_ref(&borrow_global(@aptos_experimental).extend_ref) - } - - /// Returns an object for handling the `ConfidentialAssetStore` and returns a signer for it. - fun get_user_signer(user: &signer, token: Object): signer { - let user_ctor = &object::create_named_object(user, construct_user_seed(token)); - - object::generate_signer(user_ctor) - } - - /// Returns the address that handles the user's `ConfidentialAssetStore` object for the specified user and token. - fun get_user_address(user: address, token: Object): address { - object::create_object_address(&user, construct_user_seed(token)) - } - - /// Returns an object for handling the `FAConfig`, and returns a signer for it. - fun get_fa_config_signer(token: Object): signer acquires FAController { - let fa_ext = &borrow_global(@aptos_experimental).extend_ref; - let fa_ext_signer = object::generate_signer_for_extending(fa_ext); - - let fa_ctor = &object::create_named_object(&fa_ext_signer, construct_fa_seed(token)); - - object::generate_signer(fa_ctor) - } - - /// Returns the address that handles primary FA store and `FAConfig` objects for the specified token. - fun get_fa_config_address(token: Object): address acquires FAController { - let fa_ext = &borrow_global(@aptos_experimental).extend_ref; - let fa_ext_address = object::address_from_extend_ref(fa_ext); - - object::create_object_address(&fa_ext_address, construct_fa_seed(token)) - } - - /// Constructs a unique seed for the user's `ConfidentialAssetStore` object. - /// As all the `ConfidentialAssetStore`'s have the same type, we need to differentiate them by the seed. - fun construct_user_seed(token: Object): vector { - bcs::to_bytes( - &string_utils::format2( - &b"confidential_asset::{}::token::{}::user", - @aptos_experimental, - object::object_address(&token) - ) - ) - } - - /// Constructs a unique seed for the FA's `FAConfig` object. - /// As all the `FAConfig`'s have the same type, we need to differentiate them by the seed. - fun construct_fa_seed(token: Object): vector { - bcs::to_bytes( - &string_utils::format2( - &b"confidential_asset::{}::token::{}::fa", - @aptos_experimental, - object::object_address(&token) - ) - ) - } - - /// Validates that the auditor-related fields in the confidential transfer are correct. - /// Returns `false` if the transfer amount is not the same as the auditor amounts. - /// Returns `false` if the number of auditors in the transfer proof and auditor lists do not match. - /// Returns `false` if the first auditor in the list and the asset-specific auditor do not match. - /// Note: If the asset-specific auditor is not set, the validation is successful for any list of auditors. - /// Otherwise, returns `true`. - fun validate_auditors( - token: Object, - transfer_amount: &confidential_balance::ConfidentialBalance, - auditor_eks: &vector, - auditor_amounts: &vector, - proof: &TransferProof): bool acquires FAConfig, FAController - { - if ( - !auditor_amounts.all(|auditor_amount| { - confidential_balance::balance_c_equals(transfer_amount, auditor_amount) - }) - ) { - return false - }; - - if ( - auditor_eks.length() != auditor_amounts.length() || - auditor_eks.length() != confidential_proof::auditors_count_in_transfer_proof(proof) - ) { - return false - }; - - let asset_auditor_ek = get_auditor(token); - if (asset_auditor_ek.is_none()) { - return true - }; - - if (auditor_eks.length() == 0) { - return false - }; - - let asset_auditor_ek = twisted_elgamal::pubkey_to_point(&asset_auditor_ek.extract()); - let first_auditor_ek = twisted_elgamal::pubkey_to_point(&auditor_eks[0]); - - ristretto255::point_equals(&asset_auditor_ek, &first_auditor_ek) - } - - /// Deserializes the auditor EKs from a byte array. - /// Returns `Some(vector)` if the deserialization is successful, otherwise `None`. - fun deserialize_auditor_eks( - auditor_eks_bytes: vector): Option> - { - if (auditor_eks_bytes.length() % 32 != 0) { - return std::option::none() - }; - - let auditors_count = auditor_eks_bytes.length() / 32; - - let auditor_eks = vector::range(0, auditors_count).map(|i| { - twisted_elgamal::new_pubkey_from_bytes(auditor_eks_bytes.slice(i * 32, (i + 1) * 32)) - }); - - if (auditor_eks.any(|ek| ek.is_none())) { - return std::option::none() - }; - - std::option::some(auditor_eks.map(|ek| ek.extract())) - } - - /// Deserializes the auditor amounts from a byte array. - /// Returns `Some(vector)` if the deserialization is successful, otherwise `None`. - fun deserialize_auditor_amounts( - auditor_amounts_bytes: vector): Option> - { - if (auditor_amounts_bytes.length() % 256 != 0) { - return std::option::none() - }; - - let auditors_count = auditor_amounts_bytes.length() / 256; - - let auditor_amounts = vector::range(0, auditors_count).map(|i| { - confidential_balance::new_pending_balance_from_bytes(auditor_amounts_bytes.slice(i * 256, (i + 1) * 256)) - }); - - if (auditor_amounts.any(|ek| ek.is_none())) { - return std::option::none() - }; - - std::option::some(auditor_amounts.map(|balance| balance.extract())) - } - - /// Converts coins to missing FA. - /// Returns `Some(Object)` if user has a suffucient amount of FA to proceed, otherwise `None`. - fun ensure_sufficient_fa(sender: &signer, amount: u64): Option> { - let user = signer::address_of(sender); - let fa = coin::paired_metadata(); - - if (fa.is_none()) { - return fa; - }; - - let fa_balance = primary_fungible_store::balance(user, *fa.borrow()); - - if (fa_balance >= amount) { - return fa; - }; - - if (coin::balance(user) < amount) { - return std::option::none(); - }; - - let coin_amount = coin::withdraw(sender, amount - fa_balance); - let fa_amount = coin::coin_to_fungible_asset(coin_amount); - - primary_fungible_store::deposit(user, fa_amount); - - fa - } - - // - // Test-only functions - // - - #[test_only] - public fun init_module_for_testing(deployer: &signer) { - init_module(deployer) - } - - #[test_only] - public fun verify_pending_balance( - user: address, - token: Object, - user_dk: &Scalar, - amount: u64): bool acquires ConfidentialAssetStore - { - let ca_store = borrow_global(get_user_address(user, token)); - let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); - - confidential_balance::verify_pending_balance(&pending_balance, user_dk, amount) - } - - #[test_only] - public fun verify_actual_balance( - user: address, - token: Object, - user_dk: &Scalar, - amount: u128): bool acquires ConfidentialAssetStore - { - let ca_store = borrow_global(get_user_address(user, token)); - let actual_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); - - confidential_balance::verify_actual_balance(&actual_balance, user_dk, amount) - } - - #[test_only] - public fun serialize_auditor_eks(auditor_eks: &vector): vector { - let auditor_eks_bytes = vector[]; - - auditor_eks.for_each_ref(|auditor| { - auditor_eks_bytes.append(twisted_elgamal::pubkey_to_bytes(auditor)); - }); - - auditor_eks_bytes - } - - #[test_only] - public fun serialize_auditor_amounts( - auditor_amounts: &vector - ): vector { - let auditor_amounts_bytes = vector[]; - - auditor_amounts.for_each_ref(|balance| { - auditor_amounts_bytes.append(confidential_balance::balance_to_bytes(balance)); - }); - - auditor_amounts_bytes - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_asset.spec.move b/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_asset.spec.move deleted file mode 100644 index 8ac5d79cbd9..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_asset.spec.move +++ /dev/null @@ -1,2 +0,0 @@ -spec aptos_experimental::confidential_asset { -} diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_balance.spec.move b/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_balance.spec.move deleted file mode 100644 index fc1eeb6d6cb..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_balance.spec.move +++ /dev/null @@ -1,2 +0,0 @@ -spec aptos_experimental::confidential_balance { -} diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.spec.move b/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.spec.move deleted file mode 100644 index 0c7031bdc33..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.spec.move +++ /dev/null @@ -1,2 +0,0 @@ -spec aptos_experimental::confidential_proof { -} diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/ristretto255_twisted_elgamal.spec.move b/aptos-move/framework/aptos-experimental/sources/confidential_asset/ristretto255_twisted_elgamal.spec.move deleted file mode 100644 index 10a2fa90ba0..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/ristretto255_twisted_elgamal.spec.move +++ /dev/null @@ -1,2 +0,0 @@ -spec aptos_experimental::ristretto255_twisted_elgamal { -} diff --git a/aptos-move/framework/aptos-experimental/sources/test_function_values.move b/aptos-move/framework/aptos-experimental/sources/test_function_values.move deleted file mode 100644 index a84d50b4416..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/test_function_values.move +++ /dev/null @@ -1,9 +0,0 @@ -module aptos_experimental::test_function_values { - struct Funcs { - f: |u64| u64 has drop + copy, - } - - fun transfer_and_create_account(some_f: |u64|u64): u64 { - some_f(3) - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/market/market.move b/aptos-move/framework/aptos-experimental/sources/trading/market/market.move deleted file mode 100644 index 0771349adb5..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/market/market.move +++ /dev/null @@ -1,1032 +0,0 @@ -/// This module provides a generic trading engine implementation for a market. On a high level, its a data structure, -/// that stores an order book and provides APIs to place orders, cancel orders, and match orders. The market also acts -/// as a wrapper around the order book and pluggable clearinghouse implementation. -/// A clearing house implementation is expected to implement the following APIs -/// - settle_trade(taker, maker, taker_order_id, maker_order_id, fill_id, is_taker_long, price, size): SettleTradeResult -> -/// Called by the market when there is an match between taker and maker. The clearinghouse is expected to settle the trade -/// and return the result. Please note that the clearing house settlment size might not be the same as the order match size and -/// the settlement might also fail. The fill_id is an incremental counter for matched orders and can be used to track specific fills -/// - validate_order_placement(account, is_taker, is_long, price, size): bool -> Called by the market to validate -/// an order when its placed. The clearinghouse is expected to validate the order and return true if the order is valid. -/// Checkout clearinghouse_test as an example of the simplest form of clearing house implementation that just tracks -/// the position size of the user and does not do any validation. -/// -/// - place_maker_order(account, order_id, is_bid, price, size, metadata) -> Called by the market before placing the -/// maker order in the order book. The clearinghouse can use this to track pending orders in the order book and perform -/// any other book keeping operations. -/// -/// - cleanup_order(account, order_id, is_bid, remaining_size) -> Called by the market when an order is cancelled or fully filled -/// The clearinhouse can perform any cleanup operations like removing the order from the pending orders list. For every order placement -/// that passes the validate_order_placement check, -/// the market guarantees that the cleanup_order API will be called once and only once with the remaining size of the order. -/// -/// - decrease_order_size(account, order_id, is_bid, price, size) -> Called by the market when a maker order is decreased -/// in size by the user. Please note that this API will only be called after place_maker_order is called and the order is -/// already in the order book. Size in this case is the remaining size of the order after the decrease. -/// -/// Following are some valid sequence of API calls that the market makes to the clearinghouse: -/// 1. validate_order_placement(10) -/// 2. settle_trade(2) -/// 3. settle_trade(3) -/// 4. place_maker_order(5) -/// 5. decrease_order_size(2) -/// 6. decrease_order_size(1) -/// 7. cleanup_order(2) -/// or -/// 1. validate_order_placement(10) -/// 2. cleanup_order(10) -/// -/// Upon placement of an order, the market generates an order id and emits an event with the order details - the order id -/// is a unique id for the order that can be used to later get the status of the order or cancel the order. -/// -/// Market also supports various conditions for order matching like Good Till Cancelled (GTC), Post Only, Immediate or Cancel (IOC). -/// GTC orders are orders that are valid until they are cancelled or filled. Post Only orders are orders that are valid only if they are not -/// taker orders. IOC orders are orders that are valid only if they are taker orders. -/// -/// In addition, the market also supports trigger conditions for orders. An order with trigger condition is not put -/// on the order book until its trigger conditions are met. Following trigger conditions are supported: -/// TakeProfit(price): If its a buy order its triggered when the market price is greater than or equal to the price. If -/// its a sell order its triggered when the market price is less than or equal to the price. -/// StopLoss(price): If its a buy order its triggered when the market price is less than or equal to the price. If its -/// a sell order its triggered when the market price is greater than or equal to the price. -/// TimeBased(time): The order is triggered when the current time is greater than or equal to the time. -/// -module aptos_experimental::market { - - use std::option; - use std::option::Option; - use std::signer; - use std::string::String; - use std::vector; - use aptos_framework::event; - use aptos_experimental::order_book::{OrderBook, new_order_book, new_order_request}; - use aptos_experimental::order_book_types::{TriggerCondition, Order}; - use aptos_experimental::market_types::MarketClearinghouseCallbacks; - - // Error codes - const EINVALID_ORDER: u64 = 1; - const EORDER_BOOK_FULL: u64 = 2; - const EMARKET_NOT_FOUND: u64 = 3; - const ENOT_ADMIN: u64 = 4; - const EINVALID_FEE_TIER: u64 = 5; - const EORDER_DOES_NOT_EXIST: u64 = 6; - const EINVALID_TIME_IN_FORCE_FOR_MAKER: u64 = 7; - const EINVALID_TIME_IN_FORCE_FOR_TAKER: u64 = 8; - const EINVALID_MATCHING_FOR_MAKER_REINSERT: u64 = 9; - const EINVALID_TAKER_POSITION_UPDATE: u64 = 10; - const EINVALID_LIQUIDATION: u64 = 11; - - /// Order time in force - /// Good till cancelled order type - const TIME_IN_FORCE_GTC: u8 = 0; - /// Post Only order type - ensures that the order is not a taker order - const TIME_IN_FORCE_POST_ONLY: u8 = 1; - /// Immediate or Cancel order type - ensures that the order is a taker order. Try to match as much of the - /// order as possible as taker order and cancel the rest. - const TIME_IN_FORCE_IOC: u8 = 2; - - public fun good_till_cancelled(): u8 { - TIME_IN_FORCE_GTC - } - - public fun post_only(): u8 { - TIME_IN_FORCE_POST_ONLY - } - - public fun immediate_or_cancel(): u8 { - TIME_IN_FORCE_IOC - } - - struct Market has store { - /// Address of the parent object that created this market - /// Purely for grouping events based on the source DEX, not used otherwise - parent: address, - /// Address of the market object of this market. - market: address, - // TODO: remove sequential order id generation - last_order_id: u64, - // Incremental fill id for matched orders - next_fill_id: u64, - config: MarketConfig, - order_book: OrderBook - } - - struct MarketConfig has store { - /// Weather to allow self matching orders - allow_self_trade: bool, - /// Whether to allow sending all events for the markett - allow_events_emission: bool - } - - /// Order has been accepted by the engine. - const ORDER_STATUS_OPEN: u8 = 0; - /// Order has been fully or partially filled. - const ORDER_STATUS_FILLED: u8 = 1; - /// Order has been cancelled by the user or engine. - const ORDER_STATUS_CANCELLED: u8 = 2; - /// Order has been rejected by the engine. Unlike cancelled orders, rejected - /// orders are invalid orders. Rejection reasons: - /// 1. Insufficient margin - /// 2. Order is reduce_only but does not reduce - const ORDER_STATUS_REJECTED: u8 = 3; - const ORDER_SIZE_REDUCED: u8 = 4; - - public fun order_status_open(): u8 { - ORDER_STATUS_OPEN - } - - public fun order_status_filled(): u8 { - ORDER_STATUS_FILLED - } - - public fun order_status_cancelled(): u8 { - ORDER_STATUS_CANCELLED - } - - public fun order_status_rejected(): u8 { - ORDER_STATUS_REJECTED - } - - #[event] - struct OrderEvent has drop, copy, store { - parent: address, - market: address, - order_id: u64, - user: address, - /// Original size of the order - orig_size: u64, - /// Remaining size of the order in the order book - remaining_size: u64, - // TODO(bl): Brian and Sean will revisit to see if we should have split - // into multiple events for OrderEvent - /// OPEN - size_delta will be amount of size added - /// CANCELLED - size_delta will be amount of size removed - /// FILLED - size_delta will be amount of size filled - /// REJECTED - size_delta will always be 0 - size_delta: u64, - price: u64, - is_buy: bool, - /// Whether the order crosses the orderbook. - is_taker: bool, - status: u8, - details: std::string::String - } - - enum OrderCancellationReason has drop, copy { - PostOnlyViolation, - IOCViolation, - PositionUpdateViolation, - ReduceOnlyViolation, - ClearinghouseSettleViolation, - MaxFillLimitViolation - } - - struct OrderMatchResult has drop { - order_id: u64, - remaining_size: u64, - cancel_reason: Option, - fill_sizes: vector - } - - public fun destroy_order_match_result( - self: OrderMatchResult - ): (u64, u64, Option, vector) { - let OrderMatchResult { order_id, remaining_size, cancel_reason, fill_sizes } = - self; - (order_id, remaining_size, cancel_reason, fill_sizes) - } - - public fun number_of_fills(self: &OrderMatchResult): u64 { - self.fill_sizes.length() - } - - public fun total_fill_size(self: &OrderMatchResult): u64 { - self.fill_sizes.fold(0, |acc, fill_size| acc + fill_size) - } - - public fun get_cancel_reason(self: &OrderMatchResult): Option { - self.cancel_reason - } - - public fun get_remaining_size_from_result(self: &OrderMatchResult): u64 { - self.remaining_size - } - - public fun is_ioc_violation(self: OrderCancellationReason): bool { - return self == OrderCancellationReason::IOCViolation - } - - public fun is_fill_limit_violation( - cancel_reason: OrderCancellationReason - ): bool { - return cancel_reason == OrderCancellationReason::MaxFillLimitViolation - } - - public fun get_order_id(self: OrderMatchResult): u64 { - self.order_id - } - - public fun new_market_config( - allow_self_matching: bool, allow_events_emission: bool - ): MarketConfig { - MarketConfig { allow_self_trade: allow_self_matching, allow_events_emission: allow_events_emission } - } - - public fun new_market( - parent: &signer, market: &signer, config: MarketConfig - ): Market { - // requiring signers, and not addresses, purely to guarantee different dexes - // cannot polute events to each other, accidentally or maliciously. - Market { - parent: signer::address_of(parent), - market: signer::address_of(market), - last_order_id: 0, - next_fill_id: 0, - config, - order_book: new_order_book() - } - } - - public fun get_market(self: &Market): address { - self.market - } - - public fun get_order_book(self: &Market): &OrderBook { - &self.order_book - } - - public fun get_order_book_mut( - self: &mut Market - ): &mut OrderBook { - &mut self.order_book - } - - public fun best_bid_price(self: &Market): Option { - self.order_book.best_bid_price() - } - - public fun best_ask_price(self: &Market): Option { - self.order_book.best_ask_price() - } - - public fun is_taker_order( - self: &Market, - price: u64, - is_buy: bool, - trigger_condition: Option - ): bool { - self.order_book.is_taker_order(price, is_buy, trigger_condition) - } - - /// Places an order - If its a taker order, it will be matched immediately and if its a maker order, it will simply - /// be placed in the order book. An order id is generated when the order is placed and this id can be used to - /// uniquely identify the order for this market and can also be used to get the status of the order or cancel the order. - /// The order is placed with the following parameters: - /// - user: The user who is placing the order - /// - price: The price at which the order is placed - /// - orig_size: The original size of the order - /// - is_buy: Whether the order is a buy order or a sell order - /// - time_in_force: The time in force for the order. This can be one of the following: - /// - TIME_IN_FORCE_GTC: Good till cancelled order type - /// - TIME_IN_FORCE_POST_ONLY: Post Only order type - ensures that the order is not a taker order - /// - TIME_IN_FORCE_IOC: Immediate or Cancel order type - ensures that the order is a taker order. Try to match as much of the - /// order as possible as taker order and cancel the rest. - /// - trigger_condition: The trigger condition - /// - metadata: The metadata for the order. This can be any type that the clearing house implementation supports. - /// - max_fill_limit: The maximum fill limit for the order. This is the maximum number of fills to trigger for this order. - /// This knob is present to configure maximum amount of gas any order placement transaction might consume and avoid - /// hitting the maximum has limit of the blockchain. - /// - emit_cancel_on_fill_limit: bool,: Whether to emit an order cancellation event when the fill limit is reached. - /// This is used ful as the caller might not want to cancel the order when the limit is reached and can continue - /// that order in a separate transaction. - /// - callbacks: The callbacks for the market clearinghouse. This is a struct that implements the MarketClearinghouseCallbacks - /// interface. This is used to validate the order and settle the trade. - /// Returns the order id, remaining size, cancel reason and number of fills for the order. - public fun place_order( - self: &mut Market, - user: &signer, - price: u64, - orig_size: u64, - is_bid: bool, - time_in_force: u8, - trigger_condition: Option, - metadata: M, - max_fill_limit: u64, - emit_cancel_on_fill_limit: bool, - callbacks: &MarketClearinghouseCallbacks - ): OrderMatchResult { - let order_id = self.next_order_id(); - self.place_order_with_order_id( - signer::address_of(user), - price, - orig_size, - orig_size, - is_bid, - time_in_force, - trigger_condition, - metadata, - order_id, - max_fill_limit, - emit_cancel_on_fill_limit, - true, - callbacks - ) - } - - public fun next_order_id(self: &mut Market): u64 { - self.last_order_id += 1; - self.last_order_id - } - - fun next_fill_id(self: &mut Market): u64 { - let next_fill_id = self.next_fill_id; - self.next_fill_id += 1; - next_fill_id - } - - fun emit_event_for_order( - self: &Market, - order_id: u64, - user: address, - orig_size: u64, - remaining_size: u64, - size_delta: u64, - price: u64, - is_bid: bool, - is_taker: bool, - status: u8, - details: &String - ) { - // Final check whether event sending is enabled - if (self.config.allow_events_emission) { - event::emit( - OrderEvent { - parent: self.parent, - market: self.market, - order_id, - user, - orig_size, - remaining_size, - size_delta, - price, - is_buy: is_bid, - is_taker, - status, - details: *details - } - ); - }; - } - - /// Similar to `place_order` API but instead of a signer, it takes a user address - can be used in case trading - /// functionality is delegated to a different address. Please note that it is the responsibility of the caller - /// to verify that the transaction signer is authorized to place orders on behalf of the user. - public fun place_order_with_user_addr( - self: &mut Market, - user_addr: address, - price: u64, - orig_size: u64, - is_bid: bool, - time_in_force: u8, - trigger_condition: Option, - metadata: M, - max_fill_limit: u64, - emit_cancel_on_fill_limit: bool, - callbacks: &MarketClearinghouseCallbacks - ): OrderMatchResult { - let order_id = self.next_order_id(); - self.place_order_with_order_id( - user_addr, - price, - orig_size, - orig_size, - is_bid, - time_in_force, - trigger_condition, - metadata, - order_id, - max_fill_limit, - emit_cancel_on_fill_limit, - true, - callbacks - ) - } - - fun place_maker_order_internal( - self: &mut Market, - user_addr: address, - price: u64, - orig_size: u64, - remaining_size: u64, - fill_sizes: vector, - is_bid: bool, - time_in_force: u8, - trigger_condition: Option, - metadata: M, - order_id: u64, - emit_order_open: bool, - callbacks: &MarketClearinghouseCallbacks - ): OrderMatchResult { - // Validate that the order is valid from position management perspective - if (time_in_force == TIME_IN_FORCE_IOC) { - return self.cancel_order_internal( - user_addr, - price, - order_id, - orig_size, - remaining_size, - fill_sizes, - is_bid, - false, // is_taker - OrderCancellationReason::IOCViolation, - std::string::utf8(b"IOC Violation"), - callbacks - ); - }; - - if (emit_order_open) { - emit_event_for_order( - self, - order_id, - user_addr, - orig_size, - remaining_size, - orig_size, - price, - is_bid, - false, // is_taker - ORDER_STATUS_OPEN, - &std::string::utf8(b"") - ); - }; - - callbacks.place_maker_order( - user_addr, order_id, is_bid, price, remaining_size, metadata - ); - self.order_book.place_maker_order( - new_order_request( - user_addr, - order_id, - option::none(), - price, - orig_size, - remaining_size, - is_bid, - trigger_condition, - metadata - ) - ); - return OrderMatchResult { - order_id, - remaining_size, - cancel_reason: option::none(), - fill_sizes - } - } - - fun cancel_maker_order_internal( - self: &mut Market, - maker_order: &Order, - order_id: u64, - maker_address: address, - maker_cancellation_reason: String, - unsettled_size: u64, - callbacks: &MarketClearinghouseCallbacks - ) { - let maker_cancel_size = unsettled_size + maker_order.get_remaining_size(); - - emit_event_for_order( - self, - order_id, - maker_address, - maker_order.get_orig_size(), - 0, - maker_cancel_size, - maker_order.get_price(), - maker_order.is_bid(), - false, - ORDER_STATUS_CANCELLED, - &maker_cancellation_reason - ); - // If the maker is invalid cancel the maker order and continue to the next maker order - if (maker_order.get_remaining_size() != 0) { - self.order_book.cancel_order(maker_address, order_id); - }; - callbacks.cleanup_order( - maker_address, order_id, maker_order.is_bid(), maker_cancel_size - ); - } - - fun cancel_order_internal( - self: &mut Market, - user_addr: address, - price: u64, - order_id: u64, - orig_size: u64, - size_delta: u64, - fill_sizes: vector, - is_bid: bool, - is_taker: bool, - cancel_reason: OrderCancellationReason, - cancel_details: String, - callbacks: &MarketClearinghouseCallbacks - ): OrderMatchResult { - emit_event_for_order( - self, - order_id, - user_addr, - orig_size, - 0, // remaining size - size_delta, - price, - is_bid, - is_taker, - ORDER_STATUS_CANCELLED, - &cancel_details - ); - callbacks.cleanup_order( - user_addr, order_id, is_bid, size_delta - ); - return OrderMatchResult { - order_id, - remaining_size: 0, - cancel_reason: option::some(cancel_reason), - fill_sizes - } - } - - /// Similar to `place_order` API but allows few extra parameters as follows - /// - order_id: The order id for the order - this is needed because for orders with trigger conditions, the order - /// id is generated when the order is placed and when they are triggered, the same order id is used to match the order. - /// - emit_taker_order_open: bool: Whether to emit an order open event for the taker order - this is used when - /// the caller do not wants to emit an open order event for a taker in case the taker order was intterrupted because - /// of fill limit violation in the previous transaction and the order is just a continuation of the previous order. - public fun place_order_with_order_id( - self: &mut Market, - user_addr: address, - price: u64, - orig_size: u64, - remaining_size: u64, - is_bid: bool, - time_in_force: u8, - trigger_condition: Option, - metadata: M, - order_id: u64, - max_fill_limit: u64, - cancel_on_fill_limit: bool, - emit_taker_order_open: bool, - callbacks: &MarketClearinghouseCallbacks - ): OrderMatchResult { - assert!( - orig_size > 0 && remaining_size > 0, - EINVALID_ORDER - ); - // TODO(skedia) is_taker_order API can actually return false positive as the maker orders might not be valid. - // Changes are needed to ensure the maker order is valid for this order to be a valid taker order. - // TODO(skedia) reconsile the semantics around global order id vs account local id. - if ( - !callbacks.validate_order_placement( - user_addr, - order_id, - true, // is_taker - is_bid, - price, - remaining_size, - metadata - )) { - return self.cancel_order_internal( - user_addr, - price, - order_id, - orig_size, - 0, // 0 because order was never placed - vector[], - is_bid, - true, // is_taker - OrderCancellationReason::PositionUpdateViolation, - std::string::utf8(b"Position Update violation"), - callbacks - ); - }; - - let is_taker_order = - self.order_book.is_taker_order(price, is_bid, trigger_condition); - if (emit_taker_order_open) { - emit_event_for_order( - self, - order_id, - user_addr, - orig_size, - remaining_size, - orig_size, - price, - is_bid, - is_taker_order, - ORDER_STATUS_OPEN, - &std::string::utf8(b"") - ); - }; - if (!is_taker_order) { - return self.place_maker_order_internal( - user_addr, - price, - orig_size, - remaining_size, - vector[], - is_bid, - time_in_force, - trigger_condition, - metadata, - order_id, - false, - callbacks - ); - }; - - // NOTE: We should always use is_taker: true for this order past this - // point so that indexer can consistently track the order's status - if (time_in_force == TIME_IN_FORCE_POST_ONLY) { - return self.cancel_order_internal( - user_addr, - price, - order_id, - orig_size, - remaining_size, - vector[], - is_bid, - true, // is_taker - OrderCancellationReason::PostOnlyViolation, - std::string::utf8(b"Post Only violation"), - callbacks - ); - }; - let fill_sizes = vector::empty(); - loop { - let result = - self.order_book.get_single_match_for_taker(price, remaining_size, is_bid); - let (maker_order, maker_matched_size) = result.destroy_single_order_match(); - let (maker_address, maker_order_id) = - maker_order.get_order_id().destroy_order_id_type(); - if (!self.config.allow_self_trade && maker_address == user_addr) { - self.cancel_maker_order_internal( - &maker_order, - maker_order_id, - maker_address, - std::string::utf8(b"Disallowed self trading"), - maker_matched_size, - callbacks - ); - continue; - }; - - let fill_id = self.next_fill_id(); - - let settle_result = - callbacks.settle_trade( - user_addr, - maker_address, - order_id, - maker_order_id, - fill_id, - is_bid, - maker_order.get_price(), // Order is always matched at the price of the maker - maker_matched_size, - metadata, - maker_order.get_metadata_from_order() - ); - - let unsettled_maker_size = maker_matched_size; - let settled_size = settle_result.get_settled_size(); - if (settled_size > 0) { - remaining_size -= settled_size; - unsettled_maker_size -= settled_size; - fill_sizes.push_back(settled_size); - // Event for taker fill - emit_event_for_order( - self, - order_id, - user_addr, - orig_size, - remaining_size, - settled_size, - maker_order.get_price(), - is_bid, - true, // is_taker - ORDER_STATUS_FILLED, - &std::string::utf8(b"") - ); - // Event for maker fill - emit_event_for_order( - self, - maker_order_id, - maker_address, - maker_order.get_orig_size(), - maker_order.get_remaining_size() + unsettled_maker_size, - settled_size, - maker_order.get_price(), - !is_bid, - false, // is_taker - ORDER_STATUS_FILLED, - &std::string::utf8(b"") - ); - }; - - let maker_cancellation_reason = settle_result.get_maker_cancellation_reason(); - if (maker_cancellation_reason.is_some()) { - self.cancel_maker_order_internal( - &maker_order, - maker_order_id, - maker_address, - maker_cancellation_reason.destroy_some(), - unsettled_maker_size, - callbacks - ); - }; - - let taker_cancellation_reason = settle_result.get_taker_cancellation_reason(); - if (taker_cancellation_reason.is_some()) { - let result = - self.cancel_order_internal( - user_addr, - price, - order_id, - orig_size, - remaining_size, - fill_sizes, - is_bid, - true, // is_taker - OrderCancellationReason::ClearinghouseSettleViolation, - taker_cancellation_reason.destroy_some(), - callbacks - ); - if (maker_cancellation_reason.is_none() && unsettled_maker_size > 0) { - // If the taker is cancelled but the maker is not cancelled, then we need to re-insert - // the maker order back into the order book - self.order_book.reinsert_maker_order( - new_order_request( - maker_address, - maker_order_id, - option::some(maker_order.get_unique_priority_idx()), - maker_order.get_price(), - maker_order.get_orig_size(), - unsettled_maker_size, - !is_bid, - option::none(), - maker_order.get_metadata_from_order() - ) - ); - }; - return result; - }; - - if (maker_order.get_remaining_size() == 0) { - callbacks.cleanup_order( - maker_address, - maker_order_id, - !is_bid, // is_bid is inverted for maker orders - 0 // 0 because the order is fully filled - ); - }; - if (remaining_size == 0) { - callbacks.cleanup_order( - user_addr, order_id, is_bid, 0 // 0 because the order is fully filled - ); - break; - }; - - // Check if the next iteration will still match - let is_taker_order = - self.order_book.is_taker_order(price, is_bid, option::none()); - if (!is_taker_order) { - if (time_in_force == TIME_IN_FORCE_IOC) { - return self.cancel_order_internal( - user_addr, - price, - order_id, - orig_size, - remaining_size, - fill_sizes, - is_bid, - true, // is_taker - OrderCancellationReason::IOCViolation, - std::string::utf8(b"IOC_VIOLATION"), - callbacks - ); - } else { - // If the order is not a taker order, then we can place it as a maker order - return self.place_maker_order_internal( - user_addr, - price, - orig_size, - remaining_size, - fill_sizes, - is_bid, - time_in_force, - trigger_condition, - metadata, - order_id, - true, // emit_order_open - callbacks - ); - }; - }; - - if (fill_sizes.length() >= max_fill_limit) { - if (cancel_on_fill_limit) { - return self.cancel_order_internal( - user_addr, - price, - order_id, - orig_size, - remaining_size, - fill_sizes, - is_bid, - true, // is_taker - OrderCancellationReason::MaxFillLimitViolation, - std::string::utf8(b"Max fill limit reached"), - callbacks - ); - } else { - return OrderMatchResult { - order_id, - remaining_size, - cancel_reason: option::some( - OrderCancellationReason::MaxFillLimitViolation - ), - fill_sizes - } - }; - }; - }; - OrderMatchResult { - order_id, - remaining_size, - cancel_reason: option::none(), - fill_sizes - } - } - - /// Cancels an order - this will cancel the order and emit an event for the order cancellation. - public fun cancel_order( - self: &mut Market, - user: &signer, - order_id: u64, - callbacks: &MarketClearinghouseCallbacks - ) { - let account = signer::address_of(user); - let maybe_order = self.order_book.cancel_order(account, order_id); - if (maybe_order.is_some()) { - let order = maybe_order.destroy_some(); - let ( - order_id_type, - _unique_priority_idx, - price, - orig_size, - remaining_size, - is_bid, - _trigger_condition, - _metadata - ) = order.destroy_order(); - callbacks.cleanup_order( - account, order_id, is_bid, remaining_size - ); - let (user, order_id) = order_id_type.destroy_order_id_type(); - emit_event_for_order( - self, - order_id, - user, - orig_size, - remaining_size, - remaining_size, - price, - is_bid, - false, // is_taker - ORDER_STATUS_CANCELLED, - &std::string::utf8(b"Order cancelled") - ); - } - } - - /// Cancels an order - this will cancel the order and emit an event for the order cancellation. - public fun decrease_order_size( - self: &mut Market, - user: &signer, - order_id: u64, - size_delta: u64, - callbacks: &MarketClearinghouseCallbacks - ) { - let account = signer::address_of(user); - self.order_book.decrease_order_size(account, order_id, size_delta); - let maybe_order = self.order_book.get_order(account, order_id); - assert!(maybe_order.is_some(), EORDER_DOES_NOT_EXIST); - let (order, _) = maybe_order.destroy_some().destroy_order_from_state(); - let ( - order_id_type, - _unique_priority_idx, - price, - orig_size, - remaining_size, - is_bid, - _trigger_condition, - _metadata - ) = order.destroy_order(); - let (user, order_id) = order_id_type.destroy_order_id_type(); - callbacks.decrease_order_size( - user, order_id, is_bid, price, remaining_size - ); - - emit_event_for_order( - self, - order_id, - user, - orig_size, - remaining_size, - size_delta, - price, - is_bid, - false, // is_taker - ORDER_SIZE_REDUCED, - &std::string::utf8(b"Order size reduced") - ); - } - - /// Remaining size of the order in the order book. - public fun get_remaining_size( - self: &Market, user: address, order_id: u64 - ): u64 { - self.order_book.get_remaining_size(user, order_id) - } - - /// Returns all the pending order ready to be executed based on the oracle price. The caller is responsible to - /// call the `place_order_with_order_id` API to place the order with the order id returned from this API. - public fun take_ready_price_based_orders( - self: &mut Market, oracle_price: u64 - ): vector> { - self.order_book.take_ready_price_based_orders(oracle_price) - } - - /// Returns all the pending order that are ready to be executed based on current time stamp. The caller is responsible to - /// call the `place_order_with_order_id` API to place the order with the order id returned from this API. - public fun take_ready_time_based_orders( - self: &mut Market - ): vector> { - self.order_book.take_ready_time_based_orders() - } - - // ============================= test_only APIs ==================================== - #[test_only] - public fun destroy_market(self: Market) { - let Market { - parent: _parent, - market: _market, - last_order_id: _last_order_id, - next_fill_id: _next_fill_id, - config, - order_book - } = self; - let MarketConfig { allow_self_trade: _, allow_events_emission: _ } = config; - order_book.destroy_order_book() - } - - #[test_only] - public fun is_clearinghouse_settle_violation( - cancellation_reason: OrderCancellationReason - ): bool { - if (cancellation_reason - == OrderCancellationReason::ClearinghouseSettleViolation) { - return true; - }; - false - } - - #[test_only] - public fun get_order_id_from_event(self: OrderEvent): u64 { - self.order_id - } - - #[test_only] - public fun verify_order_event( - self: OrderEvent, - order_id: u64, - market: address, - user: address, - orig_size: u64, - remaining_size: u64, - size_delta: u64, - price: u64, - is_buy: bool, - is_taker: bool, - status: u8 - ) { - assert!(self.order_id == order_id); - assert!(self.market == market); - assert!(self.user == user); - assert!(self.orig_size == orig_size); - assert!(self.remaining_size == remaining_size); - assert!(self.size_delta == size_delta); - assert!(self.price == price); - assert!(self.is_buy == is_buy); - assert!(self.is_taker == is_taker); - assert!(self.status == status); - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/market/market_types.move b/aptos-move/framework/aptos-experimental/sources/trading/market/market_types.move deleted file mode 100644 index 3d2251cc6ee..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/market/market_types.move +++ /dev/null @@ -1,125 +0,0 @@ -module aptos_experimental::market_types { - use std::option::Option; - use std::string::String; - - const EINVALID_ADDRESS: u64 = 1; - const EINVALID_SETTLE_RESULT: u64 = 2; - - struct SettleTradeResult has drop { - settled_size: u64, - maker_cancellation_reason: Option, - taker_cancellation_reason: Option - } - - struct MarketClearinghouseCallbacks has drop { - // settle_trade_f arguments: taker, maker, taker_order_id, maker_order_id, fill_id, is_taker_long, price, size - settle_trade_f: |address, address, u64, u64, u64, bool, u64, u64, M, M| SettleTradeResult has drop + copy, - // validate_settlement_update_f arguments: account, is_taker, is_long, price, size - validate_order_placement_f: |address, u64, bool, bool, u64, u64, M| bool has drop + copy, - // place_maker_order_f arguments: account, order_id, is_bid, price, size, order_metadata - place_maker_order_f: |address, u64, bool, u64, u64, M| has drop + copy, - // cleanup_order_f arguments: account, order_id, is_bid, remaining_size - cleanup_order_f: |address, u64, bool, u64| has drop + copy, - // decrease_order_size_f arguments: account, order_id, is_bid, price, size - decrease_order_size_f: |address, u64, bool, u64, u64| has drop + copy, - } - - public fun new_settle_trade_result( - settled_size: u64, - maker_cancellation_reason: Option, - taker_cancellation_reason: Option - ): SettleTradeResult { - SettleTradeResult { - settled_size, - maker_cancellation_reason, - taker_cancellation_reason - } - } - - public fun new_market_clearinghouse_callbacks( - // settle_trade_f arguments: taker, maker, taker_order_id, maker_order_id, fill_id, is_taker_long, price, size - settle_trade_f: |address, address, u64, u64, u64, bool, u64, u64, M, M| SettleTradeResult has drop + copy, - // validate_settlement_update_f arguments: accoun, is_taker, is_long, price, size - validate_order_placement_f: |address, u64, bool, bool, u64, u64, M| bool has drop + copy, - place_maker_order_f: |address, u64, bool, u64, u64, M| has drop + copy, - cleanup_order_f: |address, u64, bool, u64| has drop + copy, - decrease_order_size_f: |address, u64, bool, u64, u64| has drop + copy, - ): MarketClearinghouseCallbacks { - MarketClearinghouseCallbacks { - settle_trade_f, - validate_order_placement_f, - place_maker_order_f, - cleanup_order_f, - decrease_order_size_f - } - } - - public fun get_settled_size(self: &SettleTradeResult): u64 { - self.settled_size - } - - public fun get_maker_cancellation_reason(self: &SettleTradeResult): Option { - self.maker_cancellation_reason - } - - public fun get_taker_cancellation_reason(self: &SettleTradeResult): Option { - self.taker_cancellation_reason - } - - public fun settle_trade( - self: &MarketClearinghouseCallbacks, - taker: address, - maker: address, - taker_order_id: u64, - maker_order_id:u64, - fill_id: u64, - is_taker_long: bool, - price: u64, - size: u64, - taker_metadata: M, - maker_metadata: M): SettleTradeResult { - (self.settle_trade_f)(taker, maker, taker_order_id, maker_order_id, fill_id, is_taker_long, price, size, taker_metadata, maker_metadata) - } - - public fun validate_order_placement( - self: &MarketClearinghouseCallbacks, - account: address, - order_id: u64, - is_taker: bool, - is_bid: bool, - price: u64, - size: u64, - order_metadata: M): bool { - (self.validate_order_placement_f)(account, order_id, is_taker, is_bid, price, size, order_metadata) - } - - public fun place_maker_order( - self: &MarketClearinghouseCallbacks, - account: address, - order_id: u64, - is_bid: bool, - price: u64, - size: u64, - order_metadata: M) { - (self.place_maker_order_f)(account, order_id, is_bid, price, size, order_metadata) - } - - public fun cleanup_order( - self: &MarketClearinghouseCallbacks, - account: address, - order_id: u64, - is_bid: bool, - remaining_size: u64) { - (self.cleanup_order_f)(account, order_id, is_bid, remaining_size) - } - - public fun decrease_order_size( - self: &MarketClearinghouseCallbacks, - account: address, - order_id: u64, - is_bid: bool, - price: u64, - size: u64,) { - (self.decrease_order_size_f)(account, order_id, is_bid, price, size) - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/order_book/active_order_book.move b/aptos-move/framework/aptos-experimental/sources/trading/order_book/active_order_book.move deleted file mode 100644 index 660b52dde27..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/order_book/active_order_book.move +++ /dev/null @@ -1,704 +0,0 @@ -/// (work in progress) -module aptos_experimental::active_order_book { - use std::option::{Self, Option}; - use aptos_std::math64::mul_div; - use aptos_framework::big_ordered_map::BigOrderedMap; - use aptos_experimental::order_book_types::{ - OrderIdType, - UniqueIdxType, - new_active_matched_order, - ActiveMatchedOrder, - get_slippage_pct_precision, - new_default_big_ordered_map - }; - #[test_only] - use std::vector; - #[test_only] - use aptos_experimental::order_book_types::{new_order_id_type, new_unique_idx_type}; - - const EINVALID_MAKER_ORDER: u64 = 1; - /// There is a code bug that breaks internal invariant - const EINTERNAL_INVARIANT_BROKEN: u64 = 2; - - friend aptos_experimental::order_book; - - /// ========= Active OrderBook =========== - - // Active Order Book: - // bids: (order_id, price, unique_priority_idx, volume) - - // (price, unique_priority_idx) -> (volume, order_id) - - const U64_MAX: u64 = 0xffffffffffffffff; - - const U256_MAX: u256 = - 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - // 115792089237316195423570985008687907853269984665640564039457584007913129639935; - - struct ActiveBidKey has store, copy, drop { - price: u64, - tie_breaker: UniqueIdxType - } - - struct ActiveBidData has store, copy, drop { - order_id: OrderIdType, - size: u64 - } - - /// OrderBook tracking active (i.e. unconditional, immediately executable) limit orders. - /// - /// - invariant - all buys are smaller than sells, at all times. - /// - tie_breaker in sells is U256_MAX-value, to make sure largest value in the book - /// that is taken first, is the one inserted first, amongst those with same bid price. - enum ActiveOrderBook has store { - V1 { - buys: BigOrderedMap, - sells: BigOrderedMap - } - } - - public fun new_active_order_book(): ActiveOrderBook { - // potentially add max value to both sides (that will be skipped), - // so that max_key never changes, and doesn't create conflict. - ActiveOrderBook::V1 { - buys: new_default_big_ordered_map(), - sells: new_default_big_ordered_map() - } - } - - - /// Picks the best (i.e. highest) bid (i.e. buy) price from the active order book. - /// aborts if there are no buys - public fun best_bid_price(self: &ActiveOrderBook): Option { - if (self.buys.is_empty()) { - option::none() - } else { - let (back_key, _back_value) = self.buys.borrow_back(); - option::some(back_key.price) - } - } - - /// Picks the best (i.e. lowest) ask (i.e. sell) price from the active order book. - /// aborts if there are no sells - public fun best_ask_price(self: &ActiveOrderBook): Option { - if (self.sells.is_empty()) { - option::none() - } else { - let (front_key, _front_value) = self.sells.borrow_front(); - option::some(front_key.price) - } - } - - public fun get_mid_price(self: &ActiveOrderBook): Option { - let best_bid = self.best_bid_price(); - let best_ask = self.best_ask_price(); - if (best_bid.is_none() || best_ask.is_none()) { - option::none() - } else { - option::some( - (best_bid.destroy_some() + best_ask.destroy_some()) / 2 - ) - } - } - - public fun get_slippage_price( - self: &ActiveOrderBook, is_buy: bool, slippage_pct: u64 - ): Option { - let mid_price = self.get_mid_price(); - if (mid_price.is_none()) { - return option::none(); - }; - let mid_price = mid_price.destroy_some(); - let slippage = mul_div( - mid_price, slippage_pct, get_slippage_pct_precision() * 100 - ); - if (is_buy) { - option::some(mid_price + slippage) - } else { - option::some(mid_price - slippage) - } - } - - // TODO check if keeping depth book is more efficient than computing impact prices manually - - fun get_impact_bid_price(self: &ActiveOrderBook, impact_size: u64): Option { - let total_value = (0 as u128); - let total_size = 0; - let orders = &self.buys; - if (orders.is_empty()) { - return option::none(); - }; - let (front_key, front_value) = orders.borrow_back(); - while (total_size < impact_size) { - let matched_size = - if (total_size + front_value.size > impact_size) { - impact_size - total_size - } else { - front_value.size - }; - total_value = total_value - + (matched_size as u128) * (front_key.price as u128); - total_size = total_size + matched_size; - let next_key = orders.prev_key(&front_key); - if (next_key.is_none()) { - // TODO maybe we should return none if there is not enough depth? - break; - }; - front_key = next_key.destroy_some(); - front_value = orders.borrow(&front_key); - }; - option::some((total_value / (total_size as u128)) as u64) - } - - fun get_impact_ask_price(self: &ActiveOrderBook, impact_size: u64): Option { - let total_value = 0 as u128; - let total_size = 0; - let orders = &self.sells; - if (orders.is_empty()) { - return option::none(); - }; - let (front_key, front_value) = orders.borrow_front(); - while (total_size < impact_size) { - let matched_size = - if (total_size + front_value.size > impact_size) { - impact_size - total_size - } else { - front_value.size - }; - total_value = total_value - + (matched_size as u128) * (front_key.price as u128); - total_size = total_size + matched_size; - let next_key = orders.next_key(&front_key); - if (next_key.is_none()) { - break; - }; - front_key = next_key.destroy_some(); - front_value = orders.borrow(&front_key); - }; - option::some((total_value / (total_size as u128)) as u64) - } - - inline fun get_tie_breaker( - unique_priority_idx: UniqueIdxType, is_buy: bool - ): UniqueIdxType { - if (is_buy) { - unique_priority_idx - } else { - unique_priority_idx.descending_idx() - } - } - - public fun cancel_active_order( - self: &mut ActiveOrderBook, - price: u64, - unique_priority_idx: UniqueIdxType, - is_buy: bool - ): u64 { - let tie_breaker = get_tie_breaker(unique_priority_idx, is_buy); - let key = ActiveBidKey { price: price, tie_breaker }; - let value = - if (is_buy) { - self.buys.remove(&key) - } else { - self.sells.remove(&key) - }; - value.size - } - - public fun is_active_order( - self: &ActiveOrderBook, - price: u64, - unique_priority_idx: UniqueIdxType, - is_buy: bool - ): bool { - let tie_breaker = get_tie_breaker(unique_priority_idx, is_buy); - let key = ActiveBidKey { price: price, tie_breaker }; - if (is_buy) { - self.buys.contains(&key) - } else { - self.sells.contains(&key) - } - } - - /// Check if the order is a taker order - i.e. if it can be immediately matched with the order book fully or partially. - public fun is_taker_order( - self: &ActiveOrderBook, price: u64, is_buy: bool - ): bool { - if (is_buy) { - let best_ask_price = self.best_ask_price(); - best_ask_price.is_some() && price >= best_ask_price.destroy_some() - } else { - let best_bid_price = self.best_bid_price(); - best_bid_price.is_some() && price <= best_bid_price.destroy_some() - } - } - - fun single_match_with_current_active_order( - remaining_size: u64, - cur_key: ActiveBidKey, - cur_value: ActiveBidData, - orders: &mut BigOrderedMap - ): ActiveMatchedOrder { - let is_cur_match_fully_consumed = cur_value.size <= remaining_size; - - let matched_size_for_this_order = - if (is_cur_match_fully_consumed) { - cur_value.size - } else { - remaining_size - }; - - let result = - new_active_matched_order( - cur_value.order_id, - matched_size_for_this_order, // Matched size on the maker order - cur_value.size - matched_size_for_this_order // Remaining size on the maker order - ); - - if (is_cur_match_fully_consumed) { - orders.remove(&cur_key); - } else { - orders.borrow_mut(&cur_key).size -= matched_size_for_this_order; - }; - result - } - - fun get_single_match_for_buy_order( - self: &mut ActiveOrderBook, price: u64, size: u64 - ): ActiveMatchedOrder { - let (smallest_key, smallest_value) = self.sells.borrow_front(); - assert!(price >= smallest_key.price, EINTERNAL_INVARIANT_BROKEN); - single_match_with_current_active_order( - size, - smallest_key, - *smallest_value, - &mut self.sells - ) - } - - fun get_single_match_for_sell_order( - self: &mut ActiveOrderBook, price: u64, size: u64 - ): ActiveMatchedOrder { - let (largest_key, largest_value) = self.buys.borrow_back(); - assert!(price <= largest_key.price, EINTERNAL_INVARIANT_BROKEN); - single_match_with_current_active_order( - size, - largest_key, - *largest_value, - &mut self.buys - ) - } - - public fun get_single_match_result( - self: &mut ActiveOrderBook, - price: u64, - size: u64, - is_buy: bool - ): ActiveMatchedOrder { - if (is_buy) { - self.get_single_match_for_buy_order(price, size) - } else { - self.get_single_match_for_sell_order(price, size) - } - } - - /// Increase the size of the order in the orderbook without altering its position in the price-time priority. - public fun increase_order_size( - self: &mut ActiveOrderBook, - price: u64, - unique_priority_idx: UniqueIdxType, - size_delta: u64, - is_buy: bool - ) { - let tie_breaker = get_tie_breaker(unique_priority_idx, is_buy); - let key = ActiveBidKey { price, tie_breaker }; - if (is_buy) { - self.buys.borrow_mut(&key).size += size_delta; - } else { - self.sells.borrow_mut(&key).size += size_delta; - }; - } - - /// Decrease the size of the order in the order book without altering its position in the price-time priority. - public fun decrease_order_size( - self: &mut ActiveOrderBook, - price: u64, - unique_priority_idx: UniqueIdxType, - size_delta: u64, - is_buy: bool - ) { - let tie_breaker = get_tie_breaker(unique_priority_idx, is_buy); - let key = ActiveBidKey { price, tie_breaker }; - if (is_buy) { - self.buys.borrow_mut(&key).size -= size_delta; - } else { - self.sells.borrow_mut(&key).size -= size_delta; - }; - } - - public fun place_maker_order( - self: &mut ActiveOrderBook, - order_id: OrderIdType, - price: u64, - unique_priority_idx: UniqueIdxType, - size: u64, - is_buy: bool - ) { - let tie_breaker = get_tie_breaker(unique_priority_idx, is_buy); - let key = ActiveBidKey { price, tie_breaker }; - let value = ActiveBidData { order_id, size }; - // Assert that this is not a taker order - assert!(!self.is_taker_order(price, is_buy), EINVALID_MAKER_ORDER); - if (is_buy) { - self.buys.add(key, value); - } else { - self.sells.add(key, value); - }; - } - - #[test_only] - public fun destroy_active_order_book(self: ActiveOrderBook) { - let ActiveOrderBook::V1 { sells, buys } = self; - sells.destroy(|_v| {}); - buys.destroy(|_v| {}); - } - - #[test_only] - struct TestOrder has copy, drop { - account: address, - account_order_id: u64, - price: u64, - size: u64, - unique_idx: UniqueIdxType, - is_buy: bool - } - - #[test_only] - fun place_test_order(self: &mut ActiveOrderBook, order: TestOrder): - vector { - let result = vector::empty(); - let remaining_size = order.size; - while (remaining_size > 0) { - if (!self.is_taker_order(order.price, order.is_buy)) { - self.place_maker_order( - new_order_id_type(order.account, order.account_order_id), - order.price, - order.unique_idx, - order.size, - order.is_buy - ); - return result; - }; - let match_result = - self.get_single_match_result(order.price, remaining_size, order.is_buy); - remaining_size -= match_result.get_active_matched_size(); - result.push_back(match_result); - }; - result - } - - #[test] - // TODO (skedia) Add more comprehensive tests for the acive order book - fun test_active_order_book() { - let active_order_book = new_active_order_book(); - - assert!(active_order_book.best_bid_price().is_none()); - assert!(active_order_book.best_ask_price().is_none()); - - // $200 - 10000 - // -- - let match_result = - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 0, - price: 200, - size: 1000, - unique_idx: new_unique_idx_type(0), - is_buy: false - } - ); - assert!(match_result.is_empty()); - - // $200 - 10000 - // -- - // $100 - 1000 - let match_result = - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 1, - price: 100, - size: 1000, - unique_idx: new_unique_idx_type(1), - is_buy: true - } - ); - assert!(match_result.is_empty()); - - assert!(active_order_book.best_bid_price().destroy_some() == 100); - assert!(active_order_book.best_ask_price().destroy_some() == 200); - - // $200 - 10000 - // $150 - 100 - // -- - // $100 - 1000 - let match_result = - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 2, - price: 150, - size: 100, - unique_idx: new_unique_idx_type(2), - is_buy: false - } - ); - assert!(match_result.is_empty()); - - // $200 - 10000 - // $175 - 100 - // $150 - 100 - // -- - // $100 - 1000 - let match_result = - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 3, - price: 175, - size: 100, - unique_idx: new_unique_idx_type(3), - is_buy: false - } - ); - assert!(match_result.is_empty()); - - assert!(active_order_book.best_bid_price().destroy_some() == 100); - assert!(active_order_book.best_ask_price().destroy_some() == 150); - - // $200 - 10000 - // $175 - 100 - // $150 - 50 <-- match 50 units - // -- - // $100 - 1000 - let match_result = - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 4, - price: 160, - size: 50, - unique_idx: new_unique_idx_type(4), - is_buy: true - } - ); - assert!(match_result.length() == 1); - // TODO - seems like we have no match price in ActiveMatchResult any more - // we need to add it back, and assert? - // Maker ask order was partially filled 100 -> 50 - assert!( - match_result - == vector[ - new_active_matched_order( - new_order_id_type(@0xAA, 2), - 50, // matched size - 50 // remaining size - ) - ], - 7 - ); - active_order_book.destroy_active_order_book(); - } - - #[test] - fun test_get_impact_sell_price() { - let active_order_book = new_active_order_book(); - - // Add sell orders at different prices - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 1, - price: 100, - size: 50, - unique_idx: new_unique_idx_type(1), - is_buy: false - } - ); - - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 2, - price: 150, - size: 100, - unique_idx: new_unique_idx_type(2), - is_buy: false - } - ); - - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 3, - price: 200, - size: 150, - unique_idx: new_unique_idx_type(3), - is_buy: false - } - ); - - // Test impact price calculations - // Impact size 50 should give price of lowest order (100) - assert!(active_order_book.get_impact_ask_price(50).destroy_some() == 100, 1); - - // Impact size 100 should give weighted average of first two orders - // (50 * 100 + 50 * 150) / 100 = 125 - assert!(active_order_book.get_impact_ask_price(100).destroy_some() == 125, 2); - - // Impact size 200 should give weighted average of all orders - // (50 * 100 + 100 * 150 + 50 * 200) / 200 = 150 - assert!(active_order_book.get_impact_ask_price(200).destroy_some() == 150, 3); - - // Impact size larger than total available should still use all orders - // (50 * 100 + 100 * 150 + 150 * 200) / 300 = 166 - assert!(active_order_book.get_impact_ask_price(1000).destroy_some() == 166, 4); - - active_order_book.destroy_active_order_book(); - } - - #[test] - fun test_get_impact_bid_price() { - let active_order_book = new_active_order_book(); - - // Place test buy orders at different prices - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 1, - price: 200, - size: 50, - unique_idx: new_unique_idx_type(1), - is_buy: true - } - ); - - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 2, - price: 150, - size: 100, - unique_idx: new_unique_idx_type(2), - is_buy: true - } - ); - - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 3, - price: 100, - size: 150, - unique_idx: new_unique_idx_type(3), - is_buy: true - } - ); - - // Test impact price calculations - // Impact size 50 should give price of first order (200) - assert!(active_order_book.get_impact_bid_price(50).destroy_some() == 200, 1); - - // Impact size 100 should give weighted average of first two orders - // (50 * 200 + 50 * 150) / 100 = 175 - assert!(active_order_book.get_impact_bid_price(100).destroy_some() == 175, 2); - - // Impact size 200 should give weighted average of all orders - // (50 * 200 + 100 * 150 + 50 * 100) / 200 = 150 - assert!(active_order_book.get_impact_bid_price(200).destroy_some() == 150, 3); - - // Impact size larger than total available should still use all orders - // (50 * 200 + 100 * 150 + 150 * 100) / 300 = 133 - assert!(active_order_book.get_impact_bid_price(1000).destroy_some() == 133, 4); - - active_order_book.destroy_active_order_book(); - } - - #[test] - fun test_get_slippage_price() { - let active_order_book = new_active_order_book(); - - // Add sell orders at different prices - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 1, - price: 101, - size: 50, - unique_idx: new_unique_idx_type(1), - is_buy: false - } - ); - - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 2, - price: 102, - size: 100, - unique_idx: new_unique_idx_type(2), - is_buy: false - } - ); - - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 3, - price: 103, - size: 150, - unique_idx: new_unique_idx_type(3), - is_buy: false - } - ); - - // Add some buy orders - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 4, - price: 99, - size: 50, - unique_idx: new_unique_idx_type(4), - is_buy: true - } - ); - - active_order_book.place_test_order( - TestOrder { - account: @0xAA, - account_order_id: 5, - price: 98, - size: 100, - unique_idx: new_unique_idx_type(5), - is_buy: true - } - ); - - // Test slippage price calculations - assert!(active_order_book.get_mid_price().destroy_some() == 100); - // Slippage 10% for buy order should give price of mid price (100) + 10% = 110 - assert!(active_order_book.get_slippage_price(true, 1000).destroy_some() == 110); - assert!(active_order_book.get_slippage_price(true, 100).destroy_some() == 101); - assert!(active_order_book.get_slippage_price(true, 10).destroy_some() == 100); - - assert!(active_order_book.get_slippage_price(false, 1500).destroy_some() == 85); - assert!(active_order_book.get_slippage_price(false, 100).destroy_some() == 99); - assert!(active_order_book.get_slippage_price(false, 10).destroy_some() == 100); - assert!(active_order_book.get_slippage_price(false, 0).destroy_some() == 100); - - active_order_book.destroy_active_order_book(); - - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/order_book/order_book.move b/aptos-move/framework/aptos-experimental/sources/trading/order_book/order_book.move deleted file mode 100644 index 9382ec7f33d..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/order_book/order_book.move +++ /dev/null @@ -1,1339 +0,0 @@ -/// This module provides a core order book functionality for a trading system. On a high level, it has three major -/// components -/// 1. ActiveOrderBook: This is the main order book that keeps track of active orders and their states. The active order -/// book is backed by a BigOrderedMap, which is a data structure that allows for efficient insertion, deletion, and matching of the order -/// The orders are matched based on time-price priority. -/// 2. PendingOrderBookIndex: This keeps track of pending orders. The pending orders are those that are not active yet. Three -/// types of pending orders are supported. -/// - Price move up - Trigggered when the price moves above a certain price level -/// - Price move down - Triggered when the price moves below a certain price level -/// - Time based - Triggered when a certain time has passed -/// 3. Orders: This is a BigOrderMap of order id to order details. -/// -module aptos_experimental::order_book { - use std::vector; - use std::error; - use std::option::{Self, Option}; - use aptos_framework::big_ordered_map::BigOrderedMap; - - use aptos_experimental::order_book_types::{ - OrderIdType, - OrderWithState, - generate_unique_idx_fifo_tiebraker, - new_order_id_type, - new_order, - new_order_with_state, - new_single_order_match, - new_default_big_ordered_map, - TriggerCondition, - UniqueIdxType, - SingleOrderMatch, - Order - }; - use aptos_experimental::active_order_book::{ActiveOrderBook, new_active_order_book}; - use aptos_experimental::pending_order_book_index::{ - PendingOrderBookIndex, - new_pending_order_book_index - }; - #[test_only] - use aptos_experimental::order_book_types::tp_trigger_condition; - - const U256_MAX: u256 = - 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - - const EORDER_ALREADY_EXISTS: u64 = 1; - const EPOST_ONLY_FILLED: u64 = 2; - const EORDER_NOT_FOUND: u64 = 4; - const EINVALID_INACTIVE_ORDER_STATE: u64 = 5; - const EINVALID_ADD_SIZE_TO_ORDER: u64 = 6; - const E_NOT_ACTIVE_ORDER: u64 = 7; - - struct OrderRequest has copy, drop { - account: address, - account_order_id: u64, - unique_priority_idx: Option, - price: u64, - orig_size: u64, - remaining_size: u64, - is_buy: bool, - trigger_condition: Option, - metadata: M - } - - enum OrderBook has store { - V1 { - orders: BigOrderedMap>, - active_orders: ActiveOrderBook, - pending_orders: PendingOrderBookIndex - } - } - - enum OrderType has store, drop, copy { - GoodTilCancelled, - PostOnly, - FillOrKill - } - - public fun new_order_request( - account: address, - account_order_id: u64, - unique_priority_idx: Option, - price: u64, - orig_size: u64, - remaining_size: u64, - is_buy: bool, - trigger_condition: Option, - metadata: M - ): OrderRequest { - OrderRequest { - account, - account_order_id, - unique_priority_idx, - price, - orig_size, - remaining_size, - is_buy, - trigger_condition, - metadata - } - } - - public fun new_order_book(): OrderBook { - OrderBook::V1 { - orders: new_default_big_ordered_map(), - active_orders: new_active_order_book(), - pending_orders: new_pending_order_book_index() - } - } - - - - /// Cancels an order from the order book. If the order is active, it is removed from the active order book else - /// it is removed from the pending order book. The API doesn't abort if the order is not found in the order book - - /// this is a TODO for now. - public fun cancel_order( - self: &mut OrderBook, account: address, account_order_id: u64 - ): Option> { - let order_id = new_order_id_type(account, account_order_id); - assert!(self.orders.contains(&order_id), EORDER_NOT_FOUND); - let order_with_state = self.orders.remove(&order_id); - let (order, is_active) = order_with_state.destroy_order_from_state(); - if (is_active) { - let (_, unique_priority_idx, bid_price, _orig_size, _size, is_buy, _, _) = - order.destroy_order(); - self.active_orders.cancel_active_order(bid_price, unique_priority_idx, is_buy); - } else { - let ( - _, - unique_priority_idx, - _bid_price, - _orig_size, - _size, - is_buy, - trigger_condition, - _ - ) = order.destroy_order(); - self.pending_orders.cancel_pending_order( - trigger_condition.destroy_some(), unique_priority_idx, is_buy - ); - }; - return option::some(order) - } - - /// Checks if the order is a taker order i.e., matched immediatedly with the active order book. - public fun is_taker_order( - self: &OrderBook, - price: u64, - is_buy: bool, - trigger_condition: Option - ): bool { - if (trigger_condition.is_some()) { - return false; - }; - return self.active_orders.is_taker_order(price, is_buy) - } - - /// Places a maker order to the order book. If the order is a pending order, it is added to the pending order book - /// else it is added to the active order book. The API aborts if its not a maker order or if the order already exists - public fun place_maker_order( - self: &mut OrderBook, order_req: OrderRequest - ) { - if (order_req.trigger_condition.is_some()) { - return self.place_pending_maker_order(order_req); - }; - - let order_id = new_order_id_type(order_req.account, order_req.account_order_id); - let unique_priority_idx = - if (order_req.unique_priority_idx.is_some()) { - order_req.unique_priority_idx.destroy_some() - } else { - generate_unique_idx_fifo_tiebraker() - }; - - assert!( - !self.orders.contains(&order_id), - error::invalid_argument(EORDER_ALREADY_EXISTS) - ); - - let order = - new_order( - order_id, - unique_priority_idx, - order_req.price, - order_req.orig_size, - order_req.remaining_size, - order_req.is_buy, - order_req.trigger_condition, - order_req.metadata - ); - self.orders.add(order_id, new_order_with_state(order, true)); - self.active_orders.place_maker_order( - order_id, - order_req.price, - unique_priority_idx, - order_req.remaining_size, - order_req.is_buy - ); - } - - /// Reinserts a maker order to the order book. This is used when the order is removed from the order book - /// but the clearinghouse fails to settle all or part of the order. If the order doesn't exist in the order book, - /// it is added to the order book, if it exists, it's size is updated. - public fun reinsert_maker_order( - self: &mut OrderBook, order_req: OrderRequest - ) { - assert!(order_req.trigger_condition.is_none(), E_NOT_ACTIVE_ORDER); - let order_id = new_order_id_type(order_req.account, order_req.account_order_id); - if (!self.orders.contains(&order_id)) { - return self.place_maker_order(order_req); - }; - let order_with_state = self.orders.remove(&order_id); - order_with_state.increase_remaining_size(order_req.remaining_size); - self.orders.add(order_id, order_with_state); - self.active_orders.increase_order_size( - order_req.price, - order_req.unique_priority_idx.destroy_some(), - order_req.remaining_size, - order_req.is_buy - ); - } - - fun place_pending_maker_order( - self: &mut OrderBook, order_req: OrderRequest - ) { - let order_id = new_order_id_type(order_req.account, order_req.account_order_id); - let unique_priority_idx = - if (order_req.unique_priority_idx.is_some()) { - order_req.unique_priority_idx.destroy_some() - } else { - generate_unique_idx_fifo_tiebraker() - }; - let order = - new_order( - order_id, - unique_priority_idx, - order_req.price, - order_req.orig_size, - order_req.remaining_size, - order_req.is_buy, - order_req.trigger_condition, - order_req.metadata - ); - - self.orders.add(order_id, new_order_with_state(order, false)); - - self.pending_orders.place_pending_maker_order( - order_id, - order_req.trigger_condition.destroy_some(), - unique_priority_idx, - order_req.is_buy - ); - } - - /// Returns a single match for a taker order. It is responsibility of the caller to first call the `is_taker_order` - /// API to ensure that the order is a taker order before calling this API, otherwise it will abort. - public fun get_single_match_for_taker( - self: &mut OrderBook, - price: u64, - size: u64, - is_buy: bool - ): SingleOrderMatch { - let result = self.active_orders.get_single_match_result(price, size, is_buy); - let (order_id, matched_size, remaining_size) = - result.destroy_active_matched_order(); - let order_with_state = self.orders.remove(&order_id); - order_with_state.set_remaining_size(remaining_size); - if (remaining_size > 0) { - self.orders.add(order_id, order_with_state); - }; - let (order, is_active) = order_with_state.destroy_order_from_state(); - assert!(is_active, EINVALID_INACTIVE_ORDER_STATE); - new_single_order_match(order, matched_size) - } - - /// Decrease the size of the order by the given size delta. The API aborts if the order is not found in the order book or - /// if the size delta is greater than or equal to the remaining size of the order. Please note that the API will abort and - /// not cancel the order if the size delta is equal to the remaining size of the order, to avoid unintended - /// cancellation of the order. Please use the `cancel_order` API to cancel the order. - public fun decrease_order_size( - self: &mut OrderBook, account: address, account_order_id: u64, size_delta: u64 - ) { - let order_id = new_order_id_type(account, account_order_id); - assert!(self.orders.contains(&order_id), EORDER_NOT_FOUND); - let order_with_state = self.orders.remove(&order_id); - order_with_state.decrease_remaining_size(size_delta); - if (order_with_state.is_active_order()) { - let order = order_with_state.get_order_from_state(); - self.active_orders.decrease_order_size( - order.get_price(), - order_with_state.get_unique_priority_idx_from_state(), - size_delta, - order.is_bid() - ); - }; - self.orders.add(order_id, order_with_state); - } - - public fun is_active_order( - self: &OrderBook, account: address, account_order_id: u64 - ): bool { - let order_id = new_order_id_type(account, account_order_id); - if (!self.orders.contains(&order_id)) { - return false; - }; - self.orders.borrow(&order_id).is_active_order() - } - - public fun get_order( - self: &OrderBook, account: address, account_order_id: u64 - ): Option> { - let order_id = new_order_id_type(account, account_order_id); - if (!self.orders.contains(&order_id)) { - return option::none(); - }; - option::some(*self.orders.borrow(&order_id)) - } - - public fun get_remaining_size( - self: &OrderBook, account: address, account_order_id: u64 - ): u64 { - let order_id = new_order_id_type(account, account_order_id); - if (!self.orders.contains(&order_id)) { - return 0; - }; - self.orders.borrow(&order_id).get_remaining_size_from_state() - } - - /// Removes and returns the orders that are ready to be executed based on the current price. - public fun take_ready_price_based_orders( - self: &mut OrderBook, current_price: u64 - ): vector> { - let self_orders = &mut self.orders; - let order_ids = self.pending_orders.take_ready_price_based_orders(current_price); - let orders = vector::empty(); - - order_ids.for_each(|order_id| { - let order_with_state = self_orders.remove(&order_id); - let (order, _) = order_with_state.destroy_order_from_state(); - orders.push_back(order); - }); - orders - } - - public fun best_bid_price(self: &OrderBook): Option { - self.active_orders.best_bid_price() - } - - public fun best_ask_price(self: &OrderBook): Option { - self.active_orders.best_ask_price() - } - - public fun get_slippage_price( - self: &OrderBook, is_buy: bool, slippage_pct: u64 - ): Option { - self.active_orders.get_slippage_price(is_buy, slippage_pct) - } - - /// Removes and returns the orders that are ready to be executed based on the time condition. - public fun take_ready_time_based_orders( - self: &mut OrderBook - ): vector> { - let self_orders = &mut self.orders; - let order_ids = self.pending_orders.take_time_time_based_orders(); - let orders = vector::empty(); - - order_ids.for_each(|order_id| { - let order_with_state = self_orders.remove(&order_id); - let (order, _) = order_with_state.destroy_order_from_state(); - orders.push_back(order); - }); - orders - } - - // ============================= test_only APIs ==================================== - - #[test_only] - public fun destroy_order_book(self: OrderBook) { - let OrderBook::V1 { orders, active_orders, pending_orders } = self; - orders.destroy(|_v| {}); - active_orders.destroy_active_order_book(); - pending_orders.destroy_pending_order_book_index(); - } - - #[test_only] - public fun get_unique_priority_idx( - self: &OrderBook, account: address, account_order_id: u64 - ): Option { - let order_id = new_order_id_type(account, account_order_id); - if (!self.orders.contains(&order_id)) { - return option::none(); - }; - option::some(self.orders.borrow(&order_id).get_unique_priority_idx_from_state()) - } - - public fun place_order_and_get_matches( - self: &mut OrderBook, order_req: OrderRequest - ): vector> { - let match_results = vector::empty(); - let remainig_size = order_req.remaining_size; - while (remainig_size > 0) { - if (!self.is_taker_order(order_req.price, order_req.is_buy, order_req.trigger_condition)) { - self.place_maker_order( - OrderRequest { - account: order_req.account, - account_order_id: order_req.account_order_id, - unique_priority_idx: option::none(), - price: order_req.price, - orig_size: order_req.orig_size, - remaining_size: remainig_size, - is_buy: order_req.is_buy, - trigger_condition: order_req.trigger_condition, - metadata: order_req.metadata - } - ); - return match_results; - }; - let match_result = - self.get_single_match_for_taker( - order_req.price, remainig_size, order_req.is_buy - ); - let matched_size = match_result.get_matched_size(); - match_results.push_back(match_result); - remainig_size -= matched_size; - }; - return match_results - } - - #[test_only] - public fun update_order_and_get_matches( - self: &mut OrderBook, order_req: OrderRequest - ): vector> { - let unique_priority_idx = - self.get_unique_priority_idx(order_req.account, order_req.account_order_id); - assert!(unique_priority_idx.is_some(), EORDER_NOT_FOUND); - let unique_priority_idx = unique_priority_idx.destroy_some(); - self.cancel_order(order_req.account, order_req.account_order_id); - let order_req = OrderRequest { - account: order_req.account, - account_order_id: order_req.account_order_id, - unique_priority_idx: option::some(unique_priority_idx), - price: order_req.price, - orig_size: order_req.orig_size, - remaining_size: order_req.remaining_size, - is_buy: order_req.is_buy, - trigger_condition: order_req.trigger_condition, - metadata: order_req.metadata - }; - self.place_order_and_get_matches(order_req) - } - - #[test_only] - public fun trigger_pending_orders( - self: &mut OrderBook, oracle_price: u64 - ): vector> { - let ready_orders = self.take_ready_price_based_orders(oracle_price); - let all_matches = vector::empty(); - let i = 0; - while (i < ready_orders.length()) { - let order = ready_orders[i]; - let ( - order_id, - unique_priority_idx, - price, - orig_size, - remaining_size, - is_buy, - _, - metadata - ) = order.destroy_order(); - let (account, account_order_id) = order_id.destroy_order_id_type(); - let order_req = OrderRequest { - account, - account_order_id, - unique_priority_idx: option::some(unique_priority_idx), - price, - orig_size, - remaining_size, - is_buy, - trigger_condition: option::none(), - metadata - }; - let match_results = self.place_order_and_get_matches(order_req); - all_matches.append(match_results); - i = i + 1; - }; - all_matches - } - - #[test_only] - public fun total_matched_size( - match_results: &vector> - ): u64 { - let total_matched_size = 0; - let i = 0; - while (i < match_results.length()) { - total_matched_size = total_matched_size - + match_results[i].get_matched_size(); - i = i + 1; - }; - total_matched_size - } - - struct TestMetadata has store, copy, drop {} - - // ============================= Tests ==================================== - - #[test] - fun test_good_til_cancelled_order() { - let order_book = new_order_book(); - - // Place a GTC sell order - let order_req = OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - }; - let match_results = order_book.place_order_and_get_matches(order_req); - assert!(match_results.is_empty()); // No matches for first order - - // Verify order exists and is active - let order_id = new_order_id_type(@0xAA, 1); - let order_state = *order_book.orders.borrow(&order_id); - let (order, is_active) = order_state.destroy_order_from_state(); - let (_order_id, _unique_priority_idx, price, orig_size, size, is_buy, _, _) = - order.destroy_order(); - assert!(is_active == true); - assert!(price == 100); - assert!(orig_size == 1000); - assert!(size == 1000); - assert!(is_buy == false); - - // Place a matching buy order for partial fill - let match_results = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 400, - remaining_size: 400, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - // // Verify taker match details - assert!(total_matched_size(&match_results) == 400); - assert!(order_book.get_remaining_size(@0xBB, 1) == 0); - - // Verify maker match details - assert!(match_results.length() == 1); // One match result - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 400); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 600); // Maker order partially filled - - // Verify original order still exists but with reduced size - let order_state = *order_book.orders.borrow(&order_id); - let (order, is_active) = order_state.destroy_order_from_state(); - let (_, _unique_priority_idx, price, orig_size, size, is_buy, _, _) = - order.destroy_order(); - assert!(is_active == true); - assert!(price == 100); - assert!(orig_size == 1000); - assert!(size == 600); - assert!(is_buy == false); - - // Cancel the remaining order - order_book.cancel_order(@0xAA, 1); - - // Verify order no longer exists - assert!(order_book.get_remaining_size(@0xAA, 1) == 0); - - // Since we cannot drop the order book, we move it to a test struct - order_book.destroy_order_book(); - } - - #[test] - fun test_update_buy_order() { - let order_book = new_order_book(); - - // Place a GTC sell order - let match_results = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 101, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_results.is_empty()); - - let match_results = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 500, - remaining_size: 500, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_results.is_empty()); - - // Update the order so that it would match immediately - let match_results = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 2, - unique_priority_idx: option::none(), - price: 101, - orig_size: 500, - remaining_size: 500, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - - // Verify taker (buy order) was fully filled - assert!(total_matched_size(&match_results) == 500); - assert!(order_book.get_remaining_size(@0xBB, 2) == 0); - - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 500); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 500); // Partial fill - - order_book.destroy_order_book(); - } - - #[test] - fun test_update_sell_order() { - let order_book = new_order_book(); - - // Place a GTC sell order - let order_req = OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - }; - let match_result = order_book.place_order_and_get_matches(order_req); - assert!(match_result.is_empty()); // No matches for first order - - // Place a buy order at lower price - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 99, - orig_size: 500, - remaining_size: 500, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_result.is_empty()); - - // Update sell order to match with buy order - let match_results = - order_book.update_order_and_get_matches( - OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 99, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - - // Verify taker (sell order) was partially filled - assert!(total_matched_size(&match_results) == 500); - - assert!(match_results.length() == 1); // One match result - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xBB, 1)); - assert!(matched_size == 500); - assert!(order.get_orig_size() == 500); - assert!(order.get_remaining_size() == 0); // Fully filled - - order_book.destroy_order_book(); - } - - #[test] - #[expected_failure(abort_code = EORDER_NOT_FOUND)] - fun test_update_order_not_found() { - let order_book = new_order_book(); - - // Place a GTC sell order - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 101, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_result.is_empty()); // No matches for first order - - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 500, - remaining_size: 500, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_result.is_empty()); - - // Try to update non existant order - let match_result = - order_book.update_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 3, - unique_priority_idx: option::none(), - price: 100, - orig_size: 500, - remaining_size: 500, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - // This should fail with EORDER_NOT_FOUND - assert!(match_result.is_empty()); - order_book.destroy_order_book(); - } - - #[test] - fun test_good_til_cancelled_partial_fill() { - let order_book = new_order_book(); - - // Place a GTC sell order for 1000 units at price 100 - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_result.is_empty()); // No matches for first order - - // Place a smaller buy order (400 units) at the same price - let match_results = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 400, - remaining_size: 400, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - - // Verify taker (buy order) was fully filled - assert!(total_matched_size(&match_results) == 400); - - // Verify maker (sell order) was partially filled - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 400); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 600); // Partial fill - - // Place another buy order for 300 units - let match_results = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 2, - unique_priority_idx: option::none(), - price: 100, - orig_size: 300, - remaining_size: 300, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_results.length() == 1); // Should match with the sell order - - // Verify second taker was fully filled - assert!(total_matched_size(&match_results) == 300); - - // Verify original maker was partially filled again - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 300); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 300); // Still partial as 300 units remain - - // Original sell order should still exist with 300 units remaining - let order_id = new_order_id_type(@0xAA, 1); - let order_state = *order_book.orders.borrow(&order_id); - let (order, is_active) = order_state.destroy_order_from_state(); - let (_order_id, _unique_priority_idx, price, orig_size, size, is_buy, _, _) = - order.destroy_order(); - assert!(is_active == true); - assert!(price == 100); - assert!(orig_size == 1000); - assert!(size == 300); // 1000 - 400 - 300 = 300 remaining - assert!(is_buy == false); - - order_book.destroy_order_book(); - } - - #[test] - fun test_good_til_cancelled_taker_partial_fill() { - let order_book = new_order_book(); - - // Place a GTC sell order for 500 units at price 100 - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 500, - remaining_size: 500, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_result.is_empty()); // No matches for first order - - // Place a larger buy order (800 units) at the same price - // Should partially fill against the sell order and remain in book - let match_results = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 800, - remaining_size: 800, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - - // Verify taker (buy order) was partially filled - assert!(total_matched_size(&match_results) == 500); - - // Verify maker (sell order) was fully filled - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 500); - assert!(order.get_orig_size() == 500); - assert!(order.get_remaining_size() == 0); // Fully filled - - // Verify original sell order no longer exists (fully filled) - let order_id = new_order_id_type(@0xAA, 1); - assert!(!order_book.orders.contains(&order_id)); - - // Verify buy order still exists with remaining size - let order_id = new_order_id_type(@0xBB, 1); - let order_state = *order_book.orders.borrow(&order_id); - let (order, is_active) = order_state.destroy_order_from_state(); - let (_order_id, _unique_priority_idx, price, orig_size, size, is_buy, _, _) = - order.destroy_order(); - assert!(is_active == true); - assert!(price == 100); - assert!(orig_size == 800); - assert!(size == 300); // 800 - 500 = 300 remaining - assert!(is_buy == true); - - order_book.destroy_order_book(); - } - - #[test] - fun test_TP_order() { - let order_book = new_order_book(); - - // Place a GTC sell order for 1000 units at price 100 - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_result.is_empty()); // No matches for first order - - assert!(order_book.trigger_pending_orders(100).is_empty()); - - // Place a smaller buy order (400 units) at the same price - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 400, - remaining_size: 400, - is_buy: true, - trigger_condition: option::some(tp_trigger_condition(90)), - metadata: TestMetadata {} - } - ); - // Even if the price of 100 can be matched in the order book the trigger condition 90 should not trigger - // the matching - assert!(match_result.is_empty()); - assert!( - order_book.pending_orders.get_price_move_down_index().keys().length() == 1 - ); - - // Trigger the pending orders with a price of 90 - let match_results = order_book.trigger_pending_orders(90); - - // Verify taker (buy order) was fully filled - assert!(total_matched_size(&match_results) == 400); - - // Verify maker (sell order) was partially filled - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 400); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 600); // Partial fill - - // Place another buy order for 300 units - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 2, - unique_priority_idx: option::none(), - price: 100, - orig_size: 300, - remaining_size: 300, - is_buy: true, - trigger_condition: option::some(tp_trigger_condition(80)), - metadata: TestMetadata {} - } - ); - - assert!(match_result.is_empty()); - assert!( - order_book.pending_orders.get_price_move_down_index().keys().length() == 1 - ); - - // Oracle price moves up to 95, this should not trigger any order - let match_results = order_book.trigger_pending_orders(95); - assert!(match_results.length() == 0); - - // Move the oracle price down to 80, this should trigger the order - let match_results = order_book.trigger_pending_orders(80); - // Verify second taker was fully filled - assert!(total_matched_size(&match_results) == 300); - - // Verify original maker was partially filled again - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 300); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 300); // Still partial as 300 units remain - - // Original sell order should still exist with 300 units remaining - let order_id = new_order_id_type(@0xAA, 1); - let order_state = *order_book.orders.borrow(&order_id); - let (order, is_active) = order_state.destroy_order_from_state(); - let (_order_id, _unique_priority_idx, price, orig_size, size, is_buy, _, _) = - order.destroy_order(); - assert!(is_active == true); - assert!(price == 100); - assert!(orig_size == 1000); - assert!(size == 300); // 1000 - 400 - 300 = 300 remaining - assert!(is_buy == false); - - order_book.destroy_order_book(); - } - - #[test] - fun test_SL_order() { - let order_book = new_order_book(); - - // Place a GTC sell order for 1000 units at price 100 - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - } - ); - assert!(match_result.is_empty()); // No matches for first order - - assert!(order_book.trigger_pending_orders(100).is_empty()); - - // Place a smaller buy order (400 units) at the same price - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 400, - remaining_size: 400, - is_buy: false, - trigger_condition: option::some(tp_trigger_condition(110)), - metadata: TestMetadata {} - } - ); - // Even if the price of 100 can be matched in the order book the trigger condition 110 should not trigger - // the matching - assert!(match_result.is_empty()); - assert!( - order_book.pending_orders.get_price_move_up_index().keys().length() == 1 - ); - - // Trigger the pending orders with a price of 110 - let match_results = order_book.trigger_pending_orders(110); - assert!(match_results.length() == 1); - - // Verify taker (buy order) was fully filled - assert!(total_matched_size(&match_results) == 400); - - // Verify maker (sell order) was partially filled - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 400); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 600); // Partial fill - - // Place another buy order for 300 units - let match_result = - order_book.place_order_and_get_matches( - OrderRequest { - account: @0xBB, - account_order_id: 2, - unique_priority_idx: option::none(), - price: 100, - orig_size: 300, - remaining_size: 300, - is_buy: false, - trigger_condition: option::some(tp_trigger_condition(120)), - metadata: TestMetadata {} - } - ); - - assert!(match_result.is_empty()); - assert!( - order_book.pending_orders.get_price_move_up_index().keys().length() == 1 - ); - - // Oracle price moves down to 100, this should not trigger any order - let match_results = order_book.trigger_pending_orders(100); - assert!(match_results.is_empty()); - - // Move the oracle price up to 120, this should trigger the order - let match_results = order_book.trigger_pending_orders(120); - - // Verify second taker was fully filled - assert!(total_matched_size(&match_results) == 300); - - // Verify original maker was partially filled again - assert!(match_results.length() == 1); - let maker_match = match_results[0]; - let (order, matched_size) = maker_match.destroy_single_order_match(); - assert!(order.get_order_id() == new_order_id_type(@0xAA, 1)); - assert!(matched_size == 300); - assert!(order.get_orig_size() == 1000); - assert!(order.get_remaining_size() == 300); // Still partial as 300 units remain - - // Original sell order should still exist with 300 units remaining - let order_id = new_order_id_type(@0xAA, 1); - let order_state = *order_book.orders.borrow(&order_id); - let (order, is_active) = order_state.destroy_order_from_state(); - let (_order_id, _unique_priority_idx, price, orig_size, size, is_buy, _, _) = - order.destroy_order(); - assert!(is_active == true); - assert!(price == 100); - assert!(orig_size == 1000); - assert!(size == 300); // 1000 - 400 - 300 = 300 remaining - assert!(is_buy == true); - order_book.destroy_order_book(); - } - - #[test] - fun test_maker_order_reinsert_already_exists() { - let order_book = new_order_book(); - - // Place a GTC sell order - let order_req = OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - }; - order_book.place_maker_order(order_req); - assert!(order_book.get_remaining_size(@0xAA, 1) == 1000); - - // Taker order - let order_req = OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 100, - remaining_size: 100, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - }; - - let match_results = order_book.place_order_and_get_matches(order_req); - assert!(total_matched_size(&match_results) == 100); - - let (matched_order, _) = match_results[0].destroy_single_order_match(); - let ( - _order_id, - unique_idx, - price, - orig_size, - _remaining_size, - is_buy, - _trigger_condition, - metadata - ) = matched_order.destroy_order(); - // Assume half of the order was matched and remaining 50 size is reinserted back to the order book - let order_req = OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::some(unique_idx), - price, - orig_size, - remaining_size: 50, - is_buy, - trigger_condition: option::none(), - metadata - }; - order_book.reinsert_maker_order(order_req); - // Verify order was reinserted with updated size - assert!(order_book.get_remaining_size(@0xAA, 1) == 950); - order_book.destroy_order_book(); - } - - #[test] - fun test_maker_order_reinsert_not_exists() { - let order_book = new_order_book(); - - // Place a GTC sell order - let order_req = OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - }; - order_book.place_maker_order(order_req); - assert!(order_book.get_remaining_size(@0xAA, 1) == 1000); - - // Taker order - let order_req = OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: true, - trigger_condition: option::none(), - metadata: TestMetadata {} - }; - - let match_results = order_book.place_order_and_get_matches(order_req); - assert!(total_matched_size(&match_results) == 1000); - - let (matched_order, _) = match_results[0].destroy_single_order_match(); - let ( - _order_id, - unique_idx, - price, - orig_size, - _remaining_size, - is_buy, - _trigger_condition, - metadata - ) = matched_order.destroy_order(); - // Assume half of the order was matched and remaining 50 size is reinserted back to the order book - let order_req = OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::some(unique_idx), - price, - orig_size, - remaining_size: 500, - is_buy, - trigger_condition: option::none(), - metadata - }; - order_book.reinsert_maker_order(order_req); - // Verify order was reinserted with updated size - assert!(order_book.get_remaining_size(@0xAA, 1) == 500); - order_book.destroy_order_book(); - } - - #[test] - fun test_decrease_order_size() { - let order_book = new_order_book(); - - // Place an active order - let order_req = OrderRequest { - account: @0xAA, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::none(), - metadata: TestMetadata {} - }; - order_book.place_maker_order(order_req); - assert!(order_book.get_remaining_size(@0xAA, 1) == 1000); - - order_book.decrease_order_size(@0xAA, 1, 700); - // Verify order was decreased with updated size - assert!(order_book.get_remaining_size(@0xAA, 1) == 300); - - let order_req = OrderRequest { - account: @0xBB, - account_order_id: 1, - unique_priority_idx: option::none(), - price: 100, - orig_size: 1000, - remaining_size: 1000, - is_buy: false, - trigger_condition: option::some(tp_trigger_condition(90)), - metadata: TestMetadata {} - }; - order_book.place_maker_order(order_req); - assert!(order_book.get_remaining_size(@0xBB, 1) == 1000); - order_book.decrease_order_size(@0xBB, 1, 600); - // Verify order was decreased with updated size - assert!(order_book.get_remaining_size(@0xBB, 1) == 400); - - order_book.destroy_order_book(); - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/order_book/order_book_types.move b/aptos-move/framework/aptos-experimental/sources/trading/order_book/order_book_types.move deleted file mode 100644 index d3cccedfac7..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/order_book/order_book_types.move +++ /dev/null @@ -1,310 +0,0 @@ -/// (work in progress) -module aptos_experimental::order_book_types { - use std::option; - use std::option::Option; - use aptos_std::bcs; - use aptos_std::from_bcs; - use aptos_framework::transaction_context; - use aptos_framework::big_ordered_map::{Self, BigOrderedMap}; - friend aptos_experimental::active_order_book; - friend aptos_experimental::order_book; - friend aptos_experimental::pending_order_book_index; - - const U256_MAX: u256 = - 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; - - const BIG_MAP_INNER_DEGREE: u16 = 64; - const BIG_MAP_LEAF_DEGREE: u16 = 32; - - const EORDER_ALREADY_EXISTS: u64 = 1; - const EINVALID_TRIGGER_CONDITION: u64 = 2; - const INVALID_MATCH_RESULT: u64 = 3; - const EINVALID_ORDER_SIZE_DECREASE: u64 = 4; - - const SLIPPAGE_PCT_PRECISION: u64 = 100; // 100 = 1% - - // to replace types: - struct OrderIdType has store, copy, drop { - account: address, - account_order_id: u64 - } - - struct UniqueIdxType has store, copy, drop { - idx: u256 - } - - struct ActiveMatchedOrder has copy, drop { - order_id: OrderIdType, - matched_size: u64, - /// Remaining size of the maker order - remaining_size: u64 - } - - struct SingleOrderMatch has drop, copy { - order: Order, - matched_size: u64 - } - - struct Order has store, copy, drop { - order_id: OrderIdType, - unique_priority_idx: UniqueIdxType, - price: u64, - orig_size: u64, - remaining_size: u64, - is_bid: bool, - trigger_condition: Option, - metadata: M - } - - enum TriggerCondition has store, drop, copy { - TakeProfit(u64), - StopLoss(u64), - TimeBased(u64) - } - - struct OrderWithState has store, drop, copy { - order: Order, - is_active: bool // i.e. where to find it. - } - - public(friend) fun new_default_big_ordered_map(): BigOrderedMap { - big_ordered_map::new_with_config( - BIG_MAP_INNER_DEGREE, - BIG_MAP_LEAF_DEGREE, - true - ) - } - - public fun get_slippage_pct_precision(): u64 { - SLIPPAGE_PCT_PRECISION - } - - public fun new_time_based_trigger_condition(time: u64): TriggerCondition { - TriggerCondition::TimeBased(time) - } - - public fun new_order_id_type(account: address, account_order_id: u64): OrderIdType { - OrderIdType { account, account_order_id } - } - - public fun generate_unique_idx_fifo_tiebraker(): UniqueIdxType { - // TODO change from random to monothonically increasing value - new_unique_idx_type( - from_bcs::to_u256( - bcs::to_bytes(&transaction_context::generate_auid_address()) - ) - ) - } - - public fun new_unique_idx_type(idx: u256): UniqueIdxType { - UniqueIdxType { idx } - } - - public fun descending_idx(self: &UniqueIdxType): UniqueIdxType { - UniqueIdxType { idx: U256_MAX - self.idx } - } - - public fun new_active_matched_order( - order_id: OrderIdType, matched_size: u64, remaining_size: u64 - ): ActiveMatchedOrder { - ActiveMatchedOrder { order_id, matched_size, remaining_size } - } - - public fun destroy_active_matched_order(self: ActiveMatchedOrder): (OrderIdType, u64, u64) { - (self.order_id, self.matched_size, self.remaining_size) - } - - public fun new_order( - order_id: OrderIdType, - unique_priority_idx: UniqueIdxType, - price: u64, - orig_size: u64, - size: u64, - is_buy: bool, - trigger_condition: Option, - metadata: M - ): Order { - Order { - order_id, - unique_priority_idx, - price, - orig_size, - remaining_size: size, - is_bid: is_buy, - trigger_condition, - metadata - } - } - - public fun new_single_order_match( - order: Order, matched_size: u64 - ): SingleOrderMatch { - SingleOrderMatch { order, matched_size } - } - - public fun get_active_matched_size(self: &ActiveMatchedOrder): u64 { - self.matched_size - } - - public fun get_matched_size( - self: &SingleOrderMatch - ): u64 { - self.matched_size - } - - public fun new_order_with_state( - order: Order, is_active: bool - ): OrderWithState { - OrderWithState { order, is_active } - } - - public fun tp_trigger_condition(take_profit: u64): TriggerCondition { - TriggerCondition::TakeProfit(take_profit) - } - - public fun sl_trigger_condition(stop_loss: u64): TriggerCondition { - TriggerCondition::StopLoss(stop_loss) - } - - // Returns the price move down index and price move up index for a particular trigger condition - public fun index(self: &TriggerCondition, is_buy: bool): - (Option, Option, Option) { - match(self) { - TriggerCondition::TakeProfit(tp) => { - if (is_buy) { - (option::some(*tp), option::none(), option::none()) - } else { - (option::none(), option::some(*tp), option::none()) - } - } - TriggerCondition::StopLoss(sl) => { - if (is_buy) { - (option::none(), option::some(*sl), option::none()) - } else { - (option::some(*sl), option::none(), option::none()) - } - } - TriggerCondition::TimeBased(time) => { - (option::none(), option::none(), option::some(*time)) - } - } - } - - public fun get_order_from_state( - self: &OrderWithState - ): &Order { - &self.order - } - - public fun get_metadata_from_state( - self: &OrderWithState - ): M { - self.order.metadata - } - - public fun get_order_id(self: &Order): OrderIdType { - self.order_id - } - - public fun get_unique_priority_idx(self: &Order): UniqueIdxType { - self.unique_priority_idx - } - - public fun get_metadata_from_order(self: &Order): M { - self.metadata - } - - public fun get_trigger_condition_from_order( - self: &Order - ): Option { - self.trigger_condition - } - - public fun increase_remaining_size( - self: &mut OrderWithState, size: u64 - ) { - self.order.remaining_size += size; - } - - public fun decrease_remaining_size( - self: &mut OrderWithState, size: u64 - ) { - assert!(self.order.remaining_size > size, EINVALID_ORDER_SIZE_DECREASE); - self.order.remaining_size -= size; - } - - public fun set_remaining_size( - self: &mut OrderWithState, remaining_size: u64 - ) { - self.order.remaining_size = remaining_size; - } - - public fun get_remaining_size_from_state( - self: &OrderWithState - ): u64 { - self.order.remaining_size - } - - public fun get_unique_priority_idx_from_state( - self: &OrderWithState - ): UniqueIdxType { - self.order.unique_priority_idx - } - - public fun get_remaining_size(self: &Order): u64 { - self.remaining_size - } - - public fun get_orig_size(self: &Order): u64 { - self.orig_size - } - - public fun destroy_order_from_state( - self: OrderWithState - ): (Order, bool) { - (self.order, self.is_active) - } - - public fun destroy_active_match_order(self: ActiveMatchedOrder): (OrderIdType, u64, u64) { - (self.order_id, self.matched_size, self.remaining_size) - } - - public fun destroy_order( - self: Order - ): (OrderIdType, UniqueIdxType, u64, u64, u64, bool, Option, M) { - ( - self.order_id, - self.unique_priority_idx, - self.price, - self.orig_size, - self.remaining_size, - self.is_bid, - self.trigger_condition, - self.metadata - ) - } - - public fun destroy_single_order_match( - self: SingleOrderMatch - ): (Order, u64) { - (self.order, self.matched_size) - } - - public fun destroy_order_id_type(self: OrderIdType): (address, u64) { - (self.account, self.account_order_id) - } - - public fun is_active_order( - self: &OrderWithState - ): bool { - self.is_active - } - - public fun get_price(self: &Order): u64 { - self.price - } - - public fun is_bid(self: &Order): bool { - self.is_bid - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/order_book/pending_order_book_index.move b/aptos-move/framework/aptos-experimental/sources/trading/order_book/pending_order_book_index.move deleted file mode 100644 index abdda107a9c..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/order_book/pending_order_book_index.move +++ /dev/null @@ -1,181 +0,0 @@ -/// (work in progress) -module aptos_experimental::pending_order_book_index { - use std::vector; - use aptos_framework::timestamp; - use aptos_framework::big_ordered_map::BigOrderedMap; - use aptos_experimental::order_book_types::{ - OrderIdType, - UniqueIdxType, - TriggerCondition, - new_default_big_ordered_map - }; - - friend aptos_experimental::order_book; - - struct PendingOrderKey has store, copy, drop { - price: u64, - tie_breaker: UniqueIdxType - } - - enum PendingOrderBookIndex has store { - V1 { - // Order to trigger when the oracle price move less than - price_move_down_index: BigOrderedMap, - // Orders to trigger whem the oracle price move greater than - price_move_up_index: BigOrderedMap, - //time_based_index: BigOrderedMap, ActiveBidData>, - // Orders to trigger when the time is greater than - time_based_index: BigOrderedMap - } - } - - public(friend) fun new_pending_order_book_index(): PendingOrderBookIndex { - PendingOrderBookIndex::V1 { - price_move_up_index: new_default_big_ordered_map(), - price_move_down_index: new_default_big_ordered_map(), - time_based_index: new_default_big_ordered_map() - } - } - - - - public(friend) fun cancel_pending_order( - self: &mut PendingOrderBookIndex, - trigger_condition: TriggerCondition, - unique_priority_idx: UniqueIdxType, - is_buy: bool - ) { - let (price_move_up_index, price_move_down_index, time_based_index) = - trigger_condition.index(is_buy); - if (price_move_up_index.is_some()) { - self.price_move_up_index.remove( - &PendingOrderKey { - price: price_move_up_index.destroy_some(), - tie_breaker: unique_priority_idx - } - ); - }; - if (price_move_down_index.is_some()) { - self.price_move_down_index.remove( - &PendingOrderKey { - price: price_move_down_index.destroy_some(), - tie_breaker: unique_priority_idx - } - ); - }; - if (time_based_index.is_some()) { - self.time_based_index.remove(&time_based_index.destroy_some()); - }; - } - - public(friend) fun place_pending_maker_order( - self: &mut PendingOrderBookIndex, - order_id: OrderIdType, - trigger_condition: TriggerCondition, - unique_priority_idx: UniqueIdxType, - is_buy: bool - ) { - // Add this order to the pending order book index - let (price_move_down_index, price_move_up_index, time_based_index) = - trigger_condition.index(is_buy); - - if (price_move_up_index.is_some()) { - self.price_move_up_index.add( - PendingOrderKey { - price: price_move_up_index.destroy_some(), - tie_breaker: unique_priority_idx - }, - order_id - ); - } else if (price_move_down_index.is_some()) { - self.price_move_down_index.add( - PendingOrderKey { - price: price_move_down_index.destroy_some(), - tie_breaker: unique_priority_idx - }, - order_id - ); - } else if (time_based_index.is_some()) { - self.time_based_index.add(time_based_index.destroy_some(), order_id); - }; - } - - public fun take_ready_price_based_orders( - self: &mut PendingOrderBookIndex, current_price: u64 - ): vector { - let orders = vector::empty(); - while (!self.price_move_up_index.is_empty()) { - let (key, order_id) = self.price_move_up_index.borrow_front(); - if (current_price >= key.price) { - orders.push_back(*order_id); - self.price_move_up_index.remove(&key); - } else { - break; - } - }; - while (!self.price_move_down_index.is_empty()) { - let (key, order_id) = self.price_move_down_index.borrow_back(); - if (current_price <= key.price) { - orders.push_back(*order_id); - self.price_move_down_index.remove(&key); - } else { - break; - } - }; - orders - } - - public fun take_time_time_based_orders( - self: &mut PendingOrderBookIndex - ): vector { - let orders = vector::empty(); - while (!self.time_based_index.is_empty()) { - let current_time = timestamp::now_seconds(); - let (time, order_id) = self.time_based_index.borrow_front(); - if (current_time >= time) { - orders.push_back(*order_id); - self.time_based_index.remove(&time); - } else { - break; - } - }; - orders - } - - #[test_only] - public(friend) fun destroy_pending_order_book_index( - self: PendingOrderBookIndex - ) { - let PendingOrderBookIndex::V1 { - price_move_up_index, - price_move_down_index, - time_based_index - } = self; - price_move_up_index.destroy(|_v| {}); - price_move_down_index.destroy(|_v| {}); - time_based_index.destroy(|_v| {}); - } - - #[test_only] - public(friend) fun get_price_move_down_index( - self: &PendingOrderBookIndex - ): &BigOrderedMap { - &self.price_move_down_index - } - - #[test_only] - public(friend) fun get_price_move_up_index( - self: &PendingOrderBookIndex - ): &BigOrderedMap { - &self.price_move_up_index - } - - #[test_only] - public(friend) fun get_time_based_index( - self: &PendingOrderBookIndex - ): &BigOrderedMap { - &self.time_based_index - } - - -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/tests/event_utils.move b/aptos-move/framework/aptos-experimental/sources/trading/tests/event_utils.move deleted file mode 100644 index 4499253a61e..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/tests/event_utils.move +++ /dev/null @@ -1,28 +0,0 @@ -#[test_only] -module aptos_experimental::event_utils { - use std::option::Option; - use aptos_framework::event; - struct EventStore has drop { - last_index: u64 - } - - public fun new_event_store(): EventStore { - EventStore { last_index: 0 } - } - - public fun latest_emitted_events( - store: &mut EventStore, limit: Option - ): vector { - let events = event::emitted_events(); - let end_index = - if (limit.is_none()) { - events.length() - } else { - let limit = limit.destroy_some(); - store.last_index + limit - }; - let latest_events = events.slice(store.last_index, end_index); - store.last_index = end_index; - latest_events - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/tests/market/clearinghouse_test.move b/aptos-move/framework/aptos-experimental/sources/trading/tests/market/clearinghouse_test.move deleted file mode 100644 index 1065968a948..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/tests/market/clearinghouse_test.move +++ /dev/null @@ -1,183 +0,0 @@ -#[test_only] -module aptos_experimental::clearinghouse_test { - use std::error; - use std::option; - use std::signer; - use aptos_std::table; - use aptos_std::table::Table; - use aptos_experimental::market_types::{ - SettleTradeResult, - new_settle_trade_result, - MarketClearinghouseCallbacks, - new_market_clearinghouse_callbacks - }; - - const EINVALID_ADDRESS: u64 = 1; - const E_DUPLICATE_ORDER: u64 = 2; - const E_ORDER_NOT_FOUND: u64 = 3; - const E_ORDER_NOT_CLEANED_UP: u64 = 4; - - struct TestOrderMetadata has store, copy, drop {} - - public fun new_test_order_metadata(): TestOrderMetadata { - TestOrderMetadata {} - } - - struct Position has store, drop { - size: u64, - is_long: bool - } - - struct GlobalState has key { - user_positions: Table, - open_orders: Table, - maker_order_calls: Table - } - - public(package) fun initialize(admin: &signer) { - assert!( - signer::address_of(admin) == @0x1, - error::invalid_argument(EINVALID_ADDRESS) - ); - move_to(admin, GlobalState { - user_positions: table::new(), - open_orders: table::new(), - maker_order_calls: table::new() - }); - } - - public(package) fun validate_order_placement(order_id: u64): bool acquires GlobalState { - let open_orders = &mut borrow_global_mut(@0x1).open_orders; - assert!(!open_orders.contains(order_id), error::invalid_argument(E_DUPLICATE_ORDER)); - open_orders.add(order_id, true); - return true - } - - public(package) fun get_position_size(user: address): u64 acquires GlobalState { - let user_positions = &borrow_global(@0x1).user_positions; - if (!user_positions.contains(user)) { - return 0; - }; - user_positions.borrow(user).size - } - - fun update_position( - position: &mut Position, size: u64, is_bid: bool - ) { - if (position.is_long != is_bid) { - if (size > position.size) { - position.size = size - position.size; - position.is_long = is_bid; - } else { - position.size -= size; - } - } else { - position.size += size; - } - } - - public(package) fun settle_trade( - taker: address, - maker: address, - size: u64, - is_taker_long: bool - ): SettleTradeResult acquires GlobalState { - let user_positions = &mut borrow_global_mut(@0x1).user_positions; - let taker_position = - user_positions.borrow_mut_with_default( - taker, Position { size: 0, is_long: true } - ); - update_position(taker_position, size, is_taker_long); - let maker_position = - user_positions.borrow_mut_with_default( - maker, Position { size: 0, is_long: true } - ); - update_position(maker_position, size, !is_taker_long); - new_settle_trade_result(size, option::none(), option::none()) - } - - public(package) fun place_maker_order( - order_id: u64, - ) acquires GlobalState { - let maker_order_calls = &mut borrow_global_mut(@0x1).maker_order_calls; - assert!(!maker_order_calls.contains(order_id), error::invalid_argument(E_DUPLICATE_ORDER)); - maker_order_calls.add(order_id, true); - } - - public(package) fun is_maker_order_called( - order_id: u64 - ): bool acquires GlobalState { - let maker_order_calls = &borrow_global(@0x1).maker_order_calls; - maker_order_calls.contains(order_id) - } - - public(package) fun cleanup_order( - order_id: u64, - ) acquires GlobalState { - let open_orders = &mut borrow_global_mut(@0x1).open_orders; - assert!(open_orders.contains(order_id), error::invalid_argument(E_ORDER_NOT_FOUND)); - open_orders.remove(order_id); - } - - public(package) fun order_exists( - order_id: u64 - ): bool acquires GlobalState { - let open_orders = &borrow_global(@0x1).open_orders; - open_orders.contains(order_id) - } - - public(package) fun settle_trade_with_taker_cancelled( - _taker: address, - _maker: address, - size: u64, - _is_taker_long: bool - ): SettleTradeResult { - new_settle_trade_result( - size / 2, - option::none(), - option::some(std::string::utf8(b"Max open interest violation")) - ) - } - - public(package) fun test_market_callbacks(): - MarketClearinghouseCallbacks acquires GlobalState { - new_market_clearinghouse_callbacks( - |taker, maker, _taker_order_id, _maker_order_id, _fill_id, is_taker_long, _price, size, _taker_metadata, _maker_metadata| { - settle_trade(taker, maker, size, is_taker_long) - }, - | _account, order_id, _is_taker, _is_bid, _price, _size, _order_metadata| { - validate_order_placement(order_id) - }, - |_account, order_id, _is_bid, _price, _size, _order_metadata| { - place_maker_order(order_id); - }, - | _account, _order_id, _is_bid, _remaining_size| { - cleanup_order(_order_id); - }, - | _account, _order_id, _is_bid, _price, _size| { - // decrease order size is not used in this test - }, - ) - } - - public(package) fun test_market_callbacks_with_taker_cancelled(): - MarketClearinghouseCallbacks acquires GlobalState { - new_market_clearinghouse_callbacks( - |taker, maker, _taker_order_id, _maker_order_id, _fill_id, is_taker_long, _price, size, _taker_metadata, _maker_metadata| { - settle_trade_with_taker_cancelled(taker, maker, size, is_taker_long) - }, - | _account, order_id, _is_taker, _is_bid, _price, _size, _order_metadata| { - validate_order_placement(order_id) - }, - |_account, _order_id, _is_bid, _price, _size, _order_metadata| { - // place_maker_order is not used in this test - }, - | _account, _order_id, _is_bid, _remaining_size| { - cleanup_order(_order_id); - }, - | _account, _order_id, _is_bid, _price, _size| { - // decrease order size is not used in this test - }, - ) - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/tests/market/market_test_utils.move b/aptos-move/framework/aptos-experimental/sources/trading/tests/market/market_test_utils.move deleted file mode 100644 index 4cbc51af823..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/tests/market/market_test_utils.move +++ /dev/null @@ -1,326 +0,0 @@ -#[test_only] -module aptos_experimental::market_test_utils { - use std::option; - use std::option::Option; - use std::signer; - use aptos_experimental::clearinghouse_test; - use aptos_experimental::event_utils::{latest_emitted_events, EventStore}; - use aptos_experimental::market_types::MarketClearinghouseCallbacks; - - use aptos_experimental::market::{ - order_status_cancelled, - order_status_filled, - order_status_open, - OrderEvent, - Market - }; - - public fun place_maker_order_and_verify( - market: &mut Market, - user: &signer, - price: u64, - size: u64, - is_buy: bool, - time_in_force: u8, - event_store: &mut EventStore, - is_taker: bool, - is_cancelled: bool, - metadata: M, - callbacks: &MarketClearinghouseCallbacks - ): u64 { - let user_addr = signer::address_of(user); - market.place_order( - user, - price, - size, - is_buy, // is_buy - time_in_force, // order_type - option::none(), // trigger_condition - metadata, - 1000, - true, - callbacks - ); - let events = latest_emitted_events(event_store, option::none()); - if (!is_cancelled) { - assert!(events.length() == 1); - } else { - assert!(events.length() == 2); - }; - let order_place_event = events[0]; - let order_id = order_place_event.get_order_id_from_event(); - order_place_event.verify_order_event( - order_id, - market.get_market(), - user_addr, - size, - size, - size, - price, - is_buy, - is_taker, - order_status_open() - ); - if (!is_cancelled) { - // Maker order is opened - assert!(clearinghouse_test::is_maker_order_called(order_id)); - } else { - // Maker order is cancelled - assert!(!clearinghouse_test::is_maker_order_called(order_id)); - }; - if (is_cancelled) { - let order_cancel_event = events[1]; - order_cancel_event.verify_order_event( - order_id, - market.get_market(), - user_addr, - size, - 0, // Remaining size is always 0 when the order is cancelled - size, - price, - is_buy, - is_taker, - order_status_cancelled() - ) - }; - order_id - } - - public fun place_taker_order( - market: &mut Market, - taker: &signer, - taker_price: u64, - size: u64, - is_buy: bool, - time_in_force: u8, - event_store: &mut EventStore, - max_fills: Option, - metadata: M, - callbacks: &MarketClearinghouseCallbacks - ): u64 { - let taker_addr = signer::address_of(taker); - let max_fills = - if (max_fills.is_none()) { 1000 } - else { - max_fills.destroy_some() - }; - // Taker order will be immediately match in the same transaction - market.place_order( - taker, - taker_price, - size, - is_buy, // is_buy - time_in_force, // order_type - option::none(), // trigger_condition - metadata, - max_fills, - true, - callbacks - ); - - let events = latest_emitted_events(event_store, option::some(1)); - let order_place_event = events[0]; - let order_id = order_place_event.get_order_id_from_event(); - // Taker order is opened - order_place_event.verify_order_event( - order_id, - market.get_market(), - taker_addr, - size, - size, - size, - taker_price, - is_buy, - true, - order_status_open() - ); - order_id - } - - public fun place_taker_order_and_verify_fill( - market: &mut Market, - taker: &signer, - taker_price: u64, - size: u64, - is_buy: bool, - time_in_force: u8, - fill_sizes: vector, - fill_prices: vector, - maker_addr: address, - maker_order_ids: vector, - maker_orig_sizes: vector, - maker_remaining_sizes: vector, - event_store: &mut EventStore, - is_cancelled: bool, - max_fills: Option, - metadata: M, - callbacks: &MarketClearinghouseCallbacks - ): u64 { - let order_id = - place_taker_order( - market, - taker, - taker_price, - size, - is_buy, - time_in_force, - event_store, - max_fills, - metadata, - callbacks - ); - - verify_fills( - market, - taker, - order_id, // taker_order_id - taker_price, - size, - is_buy, - fill_sizes, - fill_prices, - maker_addr, - maker_order_ids, - maker_orig_sizes, - maker_remaining_sizes, - event_store, - is_cancelled - ); - - order_id - } - - public fun verify_cancel_event( - market: &mut Market, - user: &signer, - is_taker: bool, - order_id: u64, - price: u64, - orig_size: u64, - remaining_size: u64, - size_delta: u64, - is_buy: bool, - event_store: &mut EventStore - ) { - let user_addr = signer::address_of(user); - let events = latest_emitted_events(event_store, option::some(1)); - assert!(events.length() == 1); - let order_cancel_event = events[0]; - order_cancel_event.verify_order_event( - order_id, - market.get_market(), - user_addr, - orig_size, - remaining_size, - size_delta, - price, // price - is_buy, - is_taker, - order_status_cancelled() - ); - } - - public fun verify_fills( - market: &mut Market, - taker: &signer, - taker_order_id: u64, - taker_price: u64, - size: u64, - is_buy: bool, - fill_sizes: vector, - fill_prices: vector, - maker_addr: address, - maker_order_ids: vector, - maker_orig_sizes: vector, - maker_remaining_sizes: vector, - event_store: &mut EventStore, - is_cancelled: bool - ) { - let taker_addr = signer::address_of(taker); - let total_fill_size = fill_sizes.fold(0, |acc, fill_size| acc + fill_size); - let events = latest_emitted_events(event_store, option::none()); - assert!(fill_sizes.length() == maker_order_ids.length()); - assert!(fill_prices.length() == fill_sizes.length()); - assert!(maker_orig_sizes.length() == fill_sizes.length()); - assert!(size >= total_fill_size); - let is_partial_fill = size > total_fill_size; - let num_expected_events = 2 * fill_sizes.length(); - if (is_cancelled || is_partial_fill) { - // Cancelling (from IOC) will add an extra cancel event - // Partial fill will add an extra open event - num_expected_events += 1; - }; - assert!(events.length() == num_expected_events); - - let fill_index = 0; - let taker_total_fill = 0; - while (fill_index < fill_sizes.length()) { - let fill_size = fill_sizes[fill_index]; - let fill_price = fill_prices[fill_index]; - let maker_orig_size = maker_orig_sizes[fill_index]; - let maker_remaining_size = maker_remaining_sizes[fill_index]; - taker_total_fill += fill_size; - let maker_order_id = maker_order_ids[fill_index]; - // Taker order is filled - let taker_order_fill_event = events[2 * fill_index]; - taker_order_fill_event.verify_order_event( - taker_order_id, - market.get_market(), - taker_addr, - size, - size - taker_total_fill, - fill_size, - fill_price, - is_buy, - true, - order_status_filled() - ); - // Maker order is filled - let maker_order_fill_event = events[1 + 2 * fill_index]; - maker_order_fill_event.verify_order_event( - maker_order_id, - market.get_market(), - maker_addr, - maker_orig_size, - maker_remaining_size - fill_size, - fill_size, - fill_price, - !is_buy, - false, - order_status_filled() - ); - fill_index += 1; - }; - if (is_cancelled) { - // Taker order is cancelled - let order_cancel_event = events[num_expected_events - 1]; - order_cancel_event.verify_order_event( - taker_order_id, - market.get_market(), - taker_addr, - size, - 0, // Remaining size is always 0 when the order is cancelled - size - taker_total_fill, - taker_price, - is_buy, - true, - order_status_cancelled() - ) - } else if (is_partial_fill) { - // Maker order is opened - let order_open_event = events[num_expected_events - 1]; - order_open_event.verify_order_event( - taker_order_id, - market.get_market(), - taker_addr, - size, - size - total_fill_size, - size, - taker_price, - is_buy, - false, - order_status_open() - ) - }; - } -} diff --git a/aptos-move/framework/aptos-experimental/sources/trading/tests/market/market_tests.move b/aptos-move/framework/aptos-experimental/sources/trading/tests/market/market_tests.move deleted file mode 100644 index 8e32b2907ab..00000000000 --- a/aptos-move/framework/aptos-experimental/sources/trading/tests/market/market_tests.move +++ /dev/null @@ -1,950 +0,0 @@ -#[test_only] -module aptos_experimental::market_tests { - use std::option; - use std::signer; - use std::vector; - use aptos_experimental::clearinghouse_test; - use aptos_experimental::clearinghouse_test::{ - test_market_callbacks, - new_test_order_metadata, - get_position_size, - test_market_callbacks_with_taker_cancelled - }; - use aptos_experimental::market_test_utils::{ - place_maker_order_and_verify, - place_taker_order_and_verify_fill, - place_taker_order, - verify_cancel_event, - verify_fills - }; - use aptos_experimental::event_utils; - use aptos_experimental::market::{ - good_till_cancelled, - post_only, - immediate_or_cancel, - new_market, - new_market_config - }; - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_gtc_taker_fully_filled( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - let event_store = event_utils::new_event_store(); - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, - 2000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Order not filled yet, so size is 0 - assert!(get_position_size(maker_addr) == 0); - assert!(get_position_size(taker_addr) == 0); - - let taker_order_id = - place_taker_order_and_verify_fill( - &mut market, - taker, - 1000, - 1000000, - false, - good_till_cancelled(), - vector[1000000], - vector[1000], - maker_addr, - vector[maker_order_id], - vector[2000000], - vector[2000000], - &mut event_store, - false, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - assert!(get_position_size(maker_addr) == 1000000); - assert!(get_position_size(taker_addr) == 1000000); - assert!(clearinghouse_test::order_exists(maker_order_id)); - assert!(!clearinghouse_test::order_exists(taker_order_id)); - - let taker_order_id2 = - place_taker_order_and_verify_fill( - &mut market, - taker, - 1000, - 1000000, - false, - good_till_cancelled(), - vector[1000000], - vector[1000], - maker_addr, - vector[maker_order_id], - vector[2000000], - vector[1000000], - &mut event_store, - false, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - - assert!(get_position_size(maker_addr) == 2000000); - assert!(get_position_size(taker_addr) == 2000000); - // Both orders should be filled and cleaned up - assert!(!clearinghouse_test::order_exists(maker_order_id)); - assert!(!clearinghouse_test::order_exists(taker_order_id2)); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_gtc_taker_partially_filled( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - let event_store = event_utils::new_event_store(); - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, - 1000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - let taker_order_id = - place_taker_order_and_verify_fill( - &mut market, - taker, - 1000, - 2000000, - false, - good_till_cancelled(), - vector[1000000], - vector[1000], - maker_addr, - vector[maker_order_id], - vector[1000000], - vector[1000000], - &mut event_store, - false, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - assert!(get_position_size(maker_addr) == 1000000); - assert!(get_position_size(taker_addr) == 1000000); - assert!(clearinghouse_test::order_exists(taker_order_id)); - assert!(!clearinghouse_test::order_exists(maker_order_id)); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker1 = @0x456, maker2 = @0x789 - )] - public fun test_post_only_success( - admin: &signer, - market_signer: &signer, - maker1: &signer, - maker2: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let maker1_addr = signer::address_of(maker1); - let maker2_addr = signer::address_of(maker2); - - let event_store = event_utils::new_event_store(); - - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker1, - 1000, - 1000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Place a post only order that should not match with the maker order - let maker2_order_id = - place_maker_order_and_verify( - &mut market, - maker2, - 1100, - 1000000, - false, // is_buy - post_only(), // order_type - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Make sure no matches triggered by post only order - assert!(get_position_size(maker1_addr) == 0); - assert!(get_position_size(maker2_addr) == 0); - - // Ensure the post only order was posted to the order book - assert!( - market.get_remaining_size(signer::address_of(maker1), maker_order_id) - == 1000000 - ); - assert!( - market.get_remaining_size(signer::address_of(maker2), maker2_order_id) - == 1000000 - ); - - // Verify that the maker order is still active - assert!(clearinghouse_test::order_exists(maker_order_id)); - assert!(clearinghouse_test::order_exists(maker2_order_id)); - - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_post_only_failure( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let event_store = event_utils::new_event_store(); - - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, - 1000000, - true, // is_buy - good_till_cancelled(), // order_type - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Taker order which is marked as post only but will immediately match - this should fail - let taker_order_id = - place_maker_order_and_verify( - &mut market, - taker, - 1000, - 1000000, - false, // is_buy - post_only(), // order_type - &mut event_store, - true, - true, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Make sure no matches triggered by post only order - assert!(get_position_size(maker_addr) == 0); - assert!(get_position_size(taker_addr) == 0); - - // Ensure the post only order was not posted in the order book - assert!( - market.get_remaining_size(signer::address_of(taker), taker_order_id) == 0 - ); - // Verify that the taker order is not active - assert!(!clearinghouse_test::order_exists(taker_order_id)); - // The maker order should still be active - assert!(clearinghouse_test::order_exists(maker_order_id)); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_ioc_full_match( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let event_store = event_utils::new_event_store(); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, - 1000000, - true, // is_buy - good_till_cancelled(), // order_type - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Taker order will be immediately match in the same transaction - let taker_order_id = - place_taker_order_and_verify_fill( - &mut market, - taker, - 1000, - 1000000, - false, // is_buy - immediate_or_cancel(), // order_type - vector[1000000], - vector[1000], - maker_addr, - vector[maker_order_id], - vector[1000000], - vector[1000000], - &mut event_store, - false, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - - assert!(get_position_size(maker_addr) == 1000000); - assert!(get_position_size(taker_addr) == 1000000); - - // Both orders should be filled and cleaned up - assert!(!clearinghouse_test::order_exists(maker_order_id)); - assert!(!clearinghouse_test::order_exists(taker_order_id)); - - assert!( - market.get_remaining_size(signer::address_of(taker), taker_order_id) == 0 - ); - assert!( - market.get_remaining_size(signer::address_of(maker), maker_order_id) == 0 - ); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_ioc_partial_match( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let event_store = event_utils::new_event_store(); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, - 1000000, - true, // is_buy - good_till_cancelled(), // order_type - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Taker order is IOC, which will partially match and remaining will be cancelled - let taker_order_id = - place_taker_order_and_verify_fill( - &mut market, - taker, - 1000, - 2000000, - false, // is_buy - immediate_or_cancel(), // order_type - vector[1000000], - vector[1000], - maker_addr, - vector[maker_order_id], - vector[1000000], - vector[1000000], - &mut event_store, - true, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - - assert!(get_position_size(maker_addr) == 1000000); - assert!(get_position_size(taker_addr) == 1000000); - - // Ensure both orders are cleaned up - assert!(!clearinghouse_test::order_exists(maker_order_id)); - assert!(!clearinghouse_test::order_exists(taker_order_id)); - - assert!( - market.get_remaining_size(signer::address_of(taker), taker_order_id) == 0 - ); - assert!( - market.get_remaining_size(signer::address_of(maker), maker_order_id) == 0 - ); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_ioc_no_match( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let event_store = event_utils::new_event_store(); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, - 1000000, // 1 BTC - true, // is_buy - good_till_cancelled(), // order_type - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Taker order is IOC, which will not be matched and should be cancelled - let taker_order_id = - place_maker_order_and_verify( - &mut market, - taker, - 1200, - 1000000, // 1 BTC - false, // is_buy - immediate_or_cancel(), // order_type - &mut event_store, - false, // Despite it being a "taker", this order will not cross - true, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Make sure no matches triggered by post only order - assert!(get_position_size(maker_addr) == 0); - assert!(get_position_size(taker_addr) == 0); - - // Ensure the taker order was not posted in the order book and was cleaned up - assert!(!clearinghouse_test::order_exists(taker_order_id)); - // The maker order should still be active - assert!(clearinghouse_test::order_exists(maker_order_id)); - assert!( - market.get_remaining_size(signer::address_of(maker), maker_order_id) - == 1000000 - ); - assert!( - market.get_remaining_size(signer::address_of(taker), taker_order_id) == 0 - ); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_taker_order_partial_fill( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let event_store = event_utils::new_event_store(); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - // Place maker order - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, // price - 500000, // 0.5 BTC - true, // is_buy - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Taker order that will fully consume maker order but still have remaining size - let taker_order_id = - place_taker_order_and_verify_fill( - &mut market, - taker, - 1000, - 1000000, // 1 BTC - false, // is_buy - good_till_cancelled(), - vector[500000], // 0.5 BTC - vector[1000], - maker_addr, - vector[maker_order_id], - vector[500000], - vector[500000], - &mut event_store, - false, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Check positions after fill - assert!(get_position_size(maker_addr) == 500000); // Long 0.5 BTC - assert!(get_position_size(taker_addr) == 500000); // Short 0.5 BTC - - // Verify maker order fully filled - assert!(market.get_remaining_size(maker_addr, maker_order_id) == 0); - assert!(!clearinghouse_test::order_exists(maker_order_id)); - - // Taker order partially filled - assert!( - market.get_remaining_size(taker_addr, taker_order_id) == 500000 // 0.5 BTC remaining - ); - assert!(clearinghouse_test::order_exists(taker_order_id)); - - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_taker_order_multiple_fills( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let event_store = event_utils::new_event_store(); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - // Place several maker order with small sizes. - let i = 1; - let maker_order_ids = vector::empty(); - let expected_fill_sizes = vector::empty(); - let fill_prices = vector::empty(); - let maker_orig_sizes = vector::empty(); - while (i < 6) { - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000 - i, - 10000 * i, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - maker_order_ids.push_back(maker_order_id); - expected_fill_sizes.push_back(10000 * i); - maker_orig_sizes.push_back(10000 * i); - fill_prices.push_back(1000 - i); - i += 1; - }; - let total_fill_size = expected_fill_sizes.fold(0, |acc, x| acc + x); - - // Order not matched yet, so the balance should not change - assert!(get_position_size(maker_addr) == 0); - assert!(get_position_size(taker_addr) == 0); - let taker_order_id = - place_taker_order_and_verify_fill( - &mut market, - taker, - 990, - 1000000, - false, - good_till_cancelled(), - expected_fill_sizes, - fill_prices, - maker_addr, - maker_order_ids, - maker_orig_sizes, - maker_orig_sizes, - &mut event_store, - false, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - assert!(get_position_size(maker_addr) == total_fill_size); - assert!(get_position_size(taker_addr) == total_fill_size); - // Ensure all maker orders are cleaned up - while (maker_order_ids.length() > 0) { - let maker_order_id = maker_order_ids.pop_back(); - assert!(!clearinghouse_test::order_exists(maker_order_id)); - }; - // Taker order should not be cleaned up since it is partially filled - assert!(clearinghouse_test::order_exists(taker_order_id)); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker = @0x456, taker = @0x789 - )] - public fun test_taker_partial_cancelled_maker_reinserted( - admin: &signer, - market_signer: &signer, - maker: &signer, - taker: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let maker_addr = signer::address_of(maker); - let taker_addr = signer::address_of(taker); - - let event_store = event_utils::new_event_store(); - let maker_order_id = - place_maker_order_and_verify( - &mut market, - maker, - 1000, - 2000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Order not filled yet, so size is 0 - assert!(get_position_size(maker_addr) == 0); - assert!(get_position_size(taker_addr) == 0); - - let taker_order_id = - place_taker_order_and_verify_fill( - &mut market, - taker, - 1000, - 1000000, - false, - good_till_cancelled(), - vector[500000], // Half of the taker order is filled and half is cancelled - vector[1000], - maker_addr, - vector[maker_order_id], - vector[2000000], - vector[2000000], - &mut event_store, - true, - option::none(), - new_test_order_metadata(), - &test_market_callbacks_with_taker_cancelled() - ); - // Make sure the maker order is reinserted - assert!(market.get_remaining_size(maker_addr, maker_order_id) == 1500000); - assert!(clearinghouse_test::order_exists(maker_order_id)); - assert!(!clearinghouse_test::order_exists(taker_order_id)); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker1 = @0x456, maker2 = @0x789 - )] - public fun test_self_matching_not_allowed( - admin: &signer, - market_signer: &signer, - maker1: &signer, - maker2: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(false, true) - ); - clearinghouse_test::initialize(admin); - let maker1_addr = signer::address_of(maker1); - let maker2_addr = signer::address_of(maker2); - let event_store = event_utils::new_event_store(); - let maker1_order_id = - place_maker_order_and_verify( - &mut market, - maker1, - 1001, - 2000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - let maker2_order_id = - place_maker_order_and_verify( - &mut market, - maker2, - 1000, - 2000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Order not filled yet, so size is 0 - assert!(get_position_size(maker1_addr) == 0); - - // This should result in a self match order which should be cancelled and maker2 order should be filled - let taker_order_id = - place_taker_order( - &mut market, - maker1, - 1000, - 1000000, - false, - good_till_cancelled(), - &mut event_store, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - - verify_cancel_event( - &mut market, - maker1, - false, - maker1_order_id, - 1001, - 2000000, - 0, - 2000000, - true, - &mut event_store - ); - - verify_fills( - &mut market, - maker1, - taker_order_id, - 1000, - 1000000, - false, - vector[1000000], - vector[1000], - maker2_addr, - vector[maker2_order_id], - vector[2000000], - vector[2000000], - &mut event_store, - false - ); - - assert!(get_position_size(maker1_addr) == 1000000); - assert!(get_position_size(maker2_addr) == 1000000); - market.destroy_market() - } - - #[test( - admin = @0x1, market_signer = @0x123, maker1 = @0x456, maker2 = @0x789 - )] - public fun test_self_matching_allowed( - admin: &signer, - market_signer: &signer, - maker1: &signer, - maker2: &signer - ) { - // Setup accounts - let market = new_market( - admin, - market_signer, - new_market_config(true, true) - ); - clearinghouse_test::initialize(admin); - let maker1_addr = signer::address_of(maker1); - let event_store = event_utils::new_event_store(); - let maker1_order_id = - place_maker_order_and_verify( - &mut market, - maker1, - 1001, - 2000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - let _ = - place_maker_order_and_verify( - &mut market, - maker2, - 1000, - 2000000, - true, - good_till_cancelled(), - &mut event_store, - false, - false, - new_test_order_metadata(), - &test_market_callbacks() - ); - - // Order not filled yet, so size is 0 - assert!(get_position_size(maker1_addr) == 0); - - // This should result in a self match order which should be matched against self. - let taker_order_id = - place_taker_order( - &mut market, - maker1, - 1000, - 1000000, - false, - good_till_cancelled(), - &mut event_store, - option::none(), - new_test_order_metadata(), - &test_market_callbacks() - ); - - verify_fills( - &mut market, - maker1, - taker_order_id, - 1001, - 1000000, - false, - vector[1000000], - vector[1001], - maker1_addr, - vector[maker1_order_id], - vector[2000000], - vector[2000000], - &mut event_store, - false - ); - market.destroy_market() - } -} diff --git a/aptos-move/framework/aptos-experimental/tests/confidential_asset/confidential_asset_tests.move b/aptos-move/framework/aptos-experimental/tests/confidential_asset/confidential_asset_tests.move deleted file mode 100644 index 1a2c0ed24d9..00000000000 --- a/aptos-move/framework/aptos-experimental/tests/confidential_asset/confidential_asset_tests.move +++ /dev/null @@ -1,685 +0,0 @@ -#[test_only] -module aptos_experimental::confidential_asset_tests { - use std::features; - use std::option; - use std::signer; - use std::string::utf8; - use aptos_std::ristretto255::Scalar; - use aptos_framework::account; - use aptos_framework::chain_id; - use aptos_framework::coin; - use aptos_framework::fungible_asset::{Self, Metadata}; - use aptos_framework::object::{Self, Object}; - use aptos_framework::primary_fungible_store; - - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_balance; - use aptos_experimental::confidential_proof; - use aptos_experimental::ristretto255_twisted_elgamal::{Self as twisted_elgamal, generate_twisted_elgamal_keypair}; - - struct MockCoin {} - - fun withdraw( - sender: &signer, - sender_dk: &Scalar, - token: Object, - to: address, - amount: u64, - new_amount: u128) - { - let from = signer::address_of(sender); - let sender_ek = confidential_asset::encryption_key(from, token); - let current_balance = confidential_balance::decompress_balance( - &confidential_asset::actual_balance(from, token) - ); - - let (proof, new_balance) = confidential_proof::prove_withdrawal( - sender_dk, - &sender_ek, - amount, - new_amount, - ¤t_balance - ); - - let new_balance = confidential_balance::balance_to_bytes(&new_balance); - let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_withdrawal_proof(&proof); - - if (signer::address_of(sender) == to) { - confidential_asset::withdraw(sender, token, amount, new_balance, zkrp_new_balance, sigma_proof); - } else { - confidential_asset::withdraw_to(sender, token, to, amount, new_balance, zkrp_new_balance, sigma_proof); - } - } - - fun transfer( - sender: &signer, - sender_dk: &Scalar, - token: Object, - to: address, - amount: u64, - new_amount: u128) - { - let from = signer::address_of(sender); - let sender_ek = confidential_asset::encryption_key(from, token); - let recipient_ek = confidential_asset::encryption_key(to, token); - let current_balance = confidential_balance::decompress_balance( - &confidential_asset::actual_balance(from, token) - ); - - let ( - proof, - new_balance, - sender_amount, - recipient_amount, - _ - ) = confidential_proof::prove_transfer( - sender_dk, - &sender_ek, - &recipient_ek, - amount, - new_amount, - ¤t_balance, - &vector[], - ); - - let (sigma_proof, zkrp_new_balance, zkrp_transfer_amount) = confidential_proof::serialize_transfer_proof( - &proof - ); - - confidential_asset::confidential_transfer( - sender, - token, - to, - confidential_balance::balance_to_bytes(&new_balance), - confidential_balance::balance_to_bytes(&sender_amount), - confidential_balance::balance_to_bytes(&recipient_amount), - b"", - b"", - zkrp_new_balance, - zkrp_transfer_amount, - sigma_proof - ); - } - - fun audit_transfer( - sender: &signer, - sender_dk: &Scalar, - token: Object, - to: address, - amount: u64, - new_amount: u128, - auditor_eks: &vector): vector - { - let from = signer::address_of(sender); - let sender_ek = confidential_asset::encryption_key(from, token); - let recipient_ek = confidential_asset::encryption_key(to, token); - let current_balance = confidential_balance::decompress_balance( - &confidential_asset::actual_balance(from, token) - ); - - let ( - proof, - new_balance, - sender_amount, - recipient_amount, - auditor_amounts - ) = confidential_proof::prove_transfer( - sender_dk, - &sender_ek, - &recipient_ek, - amount, - new_amount, - ¤t_balance, - auditor_eks, - ); - - let (sigma_proof, zkrp_new_balance, zkrp_transfer_amount) = confidential_proof::serialize_transfer_proof( - &proof - ); - - confidential_asset::confidential_transfer( - sender, - token, - to, - confidential_balance::balance_to_bytes(&new_balance), - confidential_balance::balance_to_bytes(&sender_amount), - confidential_balance::balance_to_bytes(&recipient_amount), - confidential_asset::serialize_auditor_eks(auditor_eks), - confidential_asset::serialize_auditor_amounts(&auditor_amounts), - zkrp_new_balance, - zkrp_transfer_amount, - sigma_proof - ); - - auditor_amounts - } - - fun rotate( - sender: &signer, - sender_dk: &Scalar, - token: Object, - new_dk: &Scalar, - new_ek: &twisted_elgamal::CompressedPubkey, - amount: u128) - { - let from = signer::address_of(sender); - let sender_ek = confidential_asset::encryption_key(from, token); - let current_balance = confidential_balance::decompress_balance( - &confidential_asset::actual_balance(from, token) - ); - - let (proof, new_balance) = confidential_proof::prove_rotation( - sender_dk, - new_dk, - &sender_ek, - new_ek, - amount, - ¤t_balance - ); - - let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_rotation_proof(&proof); - - confidential_asset::rotate_encryption_key( - sender, - token, - twisted_elgamal::pubkey_to_bytes(new_ek), - confidential_balance::balance_to_bytes(&new_balance), - zkrp_new_balance, - sigma_proof - ); - } - - fun normalize( - sender: &signer, - sender_dk: &Scalar, - token: Object, - amount: u128) - { - let from = signer::address_of(sender); - let sender_ek = confidential_asset::encryption_key(from, token); - let current_balance = confidential_balance::decompress_balance( - &confidential_asset::actual_balance(from, token) - ); - - let (proof, new_balance) = confidential_proof::prove_normalization( - sender_dk, - &sender_ek, - amount, - ¤t_balance); - - let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_normalization_proof(&proof); - - confidential_asset::normalize( - sender, - token, - confidential_balance::balance_to_bytes(&new_balance), - zkrp_new_balance, - sigma_proof - ); - } - - public fun set_up_for_confidential_asset_test( - confidential_asset: &signer, - aptos_fx: &signer, - fa: &signer, - sender: &signer, - recipient: &signer, - sender_amount: u64, - recipient_amount: u64): Object - { - chain_id::initialize_for_test(aptos_fx, 4); - - let ctor_ref = &object::create_sticky_object(signer::address_of(fa)); - - primary_fungible_store::create_primary_store_enabled_fungible_asset( - ctor_ref, - option::none(), - utf8(b"MockToken"), - utf8(b"MT"), - 18, - utf8(b"https://"), - utf8(b"https://"), - ); - - let mint_ref = fungible_asset::generate_mint_ref(ctor_ref); - - assert!(signer::address_of(aptos_fx) != signer::address_of(sender), 1); - assert!(signer::address_of(aptos_fx) != signer::address_of(recipient), 2); - - confidential_asset::init_module_for_testing(confidential_asset); - - features::change_feature_flags_for_testing(aptos_fx, vector[features::get_bulletproofs_feature()], vector[]); - - let token = object::object_from_constructor_ref(ctor_ref); - - let sender_store = primary_fungible_store::ensure_primary_store_exists(signer::address_of(sender), token); - fungible_asset::mint_to(&mint_ref, sender_store, sender_amount); - - let recipient_store = primary_fungible_store::ensure_primary_store_exists(signer::address_of(recipient), token); - fungible_asset::mint_to(&mint_ref, recipient_store, recipient_amount); - - token - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1, - bob = @0xb0 - )] - fun success_deposit_test( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer, - bob: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); - - let alice_addr = signer::address_of(&alice); - let bob_addr = signer::address_of(&bob); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - confidential_asset::register(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); - - confidential_asset::deposit(&alice, token, 100); - confidential_asset::deposit_to(&alice, token, bob_addr, 150); - - assert!(primary_fungible_store::balance(alice_addr, token) == 250, 1); - assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 100), 1); - assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 150), 1); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1, - bob = @0xb0 - )] - fun success_withdraw_test( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer, - bob: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); - - let alice_addr = signer::address_of(&alice); - let bob_addr = signer::address_of(&bob); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - - confidential_asset::deposit(&alice, token, 200); - confidential_asset::rollover_pending_balance(&alice, token); - - withdraw(&alice, &alice_dk, token, bob_addr, 50, 150); - - assert!(primary_fungible_store::balance(bob_addr, token) == 550, 1); - assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 150), 1); - - withdraw(&alice, &alice_dk, token, alice_addr, 50, 100); - - assert!(primary_fungible_store::balance(alice_addr, token) == 350, 1); - assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1, - bob = @0xb0 - )] - fun success_transfer_test( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer, - bob: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); - - let alice_addr = signer::address_of(&alice); - let bob_addr = signer::address_of(&bob); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - confidential_asset::register(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); - - confidential_asset::deposit(&alice, token, 200); - confidential_asset::rollover_pending_balance(&alice, token); - - transfer(&alice, &alice_dk, token, bob_addr, 100, 100); - - assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); - assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 100), 1); - - transfer(&alice, &alice_dk, token, alice_addr, 100, 0); - - assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 0), 1); - assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 100), 1); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1, - bob = @0xb0 - )] - fun success_audit_transfer_test( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer, - bob: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); - - let alice_addr = signer::address_of(&alice); - let bob_addr = signer::address_of(&bob); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); - let (auditor1_dk, auditor1_ek) = generate_twisted_elgamal_keypair(); - let (auditor2_dk, auditor2_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::set_auditor( - &aptos_fx, - token, - twisted_elgamal::pubkey_to_bytes(&auditor1_ek)); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - confidential_asset::register(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); - - confidential_asset::deposit(&alice, token, 200); - confidential_asset::rollover_pending_balance(&alice, token); - - let auditor_amounts = audit_transfer( - &alice, - &alice_dk, - token, - bob_addr, - 100, - 100, - &vector[auditor1_ek, auditor2_ek]); - - assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); - assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 100), 1); - - assert!(confidential_balance::verify_pending_balance(&auditor_amounts[0], &auditor1_dk, 100), 1); - assert!(confidential_balance::verify_pending_balance(&auditor_amounts[1], &auditor2_dk, 100), 1); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1, - bob = @0xb0 - )] - #[expected_failure(abort_code = 0x010006, location = confidential_asset)] - fun fail_audit_transfer_if_wrong_auditor_list( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer, - bob: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); - - let bob_addr = signer::address_of(&bob); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - let (_, bob_ek) = generate_twisted_elgamal_keypair(); - let (_, auditor1_ek) = generate_twisted_elgamal_keypair(); - let (_, auditor2_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::set_auditor( - &aptos_fx, - token, - twisted_elgamal::pubkey_to_bytes(&auditor1_ek)); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - confidential_asset::register(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); - - confidential_asset::deposit(&alice, token, 200); - confidential_asset::rollover_pending_balance(&alice, token); - - // This fails because the `auditor1` is set for `token`, - // so each transfer must include `auditor1` in the auditor list as the FIRST element. - // Please, see `confidential_asset::validate_auditors` for more details. - audit_transfer( - &alice, - &alice_dk, - token, - bob_addr, - 100, - 100, - &vector[auditor2_ek, auditor1_ek]); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1, - bob = @0xb0 - )] - fun success_rotate( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer, - bob: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); - - let alice_addr = signer::address_of(&alice); - let bob_addr = signer::address_of(&bob); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - - confidential_asset::deposit(&alice, token, 200); - confidential_asset::rollover_pending_balance(&alice, token); - - withdraw(&alice, &alice_dk, token, bob_addr, 50, 150); - - let (new_alice_dk, new_alice_ek) = generate_twisted_elgamal_keypair(); - - rotate(&alice, &alice_dk, token, &new_alice_dk, &new_alice_ek, 150); - - assert!(confidential_asset::encryption_key(alice_addr, token) == new_alice_ek, 1); - assert!(confidential_asset::verify_actual_balance(alice_addr, token, &new_alice_dk, 150), 1); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1, - bob = @0xb0 - )] - fun success_normalize( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer, - bob: signer) - { - let max_chunk_value = 1 << 16 - 1; - let token = set_up_for_confidential_asset_test( - &confidential_asset, - &aptos_fx, - &fa, - &alice, - &bob, - max_chunk_value, - max_chunk_value - ); - - let alice_addr = signer::address_of(&alice); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - - confidential_asset::deposit(&alice, token, max_chunk_value); - confidential_asset::deposit_to(&bob, token, alice_addr, max_chunk_value); - - confidential_asset::rollover_pending_balance(&alice, token); - - assert!(!confidential_asset::is_normalized(alice_addr, token)); - assert!( - !confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, (2 * max_chunk_value as u128)), - 1 - ); - - normalize(&alice, &alice_dk, token, (2 * max_chunk_value as u128)); - - assert!(confidential_asset::is_normalized(alice_addr, token)); - assert!( - confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, (2 * max_chunk_value as u128)), 1); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1 - )] - #[expected_failure(abort_code = 0x01000D, location = confidential_asset)] - fun fail_register_if_token_disallowed( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &alice, 500, 500); - - confidential_asset::enable_allow_list(&aptos_fx); - - let (_, alice_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - fa = @0xfa, - alice = @0xa1 - )] - fun success_register_if_token_allowed( - confidential_asset: signer, - aptos_fx: signer, - fa: signer, - alice: signer) - { - let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &alice, 500, 500); - - confidential_asset::enable_allow_list(&aptos_fx); - confidential_asset::enable_token(&aptos_fx, token); - - let (_, alice_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - alice = @0xa1 - )] - fun fail_deposit_with_coins_if_insufficient_amount( - confidential_asset: signer, - aptos_fx: signer, - alice: signer) - { - chain_id::initialize_for_test(&aptos_fx, 4); - confidential_asset::init_module_for_testing(&confidential_asset); - coin::create_coin_conversion_map(&aptos_fx); - - let alice_addr = signer::address_of(&alice); - - let (burn_cap, freeze_cap, mint_cap) = coin::initialize( - &confidential_asset, utf8(b"MockCoin"), utf8(b"MC"), 0, false); - - let coin_amount = coin::mint(100, &mint_cap); - coin::destroy_burn_cap(burn_cap); - coin::destroy_freeze_cap(freeze_cap); - coin::destroy_mint_cap(mint_cap); - - account::create_account_if_does_not_exist(alice_addr); - coin::register(&alice); - coin::deposit(alice_addr, coin_amount); - - coin::create_pairing(&aptos_fx); - - let token = coin::paired_metadata().extract(); - - let (_, alice_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - confidential_asset::deposit(&alice, token, 100); - } - - #[test( - confidential_asset = @aptos_experimental, - aptos_fx = @aptos_framework, - alice = @0xa1, - )] - fun success_deposit_with_coins( - confidential_asset: signer, - aptos_fx: signer, - alice: signer) - { - chain_id::initialize_for_test(&aptos_fx, 4); - confidential_asset::init_module_for_testing(&confidential_asset); - coin::create_coin_conversion_map(&aptos_fx); - - let alice_addr = signer::address_of(&alice); - - let (burn_cap, freeze_cap, mint_cap) = coin::initialize( - &confidential_asset, utf8(b"MockCoin"), utf8(b"MC"), 0, false); - - let coin_amount = coin::mint(100, &mint_cap); - coin::destroy_burn_cap(burn_cap); - coin::destroy_freeze_cap(freeze_cap); - coin::destroy_mint_cap(mint_cap); - - account::create_account_if_does_not_exist(alice_addr); - coin::register(&alice); - coin::deposit(alice_addr, coin_amount); - - coin::create_pairing(&aptos_fx); - - let token = coin::paired_metadata().extract(); - - let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); - - confidential_asset::register(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); - - assert!(coin::balance(alice_addr) == 100, 1); - assert!(primary_fungible_store::balance(alice_addr, token) == 100, 1); - assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 0), 1); - - confidential_asset::deposit_coins(&alice, 50); - - assert!(coin::balance(alice_addr) == 50, 1); - assert!(primary_fungible_store::balance(alice_addr, token) == 50, 1); - assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 50), 1); - } -} diff --git a/aptos-move/framework/aptos-experimental/whitepaper.md b/aptos-move/framework/aptos-experimental/whitepaper.md new file mode 100644 index 00000000000..0b3ea13ddd5 --- /dev/null +++ b/aptos-move/framework/aptos-experimental/whitepaper.md @@ -0,0 +1,689 @@ +# Movement Confidential Assets: Technical Whitepaper + +**Version 1.0 — March 2026** + +--- + +## Table of Contents + +1. [Introduction](#1-introduction) +2. [Protocol Overview](#2-protocol-overview) +3. [Cryptographic Primitives](#3-cryptographic-primitives) +4. [Balance Representation](#4-balance-representation) +5. [Protocol Operations](#5-protocol-operations) + - [`Transferred` event](#transferred-module-event) +6. [Proof System](#6-proof-system) +7. [Fiat-Shamir Construction](#7-fiat-shamir-construction) +8. [Registration Proof](#8-registration-proof) +9. [Security Properties](#9-security-properties) +10. [Differences from Aptos](#10-differences-from-aptos) +11. [IP Status and References](#11-ip-status-and-references) + +--- + +## 1. Introduction + +Movement Confidential Assets is an on-chain protocol that enables private fungible token transfers on the Movement blockchain. While transaction senders and recipients remain visible, **transfer amounts are hidden** using homomorphic encryption and zero-knowledge proofs. + +The protocol builds on the Aptos Confidential Asset framework, which was originally released under the Apache 2.0 open-source license. In November 2025, Aptos Labs changed the license on their `aptos-core` repository to a more restrictive license, and subsequently introduced proprietary changes to their confidential asset module (v1.1) under the new terms. Movement's implementation uses only code that predates the license change, and all production-hardening modifications are clean-room implementations based on published, public-domain cryptography — no post-license-change Aptos code was used or referenced. These modifications include chain ID binding to prevent cross-chain proof replay, domain-separated SHA2-512 hashing for Fiat-Shamir challenges, a Schnorr-based registration proof to prevent key registration abuse, and **sender auditor hints** for private transfers: an optional opaque byte string (length-capped) that is **hashed into the transfer sigma Fiat–Shamir transcript** so it cannot be altered after the proof is generated, then **emitted** on the on-chain `Transferred` module event. + +**What observers still see.** A successful private transfer does not post the amount in cleartext, but it **does** emit `Transferred` with routing metadata, **compressed ciphertexts** for the moved amount and for the sender’s new actual balance and recipient’s new pending balance, a **flattened copy of the transfer sigma `x7s` commitment block** (`ek_volun_auds`; see [§5 `Transferred` event](#transferred-module-event)), the **`sender_auditor_hint`** bytes, and a **`memo`** field (reserved; empty in the current implementation). Indexers and compliance tooling should treat that event as the canonical on-chain record of those public payloads. + +```mermaid +flowchart LR + subgraph Public + A[Fungible Asset Store] + end + subgraph Private + B[Confidential Asset Store] + end + A -- "deposit(amount)" --> B + B -- "withdraw(amount, proof)" --> A + B -- "transfer(proof)" --> B + style Private fill:#1a1a2e,color:#e0e0e0 + style Public fill:#16213e,color:#e0e0e0 +``` + + + +--- + +## 2. Protocol Overview + +### Lifecycle + +A user's interaction with confidential assets follows this lifecycle: + +```mermaid +stateDiagram-v2 + [*] --> Registered: register(ek, registration_proof) + Registered --> Funded: deposit(amount) + Funded --> Funded: transfer(proof) + Funded --> Funded: normalize(proof) + Funded --> Funded: deposit(amount) + Funded --> Withdrawn: withdraw(amount, proof) + Withdrawn --> Funded: deposit(amount) + Funded --> Rotating: rollover_and_freeze() + Rotating --> Funded: rotate_key(new_ek, proof) + unfreeze() +``` + + + +### Dual-Balance Architecture + +Each account maintains two encrypted balances to prevent front-running attacks: + +```mermaid +flowchart TB + subgraph Account["Confidential Asset Store"] + PB[Pending Balance
4 chunks, 64-bit] + AB[Actual Balance
8 chunks, 128-bit] + end + D[Deposit / Incoming Transfer] --> PB + PB -- "rollover" --> AB + AB -- "withdraw / transfer" --> O[Outgoing] + style Account fill:#0f3460,color:#e0e0e0 +``` + + + +- **Pending balance**: receives deposits and incoming transfers. Cannot be spent directly. +- **Actual balance**: available for spending. Updated by rolling over the pending balance. + +This separation ensures that incoming transfers cannot interfere with in-progress proofs, since proofs are computed against the actual balance which is stable between rollovers. + +--- + +## 3. Cryptographic Primitives + +### Twisted ElGamal Encryption + +Balances are encrypted using a twisted variant of ElGamal encryption over Ristretto255 [RFC 9496](https://www.rfc-editor.org/rfc/rfc9496). + +**Key generation:** + +$$dk \xleftarrow{R} \mathbb{Z}_q, \quad ek = dk^{-1} \cdot H$$ + +where $H$ is a **fixed, canonically defined point** on the Ristretto255 group (from `hash_to_point_base` in the implementation): the same kind of object as the usual curve basepoint—it is an element of $\mathbb{G}$ used as a **second base** alongside the standard basepoint $G$ in $C = v \cdot G + r \cdot H$. The label “hash-to-point” refers to *how $H$ is constructed* (deterministic encoding to the group), not to a different mathematical type. Here $dk \in \mathbb{Z}_q$ is the secret **decryption** scalar and $ek \in \mathbb{G}$ is the public **encryption** key (a curve point, stored on-chain as `CompressedPubkey`). The formula comes from Twisted ElGamal in this repository: public key $Y = sk^{-1} \cdot H$ for secret $sk$ ([`ristretto255_twisted_elgamal.move`](./sources/confidential_asset/ristretto255_twisted_elgamal.move))—with $sk = dk$ and $Y = ek$—which is **not** the textbook choice $Y = sk \cdot G$. Equivalently $dk \cdot ek = H$ (scalar multiplication of the point $ek$ by $dk$). If you are used to writing **public = secret $\cdot$ generator**, the twist here is **public = secret$^{-1} \cdot H$** for this second base $H$. + +**Encryption of value $v$ with randomness $r$:** + +$$C = v \cdot G + r \cdot H, \quad D = r \cdot ek$$ + +**Homomorphic property:** + +$$\text{Enc}(v_1, r_1) + \text{Enc}(v_2, r_2) = \text{Enc}(v_1 + v_2, r_1 + r_2)$$ + +This allows the blockchain to update encrypted balances without decryption. + +```mermaid +flowchart LR + subgraph Encryption + V["value v"] --> C["C = v*G + r*H"] + R["randomness r"] --> C + R --> D["D = r*ek"] + end + subgraph Decryption + C2["C"] --> V2["v*G = C - dk*D"] + D2["D"] --> V2 + V2 --> DLP["Solve DLP for v"] + end +``` + + + +### Bulletproofs Range Proofs + +Range proofs ensure that encrypted values lie within valid bounds, preventing overflow/underflow attacks. The protocol uses batch Bulletproofs verification [Bunz et al., 2018](https://eprint.iacr.org/2017/1066) to prove that each 16-bit chunk of a balance is in range $[0, 2^{16})$. + +### Ristretto255 + +All elliptic curve operations use the Ristretto255 group [RFC 9496](https://www.rfc-editor.org/rfc/rfc9496), which provides a prime-order group suitable for cryptographic protocols, built on top of Curve25519. + +--- + +## 4. Balance Representation + +### Chunked Encoding + +Balances are split into fixed-width chunks to enable efficient range proofs and bounded-complexity decryption: + + +| Balance Type | Chunks | Bits per Chunk | Total Capacity | +| ------------ | ------ | -------------- | -------------- | +| Pending | 4 | 16 | 64-bit | +| Actual | 8 | 16 | 128-bit | + + +A balance value $b$ is decomposed as: + +$$b = \sum_{i=0}^{n-1} a_i \cdot 2^{16i}$$ + +Each chunk $a_i$ is independently encrypted as a twisted ElGamal ciphertext $(C_i, D_i)$. + +```mermaid +flowchart LR + subgraph "128-bit Actual Balance" + C0["Chunk 0
bits [0,16)"] + C1["Chunk 1
bits [16,32)"] + C2["Chunk 2
bits [32,48)"] + C3["..."] + C7["Chunk 7
bits [112,128)"] + end + C0 --> E0["(C₀, D₀)"] + C1 --> E1["(C₁, D₁)"] + C2 --> E2["(C₂, D₂)"] + C7 --> E7["(C₇, D₇)"] +``` + + + +### Normalization + +After multiple deposits, chunk values may exceed 16 bits due to homomorphic addition. **Normalization** re-encodes the balance with fresh randomness so all chunks are within $[0, 2^{16})$, accompanied by a zero-knowledge proof that the re-encoded balance represents the same value. + +### Pending Counter + +A counter tracks incoming transfers to the pending balance. After $2^{16} - 2$ transfers, the user must roll over the pending balance to the actual balance. This bounds the discrete log search space during decryption. + +--- + +## 5. Protocol Operations + +### Register + +```mermaid +sequenceDiagram + participant User + participant Chain as Movement Chain + User->>User: Generate keypair (dk, ek) + User->>User: Compute Schnorr proof of dk + User->>Chain: register(ek, proof_commitment, proof_response) + Chain->>Chain: Verify registration proof + Chain->>Chain: Create ConfidentialAssetStore with zero balances +``` + + + +The registration proof prevents an attacker from registering someone else's key or a maliciously crafted key. + +### Deposit + +```mermaid +sequenceDiagram + participant User + participant FA as Fungible Asset Store + participant CA as Confidential Asset Store + User->>FA: Debit public balance + FA->>CA: Add to pending balance (homomorphic) + CA->>CA: Increment pending counter +``` + + + +Deposits are public (amount visible on-chain) but become private after rollover into the actual balance. + +### Transfer + +```mermaid +sequenceDiagram + participant Sender + participant Chain as Movement Chain + participant Recipient + Sender->>Sender: Compute sigma proof + range proofs
(Fiat-Shamir binds sender_auditor_hint) + Sender->>Chain: confidential_transfer(encrypted_amounts, proof, sender_auditor_hint) + Chain->>Chain: Verify sigma proof (balance relation + hint binding) + Chain->>Chain: Verify range proofs (no overflow) + Chain->>Chain: Deduct from sender actual balance + Chain->>Chain: Add to recipient pending balance + Chain->>Chain: Emit Transferred(ciphertexts, ek_volun_auds, hint, …) + Note over Chain: Amount hidden from all observers + Note over Chain: Auditor can decrypt if configured +``` + + + +The transfer proof demonstrates: + +1. Sender's new balance = old balance - transfer amount +2. Transfer amount encrypted under recipient's key matches sender's committed amount +3. All new balance chunks are in range $[0, 2^{16})$ + +**Sender auditor hint (`sender_auditor_hint`).** The sender may attach up to **`MAX_SENDER_AUDITOR_HINT_BYTES` (256)** opaque bytes (e.g. for off-chain auditors, indexers, or compliance references). The implementation **serializes the hint with BCS** and **appends those bytes to the transfer sigma Fiat–Shamir message** (after the commitment points, before the DST / chain-id / sender / contract prefix is prepended and the SHA2-512 hash is taken). The same bytes must therefore be supplied when **generating** the proof off-chain and when calling **`confidential_transfer`** on-chain; changing the hint invalidates the proof. After successful verification, the hint is included on the **`Transferred`** module event (field reference below). + +### `Transferred` module event + +After `confidential_transfer` verifies the `TransferProof`, the module updates confidential balances and emits **`Transferred`**. The payload is a **flat struct** of `address` / `vector` / compressed-balance types (no cleartext amount). Integrators should not infer field names from legacy abbreviations alone; the list below is authoritative. + +| Field | Type (conceptual) | Meaning | +| ----- | ----------------- | ------- | +| **`from`** | Account address | Sender confidential account (the `signer` of the transfer). | +| **`to`** | Account address | Recipient confidential account. | +| **`asset_type`** | Object address | Fungible-asset **metadata object** address for the token (`object::object_address(&token)`). | +| **`amount`** | Compressed confidential balance | **Ciphertext** for the amount moved, under the recipient key in **pending-balance** (four 16-bit chunk) layout. | +| **`ek_volun_auds`** | `vector` | **Wire serialization of `sigma_proof.xs.x7s`:** for each auditor row in the verified transfer proof, **four** compressed Ristretto points (32 bytes each), concatenated **row-major** (auditor order matches the transfer’s auditor EK list; within each row, chunk indices 0–3). **Length = `128 × n`** bytes where `n` is the number of auditor rows (`n = 0` ⇒ empty vector). These are sigma **commitments** tied to the proof; they do **not** replace optional auditor ciphertexts and are not raw EK bytes. | +| **`sender_auditor_hint`** | `vector` | Opaque bytes bound into the transfer sigma Fiat–Shamir hash (BCS) and copied into the event (max **256** bytes). | +| **`new_sender_available_balance`** | Compressed confidential balance | Sender’s new **actual** (spendable) balance ciphertext after the debit. | +| **`new_recip_pending_balance`** | Compressed confidential balance | Recipient’s new **pending** balance ciphertext after the credit. | +| **`memo`** | `vector` | Reserved; the current implementation emits an **empty** vector. | + +**Why `ek_volun_auds` appears on-chain.** The transfer sigma proof already proves soundness; publishing the `x7s` block gives auditors and indexers a **stable, canonical byte string** that matches the verified proof’s auditor-row commitments without re-serializing the entire proof in the event. + +### Withdraw + +The inverse of deposit: the user proves that their encrypted balance contains at least the withdrawal amount, and the difference is properly range-constrained. + +### Key Rotation + +```mermaid +sequenceDiagram + participant User + participant Chain as Movement Chain + User->>Chain: rollover_pending_balance_and_freeze() + Note over Chain: Account frozen, no incoming transfers + User->>User: Re-encrypt balance under new key + User->>User: Compute rotation proof + User->>Chain: rotate_encryption_key(new_ek, new_balance, proof) + Chain->>Chain: Verify rotation proof + Chain->>Chain: Update stored encryption key + User->>Chain: unfreeze_token() +``` + + + +### End-to-End Example: Sending MOVE Privately + +This example walks through every on-chain step required for Alice to send MOVE tokens privately to Bob, from start to finish. + +```mermaid +--- +config: + theme: dark + sequence: + width: 200 + mirrorActors: false +--- +sequenceDiagram + participant Alice + participant Movement + participant Bob + + Note left of Alice: 1. Setup + Alice->>Movement: register(ek_A, proof) + Bob->>Movement: register(ek_B, proof) + + Note left of Alice: 2. Deposit + Alice->>Movement: deposit(1000 MOVE) + + Note left of Alice: 3. Rollover + Alice->>Movement: rollover() + + Note left of Alice: 4. Transfer + Alice->>Movement: confidential_transfer(..., proof, sender_auditor_hint) + Note over Movement: Amount hidden; Transferred emitted (§5) + + Note right of Bob: 5. Rollover + Bob->>Movement: rollover() + + Note right of Bob: 6. Normalize + Bob->>Movement: normalize(proof) + + Note right of Bob: 7. Withdraw + Bob->>Movement: withdraw(500, proof) + Note over Movement: Back to public MOVE +``` + + + +**Summary of transactions:** + + +| Step | Who | Transaction | Privacy | +| ---- | ----- | ----------------------------------------- | -------------------------------------- | +| 1 | Alice | `register(MOVE, ek_A, proof)` | Public (one-time setup) | +| 2 | Bob | `register(MOVE, ek_B, proof)` | Public (one-time setup) | +| 3 | Alice | `deposit(MOVE, 1000)` | Amount visible (entering private pool) | +| 4 | Alice | `rollover_pending_balance(MOVE)` | No amount revealed | +| 5 | Alice | `confidential_transfer(MOVE, Bob, …, proof, sender_auditor_hint)` | **Amount hidden**; emits `Transferred` (ciphertexts, `ek_volun_auds`, `sender_auditor_hint`, new balances; see [§5](#transferred-module-event)) | +| 6 | Bob | `rollover_pending_balance(MOVE)` | No amount revealed | +| 7 | Bob | `normalize(MOVE, ...)` | No amount revealed (only if needed) | +| 8 | Bob | `withdraw(MOVE, amount, proof)` | Amount visible (leaving private pool) | + + +The deposit (step 3) and withdrawal (step 8) amounts are visible on-chain since they interact with public balances. The transfer (step 5) is the private operation — only the sender, recipient, and optional auditor can determine the amount. + +--- + +## 6. Proof System + +### Sigma Protocol Structure + +Each operation uses a sigma protocol to prove algebraic relations between encrypted values. All proofs share a common structure: + +```mermaid +flowchart TB + subgraph Prover + R["Choose random scalars"] + X["Compute commitment points X₁..Xₙ"] + RHO["Derive challenge ρ via Fiat-Shamir"] + ALPHA["Compute response scalars α₁..αₘ"] + R --> X --> RHO --> ALPHA + end + subgraph Verifier + X2["Receive X₁..Xₙ, α₁..αₘ"] + RHO2["Recompute challenge ρ"] + MSM["Verify via single MSM equation"] + X2 --> RHO2 --> MSM + end + ALPHA --> X2 +``` + + + +### Multi-Scalar Multiplication (MSM) Verification + +Instead of checking multiple separate equations, the verifier combines all relations into a single MSM check using challenge-derived $\gamma$ scalars: + +$$\sum_i \gamma_i \cdot X_i = \text{MSM}\left(P_j, s_j\right)$$ + +where: + +- $X_i$ are commitment points from the proof +- $\gamma_i$ are derived from the challenge $\rho$ via SHA2-512 +- $P_j$ are public points (bases, balance components, encryption keys) +- $s_j$ are computed scalars combining response scalars, challenges, and public values + +This batching reduces verification to a single MSM, which is significantly faster than multiple individual scalar multiplications. + +### Proof Components by Operation + + +| Operation | Commitment Points | Response Scalars | Range Proofs | Approx. Size | +| ------------- | ----------------- | ---------------- | -------------------- | ------------ | +| Withdrawal | 18 | 18 | 1 (new balance) | ~1.8 KB | +| Transfer | 30 + 4n | 26 + 4n | 2 (balance + amount) | ~3 KB | +| Normalization | 18 | 18 | 1 (new balance) | ~1.8 KB | +| Rotation | 19 | 19 | 1 (new balance) | ~1.9 KB | +| Registration | 1 | 1 | 0 | 64 bytes | + + +*n = number of auditors* + +For **transfers**, the verifier’s commitment count includes the per-auditor `x7s` block; the same `x7s` data (flattened) is what appears on-chain as **`ek_volun_auds`** on `Transferred` (§5). + +--- + +## 7. Fiat-Shamir Construction + +### Domain-Separated SHA2-512 Hashing + +The protocol derives Fiat-Shamir challenges using SHA2-512 with a domain separation tag (DST) prefix: + +$$\text{challenge}(\text{DST}, \text{msg}) = \text{scalar\_from\_sha2\_512}\left(\text{DST} \text{msg}\right)$$ + +where `scalar_from_sha2_512` computes `SHA2-512(input)` and reduces the resulting 64-byte digest to a Ristretto255 scalar via `new_scalar_uniform_from_64_bytes`. The DST prefix provides collision resistance between different protocol contexts. + +### Domain Separation + +Each operation uses a distinct domain separation tag (DST): + + +| Operation | DST | +| ------------- | ------------------------------------------------ | +| Registration | `"MovementConfidentialAsset/Registration"` | +| Withdrawal | `"MovementConfidentialAsset/Withdrawal"` | +| Transfer | `"MovementConfidentialAsset/Transfer"` | +| Normalization | `"MovementConfidentialAsset/Normalization"` | +| Rotation | `"MovementConfidentialAsset/Rotation"` | +| Range Proofs | `"AptosConfidentialAsset/BulletproofRangeProof"` | + + +### Chain ID and Sender Binding + +Every Fiat-Shamir challenge includes the chain ID and sender address as prefix bytes (prepended to the full message that already contains curve points, keys, balance encodings, and—**for transfers only**—the BCS encoding of `sender_auditor_hint`): + +$$\rho = \text{scalar\_from\_sha2\_512}\left(\text{DST} \text{chainid} \text{sender} \text{contract} \text{publicparams} X_1 \cdots X_n\right)$$ + +Here `publicparams` for the **transfer** sigma includes the usual public inputs (bases, sender/recipient/auditor keys, balance encodings, etc.) **followed by** `BCS(sender_auditor_hint)` so the challenge depends on the exact hint bytes the sender intends to publish. + +This binding ensures: + +- A proof generated for Movement mainnet cannot be replayed on testnet (or vice versa) +- A proof generated by one sender cannot be replayed by a different sender +- Proofs are tied to the specific transaction context +- **(Transfers)** The emitted `sender_auditor_hint` cannot be swapped for another payload without regenerating the proof + +```mermaid +flowchart LR + CID["chain_id (1 byte)"] --> MSG + SENDER["sender address (32 bytes)"] --> MSG + PARAMS["public parameters"] --> MSG + HINT["BCS(sender_auditor_hint) — transfer only"] --> MSG + COMMITS["commitment points"] --> MSG + MSG["Challenge Input"] --> SHA["SHA2-512(DST ‖ msg)"] + SHA --> RHO["Challenge scalar ρ"] +``` + + + +### Gamma Scalar Derivation + +For MSM batching, additional scalars $\gamma_i$ are derived from the challenge via SHA2-512: + +$$\gamma_i = \text{SHA2-512}(\rho i)$$ + +converted to a scalar via `new_scalar_uniform_from_64_bytes`. + +--- + +## 8. Registration Proof + +### Motivation + +Without a registration proof, an attacker could register an arbitrary encryption key for a victim's account, causing funds sent to that account to be unrecoverable. The registration proof is a Schnorr zero-knowledge proof of knowledge (ZKPoK) proving that the registrant knows the decryption key corresponding to the registered encryption key. + +### Protocol + +Given keypair $(dk, ek)$ where $ek = dk^{-1} \cdot H$: + +```mermaid +flowchart TB + subgraph "Prover (off-chain)" + K["k ← random scalar"] + R["R = k · H"] + E["e = SHA2-512('Registration' DST ‖ chain_id ‖ sender ‖ token ‖ ek ‖ R)"] + S["s = k - e · dk⁻¹"] + K --> R --> E --> S + end + subgraph "Verifier (on-chain)" + E2["Recompute e from public inputs"] + CHECK["Check: s · H + e · ek == R"] + E2 --> CHECK + end + S --> E2 + R --> E2 +``` + + + +**Verification equation:** $s \cdot H + e \cdot ek = R$ + +**Correctness:** Substituting $s = k - e \cdot dk^{-1}$ and $ek = dk^{-1} \cdot H$: + +$$(k - e \cdot dk^{-1}) \cdot H + e \cdot dk^{-1} \cdot H = k \cdot H = R \quad \checkmark$$ + +**Proof size:** 64 bytes (32-byte compressed point + 32-byte scalar). + +--- + +## 9. Security Properties + +### Balance Privacy + +- Transfer amounts are hidden from all observers (validators, other users) +- Only the sender, recipient, and optional auditors can decrypt the amount +- Multiple transfers between the same parties do not leak cumulative information beyond what each party can individually compute +- The **`Transferred`** event still exposes **ciphertexts**, **sigma `x7s` bytes** (`ek_volun_auds`), and **`sender_auditor_hint`**: privacy is “amount and plaintext hidden,” not “no public cryptographic material” (see §5) + +### Proof Soundness + +- Sigma protocols prove algebraic relationships with negligible soundness error +- Bulletproofs range proofs prevent overflow/underflow attacks (each chunk proven < $2^{16}$) +- MSM batching via random $\gamma$ scalars preserves soundness with overwhelming probability + +### Batch Soundness + +Transfer proof verification uses **batched multi-scalar multiplication (MSM)** to check all sigma-protocol relations in a single equation (see [`msm_transfer_gammas`](./sources/confidential_asset/confidential_proof.move)). Each relation is assigned a random weight (gamma) derived as `SHA2-512(rho || i || j)` where `(i, j)` is a unique index pair. The per-auditor ciphertext relations (`g7s`) use indices `(7+k, j)` for auditor row `k ∈ [0, n)`, and the sender-amount relation (`g8s`) uses index `(7+n, j)` — i.e. always one past the last auditor row. This ensures every proof relation receives a distinct random weight regardless of auditor count, preserving the full soundness guarantee of the batch verifier. + +### Replay Protection + +```mermaid +flowchart TB + subgraph "Proof Context" + CID["Chain ID"] + SENDER["Sender Address"] + TOKEN["Token Address"] + DST["Operation-specific DST"] + end + CID --> CHALLENGE["Fiat-Shamir Challenge"] + SENDER --> CHALLENGE + TOKEN --> CHALLENGE + DST --> CHALLENGE + CHALLENGE --> PROOF["Bound Proof"] + PROOF -. "Cannot replay on" .-> OTHER["Different chain / sender / token / operation"] + style OTHER fill:#8B0000,color:#e0e0e0 +``` + + + + +| Attack | Mitigation | +| ---------------------- | -------------------------------------- | +| Cross-chain replay | Chain ID in challenge input | +| Cross-sender replay | Sender address in challenge input | +| Cross-operation replay | Operation-specific DST tags | +| Key registration abuse | Schnorr ZKPoK required at registration | +| Front-running | Pending/actual balance separation | +| Chunk overflow | Normalization + range proofs | +| Hint substitution (transfer) | Transfer sigma challenge includes BCS(`sender_auditor_hint`) | + + +### Decryption Complexity + + +| Operation | DLP Search Space | +| ------------------------ | ------------------------------------- | +| Pending chunk decryption | $2^{16} \times \text{pendingcounter}$ | +| Actual chunk decryption | $2^{16} \times 2^{16} = 2^{32}$ | + + +The 16-bit chunking ensures decryption remains computationally feasible for the balance holder while remaining infeasible for attackers without the decryption key. + +--- + +## 10. Differences from Aptos + +The Movement implementation diverges from Aptos's post-November 2025 proprietary changes while achieving equivalent security properties. + +### Comparison + +```mermaid +flowchart LR + subgraph Aptos["Aptos v1.1 (Proprietary)"] + A1["SHA2-512"] + A2["Generic sigma framework
10+ new modules"] + A3["BCS-serialized FiatShamirInputs"] + A4["Two-level challenge derivation"] + A5["Enum-wrapped proof types (V1)"] + end + subgraph Movement["Movement (This Implementation)"] + M1["SHA2-512 + DST prefix"] + M2["Explicit MSM per proof type
no abstraction layer"] + M3["Prefix-based domain context"] + M4["Single-level SHA2-512"] + M5["Flat struct proof types"] + end + style Aptos fill:#4a0000,color:#e0e0e0 + style Movement fill:#003300,color:#e0e0e0 +``` + + + + +| Component | Aptos v1.1 (Proprietary) | Movement | Public Basis | +| ----------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Hash function** | SHA2-512 | SHA2-512 | [NIST FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) | +| **Domain separation** | `DomainSeparator` enum with chain_id, contract address, protocol_id, session_id — BCS-serialized | DST prefix with chain_id + sender address prefix | [Fiat & Shamir, 1986](https://link.springer.com/chapter/10.1007/3-540-47721-7_12) | +| **Challenge structure** | Two-level: seed then derived challenges | Single-level: `SHA2-512(DST \|\| chain_id \|\| sender \|\| ... \|\| msg)` | [Fiat & Shamir, 1986](https://link.springer.com/chapter/10.1007/3-540-47721-7_12) | +| **Sigma framework** | Generic modules: `sigma_protocol.move`, `sigma_protocol_homomorphism.move`, etc. | Explicit MSM verification per proof type — no abstraction layer, easier to audit | [Schnorr, 1991](https://link.springer.com/article/10.1007/BF00196725); [Cramer, 1996](https://link.springer.com/chapter/10.1007/3-540-68339-9_19) | +| **Registration proof** | `sigma_protocol_registration.move` via generic framework | Inline Schnorr verification in `confidential_proof.move` | [Schnorr, 1989](https://link.springer.com/chapter/10.1007/0-387-34805-0_22) | +| **Module location** | Moved to `aptos-framework` | Remains in `aptos-experimental` | N/A | +| **Proof types** | Enum-wrapped with V1 variants | Flat struct types | N/A | +| **Transferred / auditor hint** | Confidential transfer event includes ciphertexts, optional memo, `sender_auditor_hint`, and sigma commitment bytes | **`Transferred`** documents `from` / `to` / `asset_type`, encrypted **`amount`**, flattened **`ek_volun_auds`** (`x7s`, `128×n` bytes), **`sender_auditor_hint`** (BCS-hashed into transfer sigma; max 256 bytes), post-transfer **`new_sender_available_balance`** / **`new_recip_pending_balance`**, and **`memo`** (empty today). Fiat–Shamir layout is Movement-specific | N/A | + + +### What Was Inherited (Apache 2.0 Licensed) + +The following components predate Aptos's November 2025 license change and are used under their original Apache 2.0 license: + +- Twisted ElGamal encryption scheme and chunked balance representation +- Core sigma protocol verification structure (MSM equations, gamma batching) +- Bulletproofs range proof integration +- Ristretto255 curve operations (`aptos_std::ristretto255`) +- Fungible asset integration patterns + +### What Movement Changed + +The following changes were made to the inherited pre-license-change codebase. The original Aptos code did not include chain ID binding or a registration proof; Aptos added these independently in their proprietary v1.1 update. Movement's implementations are structurally different clean-room designs. + +- **Hash function**: SHA2-512 with DST prefix for all Fiat-Shamir challenges (same hash family as Aptos, but different domain separation structure) +- **Chain ID binding**: All challenges now include chain_id and sender address (the inherited code had neither) +- **Registration proof**: New Schnorr ZKPoK requirement for key registration (the inherited code had no registration proof) +- **DST branding**: Tags changed from `"AptosConfidentialAsset/"` to `"MovementConfidentialAsset/"` +- **Sender auditor hint**: Optional per-transfer opaque bytes, length-limited, **bound into the transfer sigma challenge** and **emitted** on `Transferred` (integrators must pass the same hint when proving and when submitting `confidential_transfer`) +- **`Transferred` transparency**: The event carries **compressed ciphertexts** for the transfer amount and updated balances, plus **`ek_volun_auds`** (serialized **`x7s`** sigma commitments, `128 × n` bytes for `n` auditor rows) so indexers and auditors can align on-chain data with the verified proof without restating the full proof in the payload + +**Note:** The Bulletproofs range proof DST (`"AptosConfidentialAsset/BulletproofRangeProof"`) is unchanged from the inherited code because range proofs are verified by the pre-existing `ristretto255_bulletproofs` native module, and changing the DST would require matching changes in the native layer. + +--- + +## 11. IP Status and References + +All cryptographic primitives used are published, public-domain, or open-standard. No proprietary Aptos code (post-November 2025) was used. + + +| Primitive | Reference | Status | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| **SHA2-512** | [NIST FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf), "Secure Hash Standard (SHS)", August 2015 | NIST standard, royalty-free | +| **Schnorr proof of knowledge** | [Schnorr, "Efficient Signature Generation by Smart Cards", J. Cryptology 4(3):161-174, 1991](https://link.springer.com/article/10.1007/BF00196725) | Public domain (patent expired 2008) | +| **Fiat-Shamir transform** | [Fiat & Shamir, "How to Prove Yourself: Practical Solutions to Identification and Signature Problems", CRYPTO 1986](https://link.springer.com/chapter/10.1007/3-540-47721-7_12) | Public domain | +| **Ristretto255** | [RFC 9496, "The ristretto255 and decaf448 Groups", December 2023](https://www.rfc-editor.org/rfc/rfc9496) | Open standard (IRTF) | +| **Bulletproofs** | [Bunz, Bootle, Boneh, Poelstra, Wuille, Maxwell, "Bulletproofs: Short Proofs for Confidential Transactions and More", IEEE S&P 2018](https://eprint.iacr.org/2017/1066) | Patent-free | +| **Twisted ElGamal** | [ElGamal, "A Public Key Cryptosystem and a Signature Scheme Based on Discrete Logarithms", IEEE IT 1985](https://ieeexplore.ieee.org/document/1057074); twisted variant per [Pedersen, CRYPTO 1991](https://link.springer.com/chapter/10.1007/3-540-46766-1_9) | Public domain | +| **BCS serialization** | [Diem/Libra BCS, Apache 2.0](https://github.com/diem/bcs) | Permissive open source | +| **Curve25519** | [Bernstein, "Curve25519: New Diffie-Hellman Speed Records", PKC 2006](https://cr.yp.to/ecdh/curve25519-20060209.pdf) | Public domain | + + +### Non-Infringement Statement + +1. **No sigma protocol framework adopted.** Aptos v1.1 introduced 10+ new Move modules (`sigma_protocol*.move`) implementing a generic homomorphism-based prover/verifier. Movement does not use any of these modules. Verification logic remains in `confidential_proof.move` using direct MSM equations. +2. **Different domain separation construction.** Movement uses SHA2-512 with a DST-prefix construction for Fiat-Shamir challenges. Aptos uses SHA2-512 with BCS-serialized `DomainSeparator` input structs and a two-level derivation. The domain separation structures are different. +3. **Pre-existing code base.** The proof verification structure (MSM equations, gamma batching, deserialization) predates Aptos's November 2025 license change. Movement's modifications add chain ID parameters and switch the hash function; they do not adopt any v1.1 architectural patterns. +4. **Registration proof is standard Schnorr.** The discrete-log proof of knowledge ($s \cdot H + e \cdot ek = R$) is a textbook Schnorr protocol (1989/1991), not derived from Aptos's `sigma_protocol_registration.move`. +5. **The `ristretto255::new_scalar_from_sha2_512()` and `ristretto255::new_scalar_uniform_from_64_bytes()` are pre-existing framework primitives** available under the original Apache 2.0 license. + +--- + +## Appendix A: Protocol Constants + +``` +MAX_TRANSFERS_BEFORE_ROLLOVER = 65534 (2^16 - 2) +MAX_SENDER_AUDITOR_HINT_BYTES = 256 (max bytes for sender_auditor_hint on transfer) +EK_VOLUN_AUDS_BYTES_PER_AUDITOR_ROW = 128 (4 compressed Ristretto points × 32 bytes; transfer sigma x7s row) +PENDING_BALANCE_CHUNKS = 4 (64-bit capacity) +ACTUAL_BALANCE_CHUNKS = 8 (128-bit capacity) +CHUNK_SIZE_BITS = 16 +BULLETPROOFS_NUM_BITS = 16 +BULLETPROOFS_DST = "AptosConfidentialAsset/BulletproofRangeProof" +``` + diff --git a/aptos-move/framework/aptos-framework/doc/confidential_asset.md b/aptos-move/framework/aptos-framework/doc/confidential_asset.md new file mode 100644 index 00000000000..6c5de2ef662 --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/confidential_asset.md @@ -0,0 +1,3914 @@ + + + +# Module `0x1::confidential_asset` + +This module implements the Confidential Asset (CA) Standard, a privacy-focused protocol for managing fungible assets (FA). +It enables private transfers by obfuscating token amounts while keeping sender and recipient addresses visible. + + +- [Resource `ConfidentialAssetStore`](#0x1_confidential_asset_ConfidentialAssetStore) +- [Resource `GlobalConfig`](#0x1_confidential_asset_GlobalConfig) +- [Resource `FAConfig`](#0x1_confidential_asset_FAConfig) +- [Struct `Registered`](#0x1_confidential_asset_Registered) +- [Struct `Deposited`](#0x1_confidential_asset_Deposited) +- [Struct `Withdrawn`](#0x1_confidential_asset_Withdrawn) +- [Struct `Transferred`](#0x1_confidential_asset_Transferred) +- [Struct `Normalized`](#0x1_confidential_asset_Normalized) +- [Struct `RolledOver`](#0x1_confidential_asset_RolledOver) +- [Struct `KeyRotated`](#0x1_confidential_asset_KeyRotated) +- [Struct `FreezeChanged`](#0x1_confidential_asset_FreezeChanged) +- [Struct `AllowListChanged`](#0x1_confidential_asset_AllowListChanged) +- [Struct `TokenAllowChanged`](#0x1_confidential_asset_TokenAllowChanged) +- [Struct `AssetAuditorChanged`](#0x1_confidential_asset_AssetAuditorChanged) +- [Struct `ChainAuditorChanged`](#0x1_confidential_asset_ChainAuditorChanged) +- [Struct `ChainAuditorAdminChanged`](#0x1_confidential_asset_ChainAuditorAdminChanged) +- [Constants](#@Constants_0) +- [Function `init_module`](#0x1_confidential_asset_init_module) +- [Function `initialize`](#0x1_confidential_asset_initialize) +- [Function `register`](#0x1_confidential_asset_register) +- [Function `register_and_deposit_and_rollover_pending_balance`](#0x1_confidential_asset_register_and_deposit_and_rollover_pending_balance) +- [Function `deposit_and_rollover_pending_balance`](#0x1_confidential_asset_deposit_and_rollover_pending_balance) +- [Function `deposit_and_normalize_and_rollover_pending_balance`](#0x1_confidential_asset_deposit_and_normalize_and_rollover_pending_balance) +- [Function `deposit_to`](#0x1_confidential_asset_deposit_to) +- [Function `deposit`](#0x1_confidential_asset_deposit) +- [Function `deposit_coins_to`](#0x1_confidential_asset_deposit_coins_to) +- [Function `deposit_coins`](#0x1_confidential_asset_deposit_coins) +- [Function `withdraw_to`](#0x1_confidential_asset_withdraw_to) +- [Function `withdraw`](#0x1_confidential_asset_withdraw) +- [Function `confidential_transfer`](#0x1_confidential_asset_confidential_transfer) +- [Function `max_sender_auditor_hint_bytes`](#0x1_confidential_asset_max_sender_auditor_hint_bytes) +- [Function `rotate_encryption_key`](#0x1_confidential_asset_rotate_encryption_key) +- [Function `normalize`](#0x1_confidential_asset_normalize) +- [Function `freeze_token`](#0x1_confidential_asset_freeze_token) +- [Function `unfreeze_token`](#0x1_confidential_asset_unfreeze_token) +- [Function `rollover_pending_balance`](#0x1_confidential_asset_rollover_pending_balance) +- [Function `normalize_and_rollover_pending_balance`](#0x1_confidential_asset_normalize_and_rollover_pending_balance) +- [Function `rollover_pending_balance_and_freeze`](#0x1_confidential_asset_rollover_pending_balance_and_freeze) +- [Function `rotate_encryption_key_and_unfreeze`](#0x1_confidential_asset_rotate_encryption_key_and_unfreeze) +- [Function `enable_allow_list`](#0x1_confidential_asset_enable_allow_list) +- [Function `disable_allow_list`](#0x1_confidential_asset_disable_allow_list) +- [Function `enable_token`](#0x1_confidential_asset_enable_token) +- [Function `disable_token`](#0x1_confidential_asset_disable_token) +- [Function `set_asset_auditor`](#0x1_confidential_asset_set_asset_auditor) +- [Function `set_chain_auditor_admin`](#0x1_confidential_asset_set_chain_auditor_admin) +- [Function `set_chain_auditor`](#0x1_confidential_asset_set_chain_auditor) +- [Function `has_confidential_asset_store`](#0x1_confidential_asset_has_confidential_asset_store) +- [Function `is_token_allowed`](#0x1_confidential_asset_is_token_allowed) +- [Function `is_allow_list_enabled`](#0x1_confidential_asset_is_allow_list_enabled) +- [Function `pending_balance`](#0x1_confidential_asset_pending_balance) +- [Function `actual_balance`](#0x1_confidential_asset_actual_balance) +- [Function `encryption_key`](#0x1_confidential_asset_encryption_key) +- [Function `is_normalized`](#0x1_confidential_asset_is_normalized) +- [Function `is_frozen`](#0x1_confidential_asset_is_frozen) +- [Function `get_asset_auditor`](#0x1_confidential_asset_get_asset_auditor) +- [Function `get_asset_auditor_epoch`](#0x1_confidential_asset_get_asset_auditor_epoch) +- [Function `get_chain_auditor`](#0x1_confidential_asset_get_chain_auditor) +- [Function `get_chain_auditor_epoch`](#0x1_confidential_asset_get_chain_auditor_epoch) +- [Function `get_chain_auditor_admin`](#0x1_confidential_asset_get_chain_auditor_admin) +- [Function `confidential_asset_balance`](#0x1_confidential_asset_confidential_asset_balance) +- [Function `register_internal`](#0x1_confidential_asset_register_internal) +- [Function `deposit_to_internal`](#0x1_confidential_asset_deposit_to_internal) +- [Function `withdraw_to_internal`](#0x1_confidential_asset_withdraw_to_internal) +- [Function `confidential_transfer_internal`](#0x1_confidential_asset_confidential_transfer_internal) +- [Function `rotate_encryption_key_internal`](#0x1_confidential_asset_rotate_encryption_key_internal) +- [Function `normalize_internal`](#0x1_confidential_asset_normalize_internal) +- [Function `rollover_pending_balance_internal`](#0x1_confidential_asset_rollover_pending_balance_internal) +- [Function `freeze_token_internal`](#0x1_confidential_asset_freeze_token_internal) +- [Function `unfreeze_token_internal`](#0x1_confidential_asset_unfreeze_token_internal) +- [Function `is_safe_for_confidentiality`](#0x1_confidential_asset_is_safe_for_confidentiality) +- [Function `ensure_fa_config_exists`](#0x1_confidential_asset_ensure_fa_config_exists) +- [Function `get_fa_store_signer`](#0x1_confidential_asset_get_fa_store_signer) +- [Function `get_fa_store_address`](#0x1_confidential_asset_get_fa_store_address) +- [Function `get_pool_fa_store`](#0x1_confidential_asset_get_pool_fa_store) +- [Function `ensure_pool_fa_store`](#0x1_confidential_asset_ensure_pool_fa_store) +- [Function `get_user_signer`](#0x1_confidential_asset_get_user_signer) +- [Function `get_user_address`](#0x1_confidential_asset_get_user_address) +- [Function `get_fa_config_signer`](#0x1_confidential_asset_get_fa_config_signer) +- [Function `get_fa_config_address`](#0x1_confidential_asset_get_fa_config_address) +- [Function `construct_user_seed`](#0x1_confidential_asset_construct_user_seed) +- [Function `construct_fa_seed`](#0x1_confidential_asset_construct_fa_seed) +- [Function `validate_auditors`](#0x1_confidential_asset_validate_auditors) +- [Function `deserialize_auditor_eks`](#0x1_confidential_asset_deserialize_auditor_eks) +- [Function `deserialize_auditor_amounts`](#0x1_confidential_asset_deserialize_auditor_amounts) +- [Function `ensure_sufficient_fa`](#0x1_confidential_asset_ensure_sufficient_fa) +- [Function `serialize_auditor_eks`](#0x1_confidential_asset_serialize_auditor_eks) +- [Function `serialize_auditor_amounts`](#0x1_confidential_asset_serialize_auditor_amounts) + + +
use 0x1::bcs;
+use 0x1::chain_id;
+use 0x1::coin;
+use 0x1::confidential_balance;
+use 0x1::confidential_proof;
+use 0x1::dispatchable_fungible_asset;
+use 0x1::error;
+use 0x1::event;
+use 0x1::fungible_asset;
+use 0x1::object;
+use 0x1::option;
+use 0x1::primary_fungible_store;
+use 0x1::ristretto255;
+use 0x1::ristretto255_bulletproofs;
+use 0x1::ristretto255_twisted_elgamal;
+use 0x1::signer;
+use 0x1::string;
+use 0x1::string_utils;
+use 0x1::system_addresses;
+use 0x1::vector;
+
+ + + + + +## Resource `ConfidentialAssetStore` + +The confidential_asset module stores a ConfidentialAssetStore object for each user-token pair. + + +
struct ConfidentialAssetStore has key
+
+ + + +
+Fields + + +
+
+frozen: bool +
+
+ Indicates if the account is frozen. If true, transactions are temporarily disabled + for this account. This is particularly useful during key rotations, which require + two transactions: rolling over the pending balance to the actual balance and rotating + the encryption key. Freezing prevents the user from accepting additional payments + between these two transactions. +
+
+normalized: bool +
+
+ A flag indicating whether the actual balance is normalized. A normalized balance + ensures that all chunks fit within the defined 16-bit bounds, preventing overflows. +
+
+pending_counter: u64 +
+
+ Tracks the maximum number of transactions the user can accept before normalization + is required. For example, if the user can accept up to 2^16 transactions and each + chunk has a 16-bit limit, the maximum chunk value before normalization would be + 2^16 * 2^16 = 2^32. Maintaining this counter is crucial because users must solve + a discrete logarithm problem of this size to decrypt their balances. +
+
+pending_balance: confidential_balance::CompressedConfidentialBalance +
+
+ Stores the user's pending balance, which is used for accepting incoming payments. + Represented as four 16-bit chunks (p0 + 2^16 * p1 + 2^32 * p2 + 2^48 * p3), that can grow up to 32 bits. + All payments are accepted into this pending balance, which users must roll over into the actual balance + to perform transactions like withdrawals or transfers. + This separation helps protect against front-running attacks, where small incoming transfers could force + frequent regenerating of zk-proofs. +
+
+actual_balance: confidential_balance::CompressedConfidentialBalance +
+
+ Represents the actual user balance, which is available for sending payments. + It consists of eight 16-bit chunks (p0 + 2^16 * p1 + ... + 2^112 * p8), supporting a 128-bit balance. + Users can decrypt this balance with their decryption keys and by solving a discrete logarithm problem. +
+
+ek: ristretto255_twisted_elgamal::CompressedPubkey +
+
+ The encryption key associated with the user's confidential asset account, different for each token. +
+
+ + +
+ + + +## Resource `GlobalConfig` + +Global configuration for confidential assets: primary FA stores, FAConfig derivation, and chain-level auditor state. + + +
struct GlobalConfig has key
+
+ + + +
+Fields + + +
+
+allow_list_enabled: bool +
+
+ Indicates whether the allow list is enabled. If true, only tokens from the allow list can be transferred. + This flag is managed by the governance module. +
+
+extend_ref: object::ExtendRef +
+
+ Used to derive a signer that owns all the FAs' primary stores and FAConfig objects. +
+
+chain_auditor_ek: option::Option<ristretto255_twisted_elgamal::CompressedPubkey> +
+
+ Chain-level auditor encryption key. Required at auditor_eks[0] on every + confidential transfer. None until set via [set_chain_auditor]; transfers + abort with [ECHAIN_AUDITOR_NOT_SET] in that state. +
+
+chain_auditor_admin: option::Option<address> +
+
+ Account authorized to call [set_chain_auditor]. Set by governance via + [set_chain_auditor_admin]. None until governance assigns one, during which + window set_chain_auditor aborts with [ECHAIN_AUDITOR_ADMIN_NOT_SET]. +
+
+chain_auditor_epoch: u64 +
+
+ Bumped on every [set_chain_auditor] call (including clears). Stamped on each + [Transferred] event so off-chain auditors / gateways can identify which + historical chain-auditor key was in force at that transfer. +
+
+ + +
+ + + +## Resource `FAConfig` + +Represents the configuration of a token. + + +
struct FAConfig has key
+
+ + + +
+Fields + + +
+
+allowed: bool +
+
+ Indicates whether the token is allowed for confidential transfers. + If allow list is disabled, all tokens are allowed. + Can be toggled by the governance module. The withdrawals are always allowed. +
+
+asset_auditor_ek: option::Option<ristretto255_twisted_elgamal::CompressedPubkey> +
+
+ Per-asset auditor encryption key. When set, required at auditor_eks[1] on + every transfer of this asset (additive to the chain auditor at [0]). Set via + [set_asset_auditor] by the FA metadata object's root owner. +
+
+asset_auditor_epoch: u64 +
+
+ Bumped on every [set_asset_auditor] call (including clears). Stamped on each + [Transferred] event for this asset so off-chain auditors / gateways can + identify which historical asset-auditor key was in force at that transfer. +
+
+ + +
+ + + +## Struct `Registered` + +Emitted when a new confidential asset store is registered. + + +
#[event]
+struct Registered has drop, store
+
+ + + +
+Fields + + +
+
+addr: address +
+
+ +
+
+asset_type: address +
+
+ Fungible asset metadata object address. +
+
+ek: ristretto255_twisted_elgamal::CompressedPubkey +
+
+ +
+
+ + +
+ + + +## Struct `Deposited` + +Emitted when tokens are brought into the protocol. + + +
#[event]
+struct Deposited has drop, store
+
+ + + +
+Fields + + +
+
+from: address +
+
+ +
+
+to: address +
+
+ +
+
+asset_type: address +
+
+ Fungible asset metadata object address. +
+
+amount: u64 +
+
+ +
+
+new_pending_balance: confidential_balance::CompressedConfidentialBalance +
+
+ Recipient's new pending balance after the deposit. +
+
+ + +
+ + + +## Struct `Withdrawn` + +Emitted when tokens are brought out of the protocol. + + +
#[event]
+struct Withdrawn has drop, store
+
+ + + +
+Fields + + +
+
+from: address +
+
+ +
+
+to: address +
+
+ +
+
+asset_type: address +
+
+ Fungible asset metadata object address. +
+
+amount: u64 +
+
+ +
+
+new_available_balance: confidential_balance::CompressedConfidentialBalance +
+
+ Sender's new available (actual) balance after the withdrawal. +
+
+ + +
+ + + +## Struct `Transferred` + +Emitted after a successful confidential_transfer between two registered confidential accounts. + +This is the primary on-chain signal for indexers and tooling: **plaintext amounts are not** included; +fields carry **compressed Twisted-ElGamal ciphertexts** and a **subset of sigma commitment bytes** copied +from the verified proof. See the technical whitepaper (whitepaper.md, §5) for a field-by-field guide. + + +
#[event]
+struct Transferred has drop, store
+
+ + + +
+Fields + + +
+
+from: address +
+
+ Address of the sender's confidential account (the signer of the transfer entry). +
+
+to: address +
+
+ Recipient confidential account address. +
+
+asset_type: address +
+
+ Fungible-asset metadata object address (object::object_address(&token)); identifies which token moved. +
+
+amount: confidential_balance::CompressedConfidentialBalance +
+
+ Encrypted transfer amount under the recipient key (pending-balance / four-chunk layout). +
+
+ek_volun_auds: vector<u8> +
+
+ Flattened **transfer sigma x7s** commitments taken from the verified TransferProof: for each + auditor encryption key row in the proof, exactly **four** compressed Ristretto points (32 bytes each), + concatenated in **row-major** order (auditor index, then inner index 0..3). Empty when the proof carries + **no** auditor rows. Total byte length is always **128 × n** with n = number of auditor rows + (confidential_proof::auditors_count_in_transfer_proof / proof.sigma_proof.xs.x7s.length()). +
+
+sender_auditor_hint: vector<u8> +
+
+ Opaque sender-supplied bytes (bounded by [MAX_SENDER_AUDITOR_HINT_BYTES]); same bytes bound into + the transfer sigma Fiat–Shamir challenge and passed as the sender_auditor_hint entry argument. +
+
+new_sender_available_balance: confidential_balance::CompressedConfidentialBalance +
+
+ Sender's new **actual** (spendable) balance ciphertext after the debit, compressed for storage/events. +
+
+new_recip_pending_balance: confidential_balance::CompressedConfidentialBalance +
+
+ Recipient's new **pending** balance ciphertext after the credit, compressed for storage/events. +
+
+memo: vector<u8> +
+
+ Reserved memo payload for future or off-chain conventions; currently emitted as an empty vector. +
+
+chain_auditor_epoch: u64 +
+
+ Value of [GlobalConfig.chain_auditor_epoch] at the time of the transfer. + Required for compliance: lets future audits identify which historical + chain-level auditor key was in force, so that the transcript can still be + decrypted years after a key rotation. +
+
+asset_auditor_epoch: u64 +
+
+ Value of [FAConfig.asset_auditor_epoch] for this asset at the time of the + transfer. 0 only when [set_asset_auditor] has never been called for this + asset; once called (including a clear with empty bytes) the epoch is bumped + and stamped here even if the current asset_auditor_ek is None. Off-chain + auditors / gateways resolve (asset_type, asset_auditor_epoch) to the active + key by indexing [AssetAuditorChanged] events. +
+
+ + +
+ + + +## Struct `Normalized` + +Emitted when the available balance is re-encrypted to normalize chunk bounds. + + +
#[event]
+struct Normalized has drop, store
+
+ + + +
+Fields + + +
+
+addr: address +
+
+ +
+
+asset_type: address +
+
+ +
+
+new_available_balance: confidential_balance::CompressedConfidentialBalance +
+
+ +
+
+ + +
+ + + +## Struct `RolledOver` + +Emitted when the pending balance is rolled over into the available balance. + + +
#[event]
+struct RolledOver has drop, store
+
+ + + +
+Fields + + +
+
+addr: address +
+
+ +
+
+asset_type: address +
+
+ +
+
+new_available_balance: confidential_balance::CompressedConfidentialBalance +
+
+ +
+
+ + +
+ + + +## Struct `KeyRotated` + +Emitted when the encryption key is rotated and the balance is re-encrypted. + + +
#[event]
+struct KeyRotated has drop, store
+
+ + + +
+Fields + + +
+
+addr: address +
+
+ +
+
+asset_type: address +
+
+ +
+
+new_ek: ristretto255_twisted_elgamal::CompressedPubkey +
+
+ +
+
+new_available_balance: confidential_balance::CompressedConfidentialBalance +
+
+ +
+
+ + +
+ + + +## Struct `FreezeChanged` + +Emitted when a confidential account's incoming-transfer pause state changes (freeze/unfreeze). + + +
#[event]
+struct FreezeChanged has drop, store
+
+ + + +
+Fields + + +
+
+addr: address +
+
+ +
+
+asset_type: address +
+
+ +
+
+frozen: bool +
+
+ +
+
+ + +
+ + + +## Struct `AllowListChanged` + +Emitted when the global allow list is enabled or disabled. + + +
#[event]
+struct AllowListChanged has drop, store
+
+ + + +
+Fields + + +
+
+enabled: bool +
+
+ +
+
+ + +
+ + + +## Struct `TokenAllowChanged` + +Emitted when a token's confidential-transfer permission is toggled. + + +
#[event]
+struct TokenAllowChanged has drop, store
+
+ + + +
+Fields + + +
+
+asset_type: address +
+
+ +
+
+allowed: bool +
+
+ +
+
+ + +
+ + + +## Struct `AssetAuditorChanged` + +Asset auditor set, rotated, or cleared. + + +
#[event]
+struct AssetAuditorChanged has drop, store
+
+ + + +
+Fields + + +
+
+asset_type: address +
+
+ +
+
+new_asset_auditor_ek: option::Option<ristretto255_twisted_elgamal::CompressedPubkey> +
+
+ +
+
+new_epoch: u64 +
+
+ +
+
+ + +
+ + + +## Struct `ChainAuditorChanged` + +Chain auditor set, rotated, or cleared. + + +
#[event]
+struct ChainAuditorChanged has drop, store
+
+ + + +
+Fields + + +
+
+new_chain_auditor_ek: option::Option<ristretto255_twisted_elgamal::CompressedPubkey> +
+
+ +
+
+new_epoch: u64 +
+
+ +
+
+ + +
+ + + +## Struct `ChainAuditorAdminChanged` + +Chain-auditor admin assigned or rotated by governance. + + +
#[event]
+struct ChainAuditorAdminChanged has drop, store
+
+ + + +
+Fields + + +
+
+new_admin: address +
+
+ +
+
+ + +
+ + + +## Constants + + + + +Deposit or withdrawal amount must be greater than zero. + + +
const EZERO_AMOUNT: u64 = 25;
+
+ + + + + +An internal error occurred, indicating unexpected behavior. + + +
const EINTERNAL_ERROR: u64 = 16;
+
+ + + + + +The allow list is already disabled. + + +
const EALLOW_LIST_DISABLED: u64 = 15;
+
+ + + + + +The allow list is already enabled. + + +
const EALLOW_LIST_ENABLED: u64 = 14;
+
+ + + + + +The confidential asset account is already frozen. + + +
const EALREADY_FROZEN: u64 = 7;
+
+ + + + + +The module's GlobalConfig has already been initialized. + + +
const EALREADY_INITIALIZED: u64 = 27;
+
+ + + + + +The balance is already normalized and cannot be normalized again. + + +
const EALREADY_NORMALIZED: u64 = 11;
+
+ + + + + +The deserialization of the auditor EK failed. + + +
const EAUDITOR_EK_DESERIALIZATION_FAILED: u64 = 4;
+
+ + + + + +sender_auditor_hint exceeds [MAX_SENDER_AUDITOR_HINT_BYTES]. + + +
const EAUDITOR_HINT_TOO_LONG: u64 = 18;
+
+ + + + + +The confidential asset store has already been published for the given user-token pair. + + +
const ECA_STORE_ALREADY_PUBLISHED: u64 = 2;
+
+ + + + + +The confidential asset store has not been published for the given user-token pair. + + +
const ECA_STORE_NOT_PUBLISHED: u64 = 3;
+
+ + + + + +Chain-auditor admin not assigned by governance. + + +
const ECHAIN_AUDITOR_ADMIN_NOT_SET: u64 = 23;
+
+ + + + + +Chain auditor not configured; confidential transfers cannot proceed. + + +
const ECHAIN_AUDITOR_NOT_SET: u64 = 21;
+
+ + + + + +The provided auditors or auditor proofs are invalid. + + +
const EINVALID_AUDITORS: u64 = 6;
+
+ + + + + +Sender and recipient amounts encrypt different transfer amounts + + +
const EINVALID_SENDER_AMOUNT: u64 = 17;
+
+ + + + + +The operation requires the actual balance to be normalized. + + +
const ENORMALIZATION_REQUIRED: u64 = 10;
+
+ + + + + +Signer is not the FA metadata object's root owner. + + +
const ENOT_ASSET_ISSUER: u64 = 22;
+
+ + + + + +The sender is not the registered auditor. + + +
const ENOT_AUDITOR: u64 = 5;
+
+ + + + + +Signer is not the configured chain-auditor admin. + + +
const ENOT_CHAIN_AUDITOR_ADMIN: u64 = 24;
+
+ + + + + +The confidential asset account is not frozen. + + +
const ENOT_FROZEN: u64 = 8;
+
+ + + + + +The pending balance must be zero for this operation. + + +
const ENOT_ZERO_BALANCE: u64 = 9;
+
+ + + + + +No confidential asset pool exists for the given asset type. + + +
const ENO_CONFIDENTIAL_ASSET_POOL: u64 = 20;
+
+ + + + + +The range proof system does not support sufficient range. + + +
const ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE: u64 = 1;
+
+ + + + + +The sender and recipient of a confidential transfer must be different accounts. + + +
const ESELF_TRANSFER: u64 = 26;
+
+ + + + + +The token is not allowed for confidential transfers. + + +
const ETOKEN_DISABLED: u64 = 13;
+
+ + + + + +The token is already allowed for confidential transfers. + + +
const ETOKEN_ENABLED: u64 = 12;
+
+ + + + + +Dispatchable fungible asset types (those with custom withdraw, deposit, balance, or +supply hooks) are not yet supported in confidential transfers. + + +
const EUNSAFE_DISPATCHABLE_FA: u64 = 19;
+
+ + + + + +The Movement mainnet chain ID. If the chain ID is 126, the allow list is enabled. + + +
const MAINNET_CHAIN_ID: u8 = 126;
+
+ + + + + +Maximum length (bytes) of the opaque sender_auditor_hint passed to [confidential_transfer]. + + +
const MAX_SENDER_AUDITOR_HINT_BYTES: u64 = 256;
+
+ + + + + +The maximum number of transactions can be aggregated on the pending balance before rollover is required. + + +
const MAX_TRANSFERS_BEFORE_ROLLOVER: u64 = 65534;
+
+ + + + + +## Function `init_module` + +Runs when CA is first published onto an already-live network (governance framework upgrade). +Does not run at genesis — genesis calls initialize directly (see genesis::initialize). + + +
fun init_module(deployer: &signer)
+
+ + + +
+Implementation + + +
fun init_module(deployer: &signer) {
+    initialize(deployer)
+}
+
+ + + +
+ + + +## Function `initialize` + +Publishes the chain-level GlobalConfig. Invoked at genesis via genesis::initialize, and on +a first-time governance publish via init_module. Idempotent: aborts if already initialized. + + +
public(friend) fun initialize(aptos_framework: &signer)
+
+ + + +
+Implementation + + +
public(friend) fun initialize(aptos_framework: &signer) {
+    system_addresses::assert_aptos_framework(aptos_framework);
+    assert!(
+        !exists<GlobalConfig>(@aptos_framework),
+        error::already_exists(EALREADY_INITIALIZED)
+    );
+    assert!(
+        bulletproofs::get_max_range_bits() >= confidential_proof::get_bulletproofs_num_bits(),
+        error::internal(ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE)
+    );
+
+    let deployer_address = signer::address_of(aptos_framework);
+
+    let global_config_ctor_ref = &object::create_object(deployer_address);
+
+    move_to(aptos_framework, GlobalConfig {
+        allow_list_enabled: chain_id::get() == MAINNET_CHAIN_ID,
+        extend_ref: object::generate_extend_ref(global_config_ctor_ref),
+        chain_auditor_ek: std::option::none(),
+        chain_auditor_epoch: 0,
+        chain_auditor_admin: std::option::none(),
+    });
+}
+
+ + + +
+ + + +## Function `register` + +Registers an account for a specified token. Users must register an account for each token they +intend to transact with. + +Users are also responsible for generating a Twisted ElGamal key pair on their side. + + +
public entry fun register(sender: &signer, token: object::Object<fungible_asset::Metadata>, ek: vector<u8>, registration_proof_commitment: vector<u8>, registration_proof_response: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun register(
+    sender: &signer,
+    token: Object<Metadata>,
+    ek: vector<u8>,
+    registration_proof_commitment: vector<u8>,
+    registration_proof_response: vector<u8>) acquires GlobalConfig, FAConfig
+{
+    let ek = twisted_elgamal::new_pubkey_from_bytes(ek).extract();
+
+    // Verify registration proof (ZKPoK of decryption key)
+    let cid = (chain_id::get() as u8);
+    let user = signer::address_of(sender);
+    confidential_proof::verify_registration_proof(
+        cid,
+        user,
+        @aptos_framework,
+        &ek,
+        object::object_address(&token),
+        registration_proof_commitment,
+        registration_proof_response
+    );
+
+    register_internal(sender, token, ek);
+}
+
+ + + +
+ + + +## Function `register_and_deposit_and_rollover_pending_balance` + +Atomically [register], [deposit], and [rollover_pending_balance] for first-time users — public +FA lands as spendable confidential (actual) balance in one tx. Aborts with +[ECA_STORE_ALREADY_PUBLISHED] if the sender is already registered. + + +
public entry fun register_and_deposit_and_rollover_pending_balance(sender: &signer, token: object::Object<fungible_asset::Metadata>, amount: u64, ek: vector<u8>, registration_proof_commitment: vector<u8>, registration_proof_response: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun register_and_deposit_and_rollover_pending_balance(
+    sender: &signer,
+    token: Object<Metadata>,
+    amount: u64,
+    ek: vector<u8>,
+    registration_proof_commitment: vector<u8>,
+    registration_proof_response: vector<u8>) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    // The fresh store created by `register` is `normalized = true` with empty actual_balance,
+    // so `deposit_and_rollover_pending_balance`'s normalized-state assertion passes — no need
+    // for a separate normalize step here.
+    register(sender, token, ek, registration_proof_commitment, registration_proof_response);
+    deposit_and_rollover_pending_balance(sender, token, amount);
+}
+
+ + + +
+ + + +## Function `deposit_and_rollover_pending_balance` + +Atomically [deposit] and [rollover_pending_balance] when the sender's actual balance is already +normalized — no proofs needed. Aborts with [ENORMALIZATION_REQUIRED] otherwise; use +[deposit_and_normalize_and_rollover_pending_balance] in that case. + + +
public entry fun deposit_and_rollover_pending_balance(sender: &signer, token: object::Object<fungible_asset::Metadata>, amount: u64)
+
+ + + +
+Implementation + + +
public entry fun deposit_and_rollover_pending_balance(
+    sender: &signer,
+    token: Object<Metadata>,
+    amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    let user = signer::address_of(sender);
+    deposit_to_internal(sender, token, user, amount);
+    rollover_pending_balance_internal(sender, token);
+}
+
+ + + +
+ + + +## Function `deposit_and_normalize_and_rollover_pending_balance` + +Atomically [deposit], [normalize] the actual balance, and [rollover_pending_balance] when the +sender's actual balance is NOT normalized. Same proof arguments as [normalize]. Aborts with +[EALREADY_NORMALIZED] if already normalized; use [deposit_and_rollover_pending_balance] then. + + +
public entry fun deposit_and_normalize_and_rollover_pending_balance(sender: &signer, token: object::Object<fungible_asset::Metadata>, amount: u64, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun deposit_and_normalize_and_rollover_pending_balance(
+    sender: &signer,
+    token: Object<Metadata>,
+    amount: u64,
+    new_balance: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    sigma_proof: vector<u8>) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
+    let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract();
+
+    let user = signer::address_of(sender);
+    deposit_to_internal(sender, token, user, amount);
+    normalize_internal(sender, token, new_balance, proof);
+    rollover_pending_balance_internal(sender, token);
+}
+
+ + + +
+ + + +## Function `deposit_to` + +Brings tokens into the protocol, transferring the passed amount from the sender's primary FA store +to the pending balance of the recipient. +The initial confidential balance is publicly visible, as entering the protocol requires a normal transfer. +However, tokens within the protocol become obfuscated through confidential transfers, ensuring privacy in +subsequent transactions. + + +
public entry fun deposit_to(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64)
+
+ + + +
+Implementation + + +
public entry fun deposit_to(
+    sender: &signer,
+    token: Object<Metadata>,
+    to: address,
+    amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    deposit_to_internal(sender, token, to, amount)
+}
+
+ + + +
+ + + +## Function `deposit` + +The same as deposit_to, but the recipient is the sender. + + +
public entry fun deposit(sender: &signer, token: object::Object<fungible_asset::Metadata>, amount: u64)
+
+ + + +
+Implementation + + +
public entry fun deposit(
+    sender: &signer,
+    token: Object<Metadata>,
+    amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    deposit_to_internal(sender, token, signer::address_of(sender), amount)
+}
+
+ + + +
+ + + +## Function `deposit_coins_to` + +The same as deposit_to, but converts coins to missing FA first. + + +
public entry fun deposit_coins_to<CoinType>(sender: &signer, to: address, amount: u64)
+
+ + + +
+Implementation + + +
public entry fun deposit_coins_to<CoinType>(
+    sender: &signer,
+    to: address,
+    amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    let token = ensure_sufficient_fa<CoinType>(sender, amount).extract();
+
+    deposit_to_internal(sender, token, to, amount)
+}
+
+ + + +
+ + + +## Function `deposit_coins` + +The same as deposit, but converts coins to missing FA first. + + +
public entry fun deposit_coins<CoinType>(sender: &signer, amount: u64)
+
+ + + +
+Implementation + + +
public entry fun deposit_coins<CoinType>(
+    sender: &signer,
+    amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    let token = ensure_sufficient_fa<CoinType>(sender, amount).extract();
+
+    deposit_to_internal(sender, token, signer::address_of(sender), amount)
+}
+
+ + + +
+ + + +## Function `withdraw_to` + +Brings tokens out of the protocol by transferring the specified amount from the sender's actual balance to +the recipient's primary FA store. +The withdrawn amount is publicly visible, as this process requires a normal transfer. +The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. + + +
public entry fun withdraw_to(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun withdraw_to(
+    sender: &signer,
+    token: Object<Metadata>,
+    to: address,
+    amount: u64,
+    new_balance: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    sigma_proof: vector<u8>) acquires ConfidentialAssetStore, GlobalConfig
+{
+    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
+    let proof = confidential_proof::deserialize_withdrawal_proof(sigma_proof, zkrp_new_balance).extract();
+
+    withdraw_to_internal(sender, token, to, amount, new_balance, proof);
+}
+
+ + + +
+ + + +## Function `withdraw` + +The same as withdraw_to, but the recipient is the sender. + + +
public entry fun withdraw(sender: &signer, token: object::Object<fungible_asset::Metadata>, amount: u64, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun withdraw(
+    sender: &signer,
+    token: Object<Metadata>,
+    amount: u64,
+    new_balance: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    sigma_proof: vector<u8>) acquires ConfidentialAssetStore, GlobalConfig
+{
+    withdraw_to(
+        sender,
+        token,
+        signer::address_of(sender),
+        amount,
+        new_balance,
+        zkrp_new_balance,
+        sigma_proof
+    )
+}
+
+ + + +
+ + + +## Function `confidential_transfer` + +Transfers tokens from the sender's actual balance to the recipient's pending balance. +The function hides the transferred amount while keeping the sender and recipient addresses visible. +The sender encrypts the transferred amount with the recipient's encryption key and the function updates the +recipient's confidential balance homomorphically. +Additionally, the sender encrypts the transferred amount with each auditor's EK, allowing auditors to decrypt +it on their side. The combined auditor list (auditor_eks / auditor_amounts) has a fixed prefix layout: + +```text +[0] chain-level compliance auditor (always required; configured via set_chain_auditor) +[1] asset-specific auditor (required iff get_asset_auditor(token).is_some()) +[2..] voluntary auditors (sender's choice; ordered) +``` + +Aborts with [ECHAIN_AUDITOR_NOT_SET] when the chain-level auditor has not yet been configured. +The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. + +sender_auditor_hint is emitted on [Transferred] and is **bound into the transfer sigma Fiat–Shamir +transcript** (must match the hint used when generating the proof). Length must not exceed +[MAX_SENDER_AUDITOR_HINT_BYTES]. + + +
public entry fun confidential_transfer(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, new_balance: vector<u8>, sender_amount: vector<u8>, recipient_amount: vector<u8>, auditor_eks: vector<u8>, auditor_amounts: vector<u8>, zkrp_new_balance: vector<u8>, zkrp_transfer_amount: vector<u8>, sigma_proof: vector<u8>, sender_auditor_hint: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun confidential_transfer(
+    sender: &signer,
+    token: Object<Metadata>,
+    to: address,
+    new_balance: vector<u8>,
+    sender_amount: vector<u8>,
+    recipient_amount: vector<u8>,
+    auditor_eks: vector<u8>,
+    auditor_amounts: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    zkrp_transfer_amount: vector<u8>,
+    sigma_proof: vector<u8>,
+    sender_auditor_hint: vector<u8>) acquires ConfidentialAssetStore, FAConfig, GlobalConfig
+{
+    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
+    let sender_amount = confidential_balance::new_pending_balance_from_bytes(sender_amount).extract();
+    let recipient_amount = confidential_balance::new_pending_balance_from_bytes(recipient_amount).extract();
+    let auditor_eks = deserialize_auditor_eks(auditor_eks).extract();
+    let auditor_amounts = deserialize_auditor_amounts(auditor_amounts).extract();
+    let proof = confidential_proof::deserialize_transfer_proof(
+        sigma_proof,
+        zkrp_new_balance,
+        zkrp_transfer_amount
+    ).extract();
+
+    confidential_transfer_internal(
+        sender,
+        token,
+        to,
+        new_balance,
+        sender_amount,
+        recipient_amount,
+        auditor_eks,
+        auditor_amounts,
+        proof,
+        sender_auditor_hint
+    )
+}
+
+ + + +
+ + + +## Function `max_sender_auditor_hint_bytes` + +Returns the maximum allowed sender_auditor_hint length for [confidential_transfer]. + + +
#[view]
+public fun max_sender_auditor_hint_bytes(): u64
+
+ + + +
+Implementation + + +
public fun max_sender_auditor_hint_bytes(): u64 {
+    MAX_SENDER_AUDITOR_HINT_BYTES
+}
+
+ + + +
+ + + +## Function `rotate_encryption_key` + +Rotates the encryption key for the user's confidential balance, updating it to a new encryption key. +The function ensures that the pending balance is zero before the key rotation, requiring the sender to +call rollover_pending_balance_and_freeze beforehand if necessary. +The sender provides their new normalized confidential balance, encrypted with the new encryption key and fresh randomness +to preserve privacy. + + +
public entry fun rotate_encryption_key(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_ek: vector<u8>, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun rotate_encryption_key(
+    sender: &signer,
+    token: Object<Metadata>,
+    new_ek: vector<u8>,
+    new_balance: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    sigma_proof: vector<u8>) acquires ConfidentialAssetStore
+{
+    let new_ek = twisted_elgamal::new_pubkey_from_bytes(new_ek).extract();
+    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
+    let proof = confidential_proof::deserialize_rotation_proof(sigma_proof, zkrp_new_balance).extract();
+
+    rotate_encryption_key_internal(sender, token, new_ek, new_balance, proof);
+}
+
+ + + +
+ + + +## Function `normalize` + +Adjusts each chunk to fit into defined 16-bit bounds to prevent overflows. +Most functions perform implicit normalization by accepting a new normalized confidential balance as a parameter. +However, explicit normalization is required before rolling over the pending balance, as multiple rolls may cause +chunk overflows. +The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. + + +
public entry fun normalize(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun normalize(
+    sender: &signer,
+    token: Object<Metadata>,
+    new_balance: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    sigma_proof: vector<u8>) acquires ConfidentialAssetStore
+{
+    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
+    let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract();
+
+    normalize_internal(sender, token, new_balance, proof);
+}
+
+ + + +
+ + + +## Function `freeze_token` + +Freezes the confidential account for the specified token, disabling all incoming transactions. + + +
public entry fun freeze_token(sender: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public entry fun freeze_token(sender: &signer, token: Object<Metadata>) acquires ConfidentialAssetStore {
+    freeze_token_internal(sender, token);
+}
+
+ + + +
+ + + +## Function `unfreeze_token` + +Unfreezes the confidential account for the specified token, re-enabling incoming transactions. + + +
public entry fun unfreeze_token(sender: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public entry fun unfreeze_token(sender: &signer, token: Object<Metadata>) acquires ConfidentialAssetStore {
+    unfreeze_token_internal(sender, token);
+}
+
+ + + +
+ + + +## Function `rollover_pending_balance` + +Adds the pending balance to the actual balance for the specified token, resetting the pending balance to zero. +This operation is necessary to use tokens from the pending balance for outgoing transactions. + + +
public entry fun rollover_pending_balance(sender: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public entry fun rollover_pending_balance(
+    sender: &signer,
+    token: Object<Metadata>) acquires ConfidentialAssetStore
+{
+    rollover_pending_balance_internal(sender, token);
+}
+
+ + + +
+ + + +## Function `normalize_and_rollover_pending_balance` + +Atomically [normalize] the actual balance and [rollover_pending_balance] in one transaction. Takes the +same proof arguments as [normalize]. + + +
public entry fun normalize_and_rollover_pending_balance(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_balance: vector<u8>, zkrp_new_balance: vector<u8>, sigma_proof: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun normalize_and_rollover_pending_balance(
+    sender: &signer,
+    token: Object<Metadata>,
+    new_balance: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    sigma_proof: vector<u8>) acquires ConfidentialAssetStore
+{
+    let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract();
+    let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract();
+
+    normalize_internal(sender, token, new_balance, proof);
+    rollover_pending_balance_internal(sender, token);
+}
+
+ + + +
+ + + +## Function `rollover_pending_balance_and_freeze` + +Before calling rotate_encryption_key, we need to rollover the pending balance and freeze the token to prevent +any new payments being come. + + +
public entry fun rollover_pending_balance_and_freeze(sender: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public entry fun rollover_pending_balance_and_freeze(
+    sender: &signer,
+    token: Object<Metadata>) acquires ConfidentialAssetStore
+{
+    rollover_pending_balance(sender, token);
+    freeze_token(sender, token);
+}
+
+ + + +
+ + + +## Function `rotate_encryption_key_and_unfreeze` + +After rotating the encryption key, we may want to unfreeze the token to allow payments. +This function facilitates making both calls in a single transaction. + + +
public entry fun rotate_encryption_key_and_unfreeze(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_ek: vector<u8>, new_confidential_balance: vector<u8>, zkrp_new_balance: vector<u8>, rotate_proof: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun rotate_encryption_key_and_unfreeze(
+    sender: &signer,
+    token: Object<Metadata>,
+    new_ek: vector<u8>,
+    new_confidential_balance: vector<u8>,
+    zkrp_new_balance: vector<u8>,
+    rotate_proof: vector<u8>) acquires ConfidentialAssetStore
+{
+    rotate_encryption_key(sender, token, new_ek, new_confidential_balance, zkrp_new_balance, rotate_proof);
+    unfreeze_token(sender, token);
+}
+
+ + + +
+ + + +## Function `enable_allow_list` + +Enables the allow list, restricting confidential transfers to tokens on the allow list. + + +
public fun enable_allow_list(aptos_framework: &signer)
+
+ + + +
+Implementation + + +
public fun enable_allow_list(aptos_framework: &signer) acquires GlobalConfig {
+    system_addresses::assert_aptos_framework(aptos_framework);
+
+    let global_config = borrow_global_mut<GlobalConfig>(@aptos_framework);
+
+    assert!(!global_config.allow_list_enabled, error::invalid_state(EALLOW_LIST_ENABLED));
+
+    global_config.allow_list_enabled = true;
+
+    event::emit(AllowListChanged { enabled: true });
+}
+
+ + + +
+ + + +## Function `disable_allow_list` + +Disables the allow list, allowing confidential transfers for all tokens. + + +
public fun disable_allow_list(aptos_framework: &signer)
+
+ + + +
+Implementation + + +
public fun disable_allow_list(aptos_framework: &signer) acquires GlobalConfig {
+    system_addresses::assert_aptos_framework(aptos_framework);
+
+    let global_config = borrow_global_mut<GlobalConfig>(@aptos_framework);
+
+    assert!(global_config.allow_list_enabled, error::invalid_state(EALLOW_LIST_DISABLED));
+
+    global_config.allow_list_enabled = false;
+
+    event::emit(AllowListChanged { enabled: false });
+}
+
+ + + +
+ + + +## Function `enable_token` + +Enables confidential transfers for the specified token. + + +
public fun enable_token(aptos_framework: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public fun enable_token(aptos_framework: &signer, token: Object<Metadata>) acquires FAConfig, GlobalConfig {
+    system_addresses::assert_aptos_framework(aptos_framework);
+
+    let fa_config = borrow_global_mut<FAConfig>(ensure_fa_config_exists(token));
+
+    assert!(!fa_config.allowed, error::invalid_state(ETOKEN_ENABLED));
+
+    fa_config.allowed = true;
+
+    event::emit(TokenAllowChanged {
+        asset_type: object::object_address(&token),
+        allowed: true,
+    });
+}
+
+ + + +
+ + + +## Function `disable_token` + +Disables confidential transfers for the specified token. + + +
public fun disable_token(aptos_framework: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public fun disable_token(aptos_framework: &signer, token: Object<Metadata>) acquires FAConfig, GlobalConfig {
+    system_addresses::assert_aptos_framework(aptos_framework);
+
+    let fa_config = borrow_global_mut<FAConfig>(ensure_fa_config_exists(token));
+
+    assert!(fa_config.allowed, error::invalid_state(ETOKEN_DISABLED));
+
+    fa_config.allowed = false;
+
+    event::emit(TokenAllowChanged {
+        asset_type: object::object_address(&token),
+        allowed: false,
+    });
+}
+
+ + + +
+ + + +## Function `set_asset_auditor` + +Sets, rotates, or clears the asset-specific auditor key for token. Pass an empty +new_auditor_ek to clear. Bumps asset_auditor_epoch and emits [AssetAuditorChanged]. + +Callable by object::root_owner(token); aborts with [ENOT_ASSET_ISSUER] otherwise. +Rotation invalidates pending transfer proofs (auditor key is bound into the +Fiat–Shamir transcript) — senders must regenerate against the new key. + + +
public entry fun set_asset_auditor(issuer: &signer, token: object::Object<fungible_asset::Metadata>, new_auditor_ek: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun set_asset_auditor(
+    issuer: &signer,
+    token: Object<Metadata>,
+    new_auditor_ek: vector<u8>) acquires FAConfig, GlobalConfig
+{
+    assert!(
+        object::root_owner(token) == signer::address_of(issuer),
+        error::permission_denied(ENOT_ASSET_ISSUER)
+    );
+
+    let fa_config = borrow_global_mut<FAConfig>(ensure_fa_config_exists(token));
+
+    let new_ek_opt = if (new_auditor_ek.length() == 0) {
+        std::option::none()
+    } else {
+        let parsed = twisted_elgamal::new_pubkey_from_bytes(new_auditor_ek);
+        assert!(parsed.is_some(), error::invalid_argument(EAUDITOR_EK_DESERIALIZATION_FAILED));
+        parsed
+    };
+
+    let new_epoch = fa_config.asset_auditor_epoch + 1;
+
+    fa_config.asset_auditor_ek = new_ek_opt;
+    fa_config.asset_auditor_epoch = new_epoch;
+
+    event::emit(AssetAuditorChanged {
+        asset_type: object::object_address(&token),
+        new_asset_auditor_ek: fa_config.asset_auditor_ek,
+        new_epoch,
+    });
+}
+
+ + + +
+ + + +## Function `set_chain_auditor_admin` + +Designates (or rotates) the account authorized to call [set_chain_auditor]. +Governance-only. No clear form — rotate to a successor instead. + + +
public fun set_chain_auditor_admin(aptos_framework: &signer, new_admin: address)
+
+ + + +
+Implementation + + +
public fun set_chain_auditor_admin(
+    aptos_framework: &signer,
+    new_admin: address) acquires GlobalConfig
+{
+    system_addresses::assert_aptos_framework(aptos_framework);
+
+    let global_config = borrow_global_mut<GlobalConfig>(@aptos_framework);
+    global_config.chain_auditor_admin = std::option::some(new_admin);
+
+    event::emit(ChainAuditorAdminChanged { new_admin });
+}
+
+ + + +
+ + + +## Function `set_chain_auditor` + +Sets, rotates, or clears the chain-level auditor key. Pass an empty +new_chain_auditor_ek to clear (which disables all confidential transfers until a +successor is set). Bumps chain_auditor_epoch and emits [ChainAuditorChanged]. + +Callable only by [GlobalConfig.chain_auditor_admin]. Aborts with +[ECHAIN_AUDITOR_ADMIN_NOT_SET] before an admin is assigned, or +[ENOT_CHAIN_AUDITOR_ADMIN] for any other signer. Rotation invalidates pending +transfer proofs — see [set_asset_auditor]. + + +
public entry fun set_chain_auditor(admin: &signer, new_chain_auditor_ek: vector<u8>)
+
+ + + +
+Implementation + + +
public entry fun set_chain_auditor(
+    admin: &signer,
+    new_chain_auditor_ek: vector<u8>) acquires GlobalConfig
+{
+    let global_config = borrow_global_mut<GlobalConfig>(@aptos_framework);
+
+    assert!(
+        global_config.chain_auditor_admin.is_some(),
+        error::invalid_state(ECHAIN_AUDITOR_ADMIN_NOT_SET)
+    );
+    assert!(
+        *global_config.chain_auditor_admin.borrow() == signer::address_of(admin),
+        error::permission_denied(ENOT_CHAIN_AUDITOR_ADMIN)
+    );
+
+    let new_ek_opt = if (new_chain_auditor_ek.length() == 0) {
+        std::option::none()
+    } else {
+        let parsed = twisted_elgamal::new_pubkey_from_bytes(new_chain_auditor_ek);
+        assert!(parsed.is_some(), error::invalid_argument(EAUDITOR_EK_DESERIALIZATION_FAILED));
+        parsed
+    };
+
+    let new_epoch = global_config.chain_auditor_epoch + 1;
+
+    global_config.chain_auditor_ek = new_ek_opt;
+    global_config.chain_auditor_epoch = new_epoch;
+
+    event::emit(ChainAuditorChanged {
+        new_chain_auditor_ek: global_config.chain_auditor_ek,
+        new_epoch,
+    });
+}
+
+ + + +
+ + + +## Function `has_confidential_asset_store` + +Checks if the user has a confidential asset store for the specified token. + + +
#[view]
+public fun has_confidential_asset_store(user: address, token: object::Object<fungible_asset::Metadata>): bool
+
+ + + +
+Implementation + + +
public fun has_confidential_asset_store(user: address, token: Object<Metadata>): bool {
+    exists<ConfidentialAssetStore>(get_user_address(user, token))
+}
+
+ + + +
+ + + +## Function `is_token_allowed` + +Checks if the token is allowed for confidential transfers. + + +
#[view]
+public fun is_token_allowed(token: object::Object<fungible_asset::Metadata>): bool
+
+ + + +
+Implementation + + +
public fun is_token_allowed(token: Object<Metadata>): bool acquires GlobalConfig, FAConfig {
+    if (!is_allow_list_enabled()) {
+        return true
+    };
+
+    let fa_config_address = get_fa_config_address(token);
+
+    if (!exists<FAConfig>(fa_config_address)) {
+        return false
+    };
+
+    borrow_global<FAConfig>(fa_config_address).allowed
+}
+
+ + + +
+ + + +## Function `is_allow_list_enabled` + +Checks if the allow list is enabled. +If the allow list is enabled, only tokens from the allow list can be transferred. +Otherwise, all tokens are allowed. + + +
#[view]
+public fun is_allow_list_enabled(): bool
+
+ + + +
+Implementation + + +
public fun is_allow_list_enabled(): bool acquires GlobalConfig {
+    borrow_global<GlobalConfig>(@aptos_framework).allow_list_enabled
+}
+
+ + + +
+ + + +## Function `pending_balance` + +Returns the pending balance of the user for the specified token. + + +
#[view]
+public fun pending_balance(owner: address, token: object::Object<fungible_asset::Metadata>): confidential_balance::CompressedConfidentialBalance
+
+ + + +
+Implementation + + +
public fun pending_balance(
+    owner: address,
+    token: Object<Metadata>): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore
+{
+    assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    let ca_store = borrow_global<ConfidentialAssetStore>(get_user_address(owner, token));
+
+    ca_store.pending_balance
+}
+
+ + + +
+ + + +## Function `actual_balance` + +Returns the actual balance of the user for the specified token. + + +
#[view]
+public fun actual_balance(owner: address, token: object::Object<fungible_asset::Metadata>): confidential_balance::CompressedConfidentialBalance
+
+ + + +
+Implementation + + +
public fun actual_balance(
+    owner: address,
+    token: Object<Metadata>): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore
+{
+    assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    let ca_store = borrow_global<ConfidentialAssetStore>(get_user_address(owner, token));
+
+    ca_store.actual_balance
+}
+
+ + + +
+ + + +## Function `encryption_key` + +Returns the encryption key (EK) of the user for the specified token. + + +
#[view]
+public fun encryption_key(user: address, token: object::Object<fungible_asset::Metadata>): ristretto255_twisted_elgamal::CompressedPubkey
+
+ + + +
+Implementation + + +
public fun encryption_key(
+    user: address,
+    token: Object<Metadata>): twisted_elgamal::CompressedPubkey acquires ConfidentialAssetStore
+{
+    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token)).ek
+}
+
+ + + +
+ + + +## Function `is_normalized` + +Checks if the user's actual balance is normalized for the specified token. + + +
#[view]
+public fun is_normalized(user: address, token: object::Object<fungible_asset::Metadata>): bool
+
+ + + +
+Implementation + + +
public fun is_normalized(user: address, token: Object<Metadata>): bool acquires ConfidentialAssetStore {
+    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    borrow_global<ConfidentialAssetStore>(get_user_address(user, token)).normalized
+}
+
+ + + +
+ + + +## Function `is_frozen` + +Checks if the user's confidential asset store is frozen for the specified token. + + +
#[view]
+public fun is_frozen(user: address, token: object::Object<fungible_asset::Metadata>): bool
+
+ + + +
+Implementation + + +
public fun is_frozen(user: address, token: Object<Metadata>): bool acquires ConfidentialAssetStore {
+    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    borrow_global<ConfidentialAssetStore>(get_user_address(user, token)).frozen
+}
+
+ + + +
+ + + +## Function `get_asset_auditor` + +Asset auditor encryption key for token, or None if unset. + + +
#[view]
+public fun get_asset_auditor(token: object::Object<fungible_asset::Metadata>): option::Option<ristretto255_twisted_elgamal::CompressedPubkey>
+
+ + + +
+Implementation + + +
public fun get_asset_auditor(
+    token: Object<Metadata>): Option<twisted_elgamal::CompressedPubkey> acquires FAConfig, GlobalConfig
+{
+    let fa_config_address = get_fa_config_address(token);
+
+    if (!exists<FAConfig>(fa_config_address)) {
+        return std::option::none();
+    };
+
+    borrow_global<FAConfig>(fa_config_address).asset_auditor_ek
+}
+
+ + + +
+ + + +## Function `get_asset_auditor_epoch` + +Asset auditor epoch for token. 0 if no asset auditor has been set. + + +
#[view]
+public fun get_asset_auditor_epoch(token: object::Object<fungible_asset::Metadata>): u64
+
+ + + +
+Implementation + + +
public fun get_asset_auditor_epoch(token: Object<Metadata>): u64 acquires FAConfig, GlobalConfig {
+    let fa_config_address = get_fa_config_address(token);
+    if (!exists<FAConfig>(fa_config_address)) {
+        return 0;
+    };
+    borrow_global<FAConfig>(fa_config_address).asset_auditor_epoch
+}
+
+ + + +
+ + + +## Function `get_chain_auditor` + +Chain auditor encryption key, or None if unset. + + +
#[view]
+public fun get_chain_auditor(): option::Option<ristretto255_twisted_elgamal::CompressedPubkey>
+
+ + + +
+Implementation + + +
public fun get_chain_auditor(): Option<twisted_elgamal::CompressedPubkey> acquires GlobalConfig {
+    borrow_global<GlobalConfig>(@aptos_framework).chain_auditor_ek
+}
+
+ + + +
+ + + +## Function `get_chain_auditor_epoch` + +Chain auditor epoch. 0 before any chain auditor has been configured. + + +
#[view]
+public fun get_chain_auditor_epoch(): u64
+
+ + + +
+Implementation + + +
public fun get_chain_auditor_epoch(): u64 acquires GlobalConfig {
+    borrow_global<GlobalConfig>(@aptos_framework).chain_auditor_epoch
+}
+
+ + + +
+ + + +## Function `get_chain_auditor_admin` + +Chain-auditor admin address, or None if governance hasn't assigned one yet. + + +
#[view]
+public fun get_chain_auditor_admin(): option::Option<address>
+
+ + + +
+Implementation + + +
public fun get_chain_auditor_admin(): Option<address> acquires GlobalConfig {
+    borrow_global<GlobalConfig>(@aptos_framework).chain_auditor_admin
+}
+
+ + + +
+ + + +## Function `confidential_asset_balance` + +Returns the circulating supply of the confidential asset. + + +
#[view]
+public fun confidential_asset_balance(token: object::Object<fungible_asset::Metadata>): u64
+
+ + + +
+Implementation + + +
public fun confidential_asset_balance(token: Object<Metadata>): u64 acquires GlobalConfig {
+    fungible_asset::balance(get_pool_fa_store(token))
+}
+
+ + + +
+ + + +## Function `register_internal` + +Implementation of the register entry function. + + +
public fun register_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, ek: ristretto255_twisted_elgamal::CompressedPubkey)
+
+ + + +
+Implementation + + +
public fun register_internal(
+    sender: &signer,
+    token: Object<Metadata>,
+    ek: twisted_elgamal::CompressedPubkey) acquires GlobalConfig, FAConfig
+{
+    assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA));
+    assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED));
+
+    let user = signer::address_of(sender);
+
+    assert!(!has_confidential_asset_store(user, token), error::already_exists(ECA_STORE_ALREADY_PUBLISHED));
+
+    let ca_store = ConfidentialAssetStore {
+        frozen: false,
+        normalized: true,
+        pending_counter: 0,
+        pending_balance: confidential_balance::new_compressed_pending_balance_no_randomness(),
+        actual_balance: confidential_balance::new_compressed_actual_balance_no_randomness(),
+        ek,
+    };
+
+    move_to(&get_user_signer(sender, token), ca_store);
+
+    event::emit(Registered {
+        addr: user,
+        asset_type: object::object_address(&token),
+        ek,
+    });
+}
+
+ + + +
+ + + +## Function `deposit_to_internal` + +Implementation of the deposit_to entry function. + + +
public fun deposit_to_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64)
+
+ + + +
+Implementation + + +
public fun deposit_to_internal(
+    sender: &signer,
+    token: Object<Metadata>,
+    to: address,
+    amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig
+{
+    assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA));
+    assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED));
+    assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN));
+    // A zero deposit moves no funds but still consumes one of the recipient's
+    // `MAX_TRANSFERS_BEFORE_ROLLOVER` pending slots, letting anyone force rollovers on them.
+    assert!(amount > 0, error::invalid_argument(EZERO_AMOUNT));
+
+    let from = signer::address_of(sender);
+
+    let pool_fa_store = ensure_pool_fa_store(token);
+
+    let pool_before = fungible_asset::balance(pool_fa_store);
+    let sender_fa_store = primary_fungible_store::primary_store(from, token);
+    dispatchable_fungible_asset::transfer(sender, sender_fa_store, pool_fa_store, amount);
+
+    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(to, token));
+    let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance);
+
+    confidential_balance::add_balances_mut(
+        &mut pending_balance,
+        &confidential_balance::new_pending_balance_u64_no_randonmess(amount)
+    );
+
+    ca_store.pending_balance = confidential_balance::compress_balance(&pending_balance);
+
+    assert!(
+        ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER,
+        error::invalid_argument(EINTERNAL_ERROR)
+    );
+
+    ca_store.pending_counter += 1;
+
+    event::emit(Deposited {
+        from,
+        to,
+        asset_type: object::object_address(&token),
+        amount,
+        new_pending_balance: ca_store.pending_balance,
+    });
+
+    assert!(
+        amount == fungible_asset::balance(pool_fa_store) - pool_before,
+        error::invalid_argument(EUNSAFE_DISPATCHABLE_FA)
+    );
+}
+
+ + + +
+ + + +## Function `withdraw_to_internal` + +Implementation of the withdraw_to entry function. +Withdrawals are always allowed, regardless of the token allow status. + + +
public fun withdraw_to_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, amount: u64, new_balance: confidential_balance::ConfidentialBalance, proof: confidential_proof::WithdrawalProof)
+
+ + + +
+Implementation + + +
public fun withdraw_to_internal(
+    sender: &signer,
+    token: Object<Metadata>,
+    to: address,
+    amount: u64,
+    new_balance: confidential_balance::ConfidentialBalance,
+    proof: WithdrawalProof) acquires ConfidentialAssetStore, GlobalConfig
+{
+    assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA));
+    // A zero withdrawal would re-randomize the actual balance and mark it normalized,
+    // acting as a `normalize` that skips the `EALREADY_NORMALIZED` guard.
+    assert!(amount > 0, error::invalid_argument(EZERO_AMOUNT));
+
+    let from = signer::address_of(sender);
+
+    let sender_ek = encryption_key(from, token);
+
+    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(from, token));
+    let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
+
+    let cid = (chain_id::get() as u8);
+    confidential_proof::verify_withdrawal_proof(
+        cid,
+        from,
+        @aptos_framework,
+        object::object_address(&token),
+        &sender_ek,
+        amount,
+        ¤t_balance,
+        &new_balance,
+        &proof
+    );
+
+    ca_store.normalized = true;
+    ca_store.actual_balance = confidential_balance::compress_balance(&new_balance);
+
+    let pool_fa_store = get_pool_fa_store(token);
+    let pool_before = fungible_asset::balance(pool_fa_store);
+    let recipient_fa_store = primary_fungible_store::ensure_primary_store_exists(to, token);
+    dispatchable_fungible_asset::transfer(&get_fa_store_signer(), pool_fa_store, recipient_fa_store, amount);
+
+    event::emit(Withdrawn {
+        from,
+        to,
+        asset_type: object::object_address(&token),
+        amount,
+        new_available_balance: ca_store.actual_balance,
+    });
+
+    assert!(
+        amount == pool_before - fungible_asset::balance(pool_fa_store),
+        error::invalid_argument(EUNSAFE_DISPATCHABLE_FA)
+    );
+}
+
+ + + +
+ + + +## Function `confidential_transfer_internal` + +Implementation of the confidential_transfer entry function. + + +
public fun confidential_transfer_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, to: address, new_balance: confidential_balance::ConfidentialBalance, sender_amount: confidential_balance::ConfidentialBalance, recipient_amount: confidential_balance::ConfidentialBalance, auditor_eks: vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: vector<confidential_balance::ConfidentialBalance>, proof: confidential_proof::TransferProof, sender_auditor_hint: vector<u8>)
+
+ + + +
+Implementation + + +
public fun confidential_transfer_internal(
+    sender: &signer,
+    token: Object<Metadata>,
+    to: address,
+    new_balance: confidential_balance::ConfidentialBalance,
+    sender_amount: confidential_balance::ConfidentialBalance,
+    recipient_amount: confidential_balance::ConfidentialBalance,
+    auditor_eks: vector<twisted_elgamal::CompressedPubkey>,
+    auditor_amounts: vector<confidential_balance::ConfidentialBalance>,
+    proof: TransferProof,
+    sender_auditor_hint: vector<u8>) acquires ConfidentialAssetStore, FAConfig, GlobalConfig
+{
+    assert!(signer::address_of(sender) != to, error::invalid_argument(ESELF_TRANSFER));
+    assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA));
+    assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED));
+    assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN));
+    assert!(
+        validate_auditors(token, &recipient_amount, &auditor_eks, &auditor_amounts, &proof),
+        error::invalid_argument(EINVALID_AUDITORS)
+    );
+    assert!(
+        confidential_balance::balance_c_equals(&sender_amount, &recipient_amount),
+        error::invalid_argument(EINVALID_SENDER_AMOUNT)
+    );
+    assert!(
+        sender_auditor_hint.length() <= MAX_SENDER_AUDITOR_HINT_BYTES,
+        error::invalid_argument(EAUDITOR_HINT_TOO_LONG)
+    );
+
+    let from = signer::address_of(sender);
+
+    let sender_ek = encryption_key(from, token);
+    let recipient_ek = encryption_key(to, token);
+
+    let sender_ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(from, token));
+
+    let sender_current_actual_balance = confidential_balance::decompress_balance(
+        &sender_ca_store.actual_balance
+    );
+
+    let cid = (chain_id::get() as u8);
+    confidential_proof::verify_transfer_proof(
+        cid,
+        from,
+        @aptos_framework,
+        object::object_address(&token),
+        &sender_ek,
+        &recipient_ek,
+        &sender_current_actual_balance,
+        &new_balance,
+        &sender_amount,
+        &recipient_amount,
+        &auditor_eks,
+        &auditor_amounts,
+        &sender_auditor_hint,
+        &proof);
+
+    sender_ca_store.normalized = true;
+    let new_sender_available_balance = confidential_balance::compress_balance(&new_balance);
+    sender_ca_store.actual_balance = new_sender_available_balance;
+
+    let amount = confidential_balance::compress_balance(&recipient_amount);
+    let ek_volun_auds = confidential_proof::transfer_proof_ek_volun_auds_flat_bytes(&proof);
+
+    // Cannot create multiple mutable references to the same type, so we need to drop it
+    let ConfidentialAssetStore { .. } = sender_ca_store;
+
+    let recipient_ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(to, token));
+
+    assert!(
+        recipient_ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER,
+        error::invalid_argument(EINTERNAL_ERROR)
+    );
+
+    let recipient_pending_balance = confidential_balance::decompress_balance(
+        &recipient_ca_store.pending_balance
+    );
+    confidential_balance::add_balances_mut(&mut recipient_pending_balance, &recipient_amount);
+
+    recipient_ca_store.pending_counter += 1;
+    let new_recip_pending_balance = confidential_balance::compress_balance(&recipient_pending_balance);
+    recipient_ca_store.pending_balance = new_recip_pending_balance;
+
+    let chain_auditor_epoch = borrow_global<GlobalConfig>(@aptos_framework).chain_auditor_epoch;
+    let asset_auditor_epoch = get_asset_auditor_epoch(token);
+
+    event::emit(Transferred {
+        from,
+        to,
+        asset_type: object::object_address(&token),
+        amount,
+        ek_volun_auds,
+        sender_auditor_hint,
+        new_sender_available_balance,
+        new_recip_pending_balance,
+        memo: vector[],
+        chain_auditor_epoch,
+        asset_auditor_epoch,
+    });
+}
+
+ + + +
+ + + +## Function `rotate_encryption_key_internal` + +Implementation of the rotate_encryption_key entry function. + + +
public fun rotate_encryption_key_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_ek: ristretto255_twisted_elgamal::CompressedPubkey, new_balance: confidential_balance::ConfidentialBalance, proof: confidential_proof::RotationProof)
+
+ + + +
+Implementation + + +
public fun rotate_encryption_key_internal(
+    sender: &signer,
+    token: Object<Metadata>,
+    new_ek: twisted_elgamal::CompressedPubkey,
+    new_balance: confidential_balance::ConfidentialBalance,
+    proof: RotationProof) acquires ConfidentialAssetStore
+{
+    let user = signer::address_of(sender);
+    let current_ek = encryption_key(user, token);
+
+    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
+
+    let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance);
+
+    // We need to ensure that the pending balance is zero before rotating the key.
+    // To guarantee this, the user must call `rollover_pending_balance_and_freeze` beforehand.
+    assert!(confidential_balance::is_zero_balance(&pending_balance), error::invalid_state(ENOT_ZERO_BALANCE));
+
+    let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
+
+    let cid = (chain_id::get() as u8);
+    confidential_proof::verify_rotation_proof(
+        cid,
+        user,
+        @aptos_framework,
+        object::object_address(&token),
+        ¤t_ek,
+        &new_ek,
+        ¤t_balance,
+        &new_balance,
+        &proof
+    );
+
+    ca_store.ek = new_ek;
+    // We don't need to update the pending balance here, as it has been asserted to be zero.
+    ca_store.actual_balance = confidential_balance::compress_balance(&new_balance);
+    ca_store.normalized = true;
+
+    event::emit(KeyRotated {
+        addr: user,
+        asset_type: object::object_address(&token),
+        new_ek,
+        new_available_balance: ca_store.actual_balance,
+    });
+}
+
+ + + +
+ + + +## Function `normalize_internal` + +Implementation of the normalize entry function. + + +
public fun normalize_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>, new_balance: confidential_balance::ConfidentialBalance, proof: confidential_proof::NormalizationProof)
+
+ + + +
+Implementation + + +
public fun normalize_internal(
+    sender: &signer,
+    token: Object<Metadata>,
+    new_balance: confidential_balance::ConfidentialBalance,
+    proof: NormalizationProof) acquires ConfidentialAssetStore
+{
+    let user = signer::address_of(sender);
+    let sender_ek = encryption_key(user, token);
+
+    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
+
+    assert!(!ca_store.normalized, error::invalid_state(EALREADY_NORMALIZED));
+
+    let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
+
+    let cid = (chain_id::get() as u8);
+    confidential_proof::verify_normalization_proof(
+        cid,
+        user,
+        @aptos_framework,
+        object::object_address(&token),
+        &sender_ek,
+        ¤t_balance,
+        &new_balance,
+        &proof
+    );
+
+    ca_store.actual_balance = confidential_balance::compress_balance(&new_balance);
+    ca_store.normalized = true;
+
+    event::emit(Normalized {
+        addr: user,
+        asset_type: object::object_address(&token),
+        new_available_balance: ca_store.actual_balance,
+    });
+}
+
+ + + +
+ + + +## Function `rollover_pending_balance_internal` + +Implementation of the rollover_pending_balance entry function. + + +
public fun rollover_pending_balance_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public fun rollover_pending_balance_internal(
+    sender: &signer,
+    token: Object<Metadata>) acquires ConfidentialAssetStore
+{
+    let user = signer::address_of(sender);
+
+    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
+
+    assert!(ca_store.normalized, error::invalid_state(ENORMALIZATION_REQUIRED));
+
+    let actual_balance = confidential_balance::decompress_balance(&ca_store.actual_balance);
+    let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance);
+
+    confidential_balance::add_balances_mut(&mut actual_balance, &pending_balance);
+
+    ca_store.normalized = false;
+    ca_store.pending_counter = 0;
+    ca_store.actual_balance = confidential_balance::compress_balance(&actual_balance);
+    ca_store.pending_balance = confidential_balance::new_compressed_pending_balance_no_randomness();
+
+    event::emit(RolledOver {
+        addr: user,
+        asset_type: object::object_address(&token),
+        new_available_balance: ca_store.actual_balance,
+    });
+}
+
+ + + +
+ + + +## Function `freeze_token_internal` + +Implementation of the freeze_token entry function. + + +
public fun freeze_token_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public fun freeze_token_internal(
+    sender: &signer,
+    token: Object<Metadata>) acquires ConfidentialAssetStore
+{
+    let user = signer::address_of(sender);
+
+    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
+
+    assert!(!ca_store.frozen, error::invalid_state(EALREADY_FROZEN));
+
+    ca_store.frozen = true;
+
+    event::emit(FreezeChanged {
+        addr: user,
+        asset_type: object::object_address(&token),
+        frozen: true,
+    });
+}
+
+ + + +
+ + + +## Function `unfreeze_token_internal` + +Implementation of the unfreeze_token entry function. + + +
public fun unfreeze_token_internal(sender: &signer, token: object::Object<fungible_asset::Metadata>)
+
+ + + +
+Implementation + + +
public fun unfreeze_token_internal(
+    sender: &signer,
+    token: Object<Metadata>) acquires ConfidentialAssetStore
+{
+    let user = signer::address_of(sender);
+
+    assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED));
+
+    let ca_store = borrow_global_mut<ConfidentialAssetStore>(get_user_address(user, token));
+
+    assert!(ca_store.frozen, error::invalid_state(ENOT_FROZEN));
+
+    ca_store.frozen = false;
+
+    event::emit(FreezeChanged {
+        addr: user,
+        asset_type: object::object_address(&token),
+        frozen: false,
+    });
+}
+
+ + + +
+ + + +## Function `is_safe_for_confidentiality` + +Returns whether the given asset type is safe for use in confidential transfers. + +Dispatchable fungible assets can override withdraw, deposit, balance, or supply +behaviour in ways that are incompatible with encrypted on-chain balances (e.g., +fee-on-transfer tokens, rebasing balances, custom supply hooks). Until a safe +integration path exists, only standard (non-dispatchable) FA types are accepted. + + +
fun is_safe_for_confidentiality(token: &object::Object<fungible_asset::Metadata>): bool
+
+ + + +
+Implementation + + +
fun is_safe_for_confidentiality(token: &Object<Metadata>): bool {
+    !fungible_asset::is_asset_type_dispatchable(*token)
+}
+
+ + + +
+ + + +## Function `ensure_fa_config_exists` + +Ensures that the FAConfig object exists for the specified token. +If the object does not exist, creates it. +Used only for internal purposes. + + +
fun ensure_fa_config_exists(token: object::Object<fungible_asset::Metadata>): address
+
+ + + +
+Implementation + + +
fun ensure_fa_config_exists(token: Object<Metadata>): address acquires GlobalConfig {
+    let fa_config_address = get_fa_config_address(token);
+
+    if (!exists<FAConfig>(fa_config_address)) {
+        let fa_config_singer = get_fa_config_signer(token);
+
+        move_to(&fa_config_singer, FAConfig {
+            allowed: false,
+            asset_auditor_ek: std::option::none(),
+            asset_auditor_epoch: 0,
+        });
+    };
+
+    fa_config_address
+}
+
+ + + +
+ + + +## Function `get_fa_store_signer` + +Returns an object for handling all the FA primary stores, and returns a signer for it. + + +
fun get_fa_store_signer(): signer
+
+ + + +
+Implementation + + +
fun get_fa_store_signer(): signer acquires GlobalConfig {
+    object::generate_signer_for_extending(&borrow_global<GlobalConfig>(@aptos_framework).extend_ref)
+}
+
+ + + +
+ + + +## Function `get_fa_store_address` + +Returns the address that handles all the FA primary stores. + + +
fun get_fa_store_address(): address
+
+ + + +
+Implementation + + +
fun get_fa_store_address(): address acquires GlobalConfig {
+    object::address_from_extend_ref(&borrow_global<GlobalConfig>(@aptos_framework).extend_ref)
+}
+
+ + + +
+ + + +## Function `get_pool_fa_store` + +Returns the pool's primary fungible store for the given token, aborting if it does not exist. + + +
fun get_pool_fa_store(token: object::Object<fungible_asset::Metadata>): object::Object<fungible_asset::FungibleStore>
+
+ + + +
+Implementation + + +
fun get_pool_fa_store(token: Object<Metadata>): Object<FungibleStore> acquires GlobalConfig {
+    let pool_addr = get_fa_store_address();
+    assert!(primary_fungible_store::primary_store_exists(pool_addr, token), error::not_found(ENO_CONFIDENTIAL_ASSET_POOL));
+    primary_fungible_store::primary_store(pool_addr, token)
+}
+
+ + + +
+ + + +## Function `ensure_pool_fa_store` + +Returns the pool's primary fungible store for the given token, creating it if necessary. + + +
fun ensure_pool_fa_store(token: object::Object<fungible_asset::Metadata>): object::Object<fungible_asset::FungibleStore>
+
+ + + +
+Implementation + + +
fun ensure_pool_fa_store(token: Object<Metadata>): Object<FungibleStore> acquires GlobalConfig {
+    primary_fungible_store::ensure_primary_store_exists(get_fa_store_address(), token)
+}
+
+ + + +
+ + + +## Function `get_user_signer` + +Returns an object for handling the ConfidentialAssetStore and returns a signer for it. + + +
fun get_user_signer(user: &signer, token: object::Object<fungible_asset::Metadata>): signer
+
+ + + +
+Implementation + + +
fun get_user_signer(user: &signer, token: Object<Metadata>): signer {
+    let user_ctor = &object::create_named_object(user, construct_user_seed(token));
+
+    object::generate_signer(user_ctor)
+}
+
+ + + +
+ + + +## Function `get_user_address` + +Returns the address that handles the user's ConfidentialAssetStore object for the specified user and token. + + +
fun get_user_address(user: address, token: object::Object<fungible_asset::Metadata>): address
+
+ + + +
+Implementation + + +
fun get_user_address(user: address, token: Object<Metadata>): address {
+    object::create_object_address(&user, construct_user_seed(token))
+}
+
+ + + +
+ + + +## Function `get_fa_config_signer` + +Returns an object for handling the FAConfig, and returns a signer for it. + + +
fun get_fa_config_signer(token: object::Object<fungible_asset::Metadata>): signer
+
+ + + +
+Implementation + + +
fun get_fa_config_signer(token: Object<Metadata>): signer acquires GlobalConfig {
+    let fa_ext = &borrow_global<GlobalConfig>(@aptos_framework).extend_ref;
+    let fa_ext_signer = object::generate_signer_for_extending(fa_ext);
+
+    let fa_ctor = &object::create_named_object(&fa_ext_signer, construct_fa_seed(token));
+
+    object::generate_signer(fa_ctor)
+}
+
+ + + +
+ + + +## Function `get_fa_config_address` + +Returns the address that handles primary FA store and FAConfig objects for the specified token. + + +
fun get_fa_config_address(token: object::Object<fungible_asset::Metadata>): address
+
+ + + +
+Implementation + + +
fun get_fa_config_address(token: Object<Metadata>): address acquires GlobalConfig {
+    let fa_ext = &borrow_global<GlobalConfig>(@aptos_framework).extend_ref;
+    let fa_ext_address = object::address_from_extend_ref(fa_ext);
+
+    object::create_object_address(&fa_ext_address, construct_fa_seed(token))
+}
+
+ + + +
+ + + +## Function `construct_user_seed` + +Constructs a unique seed for the user's ConfidentialAssetStore object. +As all the ConfidentialAssetStore's have the same type, we need to differentiate them by the seed. + + +
fun construct_user_seed(token: object::Object<fungible_asset::Metadata>): vector<u8>
+
+ + + +
+Implementation + + +
fun construct_user_seed(token: Object<Metadata>): vector<u8> {
+    bcs::to_bytes(
+        &string_utils::format2(
+            &b"confidential_asset::{}::token::{}::user",
+            @aptos_framework,
+            object::object_address(&token)
+        )
+    )
+}
+
+ + + +
+ + + +## Function `construct_fa_seed` + +Constructs a unique seed for the FA's FAConfig object. +As all the FAConfig's have the same type, we need to differentiate them by the seed. + + +
fun construct_fa_seed(token: object::Object<fungible_asset::Metadata>): vector<u8>
+
+ + + +
+Implementation + + +
fun construct_fa_seed(token: Object<Metadata>): vector<u8> {
+    bcs::to_bytes(
+        &string_utils::format2(
+            &b"confidential_asset::{}::token::{}::fa",
+            @aptos_framework,
+            object::object_address(&token)
+        )
+    )
+}
+
+ + + +
+ + + +## Function `validate_auditors` + +Validates the auditor-related fields of a confidential transfer. + +Aborts with [ECHAIN_AUDITOR_NOT_SET] if no chain-level auditor has been +configured (transfers cannot proceed in that state). + +Returns false (rejecting the transfer) if any of: +- any auditor_amount does not encrypt the same plaintext as transfer_amount; +- the lengths of auditor_eks, auditor_amounts, and the transfer-proof auditor +row count disagree; +- auditor_eks is missing the required prefix (see slot layout below); +- the prefix slot keys do not equal the active chain / asset auditor keys. + +**Slot layout of auditor_eks** (and auditor_amounts): +```text +[0] chain-level auditor (always required) +[1] asset-specific auditor (required iff get_asset_auditor(token).is_some()) +[2..] voluntary auditors (sender's choice, ordered) +``` +Auditor identity at slots 0 and 1 is bound into the transfer's Fiat–Shamir +transcript (via the order in which auditor_eks is hashed in +confidential_proof::fiat_shamir_transfer_sigma_proof_challenge), so a sender +cannot substitute one auditor's slot for another's. + + +
fun validate_auditors(token: object::Object<fungible_asset::Metadata>, transfer_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, proof: &confidential_proof::TransferProof): bool
+
+ + + +
+Implementation + + +
fun validate_auditors(
+    token: Object<Metadata>,
+    transfer_amount: &confidential_balance::ConfidentialBalance,
+    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
+    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
+    proof: &TransferProof): bool acquires FAConfig, GlobalConfig
+{
+    if (
+        !auditor_amounts.all(|auditor_amount| {
+            confidential_balance::balance_c_equals(transfer_amount, auditor_amount)
+        })
+    ) {
+        return false
+    };
+
+    if (
+        auditor_eks.length() != auditor_amounts.length() ||
+            auditor_eks.length() != confidential_proof::auditors_count_in_transfer_proof(proof)
+    ) {
+        return false
+    };
+
+    let chain_auditor_ek_opt = borrow_global<GlobalConfig>(@aptos_framework).chain_auditor_ek;
+    assert!(chain_auditor_ek_opt.is_some(), error::invalid_state(ECHAIN_AUDITOR_NOT_SET));
+
+    let asset_auditor_ek_opt = get_asset_auditor(token);
+    let required_prefix = if (asset_auditor_ek_opt.is_some()) 2 else 1;
+
+    if (auditor_eks.length() < required_prefix) {
+        return false
+    };
+
+    let chain_auditor_point = twisted_elgamal::pubkey_to_point(&chain_auditor_ek_opt.extract());
+    let slot0_point = twisted_elgamal::pubkey_to_point(&auditor_eks[0]);
+    if (!ristretto255::point_equals(&chain_auditor_point, &slot0_point)) {
+        return false
+    };
+
+    if (asset_auditor_ek_opt.is_some()) {
+        let asset_auditor_point = twisted_elgamal::pubkey_to_point(&asset_auditor_ek_opt.extract());
+        let slot1_point = twisted_elgamal::pubkey_to_point(&auditor_eks[1]);
+        if (!ristretto255::point_equals(&asset_auditor_point, &slot1_point)) {
+            return false
+        };
+    };
+
+    true
+}
+
+ + + +
+ + + +## Function `deserialize_auditor_eks` + +Deserializes the auditor EKs from a byte array. +Returns Some(vector<twisted_elgamal::CompressedPubkey>) if the deserialization is successful, otherwise None. + + +
fun deserialize_auditor_eks(auditor_eks_bytes: vector<u8>): option::Option<vector<ristretto255_twisted_elgamal::CompressedPubkey>>
+
+ + + +
+Implementation + + +
fun deserialize_auditor_eks(
+    auditor_eks_bytes: vector<u8>): Option<vector<twisted_elgamal::CompressedPubkey>>
+{
+    if (auditor_eks_bytes.length() % 32 != 0) {
+        return std::option::none()
+    };
+
+    let auditors_count = auditor_eks_bytes.length() / 32;
+
+    let auditor_eks = vector::range(0, auditors_count).map(|i| {
+        twisted_elgamal::new_pubkey_from_bytes(auditor_eks_bytes.slice(i * 32, (i + 1) * 32))
+    });
+
+    if (auditor_eks.any(|ek| ek.is_none())) {
+        return std::option::none()
+    };
+
+    std::option::some(auditor_eks.map(|ek| ek.extract()))
+}
+
+ + + +
+ + + +## Function `deserialize_auditor_amounts` + +Deserializes the auditor amounts from a byte array. +Returns Some(vector<confidential_balance::ConfidentialBalance>) if the deserialization is successful, otherwise None. + + +
fun deserialize_auditor_amounts(auditor_amounts_bytes: vector<u8>): option::Option<vector<confidential_balance::ConfidentialBalance>>
+
+ + + +
+Implementation + + +
fun deserialize_auditor_amounts(
+    auditor_amounts_bytes: vector<u8>): Option<vector<confidential_balance::ConfidentialBalance>>
+{
+    if (auditor_amounts_bytes.length() % 256 != 0) {
+        return std::option::none()
+    };
+
+    let auditors_count = auditor_amounts_bytes.length() / 256;
+
+    let auditor_amounts = vector::range(0, auditors_count).map(|i| {
+        confidential_balance::new_pending_balance_from_bytes(auditor_amounts_bytes.slice(i * 256, (i + 1) * 256))
+    });
+
+    if (auditor_amounts.any(|ek| ek.is_none())) {
+        return std::option::none()
+    };
+
+    std::option::some(auditor_amounts.map(|balance| balance.extract()))
+}
+
+ + + +
+ + + +## Function `ensure_sufficient_fa` + +Converts coins to missing FA. +Returns Some(Object<Metadata>) if user has a sufficient amount of FA to proceed, otherwise None. + + +
fun ensure_sufficient_fa<CoinType>(sender: &signer, amount: u64): option::Option<object::Object<fungible_asset::Metadata>>
+
+ + + +
+Implementation + + +
fun ensure_sufficient_fa<CoinType>(sender: &signer, amount: u64): Option<Object<Metadata>> {
+    let user = signer::address_of(sender);
+    let fa = coin::paired_metadata<CoinType>();
+
+    if (fa.is_none()) {
+        return fa;
+    };
+
+    let fa_balance = primary_fungible_store::balance(user, *fa.borrow());
+
+    if (fa_balance >= amount) {
+        return fa;
+    };
+
+    if (coin::balance<CoinType>(user) < amount) {
+        return std::option::none();
+    };
+
+    let coin_amount = coin::withdraw<CoinType>(sender, amount - fa_balance);
+    let fa_amount = coin::coin_to_fungible_asset(coin_amount);
+
+    primary_fungible_store::deposit(user, fa_amount);
+
+    fa
+}
+
+ + + +
+ + + +## Function `serialize_auditor_eks` + +Pure serialization helpers (no borrow_global). Public so off-chain tooling and +tooling can exercise the same entrypoints as tests without #[test_only] harness modules. + + +
public fun serialize_auditor_eks(auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>): vector<u8>
+
+ + + +
+Implementation + + +
public fun serialize_auditor_eks(auditor_eks: &vector<twisted_elgamal::CompressedPubkey>): vector<u8> {
+    let auditor_eks_bytes = vector[];
+
+    auditor_eks.for_each_ref(|auditor| {
+        auditor_eks_bytes.append(twisted_elgamal::pubkey_to_bytes(auditor));
+    });
+
+    auditor_eks_bytes
+}
+
+ + + +
+ + + +## Function `serialize_auditor_amounts` + + + +
public fun serialize_auditor_amounts(auditor_amounts: &vector<confidential_balance::ConfidentialBalance>): vector<u8>
+
+ + + +
+Implementation + + +
public fun serialize_auditor_amounts(
+    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>
+): vector<u8> {
+    let auditor_amounts_bytes = vector[];
+
+    auditor_amounts.for_each_ref(|balance| {
+        auditor_amounts_bytes.append(confidential_balance::balance_to_bytes(balance));
+    });
+
+    auditor_amounts_bytes
+}
+
+ + + +
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/doc/confidential_balance.md b/aptos-move/framework/aptos-framework/doc/confidential_balance.md new file mode 100644 index 00000000000..95785fd8779 --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/confidential_balance.md @@ -0,0 +1,790 @@ + + + +# Module `0x1::confidential_balance` + +This module implements a Confidential Balance abstraction, built on top of Twisted ElGamal encryption, +over the Ristretto255 curve. + +The Confidential Balance encapsulates encrypted representations of a balance, split into chunks and stored as pairs of +ciphertext components (C_i, D_i) under basepoints G and H and an encryption key P = dk^(-1) * H, where dk +is the corresponding decryption key. Each pair represents an encrypted value a_i - the i-th 16-bit portion of +the total encrypted amount - and its associated randomness r_i, such that C_i = a_i * G + r_i * H and D_i = r_i * P. + +The module supports two types of balances: +- Pending balances are represented by four ciphertext pairs (C_i, D_i), i = 1..4, suitable for 64-bit values. +- Actual balances are represented by eight ciphertext pairs (C_i, D_i), i = 1..8, capable of handling 128-bit values. + +This implementation leverages the homomorphic properties of Twisted ElGamal encryption to allow arithmetic operations +directly on encrypted data. + + +- [Struct `CompressedConfidentialBalance`](#0x1_confidential_balance_CompressedConfidentialBalance) +- [Struct `ConfidentialBalance`](#0x1_confidential_balance_ConfidentialBalance) +- [Constants](#@Constants_0) +- [Function `new_pending_balance_no_randomness`](#0x1_confidential_balance_new_pending_balance_no_randomness) +- [Function `new_actual_balance_no_randomness`](#0x1_confidential_balance_new_actual_balance_no_randomness) +- [Function `new_compressed_pending_balance_no_randomness`](#0x1_confidential_balance_new_compressed_pending_balance_no_randomness) +- [Function `new_compressed_actual_balance_no_randomness`](#0x1_confidential_balance_new_compressed_actual_balance_no_randomness) +- [Function `new_pending_balance_u64_no_randonmess`](#0x1_confidential_balance_new_pending_balance_u64_no_randonmess) +- [Function `new_pending_balance_from_bytes`](#0x1_confidential_balance_new_pending_balance_from_bytes) +- [Function `new_actual_balance_from_bytes`](#0x1_confidential_balance_new_actual_balance_from_bytes) +- [Function `compress_balance`](#0x1_confidential_balance_compress_balance) +- [Function `decompress_balance`](#0x1_confidential_balance_decompress_balance) +- [Function `balance_to_bytes`](#0x1_confidential_balance_balance_to_bytes) +- [Function `balance_to_points_c`](#0x1_confidential_balance_balance_to_points_c) +- [Function `balance_to_points_d`](#0x1_confidential_balance_balance_to_points_d) +- [Function `add_balances_mut`](#0x1_confidential_balance_add_balances_mut) +- [Function `balance_equals`](#0x1_confidential_balance_balance_equals) +- [Function `balance_c_equals`](#0x1_confidential_balance_balance_c_equals) +- [Function `is_zero_balance`](#0x1_confidential_balance_is_zero_balance) +- [Function `split_into_chunks_u64`](#0x1_confidential_balance_split_into_chunks_u64) +- [Function `split_into_chunks_u128`](#0x1_confidential_balance_split_into_chunks_u128) +- [Function `get_pending_balance_chunks`](#0x1_confidential_balance_get_pending_balance_chunks) +- [Function `get_actual_balance_chunks`](#0x1_confidential_balance_get_actual_balance_chunks) +- [Function `get_chunk_size_bits`](#0x1_confidential_balance_get_chunk_size_bits) + + +
use 0x1::error;
+use 0x1::option;
+use 0x1::ristretto255;
+use 0x1::ristretto255_twisted_elgamal;
+use 0x1::vector;
+
+ + + + + +## Struct `CompressedConfidentialBalance` + +Represents a compressed confidential balance, where each chunk is a compressed Twisted ElGamal ciphertext. + + +
struct CompressedConfidentialBalance has copy, drop, store
+
+ + + +
+Fields + + +
+
+chunks: vector<ristretto255_twisted_elgamal::CompressedCiphertext> +
+
+ +
+
+ + +
+ + + +## Struct `ConfidentialBalance` + +Represents a confidential balance, where each chunk is a Twisted ElGamal ciphertext. + + +
struct ConfidentialBalance has drop
+
+ + + +
+Fields + + +
+
+chunks: vector<ristretto255_twisted_elgamal::Ciphertext> +
+
+ +
+
+ + +
+ + + +## Constants + + + + +The number of chunks in an actual balance. + + +
const ACTUAL_BALANCE_CHUNKS: u64 = 8;
+
+ + + + + +The number of bits in a single chunk. + + +
const CHUNK_SIZE_BITS: u64 = 16;
+
+ + + + + +An internal error occurred, indicating unexpected behavior. + + +
const EINTERNAL_ERROR: u64 = 1;
+
+ + + + + +The number of chunks in a pending balance. + + +
const PENDING_BALANCE_CHUNKS: u64 = 4;
+
+ + + + + +## Function `new_pending_balance_no_randomness` + +Creates a new zero pending balance, where each chunk is set to zero Twisted ElGamal ciphertext. + + +
public fun new_pending_balance_no_randomness(): confidential_balance::ConfidentialBalance
+
+ + + +
+Implementation + + +
public fun new_pending_balance_no_randomness(): ConfidentialBalance {
+    ConfidentialBalance {
+        chunks: vector::range(0, PENDING_BALANCE_CHUNKS).map(|_| {
+            twisted_elgamal::ciphertext_from_points(ristretto255::point_identity(), ristretto255::point_identity())
+        })
+    }
+}
+
+ + + +
+ + + +## Function `new_actual_balance_no_randomness` + +Creates a new zero actual balance, where each chunk is set to zero Twisted ElGamal ciphertext. + + +
public fun new_actual_balance_no_randomness(): confidential_balance::ConfidentialBalance
+
+ + + +
+Implementation + + +
public fun new_actual_balance_no_randomness(): ConfidentialBalance {
+    ConfidentialBalance {
+        chunks: vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|_| {
+            twisted_elgamal::ciphertext_from_points(ristretto255::point_identity(), ristretto255::point_identity())
+        })
+    }
+}
+
+ + + +
+ + + +## Function `new_compressed_pending_balance_no_randomness` + +Creates a new compressed zero pending balance, where each chunk is set to compressed zero Twisted ElGamal ciphertext. + + +
public fun new_compressed_pending_balance_no_randomness(): confidential_balance::CompressedConfidentialBalance
+
+ + + +
+Implementation + + +
public fun new_compressed_pending_balance_no_randomness(): CompressedConfidentialBalance {
+    CompressedConfidentialBalance {
+        chunks: vector::range(0, PENDING_BALANCE_CHUNKS).map(|_| {
+            twisted_elgamal::ciphertext_from_compressed_points(
+                ristretto255::point_identity_compressed(), ristretto255::point_identity_compressed())
+        })
+    }
+}
+
+ + + +
+ + + +## Function `new_compressed_actual_balance_no_randomness` + +Creates a new compressed zero actual balance, where each chunk is set to compressed zero Twisted ElGamal ciphertext. + + +
public fun new_compressed_actual_balance_no_randomness(): confidential_balance::CompressedConfidentialBalance
+
+ + + +
+Implementation + + +
public fun new_compressed_actual_balance_no_randomness(): CompressedConfidentialBalance {
+    CompressedConfidentialBalance {
+        chunks: vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|_| {
+            twisted_elgamal::ciphertext_from_compressed_points(
+                ristretto255::point_identity_compressed(), ristretto255::point_identity_compressed())
+        })
+    }
+}
+
+ + + +
+ + + +## Function `new_pending_balance_u64_no_randonmess` + +Creates a new pending balance from a 64-bit amount with no randomness, splitting the amount into four 16-bit chunks. + + +
public fun new_pending_balance_u64_no_randonmess(amount: u64): confidential_balance::ConfidentialBalance
+
+ + + +
+Implementation + + +
public fun new_pending_balance_u64_no_randonmess(amount: u64): ConfidentialBalance {
+    ConfidentialBalance {
+        chunks: split_into_chunks_u64(amount).map(|chunk| {
+            twisted_elgamal::new_ciphertext_no_randomness(&chunk)
+        })
+    }
+}
+
+ + + +
+ + + +## Function `new_pending_balance_from_bytes` + +Creates a new pending balance from a serialized byte array representation. +Returns Some(ConfidentialBalance) if deserialization succeeds, otherwise None. + + +
public fun new_pending_balance_from_bytes(bytes: vector<u8>): option::Option<confidential_balance::ConfidentialBalance>
+
+ + + +
+Implementation + + +
public fun new_pending_balance_from_bytes(bytes: vector<u8>): Option<ConfidentialBalance> {
+    if (bytes.length() != 64 * PENDING_BALANCE_CHUNKS) {
+        return std::option::none()
+    };
+
+    let chunks = vector::range(0, PENDING_BALANCE_CHUNKS).map(|i| {
+        twisted_elgamal::new_ciphertext_from_bytes(bytes.slice(i * 64, (i + 1) * 64))
+    });
+
+    if (chunks.any(|chunk| chunk.is_none())) {
+        return std::option::none()
+    };
+
+    option::some(ConfidentialBalance {
+        chunks: chunks.map(|chunk| chunk.extract())
+    })
+}
+
+ + + +
+ + + +## Function `new_actual_balance_from_bytes` + +Creates a new actual balance from a serialized byte array representation. +Returns Some(ConfidentialBalance) if deserialization succeeds, otherwise None. + + +
public fun new_actual_balance_from_bytes(bytes: vector<u8>): option::Option<confidential_balance::ConfidentialBalance>
+
+ + + +
+Implementation + + +
public fun new_actual_balance_from_bytes(bytes: vector<u8>): Option<ConfidentialBalance> {
+    if (bytes.length() != 64 * ACTUAL_BALANCE_CHUNKS) {
+        return std::option::none()
+    };
+
+    let chunks = vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|i| {
+        twisted_elgamal::new_ciphertext_from_bytes(bytes.slice(i * 64, (i + 1) * 64))
+    });
+
+    if (chunks.any(|chunk| chunk.is_none())) {
+        return std::option::none()
+    };
+
+    option::some(ConfidentialBalance {
+        chunks: chunks.map(|chunk| chunk.extract())
+    })
+}
+
+ + + +
+ + + +## Function `compress_balance` + +Compresses a confidential balance into its CompressedConfidentialBalance representation. + + +
public fun compress_balance(balance: &confidential_balance::ConfidentialBalance): confidential_balance::CompressedConfidentialBalance
+
+ + + +
+Implementation + + +
public fun compress_balance(balance: &ConfidentialBalance): CompressedConfidentialBalance {
+    CompressedConfidentialBalance {
+        chunks: balance.chunks.map_ref(|ciphertext| twisted_elgamal::compress_ciphertext(ciphertext))
+    }
+}
+
+ + + +
+ + + +## Function `decompress_balance` + +Decompresses a compressed confidential balance into its ConfidentialBalance representation. + + +
public fun decompress_balance(balance: &confidential_balance::CompressedConfidentialBalance): confidential_balance::ConfidentialBalance
+
+ + + +
+Implementation + + +
public fun decompress_balance(balance: &CompressedConfidentialBalance): ConfidentialBalance {
+    ConfidentialBalance {
+        chunks: balance.chunks.map_ref(|ciphertext| twisted_elgamal::decompress_ciphertext(ciphertext))
+    }
+}
+
+ + + +
+ + + +## Function `balance_to_bytes` + +Serializes a confidential balance into a byte array representation. + + +
public fun balance_to_bytes(balance: &confidential_balance::ConfidentialBalance): vector<u8>
+
+ + + +
+Implementation + + +
public fun balance_to_bytes(balance: &ConfidentialBalance): vector<u8> {
+    let bytes = vector<u8>[];
+
+    balance.chunks.for_each_ref(|ciphertext| {
+        bytes.append(twisted_elgamal::ciphertext_to_bytes(ciphertext));
+    });
+
+    bytes
+}
+
+ + + +
+ + + +## Function `balance_to_points_c` + +Extracts the C value component (a * H + r * G) of each chunk in a confidential balance as a vector of RistrettoPoints. + + +
public fun balance_to_points_c(balance: &confidential_balance::ConfidentialBalance): vector<ristretto255::RistrettoPoint>
+
+ + + +
+Implementation + + +
public fun balance_to_points_c(balance: &ConfidentialBalance): vector<RistrettoPoint> {
+    balance.chunks.map_ref(|chunk| {
+        let (c, _) = twisted_elgamal::ciphertext_as_points(chunk);
+        ristretto255::point_clone(c)
+    })
+}
+
+ + + +
+ + + +## Function `balance_to_points_d` + +Extracts the D randomness component (r * Y) of each chunk in a confidential balance as a vector of RistrettoPoints. + + +
public fun balance_to_points_d(balance: &confidential_balance::ConfidentialBalance): vector<ristretto255::RistrettoPoint>
+
+ + + +
+Implementation + + +
public fun balance_to_points_d(balance: &ConfidentialBalance): vector<RistrettoPoint> {
+    balance.chunks.map_ref(|chunk| {
+        let (_, d) = twisted_elgamal::ciphertext_as_points(chunk);
+        ristretto255::point_clone(d)
+    })
+}
+
+ + + +
+ + + +## Function `add_balances_mut` + +Adds two confidential balances homomorphically, mutating the first balance in place. +The second balance must have fewer or equal chunks compared to the first. + + +
public fun add_balances_mut(lhs: &mut confidential_balance::ConfidentialBalance, rhs: &confidential_balance::ConfidentialBalance)
+
+ + + +
+Implementation + + +
public fun add_balances_mut(lhs: &mut ConfidentialBalance, rhs: &ConfidentialBalance) {
+    assert!(lhs.chunks.length() >= rhs.chunks.length(), error::internal(EINTERNAL_ERROR));
+
+    lhs.chunks.enumerate_mut(|i, chunk| {
+        if (i < rhs.chunks.length()) {
+            twisted_elgamal::ciphertext_add_assign(chunk, &rhs.chunks[i])
+        }
+    })
+}
+
+ + + +
+ + + +## Function `balance_equals` + +Checks if two confidential balances are equivalent, including both value and randomness components. + + +
public fun balance_equals(lhs: &confidential_balance::ConfidentialBalance, rhs: &confidential_balance::ConfidentialBalance): bool
+
+ + + +
+Implementation + + +
public fun balance_equals(lhs: &ConfidentialBalance, rhs: &ConfidentialBalance): bool {
+    assert!(lhs.chunks.length() == rhs.chunks.length(), error::internal(EINTERNAL_ERROR));
+
+    let ok = true;
+
+    lhs.chunks.zip_ref(&rhs.chunks, |l, r| {
+        ok = ok && twisted_elgamal::ciphertext_equals(l, r);
+    });
+
+    ok
+}
+
+ + + +
+ + + +## Function `balance_c_equals` + +Checks if the corresponding value components (C) of two confidential balances are equivalent. + + +
public fun balance_c_equals(lhs: &confidential_balance::ConfidentialBalance, rhs: &confidential_balance::ConfidentialBalance): bool
+
+ + + +
+Implementation + + +
public fun balance_c_equals(lhs: &ConfidentialBalance, rhs: &ConfidentialBalance): bool {
+    assert!(lhs.chunks.length() == rhs.chunks.length(), error::internal(EINTERNAL_ERROR));
+
+    let ok = true;
+
+    lhs.chunks.zip_ref(&rhs.chunks, |l, r| {
+        let (lc, _) = twisted_elgamal::ciphertext_as_points(l);
+        let (rc, _) = twisted_elgamal::ciphertext_as_points(r);
+
+        ok = ok && ristretto255::point_equals(lc, rc);
+    });
+
+    ok
+}
+
+ + + +
+ + + +## Function `is_zero_balance` + +Checks if a confidential balance is equivalent to zero, where all chunks are the identity element. + + +
public fun is_zero_balance(balance: &confidential_balance::ConfidentialBalance): bool
+
+ + + +
+Implementation + + +
public fun is_zero_balance(balance: &ConfidentialBalance): bool {
+    balance.chunks.all(|chunk| {
+        twisted_elgamal::ciphertext_equals(
+            chunk,
+            &twisted_elgamal::ciphertext_from_points(ristretto255::point_identity(), ristretto255::point_identity())
+        )
+    })
+}
+
+ + + +
+ + + +## Function `split_into_chunks_u64` + +Splits a 64-bit integer amount into four 16-bit chunks, represented as Scalar values. + + +
public fun split_into_chunks_u64(amount: u64): vector<ristretto255::Scalar>
+
+ + + +
+Implementation + + +
public fun split_into_chunks_u64(amount: u64): vector<Scalar> {
+    vector::range(0, PENDING_BALANCE_CHUNKS).map(|i| {
+        ristretto255::new_scalar_from_u64(amount >> (i * CHUNK_SIZE_BITS as u8) & 0xffff)
+    })
+}
+
+ + + +
+ + + +## Function `split_into_chunks_u128` + +Splits a 128-bit integer amount into eight 16-bit chunks, represented as Scalar values. + + +
public fun split_into_chunks_u128(amount: u128): vector<ristretto255::Scalar>
+
+ + + +
+Implementation + + +
public fun split_into_chunks_u128(amount: u128): vector<Scalar> {
+    vector::range(0, ACTUAL_BALANCE_CHUNKS).map(|i| {
+        ristretto255::new_scalar_from_u128(amount >> (i * CHUNK_SIZE_BITS as u8) & 0xffff)
+    })
+}
+
+ + + +
+ + + +## Function `get_pending_balance_chunks` + +Returns the number of chunks in a pending balance. + + +
#[view]
+public fun get_pending_balance_chunks(): u64
+
+ + + +
+Implementation + + +
public fun get_pending_balance_chunks(): u64 {
+    PENDING_BALANCE_CHUNKS
+}
+
+ + + +
+ + + +## Function `get_actual_balance_chunks` + +Returns the number of chunks in an actual balance. + + +
#[view]
+public fun get_actual_balance_chunks(): u64
+
+ + + +
+Implementation + + +
public fun get_actual_balance_chunks(): u64 {
+    ACTUAL_BALANCE_CHUNKS
+}
+
+ + + +
+ + + +## Function `get_chunk_size_bits` + +Returns the number of bits in a single chunk. + + +
#[view]
+public fun get_chunk_size_bits(): u64
+
+ + + +
+Implementation + + +
public fun get_chunk_size_bits(): u64 {
+    CHUNK_SIZE_BITS
+}
+
+ + + +
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/doc/confidential_proof.md b/aptos-move/framework/aptos-framework/doc/confidential_proof.md new file mode 100644 index 00000000000..fb1a57d7645 --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/confidential_proof.md @@ -0,0 +1,3303 @@ + + + +# Module `0x1::confidential_proof` + +The confidential_proof module provides the infrastructure for verifying zero-knowledge proofs used in the Confidential Asset protocol. +These proofs ensure correctness for operations such as confidential_transfer, withdraw, rotate_encryption_key, and normalize. + + +- [Struct `WithdrawalProof`](#0x1_confidential_proof_WithdrawalProof) +- [Struct `TransferProof`](#0x1_confidential_proof_TransferProof) +- [Struct `NormalizationProof`](#0x1_confidential_proof_NormalizationProof) +- [Struct `RotationProof`](#0x1_confidential_proof_RotationProof) +- [Struct `WithdrawalSigmaProofXs`](#0x1_confidential_proof_WithdrawalSigmaProofXs) +- [Struct `WithdrawalSigmaProofAlphas`](#0x1_confidential_proof_WithdrawalSigmaProofAlphas) +- [Struct `WithdrawalSigmaProofGammas`](#0x1_confidential_proof_WithdrawalSigmaProofGammas) +- [Struct `WithdrawalSigmaProof`](#0x1_confidential_proof_WithdrawalSigmaProof) +- [Struct `TransferSigmaProofXs`](#0x1_confidential_proof_TransferSigmaProofXs) +- [Struct `TransferSigmaProofAlphas`](#0x1_confidential_proof_TransferSigmaProofAlphas) +- [Struct `TransferSigmaProofGammas`](#0x1_confidential_proof_TransferSigmaProofGammas) +- [Struct `TransferSigmaProof`](#0x1_confidential_proof_TransferSigmaProof) +- [Struct `NormalizationSigmaProofXs`](#0x1_confidential_proof_NormalizationSigmaProofXs) +- [Struct `NormalizationSigmaProofAlphas`](#0x1_confidential_proof_NormalizationSigmaProofAlphas) +- [Struct `NormalizationSigmaProofGammas`](#0x1_confidential_proof_NormalizationSigmaProofGammas) +- [Struct `NormalizationSigmaProof`](#0x1_confidential_proof_NormalizationSigmaProof) +- [Struct `RotationSigmaProofXs`](#0x1_confidential_proof_RotationSigmaProofXs) +- [Struct `RotationSigmaProofAlphas`](#0x1_confidential_proof_RotationSigmaProofAlphas) +- [Struct `RotationSigmaProofGammas`](#0x1_confidential_proof_RotationSigmaProofGammas) +- [Struct `RotationSigmaProof`](#0x1_confidential_proof_RotationSigmaProof) +- [Constants](#@Constants_0) +- [Function `verify_registration_proof`](#0x1_confidential_proof_verify_registration_proof) +- [Function `verify_withdrawal_proof`](#0x1_confidential_proof_verify_withdrawal_proof) +- [Function `verify_transfer_proof`](#0x1_confidential_proof_verify_transfer_proof) +- [Function `verify_normalization_proof`](#0x1_confidential_proof_verify_normalization_proof) +- [Function `verify_rotation_proof`](#0x1_confidential_proof_verify_rotation_proof) +- [Function `verify_withdrawal_sigma_proof`](#0x1_confidential_proof_verify_withdrawal_sigma_proof) +- [Function `verify_transfer_sigma_proof`](#0x1_confidential_proof_verify_transfer_sigma_proof) +- [Function `verify_normalization_sigma_proof`](#0x1_confidential_proof_verify_normalization_sigma_proof) +- [Function `verify_rotation_sigma_proof`](#0x1_confidential_proof_verify_rotation_sigma_proof) +- [Function `verify_new_balance_range_proof`](#0x1_confidential_proof_verify_new_balance_range_proof) +- [Function `verify_transfer_amount_range_proof`](#0x1_confidential_proof_verify_transfer_amount_range_proof) +- [Function `auditors_count_in_transfer_proof`](#0x1_confidential_proof_auditors_count_in_transfer_proof) +- [Function `transfer_proof_ek_volun_auds_flat_bytes`](#0x1_confidential_proof_transfer_proof_ek_volun_auds_flat_bytes) +- [Function `deserialize_withdrawal_proof`](#0x1_confidential_proof_deserialize_withdrawal_proof) +- [Function `deserialize_transfer_proof`](#0x1_confidential_proof_deserialize_transfer_proof) +- [Function `deserialize_normalization_proof`](#0x1_confidential_proof_deserialize_normalization_proof) +- [Function `deserialize_rotation_proof`](#0x1_confidential_proof_deserialize_rotation_proof) +- [Function `deserialize_withdrawal_sigma_proof`](#0x1_confidential_proof_deserialize_withdrawal_sigma_proof) +- [Function `deserialize_transfer_sigma_proof`](#0x1_confidential_proof_deserialize_transfer_sigma_proof) +- [Function `deserialize_normalization_sigma_proof`](#0x1_confidential_proof_deserialize_normalization_sigma_proof) +- [Function `deserialize_rotation_sigma_proof`](#0x1_confidential_proof_deserialize_rotation_sigma_proof) +- [Function `get_fiat_shamir_withdrawal_sigma_dst`](#0x1_confidential_proof_get_fiat_shamir_withdrawal_sigma_dst) +- [Function `get_fiat_shamir_transfer_sigma_dst`](#0x1_confidential_proof_get_fiat_shamir_transfer_sigma_dst) +- [Function `get_fiat_shamir_normalization_sigma_dst`](#0x1_confidential_proof_get_fiat_shamir_normalization_sigma_dst) +- [Function `get_fiat_shamir_rotation_sigma_dst`](#0x1_confidential_proof_get_fiat_shamir_rotation_sigma_dst) +- [Function `get_fiat_shamir_registration_sigma_dst`](#0x1_confidential_proof_get_fiat_shamir_registration_sigma_dst) +- [Function `get_bulletproofs_dst`](#0x1_confidential_proof_get_bulletproofs_dst) +- [Function `get_bulletproofs_num_bits`](#0x1_confidential_proof_get_bulletproofs_num_bits) +- [Function `prepend_domain_context`](#0x1_confidential_proof_prepend_domain_context) +- [Function `fiat_shamir_withdrawal_sigma_proof_challenge`](#0x1_confidential_proof_fiat_shamir_withdrawal_sigma_proof_challenge) +- [Function `fiat_shamir_transfer_sigma_proof_challenge`](#0x1_confidential_proof_fiat_shamir_transfer_sigma_proof_challenge) +- [Function `fiat_shamir_normalization_sigma_proof_challenge`](#0x1_confidential_proof_fiat_shamir_normalization_sigma_proof_challenge) +- [Function `fiat_shamir_rotation_sigma_proof_challenge`](#0x1_confidential_proof_fiat_shamir_rotation_sigma_proof_challenge) +- [Function `msm_withdrawal_gammas`](#0x1_confidential_proof_msm_withdrawal_gammas) +- [Function `msm_transfer_gammas`](#0x1_confidential_proof_msm_transfer_gammas) +- [Function `msm_normalization_gammas`](#0x1_confidential_proof_msm_normalization_gammas) +- [Function `msm_rotation_gammas`](#0x1_confidential_proof_msm_rotation_gammas) +- [Function `msm_gamma_1`](#0x1_confidential_proof_msm_gamma_1) +- [Function `msm_gamma_2`](#0x1_confidential_proof_msm_gamma_2) +- [Function `scalar_mul_3`](#0x1_confidential_proof_scalar_mul_3) +- [Function `scalar_linear_combination`](#0x1_confidential_proof_scalar_linear_combination) +- [Function `new_scalar_from_pow2`](#0x1_confidential_proof_new_scalar_from_pow2) + + +
use 0x1::bcs;
+use 0x1::confidential_balance;
+use 0x1::error;
+use 0x1::option;
+use 0x1::ristretto255;
+use 0x1::ristretto255_bulletproofs;
+use 0x1::ristretto255_twisted_elgamal;
+use 0x1::vector;
+
+ + + + + +## Struct `WithdrawalProof` + +Represents the proof structure for validating a withdrawal operation. + + +
struct WithdrawalProof has drop
+
+ + + +
+Fields + + +
+
+sigma_proof: confidential_proof::WithdrawalSigmaProof +
+
+ Sigma proof ensuring that the withdrawal operation maintains balance integrity. +
+
+zkrp_new_balance: ristretto255_bulletproofs::RangeProof +
+
+ Range proof ensuring that the resulting balance chunks are normalized (i.e., within the 16-bit limit). +
+
+ + +
+ + + +## Struct `TransferProof` + +Represents the proof structure for validating a transfer operation. + + +
struct TransferProof has drop
+
+ + + +
+Fields + + +
+
+sigma_proof: confidential_proof::TransferSigmaProof +
+
+ Sigma proof ensuring that the transfer operation maintains balance integrity and correctness. +
+
+zkrp_new_balance: ristretto255_bulletproofs::RangeProof +
+
+ Range proof ensuring that the resulting balance chunks for the sender are normalized (i.e., within the 16-bit limit). +
+
+zkrp_transfer_amount: ristretto255_bulletproofs::RangeProof +
+
+ Range proof ensuring that the transferred amount chunks are normalized (i.e., within the 16-bit limit). +
+
+ + +
+ + + +## Struct `NormalizationProof` + +Represents the proof structure for validating a normalization operation. + + +
struct NormalizationProof has drop
+
+ + + +
+Fields + + +
+
+sigma_proof: confidential_proof::NormalizationSigmaProof +
+
+ Sigma proof ensuring that the normalization operation maintains balance integrity. +
+
+zkrp_new_balance: ristretto255_bulletproofs::RangeProof +
+
+ Range proof ensuring that the resulting balance chunks are normalized (i.e., within the 16-bit limit). +
+
+ + +
+ + + +## Struct `RotationProof` + +Represents the proof structure for validating a key rotation operation. + + +
struct RotationProof has drop
+
+ + + +
+Fields + + +
+
+sigma_proof: confidential_proof::RotationSigmaProof +
+
+ Sigma proof ensuring that the key rotation operation preserves balance integrity. +
+
+zkrp_new_balance: ristretto255_bulletproofs::RangeProof +
+
+ Range proof ensuring that the resulting balance chunks after key rotation are normalized (i.e., within the 16-bit limit). +
+
+ + +
+ + + +## Struct `WithdrawalSigmaProofXs` + + + +
struct WithdrawalSigmaProofXs has drop
+
+ + + +
+Fields + + +
+
+x1: ristretto255::CompressedRistretto +
+
+ +
+
+x2: ristretto255::CompressedRistretto +
+
+ +
+
+x3s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+x4s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+ + +
+ + + +## Struct `WithdrawalSigmaProofAlphas` + + + +
struct WithdrawalSigmaProofAlphas has drop
+
+ + + +
+Fields + + +
+
+a1s: vector<ristretto255::Scalar> +
+
+ +
+
+a2: ristretto255::Scalar +
+
+ +
+
+a3: ristretto255::Scalar +
+
+ +
+
+a4s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `WithdrawalSigmaProofGammas` + + + +
struct WithdrawalSigmaProofGammas has drop
+
+ + + +
+Fields + + +
+
+g1: ristretto255::Scalar +
+
+ +
+
+g2: ristretto255::Scalar +
+
+ +
+
+g3s: vector<ristretto255::Scalar> +
+
+ +
+
+g4s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `WithdrawalSigmaProof` + + + +
struct WithdrawalSigmaProof has drop
+
+ + + +
+Fields + + +
+
+alphas: confidential_proof::WithdrawalSigmaProofAlphas +
+
+ +
+
+xs: confidential_proof::WithdrawalSigmaProofXs +
+
+ +
+
+ + +
+ + + +## Struct `TransferSigmaProofXs` + + + +
struct TransferSigmaProofXs has drop
+
+ + + +
+Fields + + +
+
+x1: ristretto255::CompressedRistretto +
+
+ +
+
+x2s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+x3s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+x4s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+x5: ristretto255::CompressedRistretto +
+
+ +
+
+x6s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+x7s: vector<vector<ristretto255::CompressedRistretto>> +
+
+ +
+
+x8s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+ + +
+ + + +## Struct `TransferSigmaProofAlphas` + + + +
struct TransferSigmaProofAlphas has drop
+
+ + + +
+Fields + + +
+
+a1s: vector<ristretto255::Scalar> +
+
+ +
+
+a2: ristretto255::Scalar +
+
+ +
+
+a3s: vector<ristretto255::Scalar> +
+
+ +
+
+a4s: vector<ristretto255::Scalar> +
+
+ +
+
+a5: ristretto255::Scalar +
+
+ +
+
+a6s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `TransferSigmaProofGammas` + + + +
struct TransferSigmaProofGammas has drop
+
+ + + +
+Fields + + +
+
+g1: ristretto255::Scalar +
+
+ +
+
+g2s: vector<ristretto255::Scalar> +
+
+ +
+
+g3s: vector<ristretto255::Scalar> +
+
+ +
+
+g4s: vector<ristretto255::Scalar> +
+
+ +
+
+g5: ristretto255::Scalar +
+
+ +
+
+g6s: vector<ristretto255::Scalar> +
+
+ +
+
+g7s: vector<vector<ristretto255::Scalar>> +
+
+ +
+
+g8s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `TransferSigmaProof` + + + +
struct TransferSigmaProof has drop
+
+ + + +
+Fields + + +
+
+alphas: confidential_proof::TransferSigmaProofAlphas +
+
+ +
+
+xs: confidential_proof::TransferSigmaProofXs +
+
+ +
+
+ + +
+ + + +## Struct `NormalizationSigmaProofXs` + + + +
struct NormalizationSigmaProofXs has drop
+
+ + + +
+Fields + + +
+
+x1: ristretto255::CompressedRistretto +
+
+ +
+
+x2: ristretto255::CompressedRistretto +
+
+ +
+
+x3s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+x4s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+ + +
+ + + +## Struct `NormalizationSigmaProofAlphas` + + + +
struct NormalizationSigmaProofAlphas has drop
+
+ + + +
+Fields + + +
+
+a1s: vector<ristretto255::Scalar> +
+
+ +
+
+a2: ristretto255::Scalar +
+
+ +
+
+a3: ristretto255::Scalar +
+
+ +
+
+a4s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `NormalizationSigmaProofGammas` + + + +
struct NormalizationSigmaProofGammas has drop
+
+ + + +
+Fields + + +
+
+g1: ristretto255::Scalar +
+
+ +
+
+g2: ristretto255::Scalar +
+
+ +
+
+g3s: vector<ristretto255::Scalar> +
+
+ +
+
+g4s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `NormalizationSigmaProof` + + + +
struct NormalizationSigmaProof has drop
+
+ + + +
+Fields + + +
+
+alphas: confidential_proof::NormalizationSigmaProofAlphas +
+
+ +
+
+xs: confidential_proof::NormalizationSigmaProofXs +
+
+ +
+
+ + +
+ + + +## Struct `RotationSigmaProofXs` + + + +
struct RotationSigmaProofXs has drop
+
+ + + +
+Fields + + +
+
+x1: ristretto255::CompressedRistretto +
+
+ +
+
+x2: ristretto255::CompressedRistretto +
+
+ +
+
+x3: ristretto255::CompressedRistretto +
+
+ +
+
+x4s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+x5s: vector<ristretto255::CompressedRistretto> +
+
+ +
+
+ + +
+ + + +## Struct `RotationSigmaProofAlphas` + + + +
struct RotationSigmaProofAlphas has drop
+
+ + + +
+Fields + + +
+
+a1s: vector<ristretto255::Scalar> +
+
+ +
+
+a2: ristretto255::Scalar +
+
+ +
+
+a3: ristretto255::Scalar +
+
+ +
+
+a4: ristretto255::Scalar +
+
+ +
+
+a5s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `RotationSigmaProofGammas` + + + +
struct RotationSigmaProofGammas has drop
+
+ + + +
+Fields + + +
+
+g1: ristretto255::Scalar +
+
+ +
+
+g2: ristretto255::Scalar +
+
+ +
+
+g3: ristretto255::Scalar +
+
+ +
+
+g4s: vector<ristretto255::Scalar> +
+
+ +
+
+g5s: vector<ristretto255::Scalar> +
+
+ +
+
+ + +
+ + + +## Struct `RotationSigmaProof` + + + +
struct RotationSigmaProof has drop
+
+ + + +
+Fields + + +
+
+alphas: confidential_proof::RotationSigmaProofAlphas +
+
+ +
+
+xs: confidential_proof::RotationSigmaProofXs +
+
+ +
+
+ + +
+ + + +## Constants + + + + + + +
const BULLETPROOFS_DST: vector<u8> = [77, 111, 118, 101, 109, 101, 110, 116, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 66, 117, 108, 108, 101, 116, 112, 114, 111, 111, 102, 82, 97, 110, 103, 101, 80, 114, 111, 111, 102];
+
+ + + + + + + +
const BULLETPROOFS_NUM_BITS: u64 = 16;
+
+ + + + + + + +
const ERANGE_PROOF_VERIFICATION_FAILED: u64 = 2;
+
+ + + + + + + +
const ESIGMA_PROTOCOL_VERIFY_FAILED: u64 = 1;
+
+ + + + + + + +
const FIAT_SHAMIR_NORMALIZATION_SIGMA_DST: vector<u8> = [77, 111, 118, 101, 109, 101, 110, 116, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 78, 111, 114, 109, 97, 108, 105, 122, 97, 116, 105, 111, 110];
+
+ + + + + + + +
const FIAT_SHAMIR_REGISTRATION_SIGMA_DST: vector<u8> = [77, 111, 118, 101, 109, 101, 110, 116, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 82, 101, 103, 105, 115, 116, 114, 97, 116, 105, 111, 110];
+
+ + + + + + + +
const FIAT_SHAMIR_ROTATION_SIGMA_DST: vector<u8> = [77, 111, 118, 101, 109, 101, 110, 116, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 82, 111, 116, 97, 116, 105, 111, 110];
+
+ + + + + + + +
const FIAT_SHAMIR_TRANSFER_SIGMA_DST: vector<u8> = [77, 111, 118, 101, 109, 101, 110, 116, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 84, 114, 97, 110, 115, 102, 101, 114];
+
+ + + + + + + +
const FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST: vector<u8> = [77, 111, 118, 101, 109, 101, 110, 116, 67, 111, 110, 102, 105, 100, 101, 110, 116, 105, 97, 108, 65, 115, 115, 101, 116, 47, 87, 105, 116, 104, 100, 114, 97, 119, 97, 108];
+
+ + + + + +## Function `verify_registration_proof` + +Verifies a registration proof (ZKPoK of decryption key). + +Ensures the registrant knows the decryption key dk such that ek = dk^{-1} * H. +The proof is a Schnorr proof: verifier checks s * H + e * ek == R. + + +
public(friend) fun verify_registration_proof(chain_id: u8, sender: address, contract_address: address, ek: &ristretto255_twisted_elgamal::CompressedPubkey, token_address: address, commitment_bytes: vector<u8>, response_bytes: vector<u8>)
+
+ + + +
+Implementation + + +
public(friend) fun verify_registration_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    ek: &twisted_elgamal::CompressedPubkey,
+    token_address: address,
+    commitment_bytes: vector<u8>,
+    response_bytes: vector<u8>)
+{
+    // Decompress the commitment point R
+    let r_point = ristretto255::new_compressed_point_from_bytes(commitment_bytes);
+    assert!(option::is_some(&r_point), error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED));
+    let r_compressed = option::extract(&mut r_point);
+
+    // Parse the response scalar
+    let s = ristretto255::new_scalar_from_bytes(response_bytes);
+    assert!(option::is_some(&s), error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED));
+    let s = option::extract(&mut s);
+
+    let msg = FIAT_SHAMIR_REGISTRATION_SIGMA_DST;
+    msg.push_back(chain_id);
+    msg.append(std::bcs::to_bytes(&sender));
+    msg.append(std::bcs::to_bytes(&contract_address));
+    msg.append(std::bcs::to_bytes(&token_address));
+    msg.append(twisted_elgamal::pubkey_to_bytes(ek));
+    msg.append(ristretto255::compressed_point_to_bytes(r_compressed));
+    let e = ristretto255::new_scalar_from_sha2_512(msg);
+
+    // Verify: s * H + e * ek == R
+    let h = ristretto255::hash_to_point_base();
+    let ek_point = twisted_elgamal::pubkey_to_point(ek);
+
+    let lhs = ristretto255::point_add(
+        &ristretto255::point_mul(&h, &s),
+        &ristretto255::point_mul(&ek_point, &e)
+    );
+    let rhs = ristretto255::point_decompress(&r_compressed);
+
+    assert!(
+        ristretto255::point_equals(&lhs, &rhs),
+        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
+    );
+}
+
+ + + +
+ + + +## Function `verify_withdrawal_proof` + +Verifies the validity of the withdraw operation. + +This function ensures that the provided proof (WithdrawalProof) meets the following conditions: +1. The current balance (current_balance) and new balance (new_balance) encrypt the corresponding values +under the same encryption key (ek) before and after the withdrawal of the specified amount (amount), respectively. +2. The relationship new_balance = current_balance - amount holds, verifying that the withdrawal amount is deducted correctly. +3. The new balance (new_balance) is normalized, with each chunk adhering to the range [0, 2^16). + +If all conditions are satisfied, the proof validates the withdrawal; otherwise, the function causes an error. + + +
public fun verify_withdrawal_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, ek: &ristretto255_twisted_elgamal::CompressedPubkey, amount: u64, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::WithdrawalProof)
+
+ + + +
+Implementation + + +
public fun verify_withdrawal_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    ek: &twisted_elgamal::CompressedPubkey,
+    amount: u64,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof: &WithdrawalProof)
+{
+    verify_withdrawal_sigma_proof(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        ek,
+        amount,
+        current_balance,
+        new_balance,
+        &proof.sigma_proof
+    );
+    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
+}
+
+ + + +
+ + + +## Function `verify_transfer_proof` + +Verifies the validity of the confidential_transfer operation. + +This function ensures that the provided proof (TransferProof) meets the following conditions: +1. The transferred amount (recipient_amount and sender_amount) and the auditors' amounts +(auditor_amounts), if provided, encrypt the transfer value using the recipient's, sender's, +and auditors' encryption keys, respectively. +2. The sender's current balance (current_balance) and new balance (new_balance) encrypt the corresponding values +under the sender's encryption key (sender_ek) before and after the transfer, respectively. +3. The relationship new_balance = current_balance - transfer_amount is maintained, ensuring balance integrity. +4. The transferred value (recipient_amount) is properly normalized, with each chunk adhering to the range [0, 2^16). +5. The sender's new balance is normalized, with each chunk in new_balance also adhering to the range [0, 2^16). + +If all conditions are satisfied, the proof validates the transfer; otherwise, the function causes an error. + +sender_auditor_hint is bound into the transfer sigma Fiat–Shamir transcript (same bytes as emitted on-chain). + + +
public fun verify_transfer_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, sender_ek: &ristretto255_twisted_elgamal::CompressedPubkey, recipient_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, sender_amount: &confidential_balance::ConfidentialBalance, recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, sender_auditor_hint: &vector<u8>, proof: &confidential_proof::TransferProof)
+
+ + + +
+Implementation + + +
public fun verify_transfer_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    sender_ek: &twisted_elgamal::CompressedPubkey,
+    recipient_ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    sender_amount: &confidential_balance::ConfidentialBalance,
+    recipient_amount: &confidential_balance::ConfidentialBalance,
+    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
+    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
+    sender_auditor_hint: &vector<u8>,
+    proof: &TransferProof)
+{
+    verify_transfer_sigma_proof(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        sender_ek,
+        recipient_ek,
+        current_balance,
+        new_balance,
+        sender_amount,
+        recipient_amount,
+        auditor_eks,
+        auditor_amounts,
+        sender_auditor_hint,
+        &proof.sigma_proof
+    );
+    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
+    verify_transfer_amount_range_proof(recipient_amount, &proof.zkrp_transfer_amount);
+}
+
+ + + +
+ + + +## Function `verify_normalization_proof` + +Verifies the validity of the normalize operation. + +This function ensures that the provided proof (NormalizationProof) meets the following conditions: +1. The current balance (current_balance) and new balance (new_balance) encrypt the same value +under the same provided encryption key (ek), verifying that the normalization process preserves the balance value. +2. The new balance (new_balance) is properly normalized, with each chunk adhering to the range [0, 2^16), +as verified through the range proof in the normalization process. + +If all conditions are satisfied, the proof validates the normalization; otherwise, the function causes an error. + + +
public fun verify_normalization_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::NormalizationProof)
+
+ + + +
+Implementation + + +
public fun verify_normalization_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof: &NormalizationProof)
+{
+    verify_normalization_sigma_proof(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        ek,
+        current_balance,
+        new_balance,
+        &proof.sigma_proof
+    );
+    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
+}
+
+ + + +
+ + + +## Function `verify_rotation_proof` + +Verifies the validity of the rotate_encryption_key operation. + +This function ensures that the provided proof (RotationProof) meets the following conditions: +1. The current balance (current_balance) and new balance (new_balance) encrypt the same value under the +current encryption key (current_ek) and the new encryption key (new_ek), respectively, verifying +that the key rotation preserves the balance value. +2. The new balance (new_balance) is properly normalized, with each chunk adhering to the range [0, 2^16), +ensuring balance integrity after the key rotation. + +If all conditions are satisfied, the proof validates the key rotation; otherwise, the function causes an error. + + +
public fun verify_rotation_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, current_ek: &ristretto255_twisted_elgamal::CompressedPubkey, new_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::RotationProof)
+
+ + + +
+Implementation + + +
public fun verify_rotation_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    current_ek: &twisted_elgamal::CompressedPubkey,
+    new_ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof: &RotationProof)
+{
+    verify_rotation_sigma_proof(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        current_ek,
+        new_ek,
+        current_balance,
+        new_balance,
+        &proof.sigma_proof
+    );
+    verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance);
+}
+
+ + + +
+ + + +## Function `verify_withdrawal_sigma_proof` + +Verifies the validity of the WithdrawalSigmaProof. + + +
fun verify_withdrawal_sigma_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, ek: &ristretto255_twisted_elgamal::CompressedPubkey, amount: u64, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::WithdrawalSigmaProof)
+
+ + + +
+Implementation + + +
fun verify_withdrawal_sigma_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    ek: &twisted_elgamal::CompressedPubkey,
+    amount: u64,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof: &WithdrawalSigmaProof)
+{
+    let amount_chunks = confidential_balance::split_into_chunks_u64(amount);
+    let amount = ristretto255::new_scalar_from_u64(amount);
+
+    let rho = fiat_shamir_withdrawal_sigma_proof_challenge(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        ek,
+        &amount_chunks,
+        current_balance,
+        &proof.xs
+    );
+
+    let gammas = msm_withdrawal_gammas(&rho);
+
+    let scalars_lhs = vector[gammas.g1, gammas.g2];
+    scalars_lhs.append(gammas.g3s);
+    scalars_lhs.append(gammas.g4s);
+
+    let points_lhs = vector[
+        ristretto255::point_decompress(&proof.xs.x1),
+        ristretto255::point_decompress(&proof.xs.x2)
+    ];
+    points_lhs.append(proof.xs.x3s.map_ref(|x| ristretto255::point_decompress(x)));
+    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
+
+    let scalar_g = scalar_linear_combination(
+        &proof.alphas.a1s,
+        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
+    );
+    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
+    ristretto255::scalar_add_assign(
+        &mut scalar_g,
+        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a1s)
+    );
+    ristretto255::scalar_sub_assign(&mut scalar_g, &scalar_mul_3(&gammas.g1, &rho, &amount));
+
+    let scalar_h = ristretto255::scalar_mul(&gammas.g2, &proof.alphas.a3);
+    ristretto255::scalar_add_assign(
+        &mut scalar_h,
+        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a4s)
+    );
+
+    let scalar_ek = ristretto255::scalar_mul(&gammas.g2, &rho);
+    ristretto255::scalar_add_assign(
+        &mut scalar_ek,
+        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a4s)
+    );
+
+    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
+        ristretto255::scalar_mul(&gammas.g4s[i], &rho)
+    });
+
+    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
+        ristretto255::scalar_mul(&gammas.g3s[i], &rho)
+    });
+
+    let scalars_rhs = vector[scalar_g, scalar_h, scalar_ek];
+    scalars_rhs.append(scalars_current_balance_d);
+    scalars_rhs.append(scalars_new_balance_d);
+    scalars_rhs.append(scalars_current_balance_c);
+    scalars_rhs.append(scalars_new_balance_c);
+
+    let points_rhs = vector[
+        ristretto255::basepoint(),
+        ristretto255::hash_to_point_base(),
+        twisted_elgamal::pubkey_to_point(ek)
+    ];
+    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
+    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
+    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
+    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
+
+    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
+    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
+
+    assert!(
+        ristretto255::point_equals(&lhs, &rhs),
+        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
+    );
+}
+
+ + + +
+ + + +## Function `verify_transfer_sigma_proof` + +Verifies the validity of the TransferSigmaProof. + + +
fun verify_transfer_sigma_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, sender_ek: &ristretto255_twisted_elgamal::CompressedPubkey, recipient_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, sender_amount: &confidential_balance::ConfidentialBalance, recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, sender_auditor_hint: &vector<u8>, proof: &confidential_proof::TransferSigmaProof)
+
+ + + +
+Implementation + + +
fun verify_transfer_sigma_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    sender_ek: &twisted_elgamal::CompressedPubkey,
+    recipient_ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    sender_amount: &confidential_balance::ConfidentialBalance,
+    recipient_amount: &confidential_balance::ConfidentialBalance,
+    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
+    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
+    sender_auditor_hint: &vector<u8>,
+    proof: &TransferSigmaProof)
+{
+    let rho = fiat_shamir_transfer_sigma_proof_challenge(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        sender_ek,
+        recipient_ek,
+        current_balance,
+        new_balance,
+        sender_amount,
+        recipient_amount,
+        auditor_eks,
+        auditor_amounts,
+        sender_auditor_hint,
+        &proof.xs
+    );
+
+    let gammas = msm_transfer_gammas(&rho, proof.xs.x7s.length());
+
+    let scalars_lhs = vector[gammas.g1];
+    scalars_lhs.append(gammas.g2s);
+    scalars_lhs.append(gammas.g3s);
+    scalars_lhs.append(gammas.g4s);
+    scalars_lhs.push_back(gammas.g5);
+    scalars_lhs.append(gammas.g6s);
+    gammas.g7s.for_each(|gamma| scalars_lhs.append(gamma));
+    scalars_lhs.append(gammas.g8s);
+
+    let points_lhs = vector[
+        ristretto255::point_decompress(&proof.xs.x1),
+    ];
+    points_lhs.append(proof.xs.x2s.map_ref(|x| ristretto255::point_decompress(x)));
+    points_lhs.append(proof.xs.x3s.map_ref(|x| ristretto255::point_decompress(x)));
+    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
+    points_lhs.push_back(ristretto255::point_decompress(&proof.xs.x5));
+    points_lhs.append(proof.xs.x6s.map_ref(|x| ristretto255::point_decompress(x)));
+    proof.xs.x7s.for_each_ref(|xs| {
+        points_lhs.append(xs.map_ref(|x| ristretto255::point_decompress(x)));
+    });
+    points_lhs.append(proof.xs.x8s.map_ref(|x| ristretto255::point_decompress(x)));
+
+    let scalar_g = scalar_linear_combination(
+        &proof.alphas.a1s,
+        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
+    );
+    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
+    vector::range(0, 4).for_each(|i| {
+        ristretto255::scalar_add_assign(
+            &mut scalar_g,
+            &ristretto255::scalar_mul(&gammas.g4s[i], &proof.alphas.a4s[i])
+        );
+    });
+    ristretto255::scalar_add_assign(
+        &mut scalar_g,
+        &scalar_linear_combination(&gammas.g6s, &proof.alphas.a1s)
+    );
+
+    let scalar_h = ristretto255::scalar_mul(&gammas.g5, &proof.alphas.a5);
+    vector::range(0, 8).for_each(|i| {
+        ristretto255::scalar_add_assign(
+            &mut scalar_h,
+            &scalar_mul_3(&gammas.g1, &proof.alphas.a6s[i], &new_scalar_from_pow2(i * 16))
+        );
+    });
+    vector::range(0, 4).for_each(|i| {
+        ristretto255::scalar_sub_assign(
+            &mut scalar_h,
+            &scalar_mul_3(&gammas.g1, &proof.alphas.a3s[i], &new_scalar_from_pow2(i * 16))
+        );
+    });
+    ristretto255::scalar_add_assign(
+        &mut scalar_h,
+        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a3s)
+    );
+    ristretto255::scalar_add_assign(
+        &mut scalar_h,
+        &scalar_linear_combination(&gammas.g6s, &proof.alphas.a6s)
+    );
+
+    let scalar_sender_ek = scalar_linear_combination(&gammas.g2s, &proof.alphas.a6s);
+    ristretto255::scalar_add_assign(&mut scalar_sender_ek, &ristretto255::scalar_mul(&gammas.g5, &rho));
+    ristretto255::scalar_add_assign(
+        &mut scalar_sender_ek,
+        &scalar_linear_combination(&gammas.g8s, &proof.alphas.a3s)
+    );
+
+    let scalar_recipient_ek = ristretto255::scalar_zero();
+    vector::range(0, 4).for_each(|i| {
+        ristretto255::scalar_add_assign(
+            &mut scalar_recipient_ek,
+            &ristretto255::scalar_mul(&gammas.g3s[i], &proof.alphas.a3s[i])
+        );
+    });
+
+    let scalar_ek_auditors = gammas.g7s.map_ref(|gamma: &vector<Scalar>| {
+        let scalar_auditor_ek = ristretto255::scalar_zero();
+        vector::range(0, 4).for_each(|i| {
+            ristretto255::scalar_add_assign(
+                &mut scalar_auditor_ek,
+                &ristretto255::scalar_mul(&gamma[i], &proof.alphas.a3s[i])
+            );
+        });
+        scalar_auditor_ek
+    });
+
+    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
+        let scalar = ristretto255::scalar_mul(&gammas.g2s[i], &rho);
+        ristretto255::scalar_sub_assign(
+            &mut scalar,
+            &scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
+        );
+        scalar
+    });
+
+    let scalars_recipient_amount_d = vector::range(0, 4).map(|i| {
+        ristretto255::scalar_mul(&gammas.g3s[i], &rho)
+    });
+
+    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_auditor_amount_d = gammas.g7s.map_ref(|gamma| {
+        gamma.map_ref(|gamma| ristretto255::scalar_mul(gamma, &rho))
+    });
+
+    let scalars_sender_amount_d = vector::range(0, 4).map(|i| {
+        ristretto255::scalar_mul(&gammas.g8s[i], &rho)
+    });
+
+    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_transfer_amount_c = vector::range(0, 4).map(|i| {
+        let scalar = ristretto255::scalar_mul(&gammas.g4s[i], &rho);
+        ristretto255::scalar_sub_assign(
+            &mut scalar,
+            &scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
+        );
+        scalar
+    });
+
+    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
+        ristretto255::scalar_mul(&gammas.g6s[i], &rho)
+    });
+
+    let scalars_rhs = vector[scalar_g, scalar_h, scalar_sender_ek, scalar_recipient_ek];
+    scalars_rhs.append(scalar_ek_auditors);
+    scalars_rhs.append(scalars_new_balance_d);
+    scalars_rhs.append(scalars_recipient_amount_d);
+    scalars_rhs.append(scalars_current_balance_d);
+    scalars_auditor_amount_d.for_each(|scalars| scalars_rhs.append(scalars));
+    scalars_rhs.append(scalars_sender_amount_d);
+    scalars_rhs.append(scalars_current_balance_c);
+    scalars_rhs.append(scalars_transfer_amount_c);
+    scalars_rhs.append(scalars_new_balance_c);
+
+    let points_rhs = vector[
+        ristretto255::basepoint(),
+        ristretto255::hash_to_point_base(),
+        twisted_elgamal::pubkey_to_point(sender_ek),
+        twisted_elgamal::pubkey_to_point(recipient_ek)
+    ];
+    points_rhs.append(auditor_eks.map_ref(|ek| twisted_elgamal::pubkey_to_point(ek)));
+    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
+    points_rhs.append(confidential_balance::balance_to_points_d(recipient_amount));
+    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
+    auditor_amounts.for_each_ref(|balance| {
+        points_rhs.append(confidential_balance::balance_to_points_d(balance));
+    });
+    points_rhs.append(confidential_balance::balance_to_points_d(sender_amount));
+    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
+    points_rhs.append(confidential_balance::balance_to_points_c(recipient_amount));
+    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
+
+    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
+    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
+
+    assert!(
+        ristretto255::point_equals(&lhs, &rhs),
+        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
+    );
+}
+
+ + + +
+ + + +## Function `verify_normalization_sigma_proof` + +Verifies the validity of the NormalizationSigmaProof. + + +
fun verify_normalization_sigma_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::NormalizationSigmaProof)
+
+ + + +
+Implementation + + +
fun verify_normalization_sigma_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof: &NormalizationSigmaProof)
+{
+    let rho = fiat_shamir_normalization_sigma_proof_challenge(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        ek,
+        current_balance,
+        new_balance,
+        &proof.xs
+    );
+    let gammas = msm_normalization_gammas(&rho);
+
+    let scalars_lhs = vector[gammas.g1, gammas.g2];
+    scalars_lhs.append(gammas.g3s);
+    scalars_lhs.append(gammas.g4s);
+
+    let points_lhs = vector[
+        ristretto255::point_decompress(&proof.xs.x1),
+        ristretto255::point_decompress(&proof.xs.x2)
+    ];
+    points_lhs.append(proof.xs.x3s.map_ref(|x| ristretto255::point_decompress(x)));
+    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
+
+    let scalar_g = scalar_linear_combination(
+        &proof.alphas.a1s,
+        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
+    );
+    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
+    ristretto255::scalar_add_assign(
+        &mut scalar_g,
+        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a1s)
+    );
+
+    let scalar_h = ristretto255::scalar_mul(&gammas.g2, &proof.alphas.a3);
+    ristretto255::scalar_add_assign(
+        &mut scalar_h,
+        &scalar_linear_combination(&gammas.g3s, &proof.alphas.a4s)
+    );
+
+    let scalar_ek = ristretto255::scalar_mul(&gammas.g2, &rho);
+    ristretto255::scalar_add_assign(
+        &mut scalar_ek,
+        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a4s)
+    );
+
+    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
+        ristretto255::scalar_mul(&gammas.g4s[i], &rho)
+    });
+
+    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
+        ristretto255::scalar_mul(&gammas.g3s[i], &rho)
+    });
+
+    let scalars_rhs = vector[scalar_g, scalar_h, scalar_ek];
+    scalars_rhs.append(scalars_current_balance_d);
+    scalars_rhs.append(scalars_new_balance_d);
+    scalars_rhs.append(scalars_current_balance_c);
+    scalars_rhs.append(scalars_new_balance_c);
+
+    let points_rhs = vector[
+        ristretto255::basepoint(),
+        ristretto255::hash_to_point_base(),
+        twisted_elgamal::pubkey_to_point(ek)
+    ];
+    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
+    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
+    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
+    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
+
+    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
+    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
+
+    assert!(
+        ristretto255::point_equals(&lhs, &rhs),
+        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
+    );
+}
+
+ + + +
+ + + +## Function `verify_rotation_sigma_proof` + +Verifies the validity of the RotationSigmaProof. + + +
fun verify_rotation_sigma_proof(chain_id: u8, sender: address, contract_address: address, token_address: address, current_ek: &ristretto255_twisted_elgamal::CompressedPubkey, new_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &confidential_proof::RotationSigmaProof)
+
+ + + +
+Implementation + + +
fun verify_rotation_sigma_proof(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    current_ek: &twisted_elgamal::CompressedPubkey,
+    new_ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof: &RotationSigmaProof)
+{
+    let rho = fiat_shamir_rotation_sigma_proof_challenge(
+        chain_id,
+        sender,
+        contract_address,
+        token_address,
+        current_ek,
+        new_ek,
+        current_balance,
+        new_balance,
+        &proof.xs
+    );
+    let gammas = msm_rotation_gammas(&rho);
+
+    let scalars_lhs = vector[gammas.g1, gammas.g2, gammas.g3];
+    scalars_lhs.append(gammas.g4s);
+    scalars_lhs.append(gammas.g5s);
+
+    let points_lhs = vector[
+        ristretto255::point_decompress(&proof.xs.x1),
+        ristretto255::point_decompress(&proof.xs.x2),
+        ristretto255::point_decompress(&proof.xs.x3)
+    ];
+    points_lhs.append(proof.xs.x4s.map_ref(|x| ristretto255::point_decompress(x)));
+    points_lhs.append(proof.xs.x5s.map_ref(|x| ristretto255::point_decompress(x)));
+
+    let scalar_g = scalar_linear_combination(
+        &proof.alphas.a1s,
+        &vector::range(0, 8).map(|i| new_scalar_from_pow2(i * 16))
+    );
+    ristretto255::scalar_mul_assign(&mut scalar_g, &gammas.g1);
+    ristretto255::scalar_add_assign(
+        &mut scalar_g,
+        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a1s)
+    );
+
+    let scalar_h = ristretto255::scalar_mul(&gammas.g2, &proof.alphas.a3);
+    ristretto255::scalar_add_assign(&mut scalar_h, &ristretto255::scalar_mul(&gammas.g3, &proof.alphas.a4));
+    ristretto255::scalar_add_assign(
+        &mut scalar_h,
+        &scalar_linear_combination(&gammas.g4s, &proof.alphas.a5s)
+    );
+
+    let scalar_ek_cur = ristretto255::scalar_mul(&gammas.g2, &rho);
+
+    let scalar_ek_new = ristretto255::scalar_mul(&gammas.g3, &rho);
+    ristretto255::scalar_add_assign(
+        &mut scalar_ek_new,
+        &scalar_linear_combination(&gammas.g5s, &proof.alphas.a5s)
+    );
+
+    let scalars_current_balance_d = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &proof.alphas.a2, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_new_balance_d = vector::range(0, 8).map(|i| {
+        ristretto255::scalar_mul(&gammas.g5s[i], &rho)
+    });
+
+    let scalars_current_balance_c = vector::range(0, 8).map(|i| {
+        scalar_mul_3(&gammas.g1, &rho, &new_scalar_from_pow2(i * 16))
+    });
+
+    let scalars_new_balance_c = vector::range(0, 8).map(|i| {
+        ristretto255::scalar_mul(&gammas.g4s[i], &rho)
+    });
+
+    let scalars_rhs = vector[scalar_g, scalar_h, scalar_ek_cur, scalar_ek_new];
+    scalars_rhs.append(scalars_current_balance_d);
+    scalars_rhs.append(scalars_new_balance_d);
+    scalars_rhs.append(scalars_current_balance_c);
+    scalars_rhs.append(scalars_new_balance_c);
+
+    let points_rhs = vector[
+        ristretto255::basepoint(),
+        ristretto255::hash_to_point_base(),
+        twisted_elgamal::pubkey_to_point(current_ek),
+        twisted_elgamal::pubkey_to_point(new_ek)
+    ];
+    points_rhs.append(confidential_balance::balance_to_points_d(current_balance));
+    points_rhs.append(confidential_balance::balance_to_points_d(new_balance));
+    points_rhs.append(confidential_balance::balance_to_points_c(current_balance));
+    points_rhs.append(confidential_balance::balance_to_points_c(new_balance));
+
+    let lhs = ristretto255::multi_scalar_mul(&points_lhs, &scalars_lhs);
+    let rhs = ristretto255::multi_scalar_mul(&points_rhs, &scalars_rhs);
+
+    assert!(
+        ristretto255::point_equals(&lhs, &rhs),
+        error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)
+    );
+}
+
+ + + +
+ + + +## Function `verify_new_balance_range_proof` + +Verifies the Bulletproofs range proof for new_balance ciphertext chunks (normalized 16-bit limbs). + + +
fun verify_new_balance_range_proof(new_balance: &confidential_balance::ConfidentialBalance, zkrp_new_balance: &ristretto255_bulletproofs::RangeProof)
+
+ + + +
+Implementation + + +
fun verify_new_balance_range_proof(
+    new_balance: &confidential_balance::ConfidentialBalance,
+    zkrp_new_balance: &RangeProof)
+{
+    let balance_c = confidential_balance::balance_to_points_c(new_balance);
+
+    assert!(
+        bulletproofs::verify_batch_range_proof(
+            &balance_c,
+            &ristretto255::basepoint(),
+            &ristretto255::hash_to_point_base(),
+            zkrp_new_balance,
+            BULLETPROOFS_NUM_BITS,
+            BULLETPROOFS_DST
+        ),
+        error::out_of_range(ERANGE_PROOF_VERIFICATION_FAILED)
+    );
+}
+
+ + + +
+ + + +## Function `verify_transfer_amount_range_proof` + +Verifies the Bulletproofs range proof for the encrypted transfer amount (transfer_amount). + + +
fun verify_transfer_amount_range_proof(transfer_amount: &confidential_balance::ConfidentialBalance, zkrp_transfer_amount: &ristretto255_bulletproofs::RangeProof)
+
+ + + +
+Implementation + + +
fun verify_transfer_amount_range_proof(
+    transfer_amount: &confidential_balance::ConfidentialBalance,
+    zkrp_transfer_amount: &RangeProof)
+{
+    let balance_c = confidential_balance::balance_to_points_c(transfer_amount);
+
+    assert!(
+        bulletproofs::verify_batch_range_proof(
+            &balance_c,
+            &ristretto255::basepoint(),
+            &ristretto255::hash_to_point_base(),
+            zkrp_transfer_amount,
+            BULLETPROOFS_NUM_BITS,
+            BULLETPROOFS_DST
+        ),
+        error::out_of_range(ERANGE_PROOF_VERIFICATION_FAILED)
+    );
+}
+
+ + + +
+ + + +## Function `auditors_count_in_transfer_proof` + +Returns n, the number of **auditor rows** encoded in the transfer sigma proof — i.e. +proof.sigma_proof.xs.x7s.length(). Each row holds the four x7s curve commitments for one auditor EK. +confidential_asset uses this to cross-check auditor ciphertext vectors on confidential_transfer. + + +
public(friend) fun auditors_count_in_transfer_proof(proof: &confidential_proof::TransferProof): u64
+
+ + + +
+Implementation + + +
public(friend) fun auditors_count_in_transfer_proof(proof: &TransferProof): u64 {
+    proof.sigma_proof.xs.x7s.length()
+}
+
+ + + +
+ + + +## Function `transfer_proof_ek_volun_auds_flat_bytes` + +Serializes proof.sigma_proof.xs.x7s for the Transferred event field ek_volun_auds: every commitment +is written as **32 bytes** (ristretto255::compressed_point_to_bytes), outer vector = auditors (same order +as the transfer's auditor EK list), inner vector length is **4** (one compressed point per 16-bit amount +chunk lane). **Total length = 128 × auditors_count_in_transfer_proof(proof)** bytes (or 0 when n = 0). + + +
public(friend) fun transfer_proof_ek_volun_auds_flat_bytes(proof: &confidential_proof::TransferProof): vector<u8>
+
+ + + +
+Implementation + + +
public(friend) fun transfer_proof_ek_volun_auds_flat_bytes(proof: &TransferProof): vector<u8> {
+    let out = vector[];
+    let rows = &proof.sigma_proof.xs.x7s;
+    let i = 0u64;
+    let n = vector::length(rows);
+    while (i < n) {
+        let row = vector::borrow(rows, i);
+        let j = 0u64;
+        let m = vector::length(row);
+        while (j < m) {
+            let p = *vector::borrow(row, j);
+            out.append(ristretto255::compressed_point_to_bytes(p));
+            j = j + 1;
+        };
+        i = i + 1;
+    };
+    out
+}
+
+ + + +
+ + + +## Function `deserialize_withdrawal_proof` + +Deserializes the WithdrawalProof from the byte array. +Returns Some(WithdrawalProof) if the deserialization is successful; otherwise, returns None. + + +
public fun deserialize_withdrawal_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>): option::Option<confidential_proof::WithdrawalProof>
+
+ + + +
+Implementation + + +
public fun deserialize_withdrawal_proof(
+    sigma_proof_bytes: vector<u8>,
+    zkrp_new_balance_bytes: vector<u8>): Option<WithdrawalProof>
+{
+    let sigma_proof = deserialize_withdrawal_sigma_proof(sigma_proof_bytes);
+    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
+
+    if (sigma_proof.is_none()) {
+        return option::none()
+    };
+
+    option::some(
+        WithdrawalProof {
+            sigma_proof: sigma_proof.extract(),
+            zkrp_new_balance,
+        }
+    )
+}
+
+ + + +
+ + + +## Function `deserialize_transfer_proof` + +Deserializes the TransferProof from the byte array. +Returns Some(TransferProof) if the deserialization is successful; otherwise, returns None. + + +
public fun deserialize_transfer_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>, zkrp_transfer_amount_bytes: vector<u8>): option::Option<confidential_proof::TransferProof>
+
+ + + +
+Implementation + + +
public fun deserialize_transfer_proof(
+    sigma_proof_bytes: vector<u8>,
+    zkrp_new_balance_bytes: vector<u8>,
+    zkrp_transfer_amount_bytes: vector<u8>): Option<TransferProof>
+{
+    let sigma_proof = deserialize_transfer_sigma_proof(sigma_proof_bytes);
+    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
+    let zkrp_transfer_amount = bulletproofs::range_proof_from_bytes(zkrp_transfer_amount_bytes);
+
+    if (sigma_proof.is_none()) {
+        return option::none()
+    };
+
+    option::some(
+        TransferProof {
+            sigma_proof: sigma_proof.extract(),
+            zkrp_new_balance,
+            zkrp_transfer_amount,
+        }
+    )
+}
+
+ + + +
+ + + +## Function `deserialize_normalization_proof` + +Deserializes the NormalizationProof from the byte array. +Returns Some(NormalizationProof) if the deserialization is successful; otherwise, returns None. + + +
public fun deserialize_normalization_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>): option::Option<confidential_proof::NormalizationProof>
+
+ + + +
+Implementation + + +
public fun deserialize_normalization_proof(
+    sigma_proof_bytes: vector<u8>,
+    zkrp_new_balance_bytes: vector<u8>): Option<NormalizationProof>
+{
+    let sigma_proof = deserialize_normalization_sigma_proof(sigma_proof_bytes);
+    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
+
+    if (sigma_proof.is_none()) {
+        return option::none()
+    };
+
+    option::some(
+        NormalizationProof {
+            sigma_proof: sigma_proof.extract(),
+            zkrp_new_balance,
+        }
+    )
+}
+
+ + + +
+ + + +## Function `deserialize_rotation_proof` + +Deserializes the RotationProof from the byte array. +Returns Some(RotationProof) if the deserialization is successful; otherwise, returns None. + + +
public fun deserialize_rotation_proof(sigma_proof_bytes: vector<u8>, zkrp_new_balance_bytes: vector<u8>): option::Option<confidential_proof::RotationProof>
+
+ + + +
+Implementation + + +
public fun deserialize_rotation_proof(
+    sigma_proof_bytes: vector<u8>,
+    zkrp_new_balance_bytes: vector<u8>): Option<RotationProof>
+{
+    let sigma_proof = deserialize_rotation_sigma_proof(sigma_proof_bytes);
+    let zkrp_new_balance = bulletproofs::range_proof_from_bytes(zkrp_new_balance_bytes);
+
+    if (sigma_proof.is_none()) {
+        return option::none()
+    };
+
+    option::some(
+        RotationProof {
+            sigma_proof: sigma_proof.extract(),
+            zkrp_new_balance,
+        }
+    )
+}
+
+ + + +
+ + + +## Function `deserialize_withdrawal_sigma_proof` + +Deserializes the WithdrawalSigmaProof from the byte array. +Returns Some(WithdrawalSigmaProof) if the deserialization is successful; otherwise, returns None. + + +
fun deserialize_withdrawal_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::WithdrawalSigmaProof>
+
+ + + +
+Implementation + + +
fun deserialize_withdrawal_sigma_proof(proof_bytes: vector<u8>): Option<WithdrawalSigmaProof> {
+    let alphas_count = 18;
+    let xs_count = 18;
+
+    if (proof_bytes.length() != 32 * xs_count + 32 * alphas_count) {
+        return option::none()
+    };
+
+    let alphas = vector::range(0, alphas_count).map(|i| {
+        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
+        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+
+    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
+        return option::none()
+    };
+
+    option::some(
+        WithdrawalSigmaProof {
+            alphas: WithdrawalSigmaProofAlphas {
+                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
+                a2: alphas[8].extract(),
+                a3: alphas[9].extract(),
+                a4s: alphas.slice(10, 18).map(|alpha| alpha.extract()),
+            },
+            xs: WithdrawalSigmaProofXs {
+                x1: xs[0].extract(),
+                x2: xs[1].extract(),
+                x3s: xs.slice(2, 10).map(|x| x.extract()),
+                x4s: xs.slice(10, 18).map(|x| x.extract()),
+            },
+        }
+    )
+}
+
+ + + +
+ + + +## Function `deserialize_transfer_sigma_proof` + +Deserializes the TransferSigmaProof from the byte array. +Returns Some(TransferSigmaProof) if the deserialization is successful; otherwise, returns None. + + +
fun deserialize_transfer_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::TransferSigmaProof>
+
+ + + +
+Implementation + + +
fun deserialize_transfer_sigma_proof(proof_bytes: vector<u8>): Option<TransferSigmaProof> {
+    let alphas_count = 26;
+    let xs_count = 30;
+
+    if (proof_bytes.length() < 32 * xs_count + 32 * alphas_count) {
+        return option::none()
+    };
+
+    // Transfer proof may contain additional four Xs for each auditor.
+    let auditor_xs = proof_bytes.length() - (32 * xs_count + 32 * alphas_count);
+
+    if (auditor_xs % 128 != 0) {
+        return option::none()
+    };
+
+    xs_count += auditor_xs / 32;
+
+    let alphas = vector::range(0, alphas_count).map(|i| {
+        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
+        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+
+    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
+        return option::none()
+    };
+
+    option::some(
+        TransferSigmaProof {
+            alphas: TransferSigmaProofAlphas {
+                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
+                a2: alphas[8].extract(),
+                a3s: alphas.slice(9, 13).map(|alpha| alpha.extract()),
+                a4s: alphas.slice(13, 17).map(|alpha| alpha.extract()),
+                a5: alphas[17].extract(),
+                a6s: alphas.slice(18, 26).map(|alpha| alpha.extract()),
+            },
+            xs: TransferSigmaProofXs {
+                x1: xs[0].extract(),
+                x2s: xs.slice(1, 9).map(|x| x.extract()),
+                x3s: xs.slice(9, 13).map(|x| x.extract()),
+                x4s: xs.slice(13, 17).map(|x| x.extract()),
+                x5: xs[17].extract(),
+                x6s: xs.slice(18, 26).map(|x| x.extract()),
+                x7s: vector::range_with_step(26, xs_count - 4, 4).map(|i| {
+                    vector::range(i, i + 4).map(|j| xs[j].extract())
+                }),
+                x8s: xs.slice(xs_count - 4, xs_count).map(|x| x.extract()),
+            },
+        }
+    )
+}
+
+ + + +
+ + + +## Function `deserialize_normalization_sigma_proof` + +Deserializes the NormalizationSigmaProof from the byte array. +Returns Some(NormalizationSigmaProof) if the deserialization is successful; otherwise, returns None. + + +
fun deserialize_normalization_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::NormalizationSigmaProof>
+
+ + + +
+Implementation + + +
fun deserialize_normalization_sigma_proof(proof_bytes: vector<u8>): Option<NormalizationSigmaProof> {
+    let alphas_count = 18;
+    let xs_count = 18;
+
+    if (proof_bytes.length() != 32 * xs_count + 32 * alphas_count) {
+        return option::none()
+    };
+
+    let alphas = vector::range(0, alphas_count).map(|i| {
+        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
+        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+
+    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
+        return option::none()
+    };
+
+    option::some(
+        NormalizationSigmaProof {
+            alphas: NormalizationSigmaProofAlphas {
+                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
+                a2: alphas[8].extract(),
+                a3: alphas[9].extract(),
+                a4s: alphas.slice(10, 18).map(|alpha| alpha.extract()),
+            },
+            xs: NormalizationSigmaProofXs {
+                x1: xs[0].extract(),
+                x2: xs[1].extract(),
+                x3s: xs.slice(2, 10).map(|x| x.extract()),
+                x4s: xs.slice(10, 18).map(|x| x.extract()),
+            },
+        }
+    )
+}
+
+ + + +
+ + + +## Function `deserialize_rotation_sigma_proof` + +Deserializes the RotationSigmaProof from the byte array. +Returns Some(RotationSigmaProof) if the deserialization is successful; otherwise, returns None. + + +
fun deserialize_rotation_sigma_proof(proof_bytes: vector<u8>): option::Option<confidential_proof::RotationSigmaProof>
+
+ + + +
+Implementation + + +
fun deserialize_rotation_sigma_proof(proof_bytes: vector<u8>): Option<RotationSigmaProof> {
+    let alphas_count = 19;
+    let xs_count = 19;
+
+    if (proof_bytes.length() != 32 * xs_count + 32 * alphas_count) {
+        return option::none()
+    };
+
+    let alphas = vector::range(0, alphas_count).map(|i| {
+        ristretto255::new_scalar_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+    let xs = vector::range(alphas_count, alphas_count + xs_count).map(|i| {
+        ristretto255::new_compressed_point_from_bytes(proof_bytes.slice(i * 32, (i + 1) * 32))
+    });
+
+    if (alphas.any(|alpha| alpha.is_none()) || xs.any(|x| x.is_none())) {
+        return option::none()
+    };
+
+    option::some(
+        RotationSigmaProof {
+            alphas: RotationSigmaProofAlphas {
+                a1s: alphas.slice(0, 8).map(|alpha| alpha.extract()),
+                a2: alphas[8].extract(),
+                a3: alphas[9].extract(),
+                a4: alphas[10].extract(),
+                a5s: alphas.slice(11, 19).map(|alpha| alpha.extract()),
+            },
+            xs: RotationSigmaProofXs {
+                x1: xs[0].extract(),
+                x2: xs[1].extract(),
+                x3: xs[2].extract(),
+                x4s: xs.slice(3, 11).map(|x| x.extract()),
+                x5s: xs.slice(11, 19).map(|x| x.extract()),
+            },
+        }
+    )
+}
+
+ + + +
+ + + +## Function `get_fiat_shamir_withdrawal_sigma_dst` + +Returns the Fiat Shamir DST for the WithdrawalSigmaProof. + + +
#[view]
+public fun get_fiat_shamir_withdrawal_sigma_dst(): vector<u8>
+
+ + + +
+Implementation + + +
public fun get_fiat_shamir_withdrawal_sigma_dst(): vector<u8> {
+    FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST
+}
+
+ + + +
+ + + +## Function `get_fiat_shamir_transfer_sigma_dst` + +Returns the Fiat Shamir DST for the TransferSigmaProof. + + +
#[view]
+public fun get_fiat_shamir_transfer_sigma_dst(): vector<u8>
+
+ + + +
+Implementation + + +
public fun get_fiat_shamir_transfer_sigma_dst(): vector<u8> {
+    FIAT_SHAMIR_TRANSFER_SIGMA_DST
+}
+
+ + + +
+ + + +## Function `get_fiat_shamir_normalization_sigma_dst` + +Returns the Fiat Shamir DST for the NormalizationSigmaProof. + + +
#[view]
+public fun get_fiat_shamir_normalization_sigma_dst(): vector<u8>
+
+ + + +
+Implementation + + +
public fun get_fiat_shamir_normalization_sigma_dst(): vector<u8> {
+    FIAT_SHAMIR_NORMALIZATION_SIGMA_DST
+}
+
+ + + +
+ + + +## Function `get_fiat_shamir_rotation_sigma_dst` + +Returns the Fiat Shamir DST for the RotationSigmaProof. + + +
#[view]
+public fun get_fiat_shamir_rotation_sigma_dst(): vector<u8>
+
+ + + +
+Implementation + + +
public fun get_fiat_shamir_rotation_sigma_dst(): vector<u8> {
+    FIAT_SHAMIR_ROTATION_SIGMA_DST
+}
+
+ + + +
+ + + +## Function `get_fiat_shamir_registration_sigma_dst` + +Returns the Fiat Shamir DST for registration sigma (verify_registration_proof). + + +
#[view]
+public fun get_fiat_shamir_registration_sigma_dst(): vector<u8>
+
+ + + +
+Implementation + + +
public fun get_fiat_shamir_registration_sigma_dst(): vector<u8> {
+    FIAT_SHAMIR_REGISTRATION_SIGMA_DST
+}
+
+ + + +
+ + + +## Function `get_bulletproofs_dst` + +Returns the DST for the range proofs. + + +
#[view]
+public fun get_bulletproofs_dst(): vector<u8>
+
+ + + +
+Implementation + + +
public fun get_bulletproofs_dst(): vector<u8> {
+    BULLETPROOFS_DST
+}
+
+ + + +
+ + + +## Function `get_bulletproofs_num_bits` + +Returns the maximum number of bits of the normalized chunk for the range proofs. + + +
#[view]
+public fun get_bulletproofs_num_bits(): u64
+
+ + + +
+Implementation + + +
public fun get_bulletproofs_num_bits(): u64 {
+    BULLETPROOFS_NUM_BITS
+}
+
+ + + +
+ + + +## Function `prepend_domain_context` + +Prepends chain_id (single byte), sender, contract_address, and token_address (BCS) to a Fiat-Shamir +message buffer. Binding token_address here domain-separates proofs across different fungible assets, so that +a proof generated for one token can never be replayed against a different token even if their stored +ciphertexts ever happened to coincide. + + +
fun prepend_domain_context(bytes: &mut vector<u8>, chain_id: u8, sender: address, contract_address: address, token_address: address)
+
+ + + +
+Implementation + + +
fun prepend_domain_context(
+    bytes: &mut vector<u8>,
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address
+) {
+    let context = vector::singleton(chain_id);
+    context.append(std::bcs::to_bytes(&sender));
+    context.append(std::bcs::to_bytes(&contract_address));
+    context.append(std::bcs::to_bytes(&token_address));
+    context.append(*bytes);
+    *bytes = context;
+}
+
+ + + +
+ + + +## Function `fiat_shamir_withdrawal_sigma_proof_challenge` + +Derives the Fiat-Shamir challenge for the WithdrawalSigmaProof. + + +
fun fiat_shamir_withdrawal_sigma_proof_challenge(chain_id: u8, sender: address, contract_address: address, token_address: address, ek: &ristretto255_twisted_elgamal::CompressedPubkey, amount_chunks: &vector<ristretto255::Scalar>, current_balance: &confidential_balance::ConfidentialBalance, proof_xs: &confidential_proof::WithdrawalSigmaProofXs): ristretto255::Scalar
+
+ + + +
+Implementation + + +
fun fiat_shamir_withdrawal_sigma_proof_challenge(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    ek: &twisted_elgamal::CompressedPubkey,
+    amount_chunks: &vector<Scalar>,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    proof_xs: &WithdrawalSigmaProofXs): Scalar
+{
+    // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P || v_{1..4} || (C_cur, D_cur)_{1..8} || X_{1..18})
+    let bytes = vector[];
+
+    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
+    bytes.append(
+        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
+    );
+    bytes.append(twisted_elgamal::pubkey_to_bytes(ek));
+    amount_chunks.for_each_ref(|chunk| {
+        bytes.append(ristretto255::scalar_to_bytes(chunk));
+    });
+    bytes.append(confidential_balance::balance_to_bytes(current_balance));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x2));
+    proof_xs.x3s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+    proof_xs.x4s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+
+    prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address);
+    let msg = FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST;
+    msg.append(bytes);
+    ristretto255::new_scalar_from_sha2_512(msg)
+}
+
+ + + +
+ + + +## Function `fiat_shamir_transfer_sigma_proof_challenge` + +Derives the Fiat-Shamir challenge for the TransferSigmaProof. + + +
fun fiat_shamir_transfer_sigma_proof_challenge(chain_id: u8, sender: address, contract_address: address, token_address: address, sender_ek: &ristretto255_twisted_elgamal::CompressedPubkey, recipient_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, sender_amount: &confidential_balance::ConfidentialBalance, recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector<ristretto255_twisted_elgamal::CompressedPubkey>, auditor_amounts: &vector<confidential_balance::ConfidentialBalance>, sender_auditor_hint: &vector<u8>, proof_xs: &confidential_proof::TransferSigmaProofXs): ristretto255::Scalar
+
+ + + +
+Implementation + + +
fun fiat_shamir_transfer_sigma_proof_challenge(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    sender_ek: &twisted_elgamal::CompressedPubkey,
+    recipient_ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    sender_amount: &confidential_balance::ConfidentialBalance,
+    recipient_amount: &confidential_balance::ConfidentialBalance,
+    auditor_eks: &vector<twisted_elgamal::CompressedPubkey>,
+    auditor_amounts: &vector<confidential_balance::ConfidentialBalance>,
+    sender_auditor_hint: &vector<u8>,
+    proof_xs: &TransferSigmaProofXs): Scalar
+{
+    // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P_s || P_r || ...)
+    let bytes = vector[];
+
+    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
+    bytes.append(
+        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
+    );
+    bytes.append(twisted_elgamal::pubkey_to_bytes(sender_ek));
+    bytes.append(twisted_elgamal::pubkey_to_bytes(recipient_ek));
+    auditor_eks.for_each_ref(|ek| {
+        bytes.append(twisted_elgamal::pubkey_to_bytes(ek));
+    });
+    bytes.append(confidential_balance::balance_to_bytes(current_balance));
+    bytes.append(confidential_balance::balance_to_bytes(recipient_amount));
+    auditor_amounts.for_each_ref(|balance| {
+        confidential_balance::balance_to_points_d(balance).for_each_ref(|d| {
+            bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::point_compress(d)));
+        });
+    });
+    confidential_balance::balance_to_points_d(sender_amount).for_each_ref(|d| {
+        bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::point_compress(d)));
+    });
+    bytes.append(confidential_balance::balance_to_bytes(new_balance));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
+    proof_xs.x2s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+    proof_xs.x3s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+    proof_xs.x4s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x5));
+    proof_xs.x6s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+    proof_xs.x7s.for_each_ref(|xs| {
+        xs.for_each_ref(|x| {
+            bytes.append(ristretto255::point_to_bytes(x));
+        });
+    });
+    proof_xs.x8s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+
+    bytes.append(bcs::to_bytes(sender_auditor_hint));
+
+    prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address);
+    let msg = FIAT_SHAMIR_TRANSFER_SIGMA_DST;
+    msg.append(bytes);
+    ristretto255::new_scalar_from_sha2_512(msg)
+}
+
+ + + +
+ + + +## Function `fiat_shamir_normalization_sigma_proof_challenge` + +Derives the Fiat-Shamir challenge for the NormalizationSigmaProof. + + +
fun fiat_shamir_normalization_sigma_proof_challenge(chain_id: u8, sender: address, contract_address: address, token_address: address, ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof_xs: &confidential_proof::NormalizationSigmaProofXs): ristretto255::Scalar
+
+ + + +
+Implementation + + +
fun fiat_shamir_normalization_sigma_proof_challenge(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof_xs: &NormalizationSigmaProofXs): Scalar
+{
+    // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P || ...)
+    let bytes = vector[];
+
+    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
+    bytes.append(
+        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
+    );
+    bytes.append(twisted_elgamal::pubkey_to_bytes(ek));
+    bytes.append(confidential_balance::balance_to_bytes(current_balance));
+    bytes.append(confidential_balance::balance_to_bytes(new_balance));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x2));
+    proof_xs.x3s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+    proof_xs.x4s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+
+    prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address);
+    let msg = FIAT_SHAMIR_NORMALIZATION_SIGMA_DST;
+    msg.append(bytes);
+    ristretto255::new_scalar_from_sha2_512(msg)
+}
+
+ + + +
+ + + +## Function `fiat_shamir_rotation_sigma_proof_challenge` + +Derives the Fiat-Shamir challenge for the RotationSigmaProof. + + +
fun fiat_shamir_rotation_sigma_proof_challenge(chain_id: u8, sender: address, contract_address: address, token_address: address, current_ek: &ristretto255_twisted_elgamal::CompressedPubkey, new_ek: &ristretto255_twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof_xs: &confidential_proof::RotationSigmaProofXs): ristretto255::Scalar
+
+ + + +
+Implementation + + +
fun fiat_shamir_rotation_sigma_proof_challenge(
+    chain_id: u8,
+    sender: address,
+    contract_address: address,
+    token_address: address,
+    current_ek: &twisted_elgamal::CompressedPubkey,
+    new_ek: &twisted_elgamal::CompressedPubkey,
+    current_balance: &confidential_balance::ConfidentialBalance,
+    new_balance: &confidential_balance::ConfidentialBalance,
+    proof_xs: &RotationSigmaProofXs): Scalar
+{
+    // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P_cur || P_new || ...)
+    let bytes = vector[];
+
+    bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed()));
+    bytes.append(
+        ristretto255::compressed_point_to_bytes(ristretto255::point_compress(&ristretto255::hash_to_point_base()))
+    );
+    bytes.append(twisted_elgamal::pubkey_to_bytes(current_ek));
+    bytes.append(twisted_elgamal::pubkey_to_bytes(new_ek));
+    bytes.append(confidential_balance::balance_to_bytes(current_balance));
+    bytes.append(confidential_balance::balance_to_bytes(new_balance));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x1));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x2));
+    bytes.append(ristretto255::point_to_bytes(&proof_xs.x3));
+    proof_xs.x4s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+    proof_xs.x5s.for_each_ref(|x| {
+        bytes.append(ristretto255::point_to_bytes(x));
+    });
+
+    prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address);
+    let msg = FIAT_SHAMIR_ROTATION_SIGMA_DST;
+    msg.append(bytes);
+    ristretto255::new_scalar_from_sha2_512(msg)
+}
+
+ + + +
+ + + +## Function `msm_withdrawal_gammas` + +Returns the scalar multipliers for the WithdrawalSigmaProof. + + +
fun msm_withdrawal_gammas(rho: &ristretto255::Scalar): confidential_proof::WithdrawalSigmaProofGammas
+
+ + + +
+Implementation + + +
fun msm_withdrawal_gammas(rho: &Scalar): WithdrawalSigmaProofGammas {
+    WithdrawalSigmaProofGammas {
+        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
+        g2: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 2)),
+        g3s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 3, (i as u8)))
+        }),
+        g4s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
+        }),
+    }
+}
+
+ + + +
+ + + +## Function `msm_transfer_gammas` + +Returns the scalar multipliers for the TransferSigmaProof. + + +
fun msm_transfer_gammas(rho: &ristretto255::Scalar, auditors_count: u64): confidential_proof::TransferSigmaProofGammas
+
+ + + +
+Implementation + + +
fun msm_transfer_gammas(rho: &Scalar, auditors_count: u64): TransferSigmaProofGammas {
+    TransferSigmaProofGammas {
+        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
+        g2s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 2, (i as u8)))
+        }),
+        g3s: vector::range(0, 4).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 3, (i as u8)))
+        }),
+        g4s: vector::range(0, 4).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
+        }),
+        g5: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 5)),
+        g6s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 6, (i as u8)))
+        }),
+        g7s: vector::range(0, auditors_count).map(|i| {
+            vector::range(0, 4).map(|j| {
+                ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, (i + 7 as u8), (j as u8)))
+            })
+        }),
+        // Index starts past g7s range to avoid gamma collision when auditors_count >= 2.
+        // g7s uses indices 7..7+n-1; g8s uses 7+n.
+        g8s: vector::range(0, 4).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, (auditors_count + 7 as u8), (i as u8)))
+        }),
+    }
+}
+
+ + + +
+ + + +## Function `msm_normalization_gammas` + +Returns the scalar multipliers for the NormalizationSigmaProof. + + +
fun msm_normalization_gammas(rho: &ristretto255::Scalar): confidential_proof::NormalizationSigmaProofGammas
+
+ + + +
+Implementation + + +
fun msm_normalization_gammas(rho: &Scalar): NormalizationSigmaProofGammas {
+    NormalizationSigmaProofGammas {
+        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
+        g2: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 2)),
+        g3s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 3, (i as u8)))
+        }),
+        g4s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
+        }),
+    }
+}
+
+ + + +
+ + + +## Function `msm_rotation_gammas` + +Returns the scalar multipliers for the RotationSigmaProof. + + +
fun msm_rotation_gammas(rho: &ristretto255::Scalar): confidential_proof::RotationSigmaProofGammas
+
+ + + +
+Implementation + + +
fun msm_rotation_gammas(rho: &Scalar): RotationSigmaProofGammas {
+    RotationSigmaProofGammas {
+        g1: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 1)),
+        g2: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 2)),
+        g3: ristretto255::new_scalar_from_sha2_512(msm_gamma_1(rho, 3)),
+        g4s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 4, (i as u8)))
+        }),
+        g5s: vector::range(0, 8).map(|i| {
+            ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 5, (i as u8)))
+        }),
+    }
+}
+
+ + + +
+ + + +## Function `msm_gamma_1` + +Returns the scalar multiplier computed as a hash of the provided rho and corresponding gamma index. + + +
fun msm_gamma_1(rho: &ristretto255::Scalar, i: u8): vector<u8>
+
+ + + +
+Implementation + + +
fun msm_gamma_1(rho: &Scalar, i: u8): vector<u8> {
+    let bytes = ristretto255::scalar_to_bytes(rho);
+    bytes.push_back(i);
+    bytes
+}
+
+ + + +
+ + + +## Function `msm_gamma_2` + +Returns the scalar multiplier computed as a hash of the provided rho and corresponding gamma indices. + + +
fun msm_gamma_2(rho: &ristretto255::Scalar, i: u8, j: u8): vector<u8>
+
+ + + +
+Implementation + + +
fun msm_gamma_2(rho: &Scalar, i: u8, j: u8): vector<u8> {
+    let bytes = ristretto255::scalar_to_bytes(rho);
+    bytes.push_back(i);
+    bytes.push_back(j);
+    bytes
+}
+
+ + + +
+ + + +## Function `scalar_mul_3` + +Calculates the product of the provided scalars. + + +
fun scalar_mul_3(scalar1: &ristretto255::Scalar, scalar2: &ristretto255::Scalar, scalar3: &ristretto255::Scalar): ristretto255::Scalar
+
+ + + +
+Implementation + + +
fun scalar_mul_3(scalar1: &Scalar, scalar2: &Scalar, scalar3: &Scalar): Scalar {
+    let result = *scalar1;
+
+    ristretto255::scalar_mul_assign(&mut result, scalar2);
+    ristretto255::scalar_mul_assign(&mut result, scalar3);
+
+    result
+}
+
+ + + +
+ + + +## Function `scalar_linear_combination` + +Calculates the linear combination of the provided scalars. + + +
fun scalar_linear_combination(lhs: &vector<ristretto255::Scalar>, rhs: &vector<ristretto255::Scalar>): ristretto255::Scalar
+
+ + + +
+Implementation + + +
fun scalar_linear_combination(lhs: &vector<Scalar>, rhs: &vector<Scalar>): Scalar {
+    let result = ristretto255::scalar_zero();
+
+    lhs.zip_ref(rhs, |l, r| {
+        ristretto255::scalar_add_assign(&mut result, &ristretto255::scalar_mul(l, r));
+    });
+
+    result
+}
+
+ + + +
+ + + +## Function `new_scalar_from_pow2` + +Raises 2 to the power of the provided exponent and returns the result as a scalar. + + +
fun new_scalar_from_pow2(exp: u64): ristretto255::Scalar
+
+ + + +
+Implementation + + +
fun new_scalar_from_pow2(exp: u64): Scalar {
+    ristretto255::new_scalar_from_u128(1 << (exp as u8))
+}
+
+ + + +
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/doc/genesis.md b/aptos-move/framework/aptos-framework/doc/genesis.md index 66c7c625eb0..a622b5fee6e 100644 --- a/aptos-move/framework/aptos-framework/doc/genesis.md +++ b/aptos-move/framework/aptos-framework/doc/genesis.md @@ -42,6 +42,7 @@ use 0x1::chain_id; use 0x1::chain_status; use 0x1::coin; +use 0x1::confidential_asset; use 0x1::consensus_config; use 0x1::create_signer; use 0x1::error; @@ -364,6 +365,9 @@ Genesis step 1: Initialize aptos framework account and core modules on chain. block::initialize(&aptos_framework_account, epoch_interval_microsecs); state_storage::initialize(&aptos_framework_account); nonce_validation::initialize(&aptos_framework_account); + // Confidential asset ships in the genesis framework bundle, so its `init_module` never runs; + // publish its `GlobalConfig` explicitly. Must follow `chain_id::initialize` (read above). + confidential_asset::initialize(&aptos_framework_account); } diff --git a/aptos-move/framework/aptos-framework/doc/overview.md b/aptos-move/framework/aptos-framework/doc/overview.md index b93066b72c1..2e3d8212fd5 100644 --- a/aptos-move/framework/aptos-framework/doc/overview.md +++ b/aptos-move/framework/aptos-framework/doc/overview.md @@ -34,6 +34,9 @@ This is the reference documentation of the Aptos framework. - [`0x1::code`](code.md#0x1_code) - [`0x1::coin`](coin.md#0x1_coin) - [`0x1::common_account_abstractions_utils`](common_account_abstractions_utils.md#0x1_common_account_abstractions_utils) +- [`0x1::confidential_asset`](confidential_asset.md#0x1_confidential_asset) +- [`0x1::confidential_balance`](confidential_balance.md#0x1_confidential_balance) +- [`0x1::confidential_proof`](confidential_proof.md#0x1_confidential_proof) - [`0x1::config_buffer`](config_buffer.md#0x1_config_buffer) - [`0x1::consensus_config`](consensus_config.md#0x1_consensus_config) - [`0x1::create_signer`](create_signer.md#0x1_create_signer) @@ -74,6 +77,7 @@ This is the reference documentation of the Aptos framework. - [`0x1::reconfiguration_state`](reconfiguration_state.md#0x1_reconfiguration_state) - [`0x1::reconfiguration_with_dkg`](reconfiguration_with_dkg.md#0x1_reconfiguration_with_dkg) - [`0x1::resource_account`](resource_account.md#0x1_resource_account) +- [`0x1::ristretto255_twisted_elgamal`](ristretto255_twisted_elgamal.md#0x1_ristretto255_twisted_elgamal) - [`0x1::solana_derivable_account`](solana_derivable_account.md#0x1_solana_derivable_account) - [`0x1::stake`](stake.md#0x1_stake) - [`0x1::staking_config`](staking_config.md#0x1_staking_config) diff --git a/aptos-move/framework/aptos-framework/doc/ristretto255_twisted_elgamal.md b/aptos-move/framework/aptos-framework/doc/ristretto255_twisted_elgamal.md new file mode 100644 index 00000000000..d9b8a749dae --- /dev/null +++ b/aptos-move/framework/aptos-framework/doc/ristretto255_twisted_elgamal.md @@ -0,0 +1,775 @@ + + + +# Module `0x1::ristretto255_twisted_elgamal` + +This module implements a Twisted ElGamal encryption API, over the Ristretto255 curve, designed to work with +additional cryptographic constructs such as Bulletproofs. + +A Twisted ElGamal *ciphertext* encrypts a value v under a basepoint G and a secondary point H, +alongside a public key Y = sk^(-1) * H, where sk is the corresponding secret key. The ciphertext is of the form: +(v * G + r * H, r * Y), where r is a random scalar. + +The Twisted ElGamal scheme differs from standard ElGamal by introducing a secondary point H to enhance +flexibility and functionality in cryptographic protocols. This design still maintains the homomorphic property: +Enc_Y(v, r) + Enc_Y(v', r') = Enc_Y(v + v', r + r'), where v, v' are plaintexts, Y is the public key, +and r, r' are random scalars. + + +- [Struct `Ciphertext`](#0x1_ristretto255_twisted_elgamal_Ciphertext) +- [Struct `CompressedCiphertext`](#0x1_ristretto255_twisted_elgamal_CompressedCiphertext) +- [Struct `CompressedPubkey`](#0x1_ristretto255_twisted_elgamal_CompressedPubkey) +- [Function `new_pubkey_from_bytes`](#0x1_ristretto255_twisted_elgamal_new_pubkey_from_bytes) +- [Function `is_identity_pubkey`](#0x1_ristretto255_twisted_elgamal_is_identity_pubkey) +- [Function `is_identity_compressed`](#0x1_ristretto255_twisted_elgamal_is_identity_compressed) +- [Function `pubkey_to_bytes`](#0x1_ristretto255_twisted_elgamal_pubkey_to_bytes) +- [Function `pubkey_to_point`](#0x1_ristretto255_twisted_elgamal_pubkey_to_point) +- [Function `pubkey_to_compressed_point`](#0x1_ristretto255_twisted_elgamal_pubkey_to_compressed_point) +- [Function `new_ciphertext_from_bytes`](#0x1_ristretto255_twisted_elgamal_new_ciphertext_from_bytes) +- [Function `new_ciphertext_no_randomness`](#0x1_ristretto255_twisted_elgamal_new_ciphertext_no_randomness) +- [Function `ciphertext_from_points`](#0x1_ristretto255_twisted_elgamal_ciphertext_from_points) +- [Function `ciphertext_from_compressed_points`](#0x1_ristretto255_twisted_elgamal_ciphertext_from_compressed_points) +- [Function `ciphertext_to_bytes`](#0x1_ristretto255_twisted_elgamal_ciphertext_to_bytes) +- [Function `ciphertext_into_points`](#0x1_ristretto255_twisted_elgamal_ciphertext_into_points) +- [Function `ciphertext_as_points`](#0x1_ristretto255_twisted_elgamal_ciphertext_as_points) +- [Function `compress_ciphertext`](#0x1_ristretto255_twisted_elgamal_compress_ciphertext) +- [Function `decompress_ciphertext`](#0x1_ristretto255_twisted_elgamal_decompress_ciphertext) +- [Function `ciphertext_add`](#0x1_ristretto255_twisted_elgamal_ciphertext_add) +- [Function `ciphertext_add_assign`](#0x1_ristretto255_twisted_elgamal_ciphertext_add_assign) +- [Function `ciphertext_sub`](#0x1_ristretto255_twisted_elgamal_ciphertext_sub) +- [Function `ciphertext_sub_assign`](#0x1_ristretto255_twisted_elgamal_ciphertext_sub_assign) +- [Function `ciphertext_clone`](#0x1_ristretto255_twisted_elgamal_ciphertext_clone) +- [Function `ciphertext_equals`](#0x1_ristretto255_twisted_elgamal_ciphertext_equals) +- [Function `get_value_component`](#0x1_ristretto255_twisted_elgamal_get_value_component) + + +
use 0x1::option;
+use 0x1::ristretto255;
+use 0x1::vector;
+
+ + + + + +## Struct `Ciphertext` + +A Twisted ElGamal ciphertext, consisting of two Ristretto255 points. + + +
struct Ciphertext has drop
+
+ + + +
+Fields + + +
+
+left: ristretto255::RistrettoPoint +
+
+ +
+
+right: ristretto255::RistrettoPoint +
+
+ +
+
+ + +
+ + + +## Struct `CompressedCiphertext` + +A compressed Twisted ElGamal ciphertext, consisting of two compressed Ristretto255 points. + + +
struct CompressedCiphertext has copy, drop, store
+
+ + + +
+Fields + + +
+
+left: ristretto255::CompressedRistretto +
+
+ +
+
+right: ristretto255::CompressedRistretto +
+
+ +
+
+ + +
+ + + +## Struct `CompressedPubkey` + +A Twisted ElGamal public key, represented as a compressed Ristretto255 point. + + +
struct CompressedPubkey has copy, drop, store
+
+ + + +
+Fields + + +
+
+point: ristretto255::CompressedRistretto +
+
+ +
+
+ + +
+ + + +## Function `new_pubkey_from_bytes` + +Creates a new public key from a serialized Ristretto255 point. +Returns Some(CompressedPubkey) if the deserialization is successful and the +resulting point is non-identity, otherwise None. + +Identity-point public keys are rejected because they break both privacy and +soundness: ciphertexts encrypted under ek = identity have the form +(v*G + r*H, r*identity) = (v*G + r*H, identity), so the randomness blinding +is null and any observer can brute-force the encrypted value. Sigma protocols +that bind the public key (registration, transfer, rotation) also become +trivially forgeable: the prover does not need to know any secret key, since +e * identity = identity for any challenge e. + + +
public fun new_pubkey_from_bytes(bytes: vector<u8>): option::Option<ristretto255_twisted_elgamal::CompressedPubkey>
+
+ + + +
+Implementation + + +
public fun new_pubkey_from_bytes(bytes: vector<u8>): Option<CompressedPubkey> {
+    let point = ristretto255::new_compressed_point_from_bytes(bytes);
+    if (point.is_some()) {
+        let compressed = point.extract();
+        if (is_identity_compressed(&compressed)) {
+            return std::option::none()
+        };
+        let pk = CompressedPubkey {
+            point: compressed
+        };
+        std::option::some(pk)
+    } else {
+        std::option::none()
+    }
+}
+
+ + + +
+ + + +## Function `is_identity_pubkey` + +Returns true if the given public key is the Ristretto255 identity point. +Such keys are rejected by new_pubkey_from_bytes; this helper is exposed for +callers that obtain a CompressedPubkey through other means and want to +re-validate it before use. + + +
public fun is_identity_pubkey(pubkey: &ristretto255_twisted_elgamal::CompressedPubkey): bool
+
+ + + +
+Implementation + + +
public fun is_identity_pubkey(pubkey: &CompressedPubkey): bool {
+    is_identity_compressed(&pubkey.point)
+}
+
+ + + +
+ + + +## Function `is_identity_compressed` + + + +
fun is_identity_compressed(point: &ristretto255::CompressedRistretto): bool
+
+ + + +
+Implementation + + +
fun is_identity_compressed(point: &CompressedRistretto): bool {
+    ristretto255::compressed_point_to_bytes(*point)
+        == ristretto255::compressed_point_to_bytes(ristretto255::point_identity_compressed())
+}
+
+ + + +
+ + + +## Function `pubkey_to_bytes` + +Serializes a Twisted ElGamal public key into its byte representation. + + +
public fun pubkey_to_bytes(pubkey: &ristretto255_twisted_elgamal::CompressedPubkey): vector<u8>
+
+ + + +
+Implementation + + +
public fun pubkey_to_bytes(pubkey: &CompressedPubkey): vector<u8> {
+    ristretto255::compressed_point_to_bytes(pubkey.point)
+}
+
+ + + +
+ + + +## Function `pubkey_to_point` + +Converts a public key into its corresponding RistrettoPoint. + + +
public fun pubkey_to_point(pubkey: &ristretto255_twisted_elgamal::CompressedPubkey): ristretto255::RistrettoPoint
+
+ + + +
+Implementation + + +
public fun pubkey_to_point(pubkey: &CompressedPubkey): RistrettoPoint {
+    ristretto255::point_decompress(&pubkey.point)
+}
+
+ + + +
+ + + +## Function `pubkey_to_compressed_point` + +Converts a public key into its corresponding CompressedRistretto representation. + + +
public fun pubkey_to_compressed_point(pubkey: &ristretto255_twisted_elgamal::CompressedPubkey): ristretto255::CompressedRistretto
+
+ + + +
+Implementation + + +
public fun pubkey_to_compressed_point(pubkey: &CompressedPubkey): CompressedRistretto {
+    pubkey.point
+}
+
+ + + +
+ + + +## Function `new_ciphertext_from_bytes` + +Creates a new ciphertext from a serialized representation, consisting of two 32-byte Ristretto255 points. +Returns Some(Ciphertext) if the deserialization succeeds, otherwise None. + + +
public fun new_ciphertext_from_bytes(bytes: vector<u8>): option::Option<ristretto255_twisted_elgamal::Ciphertext>
+
+ + + +
+Implementation + + +
public fun new_ciphertext_from_bytes(bytes: vector<u8>): Option<Ciphertext> {
+    if (bytes.length() != 64) {
+        return std::option::none()
+    };
+
+    let bytes_right = bytes.trim(32);
+
+    let left_point = ristretto255::new_point_from_bytes(bytes);
+    let right_point = ristretto255::new_point_from_bytes(bytes_right);
+
+    if (left_point.is_some() && right_point.is_some()) {
+        std::option::some(Ciphertext {
+            left: left_point.extract(),
+            right: right_point.extract()
+        })
+    } else {
+        std::option::none()
+    }
+}
+
+ + + +
+ + + +## Function `new_ciphertext_no_randomness` + +Creates a ciphertext (val * G, 0 * G) where val is the plaintext, and the randomness is set to zero. + + +
public fun new_ciphertext_no_randomness(val: &ristretto255::Scalar): ristretto255_twisted_elgamal::Ciphertext
+
+ + + +
+Implementation + + +
public fun new_ciphertext_no_randomness(val: &Scalar): Ciphertext {
+    Ciphertext {
+        left: ristretto255::basepoint_mul(val),
+        right: ristretto255::point_identity(),
+    }
+}
+
+ + + +
+ + + +## Function `ciphertext_from_points` + +Constructs a Twisted ElGamal ciphertext from two RistrettoPoints. + + +
public fun ciphertext_from_points(left: ristretto255::RistrettoPoint, right: ristretto255::RistrettoPoint): ristretto255_twisted_elgamal::Ciphertext
+
+ + + +
+Implementation + + +
public fun ciphertext_from_points(left: RistrettoPoint, right: RistrettoPoint): Ciphertext {
+    Ciphertext {
+        left,
+        right,
+    }
+}
+
+ + + +
+ + + +## Function `ciphertext_from_compressed_points` + +Constructs a Twisted ElGamal ciphertext from two compressed Ristretto255 points. + + +
public fun ciphertext_from_compressed_points(left: ristretto255::CompressedRistretto, right: ristretto255::CompressedRistretto): ristretto255_twisted_elgamal::CompressedCiphertext
+
+ + + +
+Implementation + + +
public fun ciphertext_from_compressed_points(
+    left: CompressedRistretto,
+    right: CompressedRistretto
+): CompressedCiphertext {
+    CompressedCiphertext {
+        left,
+        right,
+    }
+}
+
+ + + +
+ + + +## Function `ciphertext_to_bytes` + +Serializes a Twisted ElGamal ciphertext into its byte representation. + + +
public fun ciphertext_to_bytes(ct: &ristretto255_twisted_elgamal::Ciphertext): vector<u8>
+
+ + + +
+Implementation + + +
public fun ciphertext_to_bytes(ct: &Ciphertext): vector<u8> {
+    let bytes = ristretto255::point_to_bytes(&ristretto255::point_compress(&ct.left));
+    bytes.append(ristretto255::point_to_bytes(&ristretto255::point_compress(&ct.right)));
+    bytes
+}
+
+ + + +
+ + + +## Function `ciphertext_into_points` + +Converts a ciphertext into a pair of RistrettoPoints. + + +
public fun ciphertext_into_points(c: ristretto255_twisted_elgamal::Ciphertext): (ristretto255::RistrettoPoint, ristretto255::RistrettoPoint)
+
+ + + +
+Implementation + + +
public fun ciphertext_into_points(c: Ciphertext): (RistrettoPoint, RistrettoPoint) {
+    let Ciphertext { left, right } = c;
+    (left, right)
+}
+
+ + + +
+ + + +## Function `ciphertext_as_points` + +Returns the two RistrettoPoints representing the ciphertext. + + +
public fun ciphertext_as_points(c: &ristretto255_twisted_elgamal::Ciphertext): (&ristretto255::RistrettoPoint, &ristretto255::RistrettoPoint)
+
+ + + +
+Implementation + + +
public fun ciphertext_as_points(c: &Ciphertext): (&RistrettoPoint, &RistrettoPoint) {
+    (&c.left, &c.right)
+}
+
+ + + +
+ + + +## Function `compress_ciphertext` + +Compresses a Twisted ElGamal ciphertext into its CompressedCiphertext representation. + + +
public fun compress_ciphertext(ct: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::CompressedCiphertext
+
+ + + +
+Implementation + + +
public fun compress_ciphertext(ct: &Ciphertext): CompressedCiphertext {
+    CompressedCiphertext {
+        left: ristretto255::point_compress(&ct.left),
+        right: ristretto255::point_compress(&ct.right),
+    }
+}
+
+ + + +
+ + + +## Function `decompress_ciphertext` + +Decompresses a CompressedCiphertext back into its Ciphertext representation. + + +
public fun decompress_ciphertext(ct: &ristretto255_twisted_elgamal::CompressedCiphertext): ristretto255_twisted_elgamal::Ciphertext
+
+ + + +
+Implementation + + +
public fun decompress_ciphertext(ct: &CompressedCiphertext): Ciphertext {
+    Ciphertext {
+        left: ristretto255::point_decompress(&ct.left),
+        right: ristretto255::point_decompress(&ct.right),
+    }
+}
+
+ + + +
+ + + +## Function `ciphertext_add` + +Adds two ciphertexts homomorphically, producing a new ciphertext representing the sum of the two. + + +
public fun ciphertext_add(lhs: &ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::Ciphertext
+
+ + + +
+Implementation + + +
public fun ciphertext_add(lhs: &Ciphertext, rhs: &Ciphertext): Ciphertext {
+    Ciphertext {
+        left: ristretto255::point_add(&lhs.left, &rhs.left),
+        right: ristretto255::point_add(&lhs.right, &rhs.right),
+    }
+}
+
+ + + +
+ + + +## Function `ciphertext_add_assign` + +Adds two ciphertexts homomorphically, updating the first ciphertext in place. + + +
public fun ciphertext_add_assign(lhs: &mut ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext)
+
+ + + +
+Implementation + + +
public fun ciphertext_add_assign(lhs: &mut Ciphertext, rhs: &Ciphertext) {
+    ristretto255::point_add_assign(&mut lhs.left, &rhs.left);
+    ristretto255::point_add_assign(&mut lhs.right, &rhs.right);
+}
+
+ + + +
+ + + +## Function `ciphertext_sub` + +Subtracts one ciphertext from another homomorphically, producing a new ciphertext representing the difference. + + +
public fun ciphertext_sub(lhs: &ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::Ciphertext
+
+ + + +
+Implementation + + +
public fun ciphertext_sub(lhs: &Ciphertext, rhs: &Ciphertext): Ciphertext {
+    Ciphertext {
+        left: ristretto255::point_sub(&lhs.left, &rhs.left),
+        right: ristretto255::point_sub(&lhs.right, &rhs.right),
+    }
+}
+
+ + + +
+ + + +## Function `ciphertext_sub_assign` + +Subtracts one ciphertext from another homomorphically, updating the first ciphertext in place. + + +
public fun ciphertext_sub_assign(lhs: &mut ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext)
+
+ + + +
+Implementation + + +
public fun ciphertext_sub_assign(lhs: &mut Ciphertext, rhs: &Ciphertext) {
+    ristretto255::point_sub_assign(&mut lhs.left, &rhs.left);
+    ristretto255::point_sub_assign(&mut lhs.right, &rhs.right);
+}
+
+ + + +
+ + + +## Function `ciphertext_clone` + +Creates a copy of the provided ciphertext. + + +
public fun ciphertext_clone(c: &ristretto255_twisted_elgamal::Ciphertext): ristretto255_twisted_elgamal::Ciphertext
+
+ + + +
+Implementation + + +
public fun ciphertext_clone(c: &Ciphertext): Ciphertext {
+    Ciphertext {
+        left: ristretto255::point_clone(&c.left),
+        right: ristretto255::point_clone(&c.right),
+    }
+}
+
+ + + +
+ + + +## Function `ciphertext_equals` + +Compares two ciphertexts for equality, returning true if they encrypt the same value and randomness. + + +
public fun ciphertext_equals(lhs: &ristretto255_twisted_elgamal::Ciphertext, rhs: &ristretto255_twisted_elgamal::Ciphertext): bool
+
+ + + +
+Implementation + + +
public fun ciphertext_equals(lhs: &Ciphertext, rhs: &Ciphertext): bool {
+    ristretto255::point_equals(&lhs.left, &rhs.left) &&
+        ristretto255::point_equals(&lhs.right, &rhs.right)
+}
+
+ + + +
+ + + +## Function `get_value_component` + +Returns the RistrettoPoint in the ciphertext that contains the encrypted value in the exponent. + + +
public fun get_value_component(ct: &ristretto255_twisted_elgamal::Ciphertext): &ristretto255::RistrettoPoint
+
+ + + +
+Implementation + + +
public fun get_value_component(ct: &Ciphertext): &RistrettoPoint {
+    &ct.left
+}
+
+ + + +
+ + +[move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_asset.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_asset.move new file mode 100644 index 00000000000..53494578fbf --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_asset.move @@ -0,0 +1,1922 @@ +/// This module implements the Confidential Asset (CA) Standard, a privacy-focused protocol for managing fungible assets (FA). +/// It enables private transfers by obfuscating token amounts while keeping sender and recipient addresses visible. +module aptos_framework::confidential_asset { + use std::bcs; + use std::error; + use std::option::Option; + use std::signer; + use std::vector; + use aptos_std::ristretto255::Self; + use aptos_std::ristretto255_bulletproofs::Self as bulletproofs; + use aptos_std::string_utils; + use aptos_framework::chain_id; + use aptos_framework::coin; + use aptos_framework::event; + use aptos_framework::dispatchable_fungible_asset; + use aptos_framework::fungible_asset::{Self, FungibleStore, Metadata}; + use aptos_framework::object::{Self, ExtendRef, Object}; + use aptos_framework::primary_fungible_store; + use aptos_framework::system_addresses; + + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof::{ + Self, NormalizationProof, RotationProof, TransferProof, WithdrawalProof + }; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; + + #[test_only] + use aptos_std::ristretto255::Scalar; + + friend aptos_framework::genesis; + + // + // Errors + // + + /// The range proof system does not support sufficient range. + const ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE: u64 = 1; + + /// The confidential asset store has already been published for the given user-token pair. + const ECA_STORE_ALREADY_PUBLISHED: u64 = 2; + + /// The confidential asset store has not been published for the given user-token pair. + const ECA_STORE_NOT_PUBLISHED: u64 = 3; + + /// The deserialization of the auditor EK failed. + const EAUDITOR_EK_DESERIALIZATION_FAILED: u64 = 4; + + /// The sender is not the registered auditor. + const ENOT_AUDITOR: u64 = 5; + + /// The provided auditors or auditor proofs are invalid. + const EINVALID_AUDITORS: u64 = 6; + + /// The confidential asset account is already frozen. + const EALREADY_FROZEN: u64 = 7; + + /// The confidential asset account is not frozen. + const ENOT_FROZEN: u64 = 8; + + /// The pending balance must be zero for this operation. + const ENOT_ZERO_BALANCE: u64 = 9; + + /// The operation requires the actual balance to be normalized. + const ENORMALIZATION_REQUIRED: u64 = 10; + + /// The balance is already normalized and cannot be normalized again. + const EALREADY_NORMALIZED: u64 = 11; + + /// The token is already allowed for confidential transfers. + const ETOKEN_ENABLED: u64 = 12; + + /// The token is not allowed for confidential transfers. + const ETOKEN_DISABLED: u64 = 13; + + /// The allow list is already enabled. + const EALLOW_LIST_ENABLED: u64 = 14; + + /// The allow list is already disabled. + const EALLOW_LIST_DISABLED: u64 = 15; + + /// An internal error occurred, indicating unexpected behavior. + const EINTERNAL_ERROR: u64 = 16; + + /// Sender and recipient amounts encrypt different transfer amounts + const EINVALID_SENDER_AMOUNT: u64 = 17; + + /// `sender_auditor_hint` exceeds [`MAX_SENDER_AUDITOR_HINT_BYTES`]. + const EAUDITOR_HINT_TOO_LONG: u64 = 18; + + /// Dispatchable fungible asset types (those with custom withdraw, deposit, balance, or + /// supply hooks) are not yet supported in confidential transfers. + const EUNSAFE_DISPATCHABLE_FA: u64 = 19; + + /// No confidential asset pool exists for the given asset type. + const ENO_CONFIDENTIAL_ASSET_POOL: u64 = 20; + + /// Chain auditor not configured; confidential transfers cannot proceed. + const ECHAIN_AUDITOR_NOT_SET: u64 = 21; + + /// Signer is not the FA metadata object's root owner. + const ENOT_ASSET_ISSUER: u64 = 22; + + /// Chain-auditor admin not assigned by governance. + const ECHAIN_AUDITOR_ADMIN_NOT_SET: u64 = 23; + + /// Signer is not the configured chain-auditor admin. + const ENOT_CHAIN_AUDITOR_ADMIN: u64 = 24; + + /// Deposit or withdrawal amount must be greater than zero. + const EZERO_AMOUNT: u64 = 25; + + /// The sender and recipient of a confidential transfer must be different accounts. + const ESELF_TRANSFER: u64 = 26; + + /// The module's `GlobalConfig` has already been initialized. + const EALREADY_INITIALIZED: u64 = 27; + + // + // Constants + // + + /// Maximum length (bytes) of the opaque `sender_auditor_hint` passed to [`confidential_transfer`]. + const MAX_SENDER_AUDITOR_HINT_BYTES: u64 = 256; + + /// The maximum number of transactions can be aggregated on the pending balance before rollover is required. + const MAX_TRANSFERS_BEFORE_ROLLOVER: u64 = 65534; + + /// The Movement mainnet chain ID. If the chain ID is 126, the allow list is enabled. + const MAINNET_CHAIN_ID: u8 = 126; + + // + // Structs + // + + /// The `confidential_asset` module stores a `ConfidentialAssetStore` object for each user-token pair. + struct ConfidentialAssetStore has key { + /// Indicates if the account is frozen. If `true`, transactions are temporarily disabled + /// for this account. This is particularly useful during key rotations, which require + /// two transactions: rolling over the pending balance to the actual balance and rotating + /// the encryption key. Freezing prevents the user from accepting additional payments + /// between these two transactions. + frozen: bool, + + /// A flag indicating whether the actual balance is normalized. A normalized balance + /// ensures that all chunks fit within the defined 16-bit bounds, preventing overflows. + normalized: bool, + + /// Tracks the maximum number of transactions the user can accept before normalization + /// is required. For example, if the user can accept up to 2^16 transactions and each + /// chunk has a 16-bit limit, the maximum chunk value before normalization would be + /// 2^16 * 2^16 = 2^32. Maintaining this counter is crucial because users must solve + /// a discrete logarithm problem of this size to decrypt their balances. + pending_counter: u64, + + /// Stores the user's pending balance, which is used for accepting incoming payments. + /// Represented as four 16-bit chunks (p0 + 2^16 * p1 + 2^32 * p2 + 2^48 * p3), that can grow up to 32 bits. + /// All payments are accepted into this pending balance, which users must roll over into the actual balance + /// to perform transactions like withdrawals or transfers. + /// This separation helps protect against front-running attacks, where small incoming transfers could force + /// frequent regenerating of zk-proofs. + pending_balance: confidential_balance::CompressedConfidentialBalance, + + /// Represents the actual user balance, which is available for sending payments. + /// It consists of eight 16-bit chunks (p0 + 2^16 * p1 + ... + 2^112 * p8), supporting a 128-bit balance. + /// Users can decrypt this balance with their decryption keys and by solving a discrete logarithm problem. + actual_balance: confidential_balance::CompressedConfidentialBalance, + + /// The encryption key associated with the user's confidential asset account, different for each token. + ek: twisted_elgamal::CompressedPubkey, + } + + /// Global configuration for confidential assets: primary FA stores, `FAConfig` derivation, and chain-level auditor state. + struct GlobalConfig has key { + /// Indicates whether the allow list is enabled. If `true`, only tokens from the allow list can be transferred. + /// This flag is managed by the governance module. + allow_list_enabled: bool, + + /// Used to derive a signer that owns all the FAs' primary stores and `FAConfig` objects. + extend_ref: ExtendRef, + + /// Chain-level auditor encryption key. Required at `auditor_eks[0]` on every + /// confidential transfer. `None` until set via [`set_chain_auditor`]; transfers + /// abort with [`ECHAIN_AUDITOR_NOT_SET`] in that state. + chain_auditor_ek: Option, + + /// Account authorized to call [`set_chain_auditor`]. Set by governance via + /// [`set_chain_auditor_admin`]. `None` until governance assigns one, during which + /// window `set_chain_auditor` aborts with [`ECHAIN_AUDITOR_ADMIN_NOT_SET`]. + chain_auditor_admin: Option
, + + /// Bumped on every [`set_chain_auditor`] call (including clears). Stamped on each + /// [`Transferred`] event so off-chain auditors / gateways can identify which + /// historical chain-auditor key was in force at that transfer. + chain_auditor_epoch: u64, + } + + /// Represents the configuration of a token. + struct FAConfig has key { + /// Indicates whether the token is allowed for confidential transfers. + /// If allow list is disabled, all tokens are allowed. + /// Can be toggled by the governance module. The withdrawals are always allowed. + allowed: bool, + + /// Per-asset auditor encryption key. When set, required at `auditor_eks[1]` on + /// every transfer of this asset (additive to the chain auditor at `[0]`). Set via + /// [`set_asset_auditor`] by the FA metadata object's root owner. + asset_auditor_ek: Option, + + /// Bumped on every [`set_asset_auditor`] call (including clears). Stamped on each + /// [`Transferred`] event for this asset so off-chain auditors / gateways can + /// identify which historical asset-auditor key was in force at that transfer. + asset_auditor_epoch: u64, + } + + // + // Events + // + + #[event] + /// Emitted when a new confidential asset store is registered. + struct Registered has drop, store { + addr: address, + /// Fungible asset metadata object address. + asset_type: address, + ek: twisted_elgamal::CompressedPubkey, + } + + #[event] + /// Emitted when tokens are brought into the protocol. + struct Deposited has drop, store { + from: address, + to: address, + /// Fungible asset metadata object address. + asset_type: address, + amount: u64, + /// Recipient's new pending balance after the deposit. + new_pending_balance: confidential_balance::CompressedConfidentialBalance, + } + + #[event] + /// Emitted when tokens are brought out of the protocol. + struct Withdrawn has drop, store { + from: address, + to: address, + /// Fungible asset metadata object address. + asset_type: address, + amount: u64, + /// Sender's new available (actual) balance after the withdrawal. + new_available_balance: confidential_balance::CompressedConfidentialBalance, + } + + #[event] + /// Emitted after a successful `confidential_transfer` between two registered confidential accounts. + /// + /// This is the primary on-chain signal for indexers and tooling: **plaintext amounts are not** included; + /// fields carry **compressed Twisted-ElGamal ciphertexts** and a **subset of sigma commitment bytes** copied + /// from the verified proof. See the technical whitepaper (`whitepaper.md`, §5) for a field-by-field guide. + struct Transferred has drop, store { + /// Address of the sender's confidential account (the `signer` of the transfer entry). + from: address, + /// Recipient confidential account address. + to: address, + /// Fungible-asset metadata object address (`object::object_address(&token)`); identifies which token moved. + asset_type: address, + /// Encrypted transfer amount under the recipient key (pending-balance / four-chunk layout). + amount: confidential_balance::CompressedConfidentialBalance, + /// Flattened **transfer sigma `x7s`** commitments taken from the verified `TransferProof`: for each + /// auditor encryption key row in the proof, exactly **four** compressed Ristretto points (32 bytes each), + /// concatenated in **row-major** order (auditor index, then inner index 0..3). Empty when the proof carries + /// **no** auditor rows. Total byte length is always **`128 × n`** with `n` = number of auditor rows + /// (`confidential_proof::auditors_count_in_transfer_proof` / `proof.sigma_proof.xs.x7s.length()`). + ek_volun_auds: vector, + /// Opaque sender-supplied bytes (bounded by [`MAX_SENDER_AUDITOR_HINT_BYTES`]); same bytes bound into + /// the transfer sigma Fiat–Shamir challenge and passed as the `sender_auditor_hint` entry argument. + sender_auditor_hint: vector, + /// Sender's new **actual** (spendable) balance ciphertext after the debit, compressed for storage/events. + new_sender_available_balance: confidential_balance::CompressedConfidentialBalance, + /// Recipient's new **pending** balance ciphertext after the credit, compressed for storage/events. + new_recip_pending_balance: confidential_balance::CompressedConfidentialBalance, + /// Reserved memo payload for future or off-chain conventions; currently emitted as an empty `vector`. + memo: vector, + /// Value of [`GlobalConfig.chain_auditor_epoch`] at the time of the transfer. + /// Required for compliance: lets future audits identify which historical + /// chain-level auditor key was in force, so that the transcript can still be + /// decrypted years after a key rotation. + chain_auditor_epoch: u64, + /// Value of [`FAConfig.asset_auditor_epoch`] for this asset at the time of the + /// transfer. `0` only when [`set_asset_auditor`] has never been called for this + /// asset; once called (including a clear with empty bytes) the epoch is bumped + /// and stamped here even if the current `asset_auditor_ek` is `None`. Off-chain + /// auditors / gateways resolve `(asset_type, asset_auditor_epoch)` to the active + /// key by indexing [`AssetAuditorChanged`] events. + asset_auditor_epoch: u64, + } + + #[event] + /// Emitted when the available balance is re-encrypted to normalize chunk bounds. + struct Normalized has drop, store { + addr: address, + asset_type: address, + new_available_balance: confidential_balance::CompressedConfidentialBalance, + } + + #[event] + /// Emitted when the pending balance is rolled over into the available balance. + struct RolledOver has drop, store { + addr: address, + asset_type: address, + new_available_balance: confidential_balance::CompressedConfidentialBalance, + } + + #[event] + /// Emitted when the encryption key is rotated and the balance is re-encrypted. + struct KeyRotated has drop, store { + addr: address, + asset_type: address, + new_ek: twisted_elgamal::CompressedPubkey, + new_available_balance: confidential_balance::CompressedConfidentialBalance, + } + + #[event] + /// Emitted when a confidential account's incoming-transfer pause state changes (freeze/unfreeze). + struct FreezeChanged has drop, store { + addr: address, + asset_type: address, + frozen: bool, + } + + #[event] + /// Emitted when the global allow list is enabled or disabled. + struct AllowListChanged has drop, store { + enabled: bool, + } + + #[event] + /// Emitted when a token's confidential-transfer permission is toggled. + struct TokenAllowChanged has drop, store { + asset_type: address, + allowed: bool, + } + + #[event] + /// Asset auditor set, rotated, or cleared. + struct AssetAuditorChanged has drop, store { + asset_type: address, + new_asset_auditor_ek: Option, + new_epoch: u64, + } + + #[event] + /// Chain auditor set, rotated, or cleared. + struct ChainAuditorChanged has drop, store { + new_chain_auditor_ek: Option, + new_epoch: u64, + } + + #[event] + /// Chain-auditor admin assigned or rotated by governance. + struct ChainAuditorAdminChanged has drop, store { + new_admin: address, + } + + // + // Module initialization + // + + /// Runs when CA is first published onto an already-live network (governance framework upgrade). + /// Does not run at genesis — genesis calls `initialize` directly (see `genesis::initialize`). + fun init_module(deployer: &signer) { + initialize(deployer) + } + + /// Publishes the chain-level `GlobalConfig`. Invoked at genesis via `genesis::initialize`, and on + /// a first-time governance publish via `init_module`. Idempotent: aborts if already initialized. + public(friend) fun initialize(aptos_framework: &signer) { + system_addresses::assert_aptos_framework(aptos_framework); + assert!( + !exists(@aptos_framework), + error::already_exists(EALREADY_INITIALIZED) + ); + assert!( + bulletproofs::get_max_range_bits() >= confidential_proof::get_bulletproofs_num_bits(), + error::internal(ERANGE_PROOF_SYSTEM_HAS_INSUFFICIENT_RANGE) + ); + + let deployer_address = signer::address_of(aptos_framework); + + let global_config_ctor_ref = &object::create_object(deployer_address); + + move_to(aptos_framework, GlobalConfig { + allow_list_enabled: chain_id::get() == MAINNET_CHAIN_ID, + extend_ref: object::generate_extend_ref(global_config_ctor_ref), + chain_auditor_ek: std::option::none(), + chain_auditor_epoch: 0, + chain_auditor_admin: std::option::none(), + }); + } + + // + // Entry functions + // + + /// Registers an account for a specified token. Users must register an account for each token they + /// intend to transact with. + /// + /// Users are also responsible for generating a Twisted ElGamal key pair on their side. + public entry fun register( + sender: &signer, + token: Object, + ek: vector, + registration_proof_commitment: vector, + registration_proof_response: vector) acquires GlobalConfig, FAConfig + { + let ek = twisted_elgamal::new_pubkey_from_bytes(ek).extract(); + + // Verify registration proof (ZKPoK of decryption key) + let cid = (chain_id::get() as u8); + let user = signer::address_of(sender); + confidential_proof::verify_registration_proof( + cid, + user, + @aptos_framework, + &ek, + object::object_address(&token), + registration_proof_commitment, + registration_proof_response + ); + + register_internal(sender, token, ek); + } + + /// Atomically [`register`], [`deposit`], and [`rollover_pending_balance`] for first-time users — public + /// FA lands as spendable confidential (actual) balance in one tx. Aborts with + /// [`ECA_STORE_ALREADY_PUBLISHED`] if the sender is already registered. + public entry fun register_and_deposit_and_rollover_pending_balance( + sender: &signer, + token: Object, + amount: u64, + ek: vector, + registration_proof_commitment: vector, + registration_proof_response: vector) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + // The fresh store created by `register` is `normalized = true` with empty actual_balance, + // so `deposit_and_rollover_pending_balance`'s normalized-state assertion passes — no need + // for a separate normalize step here. + register(sender, token, ek, registration_proof_commitment, registration_proof_response); + deposit_and_rollover_pending_balance(sender, token, amount); + } + + /// Atomically [`deposit`] and [`rollover_pending_balance`] when the sender's actual balance is already + /// normalized — no proofs needed. Aborts with [`ENORMALIZATION_REQUIRED`] otherwise; use + /// [`deposit_and_normalize_and_rollover_pending_balance`] in that case. + public entry fun deposit_and_rollover_pending_balance( + sender: &signer, + token: Object, + amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + let user = signer::address_of(sender); + deposit_to_internal(sender, token, user, amount); + rollover_pending_balance_internal(sender, token); + } + + /// Atomically [`deposit`], [`normalize`] the actual balance, and [`rollover_pending_balance`] when the + /// sender's actual balance is NOT normalized. Same proof arguments as [`normalize`]. Aborts with + /// [`EALREADY_NORMALIZED`] if already normalized; use [`deposit_and_rollover_pending_balance`] then. + public entry fun deposit_and_normalize_and_rollover_pending_balance( + sender: &signer, + token: Object, + amount: u64, + new_balance: vector, + zkrp_new_balance: vector, + sigma_proof: vector) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); + let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract(); + + let user = signer::address_of(sender); + deposit_to_internal(sender, token, user, amount); + normalize_internal(sender, token, new_balance, proof); + rollover_pending_balance_internal(sender, token); + } + + /// Brings tokens into the protocol, transferring the passed amount from the sender's primary FA store + /// to the pending balance of the recipient. + /// The initial confidential balance is publicly visible, as entering the protocol requires a normal transfer. + /// However, tokens within the protocol become obfuscated through confidential transfers, ensuring privacy in + /// subsequent transactions. + public entry fun deposit_to( + sender: &signer, + token: Object, + to: address, + amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + deposit_to_internal(sender, token, to, amount) + } + + /// The same as `deposit_to`, but the recipient is the sender. + public entry fun deposit( + sender: &signer, + token: Object, + amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + deposit_to_internal(sender, token, signer::address_of(sender), amount) + } + + /// The same as `deposit_to`, but converts coins to missing FA first. + public entry fun deposit_coins_to( + sender: &signer, + to: address, + amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + let token = ensure_sufficient_fa(sender, amount).extract(); + + deposit_to_internal(sender, token, to, amount) + } + + /// The same as `deposit`, but converts coins to missing FA first. + public entry fun deposit_coins( + sender: &signer, + amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + let token = ensure_sufficient_fa(sender, amount).extract(); + + deposit_to_internal(sender, token, signer::address_of(sender), amount) + } + + /// Brings tokens out of the protocol by transferring the specified amount from the sender's actual balance to + /// the recipient's primary FA store. + /// The withdrawn amount is publicly visible, as this process requires a normal transfer. + /// The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. + public entry fun withdraw_to( + sender: &signer, + token: Object, + to: address, + amount: u64, + new_balance: vector, + zkrp_new_balance: vector, + sigma_proof: vector) acquires ConfidentialAssetStore, GlobalConfig + { + let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); + let proof = confidential_proof::deserialize_withdrawal_proof(sigma_proof, zkrp_new_balance).extract(); + + withdraw_to_internal(sender, token, to, amount, new_balance, proof); + } + + /// The same as `withdraw_to`, but the recipient is the sender. + public entry fun withdraw( + sender: &signer, + token: Object, + amount: u64, + new_balance: vector, + zkrp_new_balance: vector, + sigma_proof: vector) acquires ConfidentialAssetStore, GlobalConfig + { + withdraw_to( + sender, + token, + signer::address_of(sender), + amount, + new_balance, + zkrp_new_balance, + sigma_proof + ) + } + + /// Transfers tokens from the sender's actual balance to the recipient's pending balance. + /// The function hides the transferred amount while keeping the sender and recipient addresses visible. + /// The sender encrypts the transferred amount with the recipient's encryption key and the function updates the + /// recipient's confidential balance homomorphically. + /// Additionally, the sender encrypts the transferred amount with each auditor's EK, allowing auditors to decrypt + /// it on their side. The combined auditor list (`auditor_eks` / `auditor_amounts`) has a fixed prefix layout: + /// + /// ```text + /// [0] chain-level compliance auditor (always required; configured via `set_chain_auditor`) + /// [1] asset-specific auditor (required iff `get_asset_auditor(token).is_some()`) + /// [2..] voluntary auditors (sender's choice; ordered) + /// ``` + /// + /// Aborts with [`ECHAIN_AUDITOR_NOT_SET`] when the chain-level auditor has not yet been configured. + /// The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. + /// + /// `sender_auditor_hint` is emitted on [`Transferred`] and is **bound into the transfer sigma Fiat–Shamir + /// transcript** (must match the hint used when generating the proof). Length must not exceed + /// [`MAX_SENDER_AUDITOR_HINT_BYTES`]. + public entry fun confidential_transfer( + sender: &signer, + token: Object, + to: address, + new_balance: vector, + sender_amount: vector, + recipient_amount: vector, + auditor_eks: vector, + auditor_amounts: vector, + zkrp_new_balance: vector, + zkrp_transfer_amount: vector, + sigma_proof: vector, + sender_auditor_hint: vector) acquires ConfidentialAssetStore, FAConfig, GlobalConfig + { + let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); + let sender_amount = confidential_balance::new_pending_balance_from_bytes(sender_amount).extract(); + let recipient_amount = confidential_balance::new_pending_balance_from_bytes(recipient_amount).extract(); + let auditor_eks = deserialize_auditor_eks(auditor_eks).extract(); + let auditor_amounts = deserialize_auditor_amounts(auditor_amounts).extract(); + let proof = confidential_proof::deserialize_transfer_proof( + sigma_proof, + zkrp_new_balance, + zkrp_transfer_amount + ).extract(); + + confidential_transfer_internal( + sender, + token, + to, + new_balance, + sender_amount, + recipient_amount, + auditor_eks, + auditor_amounts, + proof, + sender_auditor_hint + ) + } + + #[view] + /// Returns the maximum allowed `sender_auditor_hint` length for [`confidential_transfer`]. + public fun max_sender_auditor_hint_bytes(): u64 { + MAX_SENDER_AUDITOR_HINT_BYTES + } + + /// Rotates the encryption key for the user's confidential balance, updating it to a new encryption key. + /// The function ensures that the pending balance is zero before the key rotation, requiring the sender to + /// call `rollover_pending_balance_and_freeze` beforehand if necessary. + /// The sender provides their new normalized confidential balance, encrypted with the new encryption key and fresh randomness + /// to preserve privacy. + public entry fun rotate_encryption_key( + sender: &signer, + token: Object, + new_ek: vector, + new_balance: vector, + zkrp_new_balance: vector, + sigma_proof: vector) acquires ConfidentialAssetStore + { + let new_ek = twisted_elgamal::new_pubkey_from_bytes(new_ek).extract(); + let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); + let proof = confidential_proof::deserialize_rotation_proof(sigma_proof, zkrp_new_balance).extract(); + + rotate_encryption_key_internal(sender, token, new_ek, new_balance, proof); + } + + /// Adjusts each chunk to fit into defined 16-bit bounds to prevent overflows. + /// Most functions perform implicit normalization by accepting a new normalized confidential balance as a parameter. + /// However, explicit normalization is required before rolling over the pending balance, as multiple rolls may cause + /// chunk overflows. + /// The sender provides their new normalized confidential balance, encrypted with fresh randomness to preserve privacy. + public entry fun normalize( + sender: &signer, + token: Object, + new_balance: vector, + zkrp_new_balance: vector, + sigma_proof: vector) acquires ConfidentialAssetStore + { + let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); + let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract(); + + normalize_internal(sender, token, new_balance, proof); + } + + /// Freezes the confidential account for the specified token, disabling all incoming transactions. + public entry fun freeze_token(sender: &signer, token: Object) acquires ConfidentialAssetStore { + freeze_token_internal(sender, token); + } + + /// Unfreezes the confidential account for the specified token, re-enabling incoming transactions. + public entry fun unfreeze_token(sender: &signer, token: Object) acquires ConfidentialAssetStore { + unfreeze_token_internal(sender, token); + } + + /// Adds the pending balance to the actual balance for the specified token, resetting the pending balance to zero. + /// This operation is necessary to use tokens from the pending balance for outgoing transactions. + public entry fun rollover_pending_balance( + sender: &signer, + token: Object) acquires ConfidentialAssetStore + { + rollover_pending_balance_internal(sender, token); + } + + /// Atomically [`normalize`] the actual balance and [`rollover_pending_balance`] in one transaction. Takes the + /// same proof arguments as [`normalize`]. + public entry fun normalize_and_rollover_pending_balance( + sender: &signer, + token: Object, + new_balance: vector, + zkrp_new_balance: vector, + sigma_proof: vector) acquires ConfidentialAssetStore + { + let new_balance = confidential_balance::new_actual_balance_from_bytes(new_balance).extract(); + let proof = confidential_proof::deserialize_normalization_proof(sigma_proof, zkrp_new_balance).extract(); + + normalize_internal(sender, token, new_balance, proof); + rollover_pending_balance_internal(sender, token); + } + + /// Before calling `rotate_encryption_key`, we need to rollover the pending balance and freeze the token to prevent + /// any new payments being come. + public entry fun rollover_pending_balance_and_freeze( + sender: &signer, + token: Object) acquires ConfidentialAssetStore + { + rollover_pending_balance(sender, token); + freeze_token(sender, token); + } + + /// After rotating the encryption key, we may want to unfreeze the token to allow payments. + /// This function facilitates making both calls in a single transaction. + public entry fun rotate_encryption_key_and_unfreeze( + sender: &signer, + token: Object, + new_ek: vector, + new_confidential_balance: vector, + zkrp_new_balance: vector, + rotate_proof: vector) acquires ConfidentialAssetStore + { + rotate_encryption_key(sender, token, new_ek, new_confidential_balance, zkrp_new_balance, rotate_proof); + unfreeze_token(sender, token); + } + + // + // Public governance functions + // + + /// Enables the allow list, restricting confidential transfers to tokens on the allow list. + public fun enable_allow_list(aptos_framework: &signer) acquires GlobalConfig { + system_addresses::assert_aptos_framework(aptos_framework); + + let global_config = borrow_global_mut(@aptos_framework); + + assert!(!global_config.allow_list_enabled, error::invalid_state(EALLOW_LIST_ENABLED)); + + global_config.allow_list_enabled = true; + + event::emit(AllowListChanged { enabled: true }); + } + + /// Disables the allow list, allowing confidential transfers for all tokens. + public fun disable_allow_list(aptos_framework: &signer) acquires GlobalConfig { + system_addresses::assert_aptos_framework(aptos_framework); + + let global_config = borrow_global_mut(@aptos_framework); + + assert!(global_config.allow_list_enabled, error::invalid_state(EALLOW_LIST_DISABLED)); + + global_config.allow_list_enabled = false; + + event::emit(AllowListChanged { enabled: false }); + } + + /// Enables confidential transfers for the specified token. + public fun enable_token(aptos_framework: &signer, token: Object) acquires FAConfig, GlobalConfig { + system_addresses::assert_aptos_framework(aptos_framework); + + let fa_config = borrow_global_mut(ensure_fa_config_exists(token)); + + assert!(!fa_config.allowed, error::invalid_state(ETOKEN_ENABLED)); + + fa_config.allowed = true; + + event::emit(TokenAllowChanged { + asset_type: object::object_address(&token), + allowed: true, + }); + } + + /// Disables confidential transfers for the specified token. + public fun disable_token(aptos_framework: &signer, token: Object) acquires FAConfig, GlobalConfig { + system_addresses::assert_aptos_framework(aptos_framework); + + let fa_config = borrow_global_mut(ensure_fa_config_exists(token)); + + assert!(fa_config.allowed, error::invalid_state(ETOKEN_DISABLED)); + + fa_config.allowed = false; + + event::emit(TokenAllowChanged { + asset_type: object::object_address(&token), + allowed: false, + }); + } + + /// Sets, rotates, or clears the asset-specific auditor key for `token`. Pass an empty + /// `new_auditor_ek` to clear. Bumps `asset_auditor_epoch` and emits [`AssetAuditorChanged`]. + /// + /// Callable by `object::root_owner(token)`; aborts with [`ENOT_ASSET_ISSUER`] otherwise. + /// Rotation invalidates pending transfer proofs (auditor key is bound into the + /// Fiat–Shamir transcript) — senders must regenerate against the new key. + public entry fun set_asset_auditor( + issuer: &signer, + token: Object, + new_auditor_ek: vector) acquires FAConfig, GlobalConfig + { + assert!( + object::root_owner(token) == signer::address_of(issuer), + error::permission_denied(ENOT_ASSET_ISSUER) + ); + + let fa_config = borrow_global_mut(ensure_fa_config_exists(token)); + + let new_ek_opt = if (new_auditor_ek.length() == 0) { + std::option::none() + } else { + let parsed = twisted_elgamal::new_pubkey_from_bytes(new_auditor_ek); + assert!(parsed.is_some(), error::invalid_argument(EAUDITOR_EK_DESERIALIZATION_FAILED)); + parsed + }; + + let new_epoch = fa_config.asset_auditor_epoch + 1; + + fa_config.asset_auditor_ek = new_ek_opt; + fa_config.asset_auditor_epoch = new_epoch; + + event::emit(AssetAuditorChanged { + asset_type: object::object_address(&token), + new_asset_auditor_ek: fa_config.asset_auditor_ek, + new_epoch, + }); + } + + /// Designates (or rotates) the account authorized to call [`set_chain_auditor`]. + /// Governance-only. No clear form — rotate to a successor instead. + public fun set_chain_auditor_admin( + aptos_framework: &signer, + new_admin: address) acquires GlobalConfig + { + system_addresses::assert_aptos_framework(aptos_framework); + + let global_config = borrow_global_mut(@aptos_framework); + global_config.chain_auditor_admin = std::option::some(new_admin); + + event::emit(ChainAuditorAdminChanged { new_admin }); + } + + /// Sets, rotates, or clears the chain-level auditor key. Pass an empty + /// `new_chain_auditor_ek` to clear (which disables all confidential transfers until a + /// successor is set). Bumps `chain_auditor_epoch` and emits [`ChainAuditorChanged`]. + /// + /// Callable only by [`GlobalConfig.chain_auditor_admin`]. Aborts with + /// [`ECHAIN_AUDITOR_ADMIN_NOT_SET`] before an admin is assigned, or + /// [`ENOT_CHAIN_AUDITOR_ADMIN`] for any other signer. Rotation invalidates pending + /// transfer proofs — see [`set_asset_auditor`]. + public entry fun set_chain_auditor( + admin: &signer, + new_chain_auditor_ek: vector) acquires GlobalConfig + { + let global_config = borrow_global_mut(@aptos_framework); + + assert!( + global_config.chain_auditor_admin.is_some(), + error::invalid_state(ECHAIN_AUDITOR_ADMIN_NOT_SET) + ); + assert!( + *global_config.chain_auditor_admin.borrow() == signer::address_of(admin), + error::permission_denied(ENOT_CHAIN_AUDITOR_ADMIN) + ); + + let new_ek_opt = if (new_chain_auditor_ek.length() == 0) { + std::option::none() + } else { + let parsed = twisted_elgamal::new_pubkey_from_bytes(new_chain_auditor_ek); + assert!(parsed.is_some(), error::invalid_argument(EAUDITOR_EK_DESERIALIZATION_FAILED)); + parsed + }; + + let new_epoch = global_config.chain_auditor_epoch + 1; + + global_config.chain_auditor_ek = new_ek_opt; + global_config.chain_auditor_epoch = new_epoch; + + event::emit(ChainAuditorChanged { + new_chain_auditor_ek: global_config.chain_auditor_ek, + new_epoch, + }); + } + + // + // Public view functions + // + + #[view] + /// Checks if the user has a confidential asset store for the specified token. + public fun has_confidential_asset_store(user: address, token: Object): bool { + exists(get_user_address(user, token)) + } + + #[view] + /// Checks if the token is allowed for confidential transfers. + public fun is_token_allowed(token: Object): bool acquires GlobalConfig, FAConfig { + if (!is_allow_list_enabled()) { + return true + }; + + let fa_config_address = get_fa_config_address(token); + + if (!exists(fa_config_address)) { + return false + }; + + borrow_global(fa_config_address).allowed + } + + #[view] + /// Checks if the allow list is enabled. + /// If the allow list is enabled, only tokens from the allow list can be transferred. + /// Otherwise, all tokens are allowed. + public fun is_allow_list_enabled(): bool acquires GlobalConfig { + borrow_global(@aptos_framework).allow_list_enabled + } + + #[view] + /// Returns the pending balance of the user for the specified token. + public fun pending_balance( + owner: address, + token: Object): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore + { + assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + let ca_store = borrow_global(get_user_address(owner, token)); + + ca_store.pending_balance + } + + #[view] + /// Returns the actual balance of the user for the specified token. + public fun actual_balance( + owner: address, + token: Object): confidential_balance::CompressedConfidentialBalance acquires ConfidentialAssetStore + { + assert!(has_confidential_asset_store(owner, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + let ca_store = borrow_global(get_user_address(owner, token)); + + ca_store.actual_balance + } + + #[view] + /// Returns the encryption key (EK) of the user for the specified token. + public fun encryption_key( + user: address, + token: Object): twisted_elgamal::CompressedPubkey acquires ConfidentialAssetStore + { + assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + borrow_global_mut(get_user_address(user, token)).ek + } + + #[view] + /// Checks if the user's actual balance is normalized for the specified token. + public fun is_normalized(user: address, token: Object): bool acquires ConfidentialAssetStore { + assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + borrow_global(get_user_address(user, token)).normalized + } + + #[view] + /// Checks if the user's confidential asset store is frozen for the specified token. + public fun is_frozen(user: address, token: Object): bool acquires ConfidentialAssetStore { + assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + borrow_global(get_user_address(user, token)).frozen + } + + #[view] + /// Asset auditor encryption key for `token`, or `None` if unset. + public fun get_asset_auditor( + token: Object): Option acquires FAConfig, GlobalConfig + { + let fa_config_address = get_fa_config_address(token); + + if (!exists(fa_config_address)) { + return std::option::none(); + }; + + borrow_global(fa_config_address).asset_auditor_ek + } + + #[view] + /// Asset auditor epoch for `token`. `0` if no asset auditor has been set. + public fun get_asset_auditor_epoch(token: Object): u64 acquires FAConfig, GlobalConfig { + let fa_config_address = get_fa_config_address(token); + if (!exists(fa_config_address)) { + return 0; + }; + borrow_global(fa_config_address).asset_auditor_epoch + } + + #[view] + /// Chain auditor encryption key, or `None` if unset. + public fun get_chain_auditor(): Option acquires GlobalConfig { + borrow_global(@aptos_framework).chain_auditor_ek + } + + #[view] + /// Chain auditor epoch. `0` before any chain auditor has been configured. + public fun get_chain_auditor_epoch(): u64 acquires GlobalConfig { + borrow_global(@aptos_framework).chain_auditor_epoch + } + + #[view] + /// Chain-auditor admin address, or `None` if governance hasn't assigned one yet. + public fun get_chain_auditor_admin(): Option
acquires GlobalConfig { + borrow_global(@aptos_framework).chain_auditor_admin + } + + #[view] + /// Returns the circulating supply of the confidential asset. + public fun confidential_asset_balance(token: Object): u64 acquires GlobalConfig { + fungible_asset::balance(get_pool_fa_store(token)) + } + + // + // Public functions that correspond to the entry functions and don't require serializtion of the input data. + // These function can be useful for external contracts that want to integrate with the Confidential Asset protocol. + // + + /// Implementation of the `register` entry function. + public fun register_internal( + sender: &signer, + token: Object, + ek: twisted_elgamal::CompressedPubkey) acquires GlobalConfig, FAConfig + { + assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA)); + assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED)); + + let user = signer::address_of(sender); + + assert!(!has_confidential_asset_store(user, token), error::already_exists(ECA_STORE_ALREADY_PUBLISHED)); + + let ca_store = ConfidentialAssetStore { + frozen: false, + normalized: true, + pending_counter: 0, + pending_balance: confidential_balance::new_compressed_pending_balance_no_randomness(), + actual_balance: confidential_balance::new_compressed_actual_balance_no_randomness(), + ek, + }; + + move_to(&get_user_signer(sender, token), ca_store); + + event::emit(Registered { + addr: user, + asset_type: object::object_address(&token), + ek, + }); + } + + /// Implementation of the `deposit_to` entry function. + public fun deposit_to_internal( + sender: &signer, + token: Object, + to: address, + amount: u64) acquires ConfidentialAssetStore, GlobalConfig, FAConfig + { + assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA)); + assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED)); + assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN)); + // A zero deposit moves no funds but still consumes one of the recipient's + // `MAX_TRANSFERS_BEFORE_ROLLOVER` pending slots, letting anyone force rollovers on them. + assert!(amount > 0, error::invalid_argument(EZERO_AMOUNT)); + + let from = signer::address_of(sender); + + let pool_fa_store = ensure_pool_fa_store(token); + + let pool_before = fungible_asset::balance(pool_fa_store); + let sender_fa_store = primary_fungible_store::primary_store(from, token); + dispatchable_fungible_asset::transfer(sender, sender_fa_store, pool_fa_store, amount); + + let ca_store = borrow_global_mut(get_user_address(to, token)); + let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); + + confidential_balance::add_balances_mut( + &mut pending_balance, + &confidential_balance::new_pending_balance_u64_no_randonmess(amount) + ); + + ca_store.pending_balance = confidential_balance::compress_balance(&pending_balance); + + assert!( + ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER, + error::invalid_argument(EINTERNAL_ERROR) + ); + + ca_store.pending_counter += 1; + + event::emit(Deposited { + from, + to, + asset_type: object::object_address(&token), + amount, + new_pending_balance: ca_store.pending_balance, + }); + + assert!( + amount == fungible_asset::balance(pool_fa_store) - pool_before, + error::invalid_argument(EUNSAFE_DISPATCHABLE_FA) + ); + } + + /// Implementation of the `withdraw_to` entry function. + /// Withdrawals are always allowed, regardless of the token allow status. + public fun withdraw_to_internal( + sender: &signer, + token: Object, + to: address, + amount: u64, + new_balance: confidential_balance::ConfidentialBalance, + proof: WithdrawalProof) acquires ConfidentialAssetStore, GlobalConfig + { + assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA)); + // A zero withdrawal would re-randomize the actual balance and mark it normalized, + // acting as a `normalize` that skips the `EALREADY_NORMALIZED` guard. + assert!(amount > 0, error::invalid_argument(EZERO_AMOUNT)); + + let from = signer::address_of(sender); + + let sender_ek = encryption_key(from, token); + + let ca_store = borrow_global_mut(get_user_address(from, token)); + let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); + + let cid = (chain_id::get() as u8); + confidential_proof::verify_withdrawal_proof( + cid, + from, + @aptos_framework, + object::object_address(&token), + &sender_ek, + amount, + ¤t_balance, + &new_balance, + &proof + ); + + ca_store.normalized = true; + ca_store.actual_balance = confidential_balance::compress_balance(&new_balance); + + let pool_fa_store = get_pool_fa_store(token); + let pool_before = fungible_asset::balance(pool_fa_store); + let recipient_fa_store = primary_fungible_store::ensure_primary_store_exists(to, token); + dispatchable_fungible_asset::transfer(&get_fa_store_signer(), pool_fa_store, recipient_fa_store, amount); + + event::emit(Withdrawn { + from, + to, + asset_type: object::object_address(&token), + amount, + new_available_balance: ca_store.actual_balance, + }); + + assert!( + amount == pool_before - fungible_asset::balance(pool_fa_store), + error::invalid_argument(EUNSAFE_DISPATCHABLE_FA) + ); + } + + /// Implementation of the `confidential_transfer` entry function. + public fun confidential_transfer_internal( + sender: &signer, + token: Object, + to: address, + new_balance: confidential_balance::ConfidentialBalance, + sender_amount: confidential_balance::ConfidentialBalance, + recipient_amount: confidential_balance::ConfidentialBalance, + auditor_eks: vector, + auditor_amounts: vector, + proof: TransferProof, + sender_auditor_hint: vector) acquires ConfidentialAssetStore, FAConfig, GlobalConfig + { + assert!(signer::address_of(sender) != to, error::invalid_argument(ESELF_TRANSFER)); + assert!(is_safe_for_confidentiality(&token), error::invalid_argument(EUNSAFE_DISPATCHABLE_FA)); + assert!(is_token_allowed(token), error::invalid_argument(ETOKEN_DISABLED)); + assert!(!is_frozen(to, token), error::invalid_state(EALREADY_FROZEN)); + assert!( + validate_auditors(token, &recipient_amount, &auditor_eks, &auditor_amounts, &proof), + error::invalid_argument(EINVALID_AUDITORS) + ); + assert!( + confidential_balance::balance_c_equals(&sender_amount, &recipient_amount), + error::invalid_argument(EINVALID_SENDER_AMOUNT) + ); + assert!( + sender_auditor_hint.length() <= MAX_SENDER_AUDITOR_HINT_BYTES, + error::invalid_argument(EAUDITOR_HINT_TOO_LONG) + ); + + let from = signer::address_of(sender); + + let sender_ek = encryption_key(from, token); + let recipient_ek = encryption_key(to, token); + + let sender_ca_store = borrow_global_mut(get_user_address(from, token)); + + let sender_current_actual_balance = confidential_balance::decompress_balance( + &sender_ca_store.actual_balance + ); + + let cid = (chain_id::get() as u8); + confidential_proof::verify_transfer_proof( + cid, + from, + @aptos_framework, + object::object_address(&token), + &sender_ek, + &recipient_ek, + &sender_current_actual_balance, + &new_balance, + &sender_amount, + &recipient_amount, + &auditor_eks, + &auditor_amounts, + &sender_auditor_hint, + &proof); + + sender_ca_store.normalized = true; + let new_sender_available_balance = confidential_balance::compress_balance(&new_balance); + sender_ca_store.actual_balance = new_sender_available_balance; + + let amount = confidential_balance::compress_balance(&recipient_amount); + let ek_volun_auds = confidential_proof::transfer_proof_ek_volun_auds_flat_bytes(&proof); + + // Cannot create multiple mutable references to the same type, so we need to drop it + let ConfidentialAssetStore { .. } = sender_ca_store; + + let recipient_ca_store = borrow_global_mut(get_user_address(to, token)); + + assert!( + recipient_ca_store.pending_counter < MAX_TRANSFERS_BEFORE_ROLLOVER, + error::invalid_argument(EINTERNAL_ERROR) + ); + + let recipient_pending_balance = confidential_balance::decompress_balance( + &recipient_ca_store.pending_balance + ); + confidential_balance::add_balances_mut(&mut recipient_pending_balance, &recipient_amount); + + recipient_ca_store.pending_counter += 1; + let new_recip_pending_balance = confidential_balance::compress_balance(&recipient_pending_balance); + recipient_ca_store.pending_balance = new_recip_pending_balance; + + let chain_auditor_epoch = borrow_global(@aptos_framework).chain_auditor_epoch; + let asset_auditor_epoch = get_asset_auditor_epoch(token); + + event::emit(Transferred { + from, + to, + asset_type: object::object_address(&token), + amount, + ek_volun_auds, + sender_auditor_hint, + new_sender_available_balance, + new_recip_pending_balance, + memo: vector[], + chain_auditor_epoch, + asset_auditor_epoch, + }); + } + + /// Implementation of the `rotate_encryption_key` entry function. + public fun rotate_encryption_key_internal( + sender: &signer, + token: Object, + new_ek: twisted_elgamal::CompressedPubkey, + new_balance: confidential_balance::ConfidentialBalance, + proof: RotationProof) acquires ConfidentialAssetStore + { + let user = signer::address_of(sender); + let current_ek = encryption_key(user, token); + + let ca_store = borrow_global_mut(get_user_address(user, token)); + + let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); + + // We need to ensure that the pending balance is zero before rotating the key. + // To guarantee this, the user must call `rollover_pending_balance_and_freeze` beforehand. + assert!(confidential_balance::is_zero_balance(&pending_balance), error::invalid_state(ENOT_ZERO_BALANCE)); + + let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); + + let cid = (chain_id::get() as u8); + confidential_proof::verify_rotation_proof( + cid, + user, + @aptos_framework, + object::object_address(&token), + ¤t_ek, + &new_ek, + ¤t_balance, + &new_balance, + &proof + ); + + ca_store.ek = new_ek; + // We don't need to update the pending balance here, as it has been asserted to be zero. + ca_store.actual_balance = confidential_balance::compress_balance(&new_balance); + ca_store.normalized = true; + + event::emit(KeyRotated { + addr: user, + asset_type: object::object_address(&token), + new_ek, + new_available_balance: ca_store.actual_balance, + }); + } + + /// Implementation of the `normalize` entry function. + public fun normalize_internal( + sender: &signer, + token: Object, + new_balance: confidential_balance::ConfidentialBalance, + proof: NormalizationProof) acquires ConfidentialAssetStore + { + let user = signer::address_of(sender); + let sender_ek = encryption_key(user, token); + + let ca_store = borrow_global_mut(get_user_address(user, token)); + + assert!(!ca_store.normalized, error::invalid_state(EALREADY_NORMALIZED)); + + let current_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); + + let cid = (chain_id::get() as u8); + confidential_proof::verify_normalization_proof( + cid, + user, + @aptos_framework, + object::object_address(&token), + &sender_ek, + ¤t_balance, + &new_balance, + &proof + ); + + ca_store.actual_balance = confidential_balance::compress_balance(&new_balance); + ca_store.normalized = true; + + event::emit(Normalized { + addr: user, + asset_type: object::object_address(&token), + new_available_balance: ca_store.actual_balance, + }); + } + + /// Implementation of the `rollover_pending_balance` entry function. + public fun rollover_pending_balance_internal( + sender: &signer, + token: Object) acquires ConfidentialAssetStore + { + let user = signer::address_of(sender); + + assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + let ca_store = borrow_global_mut(get_user_address(user, token)); + + assert!(ca_store.normalized, error::invalid_state(ENORMALIZATION_REQUIRED)); + + let actual_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); + let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); + + confidential_balance::add_balances_mut(&mut actual_balance, &pending_balance); + + ca_store.normalized = false; + ca_store.pending_counter = 0; + ca_store.actual_balance = confidential_balance::compress_balance(&actual_balance); + ca_store.pending_balance = confidential_balance::new_compressed_pending_balance_no_randomness(); + + event::emit(RolledOver { + addr: user, + asset_type: object::object_address(&token), + new_available_balance: ca_store.actual_balance, + }); + } + + /// Implementation of the `freeze_token` entry function. + public fun freeze_token_internal( + sender: &signer, + token: Object) acquires ConfidentialAssetStore + { + let user = signer::address_of(sender); + + assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + let ca_store = borrow_global_mut(get_user_address(user, token)); + + assert!(!ca_store.frozen, error::invalid_state(EALREADY_FROZEN)); + + ca_store.frozen = true; + + event::emit(FreezeChanged { + addr: user, + asset_type: object::object_address(&token), + frozen: true, + }); + } + + /// Implementation of the `unfreeze_token` entry function. + public fun unfreeze_token_internal( + sender: &signer, + token: Object) acquires ConfidentialAssetStore + { + let user = signer::address_of(sender); + + assert!(has_confidential_asset_store(user, token), error::not_found(ECA_STORE_NOT_PUBLISHED)); + + let ca_store = borrow_global_mut(get_user_address(user, token)); + + assert!(ca_store.frozen, error::invalid_state(ENOT_FROZEN)); + + ca_store.frozen = false; + + event::emit(FreezeChanged { + addr: user, + asset_type: object::object_address(&token), + frozen: false, + }); + } + + // + // Private functions. + // + + /// Returns whether the given asset type is safe for use in confidential transfers. + /// + /// Dispatchable fungible assets can override withdraw, deposit, balance, or supply + /// behaviour in ways that are incompatible with encrypted on-chain balances (e.g., + /// fee-on-transfer tokens, rebasing balances, custom supply hooks). Until a safe + /// integration path exists, only standard (non-dispatchable) FA types are accepted. + fun is_safe_for_confidentiality(token: &Object): bool { + !fungible_asset::is_asset_type_dispatchable(*token) + } + + /// Ensures that the `FAConfig` object exists for the specified token. + /// If the object does not exist, creates it. + /// Used only for internal purposes. + fun ensure_fa_config_exists(token: Object): address acquires GlobalConfig { + let fa_config_address = get_fa_config_address(token); + + if (!exists(fa_config_address)) { + let fa_config_singer = get_fa_config_signer(token); + + move_to(&fa_config_singer, FAConfig { + allowed: false, + asset_auditor_ek: std::option::none(), + asset_auditor_epoch: 0, + }); + }; + + fa_config_address + } + + /// Returns an object for handling all the FA primary stores, and returns a signer for it. + fun get_fa_store_signer(): signer acquires GlobalConfig { + object::generate_signer_for_extending(&borrow_global(@aptos_framework).extend_ref) + } + + /// Returns the address that handles all the FA primary stores. + fun get_fa_store_address(): address acquires GlobalConfig { + object::address_from_extend_ref(&borrow_global(@aptos_framework).extend_ref) + } + + /// Returns the pool's primary fungible store for the given token, aborting if it does not exist. + fun get_pool_fa_store(token: Object): Object acquires GlobalConfig { + let pool_addr = get_fa_store_address(); + assert!(primary_fungible_store::primary_store_exists(pool_addr, token), error::not_found(ENO_CONFIDENTIAL_ASSET_POOL)); + primary_fungible_store::primary_store(pool_addr, token) + } + + /// Returns the pool's primary fungible store for the given token, creating it if necessary. + fun ensure_pool_fa_store(token: Object): Object acquires GlobalConfig { + primary_fungible_store::ensure_primary_store_exists(get_fa_store_address(), token) + } + + /// Returns an object for handling the `ConfidentialAssetStore` and returns a signer for it. + fun get_user_signer(user: &signer, token: Object): signer { + let user_ctor = &object::create_named_object(user, construct_user_seed(token)); + + object::generate_signer(user_ctor) + } + + /// Returns the address that handles the user's `ConfidentialAssetStore` object for the specified user and token. + fun get_user_address(user: address, token: Object): address { + object::create_object_address(&user, construct_user_seed(token)) + } + + /// Returns an object for handling the `FAConfig`, and returns a signer for it. + fun get_fa_config_signer(token: Object): signer acquires GlobalConfig { + let fa_ext = &borrow_global(@aptos_framework).extend_ref; + let fa_ext_signer = object::generate_signer_for_extending(fa_ext); + + let fa_ctor = &object::create_named_object(&fa_ext_signer, construct_fa_seed(token)); + + object::generate_signer(fa_ctor) + } + + /// Returns the address that handles primary FA store and `FAConfig` objects for the specified token. + fun get_fa_config_address(token: Object): address acquires GlobalConfig { + let fa_ext = &borrow_global(@aptos_framework).extend_ref; + let fa_ext_address = object::address_from_extend_ref(fa_ext); + + object::create_object_address(&fa_ext_address, construct_fa_seed(token)) + } + + /// Constructs a unique seed for the user's `ConfidentialAssetStore` object. + /// As all the `ConfidentialAssetStore`'s have the same type, we need to differentiate them by the seed. + fun construct_user_seed(token: Object): vector { + bcs::to_bytes( + &string_utils::format2( + &b"confidential_asset::{}::token::{}::user", + @aptos_framework, + object::object_address(&token) + ) + ) + } + + /// Constructs a unique seed for the FA's `FAConfig` object. + /// As all the `FAConfig`'s have the same type, we need to differentiate them by the seed. + fun construct_fa_seed(token: Object): vector { + bcs::to_bytes( + &string_utils::format2( + &b"confidential_asset::{}::token::{}::fa", + @aptos_framework, + object::object_address(&token) + ) + ) + } + + /// Validates the auditor-related fields of a confidential transfer. + /// + /// Aborts with [`ECHAIN_AUDITOR_NOT_SET`] if no chain-level auditor has been + /// configured (transfers cannot proceed in that state). + /// + /// Returns `false` (rejecting the transfer) if any of: + /// - any `auditor_amount` does not encrypt the same plaintext as `transfer_amount`; + /// - the lengths of `auditor_eks`, `auditor_amounts`, and the transfer-proof auditor + /// row count disagree; + /// - `auditor_eks` is missing the required prefix (see slot layout below); + /// - the prefix slot keys do not equal the active chain / asset auditor keys. + /// + /// **Slot layout of `auditor_eks`** (and `auditor_amounts`): + /// ```text + /// [0] chain-level auditor (always required) + /// [1] asset-specific auditor (required iff `get_asset_auditor(token).is_some()`) + /// [2..] voluntary auditors (sender's choice, ordered) + /// ``` + /// Auditor identity at slots 0 and 1 is bound into the transfer's Fiat–Shamir + /// transcript (via the order in which `auditor_eks` is hashed in + /// `confidential_proof::fiat_shamir_transfer_sigma_proof_challenge`), so a sender + /// cannot substitute one auditor's slot for another's. + fun validate_auditors( + token: Object, + transfer_amount: &confidential_balance::ConfidentialBalance, + auditor_eks: &vector, + auditor_amounts: &vector, + proof: &TransferProof): bool acquires FAConfig, GlobalConfig + { + if ( + !auditor_amounts.all(|auditor_amount| { + confidential_balance::balance_c_equals(transfer_amount, auditor_amount) + }) + ) { + return false + }; + + if ( + auditor_eks.length() != auditor_amounts.length() || + auditor_eks.length() != confidential_proof::auditors_count_in_transfer_proof(proof) + ) { + return false + }; + + let chain_auditor_ek_opt = borrow_global(@aptos_framework).chain_auditor_ek; + assert!(chain_auditor_ek_opt.is_some(), error::invalid_state(ECHAIN_AUDITOR_NOT_SET)); + + let asset_auditor_ek_opt = get_asset_auditor(token); + let required_prefix = if (asset_auditor_ek_opt.is_some()) 2 else 1; + + if (auditor_eks.length() < required_prefix) { + return false + }; + + let chain_auditor_point = twisted_elgamal::pubkey_to_point(&chain_auditor_ek_opt.extract()); + let slot0_point = twisted_elgamal::pubkey_to_point(&auditor_eks[0]); + if (!ristretto255::point_equals(&chain_auditor_point, &slot0_point)) { + return false + }; + + if (asset_auditor_ek_opt.is_some()) { + let asset_auditor_point = twisted_elgamal::pubkey_to_point(&asset_auditor_ek_opt.extract()); + let slot1_point = twisted_elgamal::pubkey_to_point(&auditor_eks[1]); + if (!ristretto255::point_equals(&asset_auditor_point, &slot1_point)) { + return false + }; + }; + + true + } + + /// Deserializes the auditor EKs from a byte array. + /// Returns `Some(vector)` if the deserialization is successful, otherwise `None`. + fun deserialize_auditor_eks( + auditor_eks_bytes: vector): Option> + { + if (auditor_eks_bytes.length() % 32 != 0) { + return std::option::none() + }; + + let auditors_count = auditor_eks_bytes.length() / 32; + + let auditor_eks = vector::range(0, auditors_count).map(|i| { + twisted_elgamal::new_pubkey_from_bytes(auditor_eks_bytes.slice(i * 32, (i + 1) * 32)) + }); + + if (auditor_eks.any(|ek| ek.is_none())) { + return std::option::none() + }; + + std::option::some(auditor_eks.map(|ek| ek.extract())) + } + + /// Deserializes the auditor amounts from a byte array. + /// Returns `Some(vector)` if the deserialization is successful, otherwise `None`. + fun deserialize_auditor_amounts( + auditor_amounts_bytes: vector): Option> + { + if (auditor_amounts_bytes.length() % 256 != 0) { + return std::option::none() + }; + + let auditors_count = auditor_amounts_bytes.length() / 256; + + let auditor_amounts = vector::range(0, auditors_count).map(|i| { + confidential_balance::new_pending_balance_from_bytes(auditor_amounts_bytes.slice(i * 256, (i + 1) * 256)) + }); + + if (auditor_amounts.any(|ek| ek.is_none())) { + return std::option::none() + }; + + std::option::some(auditor_amounts.map(|balance| balance.extract())) + } + + /// Converts coins to missing FA. + /// Returns `Some(Object)` if user has a sufficient amount of FA to proceed, otherwise `None`. + fun ensure_sufficient_fa(sender: &signer, amount: u64): Option> { + let user = signer::address_of(sender); + let fa = coin::paired_metadata(); + + if (fa.is_none()) { + return fa; + }; + + let fa_balance = primary_fungible_store::balance(user, *fa.borrow()); + + if (fa_balance >= amount) { + return fa; + }; + + if (coin::balance(user) < amount) { + return std::option::none(); + }; + + let coin_amount = coin::withdraw(sender, amount - fa_balance); + let fa_amount = coin::coin_to_fungible_asset(coin_amount); + + primary_fungible_store::deposit(user, fa_amount); + + fa + } + + // + // Test-only functions + // + + #[test_only] + public fun init_module_for_testing(deployer: &signer) { + initialize(deployer) + } + + #[test_only] + /// Register without requiring a registration proof (for test convenience). + public fun register_for_testing( + sender: &signer, + token: Object, + ek: vector) acquires GlobalConfig, FAConfig + { + let ek = twisted_elgamal::new_pubkey_from_bytes(ek).extract(); + register_internal(sender, token, ek); + } + + #[test_only] + public fun verify_pending_balance( + user: address, + token: Object, + user_dk: &Scalar, + amount: u64): bool acquires ConfidentialAssetStore + { + let ca_store = borrow_global(get_user_address(user, token)); + let pending_balance = confidential_balance::decompress_balance(&ca_store.pending_balance); + + confidential_balance::verify_pending_balance(&pending_balance, user_dk, amount) + } + + #[test_only] + public fun verify_actual_balance( + user: address, + token: Object, + user_dk: &Scalar, + amount: u128): bool acquires ConfidentialAssetStore + { + let ca_store = borrow_global(get_user_address(user, token)); + let actual_balance = confidential_balance::decompress_balance(&ca_store.actual_balance); + + confidential_balance::verify_actual_balance(&actual_balance, user_dk, amount) + } + + /// Pure serialization helpers (no `borrow_global`). Public so off-chain tooling and + /// tooling can exercise the same entrypoints as tests without `#[test_only]` harness modules. + public fun serialize_auditor_eks(auditor_eks: &vector): vector { + let auditor_eks_bytes = vector[]; + + auditor_eks.for_each_ref(|auditor| { + auditor_eks_bytes.append(twisted_elgamal::pubkey_to_bytes(auditor)); + }); + + auditor_eks_bytes + } + + public fun serialize_auditor_amounts( + auditor_amounts: &vector + ): vector { + let auditor_amounts_bytes = vector[]; + + auditor_amounts.for_each_ref(|balance| { + auditor_amounts_bytes.append(confidential_balance::balance_to_bytes(balance)); + }); + + auditor_amounts_bytes + } + + #[test_only] + /// Asserts the last emitted `Transferred` matches `from` / `to` / `asset_type`, the expected + /// `sender_auditor_hint`, `ek_volun_auds` length (`128 * expected_auditor_entry_count` bytes, + /// i.e. four 32-byte compressed points per auditor row), and on-chain `new_sender_available_balance` / + /// `new_recip_pending_balance` against `actual_balance` / `pending_balance`. Does not assert `amount` + /// or `memo` (memo is always empty in production transfers today). + public fun assert_last_transferred_event_matches_state( + token: Object, + expected_from: address, + expected_to: address, + expected_auditor_entry_count: u64, + expected_sender_auditor_hint: vector, + expected_chain_auditor_epoch: u64, + expected_asset_auditor_epoch: u64, + ) acquires ConfidentialAssetStore { + let evts = event::emitted_events(); + let len = vector::length(&evts); + assert!(len > 0, 1); + let e = vector::borrow(&evts, len - 1); + assert!(e.from == expected_from, 2); + assert!(e.to == expected_to, 3); + assert!(e.asset_type == object::object_address(&token), 4); + assert!(e.sender_auditor_hint == expected_sender_auditor_hint, 9); + assert!( + e.ek_volun_auds.length() == 32 * 4 * expected_auditor_entry_count, + 8 + ); + let on_chain_sender = actual_balance(expected_from, token); + let on_chain_recip_pending = pending_balance(expected_to, token); + assert!(e.new_sender_available_balance == on_chain_sender, 6); + assert!(e.new_recip_pending_balance == on_chain_recip_pending, 7); + assert!(e.chain_auditor_epoch == expected_chain_auditor_epoch, 10); + assert!(e.asset_auditor_epoch == expected_asset_auditor_epoch, 11); + } + + #[test_only] + public fun assert_last_registered_event( + token: Object, + expected_addr: address, + ) { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 100); + let e = &evts[evts.length() - 1]; + assert!(e.addr == expected_addr, 101); + assert!(e.asset_type == object::object_address(&token), 102); + } + + #[test_only] + public fun assert_last_deposited_event_matches_state( + token: Object, + expected_to: address, + expected_amount: u64, + ) acquires ConfidentialAssetStore { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 110); + let e = &evts[evts.length() - 1]; + assert!(e.to == expected_to, 111); + assert!(e.asset_type == object::object_address(&token), 112); + assert!(e.amount == expected_amount, 113); + assert!(e.new_pending_balance == pending_balance(expected_to, token), 114); + } + + #[test_only] + public fun assert_last_withdrawn_event_matches_state( + token: Object, + expected_from: address, + expected_amount: u64, + ) acquires ConfidentialAssetStore { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 120); + let e = &evts[evts.length() - 1]; + assert!(e.from == expected_from, 121); + assert!(e.asset_type == object::object_address(&token), 122); + assert!(e.amount == expected_amount, 123); + assert!(e.new_available_balance == actual_balance(expected_from, token), 124); + } + + #[test_only] + public fun assert_last_normalized_event_matches_state( + token: Object, + expected_addr: address, + ) acquires ConfidentialAssetStore { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 130); + let e = &evts[evts.length() - 1]; + assert!(e.addr == expected_addr, 131); + assert!(e.asset_type == object::object_address(&token), 132); + assert!(e.new_available_balance == actual_balance(expected_addr, token), 133); + } + + #[test_only] + public fun assert_last_rolled_over_event_matches_state( + token: Object, + expected_addr: address, + ) acquires ConfidentialAssetStore { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 140); + let e = &evts[evts.length() - 1]; + assert!(e.addr == expected_addr, 141); + assert!(e.asset_type == object::object_address(&token), 142); + assert!(e.new_available_balance == actual_balance(expected_addr, token), 143); + } + + #[test_only] + public fun assert_last_key_rotated_event_matches_state( + token: Object, + expected_addr: address, + ) acquires ConfidentialAssetStore { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 150); + let e = &evts[evts.length() - 1]; + assert!(e.addr == expected_addr, 151); + assert!(e.asset_type == object::object_address(&token), 152); + assert!(e.new_available_balance == actual_balance(expected_addr, token), 153); + assert!(e.new_ek == encryption_key(expected_addr, token), 154); + } + + #[test_only] + public fun assert_last_freeze_changed_event( + token: Object, + expected_addr: address, + expected_frozen: bool, + ) { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 160); + let e = &evts[evts.length() - 1]; + assert!(e.addr == expected_addr, 161); + assert!(e.asset_type == object::object_address(&token), 162); + assert!(e.frozen == expected_frozen, 163); + } + + #[test_only] + public fun assert_last_allow_list_changed_event(expected_enabled: bool) { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 170); + let e = &evts[evts.length() - 1]; + assert!(e.enabled == expected_enabled, 171); + } + + #[test_only] + public fun assert_last_token_allow_changed_event( + token: Object, + expected_allowed: bool, + ) { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 180); + let e = &evts[evts.length() - 1]; + assert!(e.asset_type == object::object_address(&token), 181); + assert!(e.allowed == expected_allowed, 182); + } + + #[test_only] + public fun assert_last_asset_auditor_changed_event(token: Object, expected_epoch: u64) { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 190); + let e = &evts[evts.length() - 1]; + assert!(e.asset_type == object::object_address(&token), 191); + assert!(e.new_epoch == expected_epoch, 192); + } + + #[test_only] + public fun assert_last_chain_auditor_changed_event(expected_epoch: u64) { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 195); + let e = &evts[evts.length() - 1]; + assert!(e.new_epoch == expected_epoch, 196); + } + + #[test_only] + public fun assert_last_chain_auditor_admin_changed_event(expected_admin: address) { + let evts = event::emitted_events(); + assert!(evts.length() > 0, 197); + let e = &evts[evts.length() - 1]; + assert!(e.new_admin == expected_admin, 198); + } +} diff --git a/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_asset.spec.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_asset.spec.move new file mode 100644 index 00000000000..c9a87dc323c --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_asset.spec.move @@ -0,0 +1,2 @@ +spec aptos_framework::confidential_asset { +} diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_balance.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_balance.move similarity index 95% rename from aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_balance.move rename to aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_balance.move index 32e847ce642..dd6a0872251 100644 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_balance.move +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_balance.move @@ -12,13 +12,13 @@ /// /// This implementation leverages the homomorphic properties of Twisted ElGamal encryption to allow arithmetic operations /// directly on encrypted data. -module aptos_experimental::confidential_balance { +module aptos_framework::confidential_balance { use std::error; use std::option::{Self, Option}; use std::vector; use aptos_std::ristretto255::{Self, RistrettoPoint, Scalar}; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; // // Errors @@ -196,18 +196,6 @@ module aptos_experimental::confidential_balance { }) } - /// Subtracts one confidential balance from another homomorphically, mutating the first balance in place. - /// The second balance must have fewer or equal chunks compared to the first. - public fun sub_balances_mut(lhs: &mut ConfidentialBalance, rhs: &ConfidentialBalance) { - assert!(lhs.chunks.length() >= rhs.chunks.length(), error::internal(EINTERNAL_ERROR)); - - lhs.chunks.enumerate_mut(|i, chunk| { - if (i < rhs.chunks.length()) { - twisted_elgamal::ciphertext_add_assign(chunk, &rhs.chunks[i]) - } - }) - } - /// Checks if two confidential balances are equivalent, including both value and randomness components. public fun balance_equals(lhs: &ConfidentialBalance, rhs: &ConfidentialBalance): bool { assert!(lhs.chunks.length() == rhs.chunks.length(), error::internal(EINTERNAL_ERROR)); diff --git a/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_balance.spec.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_balance.spec.move new file mode 100644 index 00000000000..53194790f43 --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_balance.spec.move @@ -0,0 +1,2 @@ +spec aptos_framework::confidential_balance { +} diff --git a/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_gas_e2e_helpers.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_gas_e2e_helpers.move new file mode 100644 index 00000000000..71e4c20f87e --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_gas_e2e_helpers.move @@ -0,0 +1,303 @@ +//! Helpers for Rust `e2e-move-tests`: bundle `confidential_proof::prove_*` output into BCS/byte vectors for +//! framework entry payloads. Callers must pass the same `sender_auditor_hint` bytes into packing helpers and into +//! `confidential_asset::confidential_transfer` so the Fiat–Shamir transcript matches. On-chain `Transferred` +//! emission (ciphertexts, `ek_volun_auds`, hint, balances) is asserted in Move unit tests (`confidential_asset_tests`), +//! not in these helpers. +#[test_only] +module aptos_framework::confidential_gas_e2e_helpers { + use std::vector; + use aptos_std::ristretto255::Scalar; + use aptos_framework::fungible_asset::Metadata; + use aptos_framework::object::{Self, Object}; + + use aptos_framework::confidential_asset; + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof; + use aptos_framework::ristretto255_twisted_elgamal::{Self as twisted_elgamal, CompressedPubkey}; + + /// `(new_balance_bytes, zkrp_new_balance, sigma_proof)` for `withdraw_to`. + public fun pack_withdraw_to_proof( + chain_id: u8, + sender: address, + dk: &Scalar, + ek: &CompressedPubkey, + withdraw_amount: u64, + new_balance_amount: u128, + token: Object, + ): (vector, vector, vector) { + let compressed = confidential_asset::actual_balance(sender, token); + let current = confidential_balance::decompress_balance(&compressed); + let (proof, new_balance) = confidential_proof::prove_withdrawal( + chain_id, + sender, + @aptos_framework, + object::object_address(&token), + dk, + ek, + withdraw_amount, + new_balance_amount, + ¤t, + ); + let new_balance_bytes = confidential_balance::balance_to_bytes(&new_balance); + let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_withdrawal_proof(&proof); + (new_balance_bytes, zkrp_new_balance, sigma_proof) + } + + /// Builds a transfer proof with no extra auditors. The chain-level auditor is fetched + /// from on-chain state and automatically placed at `auditor_eks[0]`, so the resulting + /// proof satisfies `validate_auditors` for any token without an asset auditor. + public fun pack_confidential_transfer_proof_simple( + chain_id: u8, + sender: address, + recipient: address, + sender_dk: &Scalar, + transfer_amount: u64, + new_balance_amount: u128, + token: Object, + sender_auditor_hint: vector, + ): ( + vector, + vector, + vector, + vector, + vector, + vector, + vector, + vector, + ) { + let auditors = build_auditor_list_with_chain_prefix(vector::empty>()); + pack_confidential_transfer_proof_inner( + chain_id, + sender, + recipient, + sender_dk, + transfer_amount, + new_balance_amount, + token, + &auditors, + sender_auditor_hint, + ) + } + + /// Builds a transfer proof and automatically prepends the on-chain chain-level auditor + /// at `auditor_eks[0]`. `extra_auditor_eks` (each 32-byte compressed pubkey) becomes + /// `auditor_eks[1..]` in order — i.e. the asset auditor (when set) followed by any + /// voluntary auditors. + public fun pack_confidential_transfer_proof_with_auditors( + chain_id: u8, + sender: address, + recipient: address, + sender_dk: &Scalar, + transfer_amount: u64, + new_balance_amount: u128, + token: Object, + extra_auditor_eks: vector>, + sender_auditor_hint: vector, + ): ( + vector, + vector, + vector, + vector, + vector, + vector, + vector, + vector, + ) { + let auditors = build_auditor_list_with_chain_prefix(extra_auditor_eks); + pack_confidential_transfer_proof_inner( + chain_id, + sender, + recipient, + sender_dk, + transfer_amount, + new_balance_amount, + token, + &auditors, + sender_auditor_hint, + ) + } + + fun build_auditor_list_with_chain_prefix( + extra_auditor_eks: vector>): vector + { + let chain_ek = confidential_asset::get_chain_auditor().extract(); + let acc = vector[chain_ek]; + let len = extra_auditor_eks.length(); + let i = 0; + while (i < len) { + vector::push_back( + &mut acc, + twisted_elgamal::new_pubkey_from_bytes(extra_auditor_eks[i]).extract(), + ); + i = i + 1; + }; + acc + } + + /// Like [`pack_confidential_transfer_proof_with_auditors`] but uses `auditor_eks` *verbatim* + /// without prepending the chain-level auditor. Used by rejection-path e2e tests that need + /// to construct intentionally invalid auditor prefixes (wrong slot 0, missing prefix, + /// post-rotation old key). + public fun pack_confidential_transfer_proof_verbatim( + chain_id: u8, + sender: address, + recipient: address, + sender_dk: &Scalar, + transfer_amount: u64, + new_balance_amount: u128, + token: Object, + auditor_eks: vector>, + sender_auditor_hint: vector, + ): ( + vector, + vector, + vector, + vector, + vector, + vector, + vector, + vector, + ) { + let parsed = vector::empty(); + let len = auditor_eks.length(); + let i = 0; + while (i < len) { + vector::push_back( + &mut parsed, + twisted_elgamal::new_pubkey_from_bytes(auditor_eks[i]).extract(), + ); + i = i + 1; + }; + pack_confidential_transfer_proof_inner( + chain_id, + sender, + recipient, + sender_dk, + transfer_amount, + new_balance_amount, + token, + &parsed, + sender_auditor_hint, + ) + } + + fun pack_confidential_transfer_proof_inner( + chain_id: u8, + sender: address, + recipient: address, + sender_dk: &Scalar, + transfer_amount: u64, + new_balance_amount: u128, + token: Object, + auditor_eks: &vector, + sender_auditor_hint: vector, + ): ( + vector, + vector, + vector, + vector, + vector, + vector, + vector, + vector, + ) { + let sender_ek = confidential_asset::encryption_key(sender, token); + let recipient_ek = confidential_asset::encryption_key(recipient, token); + let compressed = confidential_asset::actual_balance(sender, token); + let current = confidential_balance::decompress_balance(&compressed); + let ( + proof, + new_balance, + sender_amount, + recipient_amount, + auditor_amounts, + ) = confidential_proof::prove_transfer( + chain_id, + sender, + @aptos_framework, + object::object_address(&token), + sender_dk, + &sender_ek, + &recipient_ek, + transfer_amount, + new_balance_amount, + ¤t, + auditor_eks, + sender_auditor_hint, + ); + let (sigma_proof, zkrp_new_balance, zkrp_transfer_amount) = + confidential_proof::serialize_transfer_proof(&proof); + ( + confidential_balance::balance_to_bytes(&new_balance), + confidential_balance::balance_to_bytes(&sender_amount), + confidential_balance::balance_to_bytes(&recipient_amount), + confidential_asset::serialize_auditor_eks(auditor_eks), + confidential_asset::serialize_auditor_amounts(&auditor_amounts), + zkrp_new_balance, + zkrp_transfer_amount, + sigma_proof, + ) + } + + /// `(new_ek_bytes, new_balance_bytes, zkrp_new_balance, sigma_proof)` for `rotate_encryption_key`. + public fun pack_rotate_encryption_key_proof( + chain_id: u8, + sender: address, + sender_dk: &Scalar, + new_dk: &Scalar, + new_ek: &CompressedPubkey, + balance_amount: u128, + token: Object, + ): (vector, vector, vector, vector) { + let sender_ek = confidential_asset::encryption_key(sender, token); + let compressed = confidential_asset::actual_balance(sender, token); + let current = confidential_balance::decompress_balance(&compressed); + let (proof, new_balance) = confidential_proof::prove_rotation( + chain_id, + sender, + @aptos_framework, + object::object_address(&token), + sender_dk, + new_dk, + &sender_ek, + new_ek, + balance_amount, + ¤t, + ); + let new_balance_bytes = confidential_balance::balance_to_bytes(&new_balance); + let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_rotation_proof(&proof); + ( + twisted_elgamal::pubkey_to_bytes(new_ek), + new_balance_bytes, + zkrp_new_balance, + sigma_proof, + ) + } + + /// `(new_balance_bytes, zkrp_new_balance, sigma_proof)` for `normalize` after `rollover_pending_balance` + /// left the store denormalized (`amount` is the cleartext total to normalize to). + public fun pack_normalization_proof( + chain_id: u8, + sender: address, + dk: &Scalar, + amount: u128, + token: Object, + ): (vector, vector, vector) { + let ek = confidential_asset::encryption_key(sender, token); + let compressed = confidential_asset::actual_balance(sender, token); + let current = confidential_balance::decompress_balance(&compressed); + let (proof, new_balance) = confidential_proof::prove_normalization( + chain_id, + sender, + @aptos_framework, + object::object_address(&token), + dk, + &ek, + amount, + ¤t, + ); + let new_balance_bytes = confidential_balance::balance_to_bytes(&new_balance); + let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_normalization_proof(&proof); + (new_balance_bytes, zkrp_new_balance, sigma_proof) + } +} diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_proof.move similarity index 83% rename from aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.move rename to aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_proof.move index 866e7323132..39f8c30987f 100644 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.move +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_proof.move @@ -1,6 +1,7 @@ /// The `confidential_proof` module provides the infrastructure for verifying zero-knowledge proofs used in the Confidential Asset protocol. /// These proofs ensure correctness for operations such as `confidential_transfer`, `withdraw`, `rotate_encryption_key`, and `normalize`. -module aptos_experimental::confidential_proof { +module aptos_framework::confidential_proof { + use std::bcs; use std::error; use std::option; use std::option::Option; @@ -8,10 +9,10 @@ module aptos_experimental::confidential_proof { use aptos_std::ristretto255::{Self, CompressedRistretto, Scalar}; use aptos_std::ristretto255_bulletproofs::{Self as bulletproofs, RangeProof}; - use aptos_experimental::confidential_balance; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_balance; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; - friend aptos_experimental::confidential_asset; + friend aptos_framework::confidential_asset; // // Errors @@ -24,12 +25,13 @@ module aptos_experimental::confidential_proof { // Constants // - const FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST: vector = b"AptosConfidentialAsset/WithdrawalProofFiatShamir"; - const FIAT_SHAMIR_TRANSFER_SIGMA_DST: vector = b"AptosConfidentialAsset/TransferProofFiatShamir"; - const FIAT_SHAMIR_ROTATION_SIGMA_DST: vector = b"AptosConfidentialAsset/RotationProofFiatShamir"; - const FIAT_SHAMIR_NORMALIZATION_SIGMA_DST: vector = b"AptosConfidentialAsset/NormalizationProofFiatShamir"; + const FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST: vector = b"MovementConfidentialAsset/Withdrawal"; + const FIAT_SHAMIR_TRANSFER_SIGMA_DST: vector = b"MovementConfidentialAsset/Transfer"; + const FIAT_SHAMIR_ROTATION_SIGMA_DST: vector = b"MovementConfidentialAsset/Rotation"; + const FIAT_SHAMIR_NORMALIZATION_SIGMA_DST: vector = b"MovementConfidentialAsset/Normalization"; + const FIAT_SHAMIR_REGISTRATION_SIGMA_DST: vector = b"MovementConfidentialAsset/Registration"; - const BULLETPROOFS_DST: vector = b"AptosConfidentialAsset/BulletproofRangeProof"; + const BULLETPROOFS_DST: vector = b"MovementConfidentialAsset/BulletproofRangeProof"; const BULLETPROOFS_NUM_BITS: u64 = 16; // @@ -195,6 +197,54 @@ module aptos_experimental::confidential_proof { // Proof verification functions // + /// Verifies a registration proof (ZKPoK of decryption key). + /// + /// Ensures the registrant knows the decryption key dk such that ek = dk^{-1} * H. + /// The proof is a Schnorr proof: verifier checks s * H + e * ek == R. + public(friend) fun verify_registration_proof( + chain_id: u8, + sender: address, + contract_address: address, + ek: &twisted_elgamal::CompressedPubkey, + token_address: address, + commitment_bytes: vector, + response_bytes: vector) + { + // Decompress the commitment point R + let r_point = ristretto255::new_compressed_point_from_bytes(commitment_bytes); + assert!(option::is_some(&r_point), error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)); + let r_compressed = option::extract(&mut r_point); + + // Parse the response scalar + let s = ristretto255::new_scalar_from_bytes(response_bytes); + assert!(option::is_some(&s), error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED)); + let s = option::extract(&mut s); + + let msg = FIAT_SHAMIR_REGISTRATION_SIGMA_DST; + msg.push_back(chain_id); + msg.append(std::bcs::to_bytes(&sender)); + msg.append(std::bcs::to_bytes(&contract_address)); + msg.append(std::bcs::to_bytes(&token_address)); + msg.append(twisted_elgamal::pubkey_to_bytes(ek)); + msg.append(ristretto255::compressed_point_to_bytes(r_compressed)); + let e = ristretto255::new_scalar_from_sha2_512(msg); + + // Verify: s * H + e * ek == R + let h = ristretto255::hash_to_point_base(); + let ek_point = twisted_elgamal::pubkey_to_point(ek); + + let lhs = ristretto255::point_add( + &ristretto255::point_mul(&h, &s), + &ristretto255::point_mul(&ek_point, &e) + ); + let rhs = ristretto255::point_decompress(&r_compressed); + + assert!( + ristretto255::point_equals(&lhs, &rhs), + error::invalid_argument(ESIGMA_PROTOCOL_VERIFY_FAILED) + ); + } + /// Verifies the validity of the `withdraw` operation. /// /// This function ensures that the provided proof (`WithdrawalProof`) meets the following conditions: @@ -205,13 +255,27 @@ module aptos_experimental::confidential_proof { /// /// If all conditions are satisfied, the proof validates the withdrawal; otherwise, the function causes an error. public fun verify_withdrawal_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, ek: &twisted_elgamal::CompressedPubkey, amount: u64, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &WithdrawalProof) { - verify_withdrawal_sigma_proof(ek, amount, current_balance, new_balance, &proof.sigma_proof); + verify_withdrawal_sigma_proof( + chain_id, + sender, + contract_address, + token_address, + ek, + amount, + current_balance, + new_balance, + &proof.sigma_proof + ); verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance); } @@ -220,7 +284,7 @@ module aptos_experimental::confidential_proof { /// This function ensures that the provided proof (`TransferProof`) meets the following conditions: /// 1. The transferred amount (`recipient_amount` and `sender_amount`) and the auditors' amounts /// (`auditor_amounts`), if provided, encrypt the transfer value using the recipient's, sender's, - /// and auditors' encryption keys, repectively. + /// and auditors' encryption keys, respectively. /// 2. The sender's current balance (`current_balance`) and new balance (`new_balance`) encrypt the corresponding values /// under the sender's encryption key (`sender_ek`) before and after the transfer, respectively. /// 3. The relationship `new_balance = current_balance - transfer_amount` is maintained, ensuring balance integrity. @@ -228,7 +292,13 @@ module aptos_experimental::confidential_proof { /// 5. The sender's new balance is normalized, with each chunk in `new_balance` also adhering to the range [0, 2^16). /// /// If all conditions are satisfied, the proof validates the transfer; otherwise, the function causes an error. + /// + /// `sender_auditor_hint` is bound into the transfer sigma Fiat–Shamir transcript (same bytes as emitted on-chain). public fun verify_transfer_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, sender_ek: &twisted_elgamal::CompressedPubkey, recipient_ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, @@ -237,9 +307,14 @@ module aptos_experimental::confidential_proof { recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector, auditor_amounts: &vector, + sender_auditor_hint: &vector, proof: &TransferProof) { verify_transfer_sigma_proof( + chain_id, + sender, + contract_address, + token_address, sender_ek, recipient_ek, current_balance, @@ -248,6 +323,7 @@ module aptos_experimental::confidential_proof { recipient_amount, auditor_eks, auditor_amounts, + sender_auditor_hint, &proof.sigma_proof ); verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance); @@ -264,12 +340,25 @@ module aptos_experimental::confidential_proof { /// /// If all conditions are satisfied, the proof validates the normalization; otherwise, the function causes an error. public fun verify_normalization_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &NormalizationProof) { - verify_normalization_sigma_proof(ek, current_balance, new_balance, &proof.sigma_proof); + verify_normalization_sigma_proof( + chain_id, + sender, + contract_address, + token_address, + ek, + current_balance, + new_balance, + &proof.sigma_proof + ); verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance); } @@ -284,13 +373,27 @@ module aptos_experimental::confidential_proof { /// /// If all conditions are satisfied, the proof validates the key rotation; otherwise, the function causes an error. public fun verify_rotation_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, current_ek: &twisted_elgamal::CompressedPubkey, new_ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &RotationProof) { - verify_rotation_sigma_proof(current_ek, new_ek, current_balance, new_balance, &proof.sigma_proof); + verify_rotation_sigma_proof( + chain_id, + sender, + contract_address, + token_address, + current_ek, + new_ek, + current_balance, + new_balance, + &proof.sigma_proof + ); verify_new_balance_range_proof(new_balance, &proof.zkrp_new_balance); } @@ -300,6 +403,10 @@ module aptos_experimental::confidential_proof { /// Verifies the validity of the `WithdrawalSigmaProof`. fun verify_withdrawal_sigma_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, ek: &twisted_elgamal::CompressedPubkey, amount: u64, current_balance: &confidential_balance::ConfidentialBalance, @@ -309,7 +416,16 @@ module aptos_experimental::confidential_proof { let amount_chunks = confidential_balance::split_into_chunks_u64(amount); let amount = ristretto255::new_scalar_from_u64(amount); - let rho = fiat_shamir_withdrawal_sigma_proof_challenge(ek, &amount_chunks, current_balance, &proof.xs); + let rho = fiat_shamir_withdrawal_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, + ek, + &amount_chunks, + current_balance, + &proof.xs + ); let gammas = msm_withdrawal_gammas(&rho); @@ -390,6 +506,10 @@ module aptos_experimental::confidential_proof { /// Verifies the validity of the `TransferSigmaProof`. fun verify_transfer_sigma_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, sender_ek: &twisted_elgamal::CompressedPubkey, recipient_ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, @@ -398,9 +518,14 @@ module aptos_experimental::confidential_proof { recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector, auditor_amounts: &vector, + sender_auditor_hint: &vector, proof: &TransferSigmaProof) { let rho = fiat_shamir_transfer_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, sender_ek, recipient_ek, current_balance, @@ -409,6 +534,7 @@ module aptos_experimental::confidential_proof { recipient_amount, auditor_eks, auditor_amounts, + sender_auditor_hint, &proof.xs ); @@ -582,12 +708,25 @@ module aptos_experimental::confidential_proof { /// Verifies the validity of the `NormalizationSigmaProof`. fun verify_normalization_sigma_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof: &NormalizationSigmaProof) { - let rho = fiat_shamir_normalization_sigma_proof_challenge(ek, current_balance, new_balance, &proof.xs); + let rho = fiat_shamir_normalization_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, + ek, + current_balance, + new_balance, + &proof.xs + ); let gammas = msm_normalization_gammas(&rho); let scalars_lhs = vector[gammas.g1, gammas.g2]; @@ -666,6 +805,10 @@ module aptos_experimental::confidential_proof { /// Verifies the validity of the `RotationSigmaProof`. fun verify_rotation_sigma_proof( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, current_ek: &twisted_elgamal::CompressedPubkey, new_ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, @@ -673,6 +816,10 @@ module aptos_experimental::confidential_proof { proof: &RotationSigmaProof) { let rho = fiat_shamir_rotation_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, current_ek, new_ek, current_balance, @@ -760,7 +907,7 @@ module aptos_experimental::confidential_proof { ); } - /// Verifies the validity of the `NewBalanceRangeProof`. + /// Verifies the Bulletproofs range proof for `new_balance` ciphertext chunks (normalized 16-bit limbs). fun verify_new_balance_range_proof( new_balance: &confidential_balance::ConfidentialBalance, zkrp_new_balance: &RangeProof) @@ -780,7 +927,7 @@ module aptos_experimental::confidential_proof { ); } - /// Verifies the validity of the `TransferBalanceRangeProof`. + /// Verifies the Bulletproofs range proof for the encrypted transfer amount (`transfer_amount`). fun verify_transfer_amount_range_proof( transfer_amount: &confidential_balance::ConfidentialBalance, zkrp_transfer_amount: &RangeProof) @@ -804,12 +951,36 @@ module aptos_experimental::confidential_proof { // Friend public functions // - /// Returns the number of range proofs in the provided `WithdrawalProof`. - /// Used in the `confidential_asset` module to validate input parameters of the `confidential_transfer` function. + /// Returns `n`, the number of **auditor rows** encoded in the transfer sigma proof — i.e. + /// `proof.sigma_proof.xs.x7s.length()`. Each row holds the four `x7s` curve commitments for one auditor EK. + /// `confidential_asset` uses this to cross-check auditor ciphertext vectors on `confidential_transfer`. public(friend) fun auditors_count_in_transfer_proof(proof: &TransferProof): u64 { proof.sigma_proof.xs.x7s.length() } + /// Serializes `proof.sigma_proof.xs.x7s` for the `Transferred` event field `ek_volun_auds`: every commitment + /// is written as **32 bytes** (`ristretto255::compressed_point_to_bytes`), outer vector = auditors (same order + /// as the transfer's auditor EK list), inner vector length is **4** (one compressed point per 16-bit amount + /// chunk lane). **Total length = `128 × auditors_count_in_transfer_proof(proof)`** bytes (or `0` when `n = 0`). + public(friend) fun transfer_proof_ek_volun_auds_flat_bytes(proof: &TransferProof): vector { + let out = vector[]; + let rows = &proof.sigma_proof.xs.x7s; + let i = 0u64; + let n = vector::length(rows); + while (i < n) { + let row = vector::borrow(rows, i); + let j = 0u64; + let m = vector::length(row); + while (j < m) { + let p = *vector::borrow(row, j); + out.append(ristretto255::compressed_point_to_bytes(p)); + j = j + 1; + }; + i = i + 1; + }; + out + } + // // Deserialization functions // @@ -1108,6 +1279,12 @@ module aptos_experimental::confidential_proof { FIAT_SHAMIR_ROTATION_SIGMA_DST } + #[view] + /// Returns the Fiat Shamir DST for registration sigma (`verify_registration_proof`). + public fun get_fiat_shamir_registration_sigma_dst(): vector { + FIAT_SHAMIR_REGISTRATION_SIGMA_DST + } + #[view] /// Returns the DST for the range proofs. public fun get_bulletproofs_dst(): vector { @@ -1120,6 +1297,25 @@ module aptos_experimental::confidential_proof { BULLETPROOFS_NUM_BITS } + /// Prepends `chain_id` (single byte), `sender`, `contract_address`, and `token_address` (BCS) to a Fiat-Shamir + /// message buffer. Binding `token_address` here domain-separates proofs across different fungible assets, so that + /// a proof generated for one token can never be replayed against a different token even if their stored + /// ciphertexts ever happened to coincide. + fun prepend_domain_context( + bytes: &mut vector, + chain_id: u8, + sender: address, + contract_address: address, + token_address: address + ) { + let context = vector::singleton(chain_id); + context.append(std::bcs::to_bytes(&sender)); + context.append(std::bcs::to_bytes(&contract_address)); + context.append(std::bcs::to_bytes(&token_address)); + context.append(*bytes); + *bytes = context; + } + // // Private functions for Fiat-Shamir challenge derivation. // The Fiat Shamir is used to make the proofs non-interactive. @@ -1128,13 +1324,17 @@ module aptos_experimental::confidential_proof { /// Derives the Fiat-Shamir challenge for the `WithdrawalSigmaProof`. fun fiat_shamir_withdrawal_sigma_proof_challenge( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, ek: &twisted_elgamal::CompressedPubkey, amount_chunks: &vector, current_balance: &confidential_balance::ConfidentialBalance, proof_xs: &WithdrawalSigmaProofXs): Scalar { - // rho = H(DST, G, H, P, v_{1..4}, (C_cur, D_cur)_{1..8}, X_{1..18}) - let bytes = FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST; + // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P || v_{1..4} || (C_cur, D_cur)_{1..8} || X_{1..18}) + let bytes = vector[]; bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed())); bytes.append( @@ -1154,11 +1354,18 @@ module aptos_experimental::confidential_proof { bytes.append(ristretto255::point_to_bytes(x)); }); - ristretto255::new_scalar_from_sha2_512(bytes) + prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address); + let msg = FIAT_SHAMIR_WITHDRAWAL_SIGMA_DST; + msg.append(bytes); + ristretto255::new_scalar_from_sha2_512(msg) } /// Derives the Fiat-Shamir challenge for the `TransferSigmaProof`. fun fiat_shamir_transfer_sigma_proof_challenge( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, sender_ek: &twisted_elgamal::CompressedPubkey, recipient_ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, @@ -1167,10 +1374,11 @@ module aptos_experimental::confidential_proof { recipient_amount: &confidential_balance::ConfidentialBalance, auditor_eks: &vector, auditor_amounts: &vector, + sender_auditor_hint: &vector, proof_xs: &TransferSigmaProofXs): Scalar { - // rho = H(DST, G, H, P_s, P_r, P_a_{1..n}, (C_cur, D_cur)_{1..8}, (C_v, D_v)_{1..4}, D_a_{1..4n}, D_s_{1..4}, (C_new, D_new)_{1..8}, X_{1..30 + 4n}) - let bytes = FIAT_SHAMIR_TRANSFER_SIGMA_DST; + // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P_s || P_r || ...) + let bytes = vector[]; bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed())); bytes.append( @@ -1215,18 +1423,27 @@ module aptos_experimental::confidential_proof { bytes.append(ristretto255::point_to_bytes(x)); }); - ristretto255::new_scalar_from_sha2_512(bytes) + bytes.append(bcs::to_bytes(sender_auditor_hint)); + + prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address); + let msg = FIAT_SHAMIR_TRANSFER_SIGMA_DST; + msg.append(bytes); + ristretto255::new_scalar_from_sha2_512(msg) } /// Derives the Fiat-Shamir challenge for the `NormalizationSigmaProof`. fun fiat_shamir_normalization_sigma_proof_challenge( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof_xs: &NormalizationSigmaProofXs): Scalar { - // rho = H(DST, G, H, P, (C_cur, D_cur)_{1..8}, (C_new, D_new)_{1..8}, X_{1..18}) - let bytes = FIAT_SHAMIR_NORMALIZATION_SIGMA_DST; + // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P || ...) + let bytes = vector[]; bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed())); bytes.append( @@ -1244,19 +1461,26 @@ module aptos_experimental::confidential_proof { bytes.append(ristretto255::point_to_bytes(x)); }); - ristretto255::new_scalar_from_sha2_512(bytes) + prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address); + let msg = FIAT_SHAMIR_NORMALIZATION_SIGMA_DST; + msg.append(bytes); + ristretto255::new_scalar_from_sha2_512(msg) } /// Derives the Fiat-Shamir challenge for the `RotationSigmaProof`. fun fiat_shamir_rotation_sigma_proof_challenge( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, current_ek: &twisted_elgamal::CompressedPubkey, new_ek: &twisted_elgamal::CompressedPubkey, current_balance: &confidential_balance::ConfidentialBalance, new_balance: &confidential_balance::ConfidentialBalance, proof_xs: &RotationSigmaProofXs): Scalar { - // rho = H(DST, G, H, P_cur, P_new, (C_cur, D_cur)_{1..8}, (C_new, D_new)_{1..8}, X_{1..19}) - let bytes = FIAT_SHAMIR_ROTATION_SIGMA_DST; + // rho = SHA2-512(DST || chain_id || sender || contract || token || G || H || P_cur || P_new || ...) + let bytes = vector[]; bytes.append(ristretto255::compressed_point_to_bytes(ristretto255::basepoint_compressed())); bytes.append( @@ -1276,7 +1500,10 @@ module aptos_experimental::confidential_proof { bytes.append(ristretto255::point_to_bytes(x)); }); - ristretto255::new_scalar_from_sha2_512(bytes) + prepend_domain_context(&mut bytes, chain_id, sender, contract_address, token_address); + let msg = FIAT_SHAMIR_ROTATION_SIGMA_DST; + msg.append(bytes); + ristretto255::new_scalar_from_sha2_512(msg) } // @@ -1320,8 +1547,10 @@ module aptos_experimental::confidential_proof { ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, (i + 7 as u8), (j as u8))) }) }), + // Index starts past g7s range to avoid gamma collision when auditors_count >= 2. + // g7s uses indices 7..7+n-1; g8s uses 7+n. g8s: vector::range(0, 4).map(|i| { - ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, 8, (i as u8))) + ristretto255::new_scalar_from_sha2_512(msm_gamma_2(rho, (auditors_count + 7 as u8), (i as u8))) }), } } @@ -1437,6 +1666,10 @@ module aptos_experimental::confidential_proof { #[test_only] public fun prove_withdrawal( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, dk: &Scalar, ek: &twisted_elgamal::CompressedPubkey, amount: u64, @@ -1489,7 +1722,16 @@ module aptos_experimental::confidential_proof { let amount_chunks = confidential_balance::split_into_chunks_u64(amount); - let rho = fiat_shamir_withdrawal_sigma_proof_challenge(ek, &amount_chunks, current_balance, &proof_xs); + let rho = fiat_shamir_withdrawal_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, + ek, + &amount_chunks, + current_balance, + &proof_xs + ); let new_amount_chunks = confidential_balance::split_into_chunks_u128(new_amount); @@ -1519,13 +1761,18 @@ module aptos_experimental::confidential_proof { #[test_only] public fun prove_transfer( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, sender_dk: &Scalar, sender_ek: &twisted_elgamal::CompressedPubkey, recipient_ek: &twisted_elgamal::CompressedPubkey, amount: u64, new_amount: u128, current_balance: &confidential_balance::ConfidentialBalance, - auditor_eks: &vector + auditor_eks: &vector, + sender_auditor_hint: vector ): ( TransferProof, confidential_balance::ConfidentialBalance, @@ -1643,6 +1890,10 @@ module aptos_experimental::confidential_proof { }; let rho = fiat_shamir_transfer_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, sender_ek, recipient_ek, current_balance, @@ -1651,6 +1902,7 @@ module aptos_experimental::confidential_proof { &recipient_amount, auditor_eks, &auditor_amounts, + &sender_auditor_hint, &proof_xs ); @@ -1693,6 +1945,10 @@ module aptos_experimental::confidential_proof { #[test_only] public fun prove_normalization( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, dk: &Scalar, ek: &twisted_elgamal::CompressedPubkey, amount: u128, @@ -1745,6 +2001,10 @@ module aptos_experimental::confidential_proof { }; let rho = fiat_shamir_normalization_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, ek, current_balance, &new_balance, @@ -1779,6 +2039,10 @@ module aptos_experimental::confidential_proof { #[test_only] public fun prove_rotation( + chain_id: u8, + sender: address, + contract_address: address, + token_address: address, current_dk: &Scalar, new_dk: &Scalar, current_ek: &twisted_elgamal::CompressedPubkey, @@ -1834,6 +2098,10 @@ module aptos_experimental::confidential_proof { }; let rho = fiat_shamir_rotation_sigma_proof_challenge( + chain_id, + sender, + contract_address, + token_address, current_ek, new_ek, current_balance, @@ -2092,4 +2360,121 @@ module aptos_experimental::confidential_proof { x5s: vector::range(0, 8).map(|_| ristretto255::random_scalar()), } } + + #[test_only] + /// Same transcript and algebra as on-chain registration prove, with caller-supplied nonce `k` + /// (used by `prove_registration` after drawing random `k`). + fun prove_registration_deterministic( + chain_id: u8, + sender: address, + contract_address: address, + dk: &Scalar, + ek: &twisted_elgamal::CompressedPubkey, + token_address: address, + k: &Scalar, + ): (vector, vector) { + let h = ristretto255::hash_to_point_base(); + let r = ristretto255::point_mul(&h, k); + let r_compressed = ristretto255::point_compress(&r); + + let msg = FIAT_SHAMIR_REGISTRATION_SIGMA_DST; + msg.push_back(chain_id); + msg.append(std::bcs::to_bytes(&sender)); + msg.append(std::bcs::to_bytes(&contract_address)); + msg.append(std::bcs::to_bytes(&token_address)); + msg.append(twisted_elgamal::pubkey_to_bytes(ek)); + msg.append(ristretto255::compressed_point_to_bytes(r_compressed)); + let e = ristretto255::new_scalar_from_sha2_512(msg); + + let dk_inv = ristretto255::scalar_invert(dk).extract(); + let s = ristretto255::scalar_sub(k, &ristretto255::scalar_mul(&e, &dk_inv)); + + let commitment_bytes = ristretto255::compressed_point_to_bytes(r_compressed); + let response_bytes = ristretto255::scalar_to_bytes(&s); + + (commitment_bytes, response_bytes) + } + + #[test_only] + public fun prove_registration( + chain_id: u8, + sender: address, + contract_address: address, + dk: &Scalar, + ek: &twisted_elgamal::CompressedPubkey, + token_address: address, + ): (vector, vector) { + let k = ristretto255::random_scalar(); + prove_registration_deterministic( + chain_id, + sender, + contract_address, + dk, + ek, + token_address, + &k, + ) + } + + #[test_only] + public fun verify_registration_proof_for_test( + chain_id: u8, + sender: address, + contract_address: address, + ek: &twisted_elgamal::CompressedPubkey, + token_address: address, + commitment_bytes: vector, + response_bytes: vector) + { + verify_registration_proof( + chain_id, + sender, + contract_address, + ek, + token_address, + commitment_bytes, + response_bytes + ); + } + + // --------------------------------------------------------------------------- + // Regression test: gamma index collision for multi-auditor transfers + // --------------------------------------------------------------------------- + + #[test] + /// With 2 auditors the old hardcoded `g8s` index (8) collided with `g7s[1]` + /// (index 7+1 = 8). This test proves: + /// 1. The collision was real: `msm_gamma_2(rho, 8, j) == msm_gamma_2(rho, 1+7, j)`. + /// 2. The fix eliminates it: `msm_gamma_2(rho, auditors_count+7, j)` differs + /// from every `g7s` entry when `auditors_count >= 2`. + fun test_g8s_gamma_no_collision_with_g7s() { + let rho = ristretto255::random_scalar(); + let auditors_count: u64 = 2; + + // --- (1) Demonstrate the old collision --- + // Old g8s used hardcoded index 8. g7s[1] uses (1 + 7) = 8. + let old_g8s_0 = msm_gamma_2(&rho, 8, 0); + let g7s_1_0 = msm_gamma_2(&rho, (1 + 7 as u8), 0); + assert!(old_g8s_0 == g7s_1_0, 1); // proves the collision existed + + // --- (2) Prove the fix: g8s now uses (auditors_count + 7) = 9 --- + let new_g8s_0 = msm_gamma_2(&rho, (auditors_count + 7 as u8), 0); + // Must differ from every g7s row (indices 7 and 8). + let g7s_0_0 = msm_gamma_2(&rho, (0 + 7 as u8), 0); + assert!(new_g8s_0 != g7s_0_0, 2); // differs from g7s[0] + assert!(new_g8s_0 != g7s_1_0, 3); // differs from g7s[1] + + // Also verify all four sub-indices (j = 0..3) are collision-free. + let j = 0; + while (j < 4) { + let g8 = msm_gamma_2(&rho, (auditors_count + 7 as u8), (j as u8)); + let k = 0; + while (k < auditors_count) { + let g7 = msm_gamma_2(&rho, (k + 7 as u8), (j as u8)); + assert!(g8 != g7, 100 + j * 10 + k); + k = k + 1; + }; + j = j + 1; + }; + } } diff --git a/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_proof.spec.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_proof.spec.move new file mode 100644 index 00000000000..c70f18e7890 --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/confidential_proof.spec.move @@ -0,0 +1,2 @@ +spec aptos_framework::confidential_proof { +} diff --git a/aptos-move/framework/aptos-experimental/sources/confidential_asset/ristretto255_twisted_elgamal.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/ristretto255_twisted_elgamal.move similarity index 87% rename from aptos-move/framework/aptos-experimental/sources/confidential_asset/ristretto255_twisted_elgamal.move rename to aptos-move/framework/aptos-framework/sources/confidential_asset/ristretto255_twisted_elgamal.move index 8f31e2b68fb..ae3639e58db 100644 --- a/aptos-move/framework/aptos-experimental/sources/confidential_asset/ristretto255_twisted_elgamal.move +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/ristretto255_twisted_elgamal.move @@ -9,7 +9,7 @@ /// flexibility and functionality in cryptographic protocols. This design still maintains the homomorphic property: /// `Enc_Y(v, r) + Enc_Y(v', r') = Enc_Y(v + v', r + r')`, where `v, v'` are plaintexts, `Y` is the public key, /// and `r, r'` are random scalars. -module aptos_experimental::ristretto255_twisted_elgamal { +module aptos_framework::ristretto255_twisted_elgamal { use std::option::Option; use aptos_std::ristretto255::{Self, CompressedRistretto, RistrettoPoint, Scalar}; @@ -39,12 +39,25 @@ module aptos_experimental::ristretto255_twisted_elgamal { // /// Creates a new public key from a serialized Ristretto255 point. - /// Returns `Some(CompressedPubkey)` if the deserialization is successful, otherwise `None`. + /// Returns `Some(CompressedPubkey)` if the deserialization is successful and the + /// resulting point is non-identity, otherwise `None`. + /// + /// Identity-point public keys are rejected because they break both privacy and + /// soundness: ciphertexts encrypted under `ek = identity` have the form + /// `(v*G + r*H, r*identity) = (v*G + r*H, identity)`, so the randomness blinding + /// is null and any observer can brute-force the encrypted value. Sigma protocols + /// that bind the public key (registration, transfer, rotation) also become + /// trivially forgeable: the prover does not need to know any secret key, since + /// `e * identity = identity` for any challenge `e`. public fun new_pubkey_from_bytes(bytes: vector): Option { let point = ristretto255::new_compressed_point_from_bytes(bytes); if (point.is_some()) { + let compressed = point.extract(); + if (is_identity_compressed(&compressed)) { + return std::option::none() + }; let pk = CompressedPubkey { - point: point.extract() + point: compressed }; std::option::some(pk) } else { @@ -52,6 +65,19 @@ module aptos_experimental::ristretto255_twisted_elgamal { } } + /// Returns `true` if the given public key is the Ristretto255 identity point. + /// Such keys are rejected by `new_pubkey_from_bytes`; this helper is exposed for + /// callers that obtain a `CompressedPubkey` through other means and want to + /// re-validate it before use. + public fun is_identity_pubkey(pubkey: &CompressedPubkey): bool { + is_identity_compressed(&pubkey.point) + } + + fun is_identity_compressed(point: &CompressedRistretto): bool { + ristretto255::compressed_point_to_bytes(*point) + == ristretto255::compressed_point_to_bytes(ristretto255::point_identity_compressed()) + } + /// Serializes a Twisted ElGamal public key into its byte representation. public fun pubkey_to_bytes(pubkey: &CompressedPubkey): vector { ristretto255::compressed_point_to_bytes(pubkey.point) diff --git a/aptos-move/framework/aptos-framework/sources/confidential_asset/ristretto255_twisted_elgamal.spec.move b/aptos-move/framework/aptos-framework/sources/confidential_asset/ristretto255_twisted_elgamal.spec.move new file mode 100644 index 00000000000..b2243d73e77 --- /dev/null +++ b/aptos-move/framework/aptos-framework/sources/confidential_asset/ristretto255_twisted_elgamal.spec.move @@ -0,0 +1,2 @@ +spec aptos_framework::ristretto255_twisted_elgamal { +} diff --git a/aptos-move/framework/aptos-framework/sources/genesis.move b/aptos-move/framework/aptos-framework/sources/genesis.move index a90307d5b93..4995cc4f72c 100644 --- a/aptos-move/framework/aptos-framework/sources/genesis.move +++ b/aptos-move/framework/aptos-framework/sources/genesis.move @@ -13,6 +13,7 @@ module aptos_framework::genesis { use aptos_framework::block; use aptos_framework::chain_id; use aptos_framework::chain_status; + use aptos_framework::confidential_asset; use aptos_framework::coin; use aptos_framework::consensus_config; use aptos_framework::execution_config; @@ -132,6 +133,9 @@ module aptos_framework::genesis { block::initialize(&aptos_framework_account, epoch_interval_microsecs); state_storage::initialize(&aptos_framework_account); nonce_validation::initialize(&aptos_framework_account); + // Confidential asset ships in the genesis framework bundle, so its `init_module` never runs; + // publish its `GlobalConfig` explicitly. Must follow `chain_id::initialize` (read above). + confidential_asset::initialize(&aptos_framework_account); } /// Genesis step 2: Initialize Aptos coin. diff --git a/aptos-move/framework/aptos-framework/tests/confidential_asset/confidential_asset_tests.move b/aptos-move/framework/aptos-framework/tests/confidential_asset/confidential_asset_tests.move new file mode 100644 index 00000000000..8fc13583aa9 --- /dev/null +++ b/aptos-move/framework/aptos-framework/tests/confidential_asset/confidential_asset_tests.move @@ -0,0 +1,2204 @@ +#[test_only] +module aptos_framework::confidential_asset_tests { + use std::features; + use std::option; + use std::signer; + use std::string::utf8; + use aptos_std::ristretto255::Scalar; + use aptos_framework::account; + use aptos_framework::chain_id; + use aptos_framework::coin; + use aptos_framework::dispatchable_fungible_asset; + use aptos_framework::fungible_asset::{Self, Metadata}; + use aptos_framework::object::{Self, Object}; + use aptos_framework::primary_fungible_store; + + use aptos_framework::confidential_asset; + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof; + use aptos_framework::ristretto255_twisted_elgamal::{Self as twisted_elgamal, generate_twisted_elgamal_keypair}; + + struct MockCoin {} + + fun withdraw( + sender: &signer, + sender_dk: &Scalar, + token: Object, + to: address, + amount: u64, + new_amount: u128) + { + let from = signer::address_of(sender); + let sender_ek = confidential_asset::encryption_key(from, token); + let current_balance = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(from, token) + ); + + let cid = 4u8; // test chain ID + let (proof, new_balance) = confidential_proof::prove_withdrawal( + cid, + from, + @aptos_framework, + object::object_address(&token), + sender_dk, + &sender_ek, + amount, + new_amount, + ¤t_balance + ); + + let new_balance = confidential_balance::balance_to_bytes(&new_balance); + let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_withdrawal_proof(&proof); + + if (signer::address_of(sender) == to) { + confidential_asset::withdraw(sender, token, amount, new_balance, zkrp_new_balance, sigma_proof); + } else { + confidential_asset::withdraw_to(sender, token, to, amount, new_balance, zkrp_new_balance, sigma_proof); + } + } + + fun transfer( + sender: &signer, + sender_dk: &Scalar, + token: Object, + to: address, + amount: u64, + new_amount: u128, + sender_auditor_hint: vector) + { + // Every confidential transfer must include the chain-level auditor at slot 0; helper + // fetches it from on-chain state so individual tests don't have to thread it through. + let chain_auditor_ek = confidential_asset::get_chain_auditor().extract(); + let auditor_eks = vector[chain_auditor_ek]; + + let from = signer::address_of(sender); + let sender_ek = confidential_asset::encryption_key(from, token); + let recipient_ek = confidential_asset::encryption_key(to, token); + let current_balance = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(from, token) + ); + + let ( + proof, + new_balance, + sender_amount, + recipient_amount, + auditor_amounts + ) = confidential_proof::prove_transfer( + 4u8, // test chain ID + from, + @aptos_framework, + object::object_address(&token), + sender_dk, + &sender_ek, + &recipient_ek, + amount, + new_amount, + ¤t_balance, + &auditor_eks, + sender_auditor_hint, + ); + + let (sigma_proof, zkrp_new_balance, zkrp_transfer_amount) = confidential_proof::serialize_transfer_proof( + &proof + ); + + confidential_asset::confidential_transfer( + sender, + token, + to, + confidential_balance::balance_to_bytes(&new_balance), + confidential_balance::balance_to_bytes(&sender_amount), + confidential_balance::balance_to_bytes(&recipient_amount), + confidential_asset::serialize_auditor_eks(&auditor_eks), + confidential_asset::serialize_auditor_amounts(&auditor_amounts), + zkrp_new_balance, + zkrp_transfer_amount, + sigma_proof, + sender_auditor_hint + ); + } + + /// Like `transfer`, but lets the caller append additional auditor keys (asset-level + /// and/or voluntary). The chain-level auditor is fetched from on-chain state and + /// automatically placed at slot 0; the caller's `extra_auditor_eks` are appended in + /// order, so an asset-auditor test should pass `[asset_auditor_ek, vol1, vol2, ...]` + /// and a pure-voluntary test should pass `[vol1, vol2, ...]`. + fun audit_transfer( + sender: &signer, + sender_dk: &Scalar, + token: Object, + to: address, + amount: u64, + new_amount: u128, + extra_auditor_eks: &vector, + sender_auditor_hint: vector): vector + { + let chain_auditor_ek = confidential_asset::get_chain_auditor().extract(); + let auditor_eks = vector[chain_auditor_ek]; + extra_auditor_eks.for_each_ref(|ek| auditor_eks.push_back(*ek)); + + let from = signer::address_of(sender); + let sender_ek = confidential_asset::encryption_key(from, token); + let recipient_ek = confidential_asset::encryption_key(to, token); + let current_balance = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(from, token) + ); + + let ( + proof, + new_balance, + sender_amount, + recipient_amount, + auditor_amounts + ) = confidential_proof::prove_transfer( + 4u8, // test chain ID + from, + @aptos_framework, + object::object_address(&token), + sender_dk, + &sender_ek, + &recipient_ek, + amount, + new_amount, + ¤t_balance, + &auditor_eks, + sender_auditor_hint, + ); + + let (sigma_proof, zkrp_new_balance, zkrp_transfer_amount) = confidential_proof::serialize_transfer_proof( + &proof + ); + + confidential_asset::confidential_transfer( + sender, + token, + to, + confidential_balance::balance_to_bytes(&new_balance), + confidential_balance::balance_to_bytes(&sender_amount), + confidential_balance::balance_to_bytes(&recipient_amount), + confidential_asset::serialize_auditor_eks(&auditor_eks), + confidential_asset::serialize_auditor_amounts(&auditor_amounts), + zkrp_new_balance, + zkrp_transfer_amount, + sigma_proof, + sender_auditor_hint + ); + + auditor_amounts + } + + fun rotate( + sender: &signer, + sender_dk: &Scalar, + token: Object, + new_dk: &Scalar, + new_ek: &twisted_elgamal::CompressedPubkey, + amount: u128) + { + let from = signer::address_of(sender); + let sender_ek = confidential_asset::encryption_key(from, token); + let current_balance = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(from, token) + ); + + let (proof, new_balance) = confidential_proof::prove_rotation( + 4u8, // test chain ID + from, + @aptos_framework, + object::object_address(&token), + sender_dk, + new_dk, + &sender_ek, + new_ek, + amount, + ¤t_balance + ); + + let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_rotation_proof(&proof); + + confidential_asset::rotate_encryption_key( + sender, + token, + twisted_elgamal::pubkey_to_bytes(new_ek), + confidential_balance::balance_to_bytes(&new_balance), + zkrp_new_balance, + sigma_proof + ); + } + + fun normalize( + sender: &signer, + sender_dk: &Scalar, + token: Object, + amount: u128) + { + let from = signer::address_of(sender); + let sender_ek = confidential_asset::encryption_key(from, token); + let current_balance = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(from, token) + ); + + let (proof, new_balance) = confidential_proof::prove_normalization( + 4u8, // test chain ID + from, + @aptos_framework, + object::object_address(&token), + sender_dk, + &sender_ek, + amount, + ¤t_balance); + + let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_normalization_proof(&proof); + + confidential_asset::normalize( + sender, + token, + confidential_balance::balance_to_bytes(&new_balance), + zkrp_new_balance, + sigma_proof + ); + } + + fun normalize_and_rollover( + sender: &signer, + sender_dk: &Scalar, + token: Object, + amount: u128) + { + let from = signer::address_of(sender); + let sender_ek = confidential_asset::encryption_key(from, token); + let current_balance = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(from, token) + ); + + let (proof, new_balance) = confidential_proof::prove_normalization( + 4u8, // test chain ID + from, + @aptos_framework, + object::object_address(&token), + sender_dk, + &sender_ek, + amount, + ¤t_balance); + + let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_normalization_proof(&proof); + + confidential_asset::normalize_and_rollover_pending_balance( + sender, + token, + confidential_balance::balance_to_bytes(&new_balance), + zkrp_new_balance, + sigma_proof + ); + } + + public fun set_up_for_confidential_asset_test( + confidential_asset: &signer, + aptos_fx: &signer, + fa: &signer, + sender: &signer, + recipient: &signer, + sender_amount: u64, + recipient_amount: u64): Object + { + chain_id::initialize_for_test(aptos_fx, 4); + + let ctor_ref = &object::create_sticky_object(signer::address_of(fa)); + + primary_fungible_store::create_primary_store_enabled_fungible_asset( + ctor_ref, + option::none(), + utf8(b"MockToken"), + utf8(b"MT"), + 18, + utf8(b"https://"), + utf8(b"https://"), + ); + + let mint_ref = fungible_asset::generate_mint_ref(ctor_ref); + + assert!(signer::address_of(aptos_fx) != signer::address_of(sender), 1); + assert!(signer::address_of(aptos_fx) != signer::address_of(recipient), 2); + + confidential_asset::init_module_for_testing(confidential_asset); + + features::change_feature_flags_for_testing(aptos_fx, vector[features::get_bulletproofs_feature()], vector[]); + + // Every confidential transfer requires the chain-level auditor to be set. Since + // `set_chain_auditor` is now gated on the chain-auditor admin (governance does + // *not* hold rotation authority directly), governance first delegates the admin + // role to `aptos_fx` itself in tests so the shared setup can install a fresh key + // without standing up a separate admin account. Tests that exercise the + // governance-vs-admin separation install their own admin. + confidential_asset::set_chain_auditor_admin(aptos_fx, signer::address_of(aptos_fx)); + let (_chain_dk, chain_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(aptos_fx, twisted_elgamal::pubkey_to_bytes(&chain_ek)); + + let token = object::object_from_constructor_ref(ctor_ref); + + let sender_store = primary_fungible_store::ensure_primary_store_exists(signer::address_of(sender), token); + fungible_asset::mint_to(&mint_ref, sender_store, sender_amount); + + let recipient_store = primary_fungible_store::ensure_primary_store_exists(signer::address_of(recipient), token); + fungible_asset::mint_to(&mint_ref, recipient_store, recipient_amount); + + token + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_deposit_test( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + confidential_asset::deposit(&alice, token, 100); + confidential_asset::deposit_to(&alice, token, bob_addr, 150); + + assert!(primary_fungible_store::balance(alice_addr, token) == 250, 1); + assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 100), 1); + assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 150), 1); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_withdraw_test( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + withdraw(&alice, &alice_dk, token, bob_addr, 50, 150); + + assert!(primary_fungible_store::balance(bob_addr, token) == 550, 1); + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 150), 1); + + withdraw(&alice, &alice_dk, token, alice_addr, 50, 100); + + assert!(primary_fungible_store::balance(alice_addr, token) == 350, 1); + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 0x010019, location = confidential_asset)] + fun fail_deposit_zero_amount( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let bob_addr = signer::address_of(&bob); + + let (_alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + // Zero deposits move no funds but would consume the recipient's pending slots. + confidential_asset::deposit_to(&alice, token, bob_addr, 0); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 0x010019, location = confidential_asset)] + fun fail_withdraw_zero_amount( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + // A zero withdrawal would act as a normalize that skips the EALREADY_NORMALIZED guard. + withdraw(&alice, &alice_dk, token, alice_addr, 0, 200); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_transfer_test( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + transfer(&alice, &alice_dk, token, bob_addr, 100, 100, vector[]); + + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); + assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 100), 1); + } + + // Self-transfers are disabled: a confidential transfer whose recipient is the sender must abort + // with `ESELF_TRANSFER` (0x01001A) before mutating any balance. + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 0x01001A, location = confidential_asset)] + fun fail_self_transfer( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + transfer(&alice, &alice_dk, token, alice_addr, 100, 100, vector[]); + } + + // First-time combined entry point: register + deposit + rollover in one transaction. After + // success, the store is published, public FA moved into the protocol, and the deposited + // amount is in actual_balance (spendable) — not pending. + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_register_and_deposit_and_rollover_pending_balance( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (commitment, response) = confidential_proof::prove_registration( + 4u8, + alice_addr, + @aptos_framework, + &alice_dk, + &alice_ek, + object::object_address(&token), + ); + + confidential_asset::register_and_deposit_and_rollover_pending_balance( + &alice, + token, + 100, + twisted_elgamal::pubkey_to_bytes(&alice_ek), + commitment, + response, + ); + + assert!(confidential_asset::has_confidential_asset_store(alice_addr, token), 1); + assert!(primary_fungible_store::balance(alice_addr, token) == 400, 2); + // Funds landed in actual (spendable), not pending. Pending is empty after rollover. + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 3); + assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 0), 4); + } + + // Submitting a malformed registration proof through the combined entry must abort before any + // state mutates: store is not created, fungible balance is not moved, no rollover happens. + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 65537, location = aptos_framework::confidential_proof)] + fun fail_register_and_deposit_and_rollover_with_bad_registration_proof( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_other_dk, other_ek) = generate_twisted_elgamal_keypair(); + + // Build a registration proof for `alice_ek` but submit it alongside a different `ek`. + // verify_registration_proof recomputes the challenge against the *submitted* ek and the + // proof fails Schnorr verification. + let (commitment, response) = confidential_proof::prove_registration( + 4u8, + alice_addr, + @aptos_framework, + &alice_dk, + &alice_ek, + object::object_address(&token), + ); + + confidential_asset::register_and_deposit_and_rollover_pending_balance( + &alice, + token, + 100, + twisted_elgamal::pubkey_to_bytes(&other_ek), + commitment, + response, + ); + } + + // The combined entry aborts when the sender is already registered for the token. + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 524290, location = aptos_framework::confidential_asset)] + fun fail_register_and_deposit_and_rollover_when_already_registered( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + let (commitment, response) = confidential_proof::prove_registration( + 4u8, + alice_addr, + @aptos_framework, + &alice_dk, + &alice_ek, + object::object_address(&token), + ); + + confidential_asset::register_and_deposit_and_rollover_pending_balance( + &alice, + token, + 10, + twisted_elgamal::pubkey_to_bytes(&alice_ek), + commitment, + response, + ); + } + + // Subsequent combined entry (already registered, currently normalized): deposit + rollover. + // We arrange a normalized state by sending a confidential transfer first (which sets + // normalized=true on the sender's store). + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_deposit_and_rollover_pending_balance( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + // Bring Alice into a normalized=true state. After register_for_testing, deposit, then + // rollover, normalized=false. After a confidential_transfer the sender's store is set + // normalized=true, which is the precondition this entry point asserts. + confidential_asset::deposit(&alice, token, 100); + confidential_asset::rollover_pending_balance(&alice, token); + transfer(&alice, &alice_dk, token, bob_addr, 1, 99, vector[]); + // sanity-check our setup + assert!(confidential_asset::is_normalized(alice_addr, token), 99); + + // Now exercise the combined entry: deposit + rollover, no normalize required. + confidential_asset::deposit_and_rollover_pending_balance(&alice, token, 50); + + // 99 (post-transfer actual) + 50 (just deposited) = 149 in actual; pending empty. + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 149), 1); + assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 0), 2); + } + + // `deposit_and_rollover_pending_balance` aborts when the actual balance is not normalized. + // The state arrives after any prior `rollover_pending_balance` (which sets normalized=false), + // so this is the common post-make-private state and the wallet must route to + // `deposit_and_normalize_and_rollover_pending_balance` instead. + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 196618, location = aptos_framework::confidential_asset)] + fun fail_deposit_and_rollover_when_not_normalized( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let (_alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + // After deposit + rollover, normalized=false. Subsequent combined call must abort. + confidential_asset::deposit(&alice, token, 100); + confidential_asset::rollover_pending_balance(&alice, token); + assert!(!confidential_asset::is_normalized(alice_addr, token), 99); + + confidential_asset::deposit_and_rollover_pending_balance(&alice, token, 50); + } + + // Subsequent combined entry with normalize: deposit + normalize + rollover. Used after a + // prior rollover (which left the store with normalized=false). After this call, normalized + // is back to false (rollover always sets it false), but the actual balance is the canonical + // sum so the next deposit-then-rollover call goes through the same path. + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_deposit_and_normalize_and_rollover_pending_balance( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + // Establish a normalized=false state (deposit then rollover). + confidential_asset::deposit(&alice, token, 100); + confidential_asset::rollover_pending_balance(&alice, token); + assert!(!confidential_asset::is_normalized(alice_addr, token), 99); + + // Build the normalize proof off-chain against the *current* actual balance (100). + // `deposit_to_internal` only mutates pending, so the actual balance the proof is bound + // to matches the actual balance at on-chain `normalize_internal` time. + let cid = 4u8; + let sender_ek = confidential_asset::encryption_key(alice_addr, token); + let current_actual = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(alice_addr, token) + ); + let (proof, new_balance) = confidential_proof::prove_normalization( + cid, + alice_addr, + @aptos_framework, + object::object_address(&token), + &alice_dk, + &sender_ek, + 100, + ¤t_actual, + ); + let new_balance_bytes = confidential_balance::balance_to_bytes(&new_balance); + let (sigma, zkrp) = confidential_proof::serialize_normalization_proof(&proof); + + // deposit 30, then normalize, then rollover → actual = 100 + 30 = 130. + confidential_asset::deposit_and_normalize_and_rollover_pending_balance( + &alice, + token, + 30, + new_balance_bytes, + zkrp, + sigma, + ); + + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 130), 1); + assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 0), 2); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun transferred_event_matches_on_chain_balances( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + let hint = vector[0x01u8, 0x77u8, 0x61u8]; // arbitrary opaque bytes ("wa" with prefix) + transfer(&alice, &alice_dk, token, bob_addr, 100, 100, hint); + // setup set the chain auditor exactly once (epoch 1); no asset auditor (epoch 0). + // ek_volun_auds covers ALL auditors including chain — so 1 row, not 0. + confidential_asset::assert_last_transferred_event_matches_state( + token, + alice_addr, + bob_addr, + 1, + hint, + 1, + 0, + ); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_audit_transfer_test( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); + let (auditor1_dk, auditor1_ek) = generate_twisted_elgamal_keypair(); + let (auditor2_dk, auditor2_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::set_asset_auditor( + &fa, + token, + twisted_elgamal::pubkey_to_bytes(&auditor1_ek)); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + let auditor_amounts = audit_transfer( + &alice, + &alice_dk, + token, + bob_addr, + 100, + 100, + &vector[auditor1_ek, auditor2_ek], + vector[]); + + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); + assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 100), 1); + + // auditor_amounts[0] is the chain auditor's row; the asset & voluntary rows shift to [1]/[2]. + assert!(confidential_balance::verify_pending_balance(&auditor_amounts[1], &auditor1_dk, 100), 1); + assert!(confidential_balance::verify_pending_balance(&auditor_amounts[2], &auditor2_dk, 100), 1); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun transferred_event_matches_on_chain_balances_audited( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); + let (auditor1_dk, auditor1_ek) = generate_twisted_elgamal_keypair(); + let (auditor2_dk, auditor2_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::set_asset_auditor( + &fa, + token, + twisted_elgamal::pubkey_to_bytes(&auditor1_ek)); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + let hint = vector[0xabu8, 0xcdu8]; + let auditor_amounts = audit_transfer( + &alice, + &alice_dk, + token, + bob_addr, + 100, + 100, + &vector[auditor1_ek, auditor2_ek], + hint); + + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); + assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 100), 1); + // auditor_amounts[0] is the chain auditor's row; the asset & voluntary rows shift to [1]/[2]. + assert!(confidential_balance::verify_pending_balance(&auditor_amounts[1], &auditor1_dk, 100), 2); + assert!(confidential_balance::verify_pending_balance(&auditor_amounts[2], &auditor2_dk, 100), 3); + + // chain auditor + asset auditor + 1 voluntary = 3 auditor rows in ek_volun_auds. + confidential_asset::assert_last_transferred_event_matches_state( + token, + alice_addr, + bob_addr, + 3, + hint, + 1, + 1, + ); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 0x010006, location = confidential_asset)] + fun fail_audit_transfer_if_wrong_auditor_list( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + let (_, auditor1_ek) = generate_twisted_elgamal_keypair(); + let (_, auditor2_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::set_asset_auditor( + &fa, + token, + twisted_elgamal::pubkey_to_bytes(&auditor1_ek)); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + // Asset auditor for `token` is `auditor1`, so the first slot in `extra_auditor_eks` + // (which becomes `auditor_eks[1]` after the helper prepends the chain auditor at slot 0) + // must equal `auditor1`. Passing `auditor2` there is a slot-1 mismatch and is rejected. + // See `confidential_asset::validate_auditors`. + audit_transfer( + &alice, + &alice_dk, + token, + bob_addr, + 100, + 100, + &vector[auditor2_ek, auditor1_ek], + vector[]); + } + + fun oversized_auditor_hint(): vector { + let max = confidential_asset::max_sender_auditor_hint_bytes(); + let v = vector[]; + let i = 0u64; + while (i <= max) { + v.push_back(0u8); + i = i + 1; + }; + v + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 0x010012, location = confidential_asset)] + fun fail_transfer_if_auditor_hint_too_long( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + transfer(&alice, &alice_dk, token, bob_addr, 100, 100, oversized_auditor_hint()); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_rotate( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + withdraw(&alice, &alice_dk, token, bob_addr, 50, 150); + + let (new_alice_dk, new_alice_ek) = generate_twisted_elgamal_keypair(); + + rotate(&alice, &alice_dk, token, &new_alice_dk, &new_alice_ek, 150); + + assert!(confidential_asset::encryption_key(alice_addr, token) == new_alice_ek, 1); + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &new_alice_dk, 150), 1); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_normalize( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let max_chunk_value = 1 << 16 - 1; + let token = set_up_for_confidential_asset_test( + &confidential_asset, + &aptos_fx, + &fa, + &alice, + &bob, + max_chunk_value, + max_chunk_value + ); + + let alice_addr = signer::address_of(&alice); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + confidential_asset::deposit(&alice, token, max_chunk_value); + confidential_asset::deposit_to(&bob, token, alice_addr, max_chunk_value); + + confidential_asset::rollover_pending_balance(&alice, token); + + assert!(!confidential_asset::is_normalized(alice_addr, token)); + assert!( + !confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, (2 * max_chunk_value as u128)), + 1 + ); + + normalize(&alice, &alice_dk, token, (2 * max_chunk_value as u128)); + + assert!(confidential_asset::is_normalized(alice_addr, token)); + assert!( + confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, (2 * max_chunk_value as u128)), 1); + } + + // `normalize_and_rollover_pending_balance` from an unnormalized state combines the two + // steps in one tx. After: pending is empty, balance becomes (old available + pending), + // and `normalized` is back to `false` (rollover resets it). + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun success_normalize_and_rollover_from_unnormalized( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let max_chunk_value = 1 << 16 - 1; + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, + max_chunk_value + 50, max_chunk_value); + + let alice_addr = signer::address_of(&alice); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + // Two deposits + a rollover stack max-chunk values into a single chunk, leaving the + // available balance unnormalized. + confidential_asset::deposit(&alice, token, max_chunk_value); + confidential_asset::deposit_to(&bob, token, alice_addr, max_chunk_value); + confidential_asset::rollover_pending_balance(&alice, token); + assert!(!confidential_asset::is_normalized(alice_addr, token), 1); + + // A fresh deposit lands in pending; the combined entry must roll it in. + confidential_asset::deposit(&alice, token, 50); + + let total: u128 = (2 * max_chunk_value as u128) + 50; + normalize_and_rollover(&alice, &alice_dk, token, (2 * max_chunk_value as u128)); + + // Available reflects normalized old + pending; not normalized + // (rollover always leaves the merged balance unnormalized — same as plain rollover). + assert!(!confidential_asset::is_normalized(alice_addr, token), 3); + assert!( + confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, total), 5); + } + + // Calling `normalize_and_rollover_pending_balance` while already normalized aborts at + // the `normalize_internal` step (`EALREADY_NORMALIZED`, invalid_state = category 3). + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + #[expected_failure(abort_code = 0x03000B, location = aptos_framework::confidential_asset)] + fun fail_normalize_and_rollover_when_already_normalized( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + let alice_addr = signer::address_of(&alice); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + // A freshly registered store starts with `normalized == true` and an empty available + // balance, so the wrapper must abort at `normalize_internal`'s `EALREADY_NORMALIZED`. + assert!(confidential_asset::is_normalized(alice_addr, token), 1); + + normalize_and_rollover(&alice, &alice_dk, token, 0); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun events_balance_changing_operations( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let max_chunk_value = 1 << 16 - 1; + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, max_chunk_value, max_chunk_value); + + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + + // --- register emits Registered --- + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::assert_last_registered_event(token, alice_addr); + + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + confidential_asset::assert_last_registered_event(token, bob_addr); + + // --- deposit emits Deposited with new_pending_balance --- + confidential_asset::deposit(&alice, token, 100); + confidential_asset::assert_last_deposited_event_matches_state(token, alice_addr, 100); + + confidential_asset::deposit_to(&bob, token, alice_addr, 200); + confidential_asset::assert_last_deposited_event_matches_state(token, alice_addr, 200); + + // --- rollover emits RolledOver with new_available_balance --- + confidential_asset::rollover_pending_balance(&alice, token); + confidential_asset::assert_last_rolled_over_event_matches_state(token, alice_addr); + + // --- normalize emits Normalized with new_available_balance --- + assert!(!confidential_asset::is_normalized(alice_addr, token)); + normalize(&alice, &alice_dk, token, 300); + confidential_asset::assert_last_normalized_event_matches_state(token, alice_addr); + + // --- withdraw emits Withdrawn with new_available_balance --- + withdraw(&alice, &alice_dk, token, bob_addr, 50, 250); + confidential_asset::assert_last_withdrawn_event_matches_state(token, alice_addr, 50); + + // --- freeze / unfreeze emits FreezeChanged --- + confidential_asset::rollover_pending_balance_and_freeze(&alice, token); + confidential_asset::assert_last_freeze_changed_event(token, alice_addr, true); + + // --- rotate emits KeyRotated with new_ek and new_available_balance --- + let (new_alice_dk, new_alice_ek) = generate_twisted_elgamal_keypair(); + rotate(&alice, &alice_dk, token, &new_alice_dk, &new_alice_ek, 250); + confidential_asset::assert_last_key_rotated_event_matches_state(token, alice_addr); + + // --- unfreeze emits FreezeChanged --- + confidential_asset::unfreeze_token(&alice, token); + confidential_asset::assert_last_freeze_changed_event(token, alice_addr, false); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1, + bob = @0xb0 + )] + fun events_admin_operations( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer, + bob: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + + // --- enable_allow_list emits AllowListChanged --- + confidential_asset::enable_allow_list(&aptos_fx); + confidential_asset::assert_last_allow_list_changed_event(true); + + // --- enable_token emits TokenAllowChanged --- + confidential_asset::enable_token(&aptos_fx, token); + confidential_asset::assert_last_token_allow_changed_event(token, true); + + // --- disable_token emits TokenAllowChanged --- + confidential_asset::disable_token(&aptos_fx, token); + confidential_asset::assert_last_token_allow_changed_event(token, false); + + // --- disable_allow_list emits AllowListChanged --- + confidential_asset::disable_allow_list(&aptos_fx); + confidential_asset::assert_last_allow_list_changed_event(false); + + // --- set_asset_auditor emits AssetAuditorChanged with bumped epoch --- + let (_, auditor_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_asset_auditor( + &fa, + token, + twisted_elgamal::pubkey_to_bytes(&auditor_ek)); + confidential_asset::assert_last_asset_auditor_changed_event(token, 1); + + // clear asset auditor — still bumps epoch and emits the event + confidential_asset::set_asset_auditor(&fa, token, b""); + confidential_asset::assert_last_asset_auditor_changed_event(token, 2); + + // --- set_chain_auditor emits ChainAuditorChanged --- + // Setup already set the chain auditor once (epoch 1); rotate to a new key (epoch 2). + let (_, new_chain_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor( + &aptos_fx, + twisted_elgamal::pubkey_to_bytes(&new_chain_ek)); + confidential_asset::assert_last_chain_auditor_changed_event(2); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1 + )] + #[expected_failure(abort_code = 0x01000D, location = confidential_asset)] + fun fail_register_if_token_disallowed( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &alice, 500, 500); + + confidential_asset::enable_allow_list(&aptos_fx); + + let (_, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1 + )] + fun success_register_if_token_allowed( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer) + { + let token = set_up_for_confidential_asset_test(&confidential_asset, &aptos_fx, &fa, &alice, &alice, 500, 500); + + confidential_asset::enable_allow_list(&aptos_fx); + confidential_asset::enable_token(&aptos_fx, token); + + let (_, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + alice = @0xa1 + )] + fun fail_deposit_with_coins_if_insufficient_amount( + confidential_asset: signer, + aptos_fx: signer, + alice: signer) + { + chain_id::initialize_for_test(&aptos_fx, 4); + confidential_asset::init_module_for_testing(&confidential_asset); + coin::create_coin_conversion_map(&aptos_fx); + + let alice_addr = signer::address_of(&alice); + + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + &confidential_asset, utf8(b"MockCoin"), utf8(b"MC"), 0, false); + + let coin_amount = coin::mint(100, &mint_cap); + coin::destroy_burn_cap(burn_cap); + coin::destroy_freeze_cap(freeze_cap); + coin::destroy_mint_cap(mint_cap); + + account::create_account_if_does_not_exist(alice_addr); + coin::register(&alice); + coin::deposit(alice_addr, coin_amount); + + coin::create_pairing(&aptos_fx); + + let token = coin::paired_metadata().extract(); + + let (_, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::deposit(&alice, token, 100); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + alice = @0xa1, + )] + fun success_deposit_with_coins( + confidential_asset: signer, + aptos_fx: signer, + alice: signer) + { + chain_id::initialize_for_test(&aptos_fx, 4); + confidential_asset::init_module_for_testing(&confidential_asset); + coin::create_coin_conversion_map(&aptos_fx); + + let alice_addr = signer::address_of(&alice); + + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + &confidential_asset, utf8(b"MockCoin"), utf8(b"MC"), 0, false); + + let coin_amount = coin::mint(100, &mint_cap); + coin::destroy_burn_cap(burn_cap); + coin::destroy_freeze_cap(freeze_cap); + coin::destroy_mint_cap(mint_cap); + + account::create_account_if_does_not_exist(alice_addr); + coin::register(&alice); + coin::deposit(alice_addr, coin_amount); + + coin::create_pairing(&aptos_fx); + + let token = coin::paired_metadata().extract(); + + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + + assert!(coin::balance(alice_addr) == 100, 1); + assert!(primary_fungible_store::balance(alice_addr, token) == 100, 1); + assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 0), 1); + + confidential_asset::deposit_coins(&alice, 50); + + assert!(coin::balance(alice_addr) == 50, 1); + assert!(primary_fungible_store::balance(alice_addr, token) == 50, 1); + assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, 50), 1); + } + + fun set_up_dispatchable_fa_test( + confidential_asset_signer: &signer, + aptos_fx: &signer, + fa: &signer, + sender: &signer, + sender_amount: u64): Object + { + chain_id::initialize_for_test(aptos_fx, 4); + + let ctor_ref = &object::create_sticky_object(signer::address_of(fa)); + + primary_fungible_store::create_primary_store_enabled_fungible_asset( + ctor_ref, + option::none(), + utf8(b"DispatchToken"), + utf8(b"DT"), + 18, + utf8(b"https://"), + utf8(b"https://"), + ); + + dispatchable_fungible_asset::register_dispatch_functions( + ctor_ref, + option::none(), + option::none(), + option::none(), + ); + + let mint_ref = fungible_asset::generate_mint_ref(ctor_ref); + + confidential_asset::init_module_for_testing(confidential_asset_signer); + features::change_feature_flags_for_testing(aptos_fx, vector[features::get_bulletproofs_feature()], vector[]); + + let token = object::object_from_constructor_ref(ctor_ref); + + let sender_store = primary_fungible_store::ensure_primary_store_exists(signer::address_of(sender), token); + fungible_asset::mint_to(&mint_ref, sender_store, sender_amount); + + token + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1 + )] + #[expected_failure(abort_code = 0x010013, location = confidential_asset)] + fun fail_register_with_dispatchable_fa( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer) + { + let token = set_up_dispatchable_fa_test(&confidential_asset, &aptos_fx, &fa, &alice, 500); + + assert!(fungible_asset::is_asset_type_dispatchable(token), 1); + + let (_, alice_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1 + )] + fun success_standard_fa_not_blocked( + confidential_asset: signer, + aptos_fx: signer, + fa: signer, + alice: signer) + { + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &alice, 500, 0); + + assert!(!fungible_asset::is_asset_type_dispatchable(token), 1); + + let (_, alice_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::deposit(&alice, token, 100); + } + + // + // Auditor layering: chain-level + per-asset + voluntary auditors. + // + + /// Setup variant that does NOT install a chain-level auditor. Used to exercise the + /// `ECHAIN_AUDITOR_NOT_SET` precondition on transfers. + fun set_up_without_chain_auditor( + confidential_asset: &signer, + aptos_fx: &signer, + fa: &signer, + sender: &signer, + recipient: &signer, + sender_amount: u64, + recipient_amount: u64): Object + { + chain_id::initialize_for_test(aptos_fx, 4); + let ctor_ref = &object::create_sticky_object(signer::address_of(fa)); + primary_fungible_store::create_primary_store_enabled_fungible_asset( + ctor_ref, option::none(), utf8(b"NoChainAuditor"), utf8(b"NCA"), 18, + utf8(b"https://"), utf8(b"https://")); + let mint_ref = fungible_asset::generate_mint_ref(ctor_ref); + confidential_asset::init_module_for_testing(confidential_asset); + features::change_feature_flags_for_testing( + aptos_fx, vector[features::get_bulletproofs_feature()], vector[]); + let token = object::object_from_constructor_ref(ctor_ref); + let sender_store = primary_fungible_store::ensure_primary_store_exists( + signer::address_of(sender), token); + fungible_asset::mint_to(&mint_ref, sender_store, sender_amount); + let recipient_store = primary_fungible_store::ensure_primary_store_exists( + signer::address_of(recipient), token); + fungible_asset::mint_to(&mint_ref, recipient_store, recipient_amount); + token + } + + /// Builds and submits a transfer using the supplied `auditor_eks` *verbatim* — without + /// the chain-key prepend that `audit_transfer` performs. Used by tests that need to + /// exercise rejection paths (wrong slot 0, missing prefix, post-rotation old proof). + fun audit_transfer_raw( + sender: &signer, + sender_dk: &Scalar, + token: Object, + to: address, + amount: u64, + new_amount: u128, + auditor_eks: &vector, + sender_auditor_hint: vector) + { + let from = signer::address_of(sender); + let sender_ek = confidential_asset::encryption_key(from, token); + let recipient_ek = confidential_asset::encryption_key(to, token); + let current_balance = confidential_balance::decompress_balance( + &confidential_asset::actual_balance(from, token)); + let (proof, new_balance, sender_amount, recipient_amount, auditor_amounts) = + confidential_proof::prove_transfer( + 4u8, from, @aptos_framework, object::object_address(&token), + sender_dk, &sender_ek, &recipient_ek, amount, new_amount, + ¤t_balance, auditor_eks, sender_auditor_hint); + let (sigma_proof, zkrp_new_balance, zkrp_transfer_amount) = + confidential_proof::serialize_transfer_proof(&proof); + confidential_asset::confidential_transfer( + sender, token, to, + confidential_balance::balance_to_bytes(&new_balance), + confidential_balance::balance_to_bytes(&sender_amount), + confidential_balance::balance_to_bytes(&recipient_amount), + confidential_asset::serialize_auditor_eks(auditor_eks), + confidential_asset::serialize_auditor_amounts(&auditor_amounts), + zkrp_new_balance, zkrp_transfer_amount, sigma_proof, sender_auditor_hint); + } + + #[test(confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, + fa = @0xfa, alice = @0xa1, bob = @0xb0)] + #[expected_failure(abort_code = 0x030015, location = confidential_asset)] + /// Confidential transfers cannot run before the chain-level auditor has been + /// configured. Aborts with `ECHAIN_AUDITOR_NOT_SET`. + fun fail_transfer_if_chain_auditor_unset( + confidential_asset: signer, aptos_fx: signer, fa: signer, + alice: signer, bob: signer) + { + let token = set_up_without_chain_auditor( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + let bob_addr = signer::address_of(&bob); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + // Even an empty auditor list is rejected — chain auditor is mandatory. + audit_transfer_raw(&alice, &alice_dk, token, bob_addr, 100, 100, &vector[], vector[]); + } + + #[test(confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, + fa = @0xfa, alice = @0xa1, bob = @0xb0)] + #[expected_failure(abort_code = 0x010006, location = confidential_asset)] + /// Slot 0 of `auditor_eks` must equal the active chain auditor key — a sender cannot + /// substitute their own key in that position even if they encrypt a valid auditor + /// amount under it. + fun fail_transfer_if_slot0_not_chain_auditor( + confidential_asset: signer, aptos_fx: signer, fa: signer, + alice: signer, bob: signer) + { + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + let bob_addr = signer::address_of(&bob); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + // Use a fresh keypair as slot 0 — proof verifies (consistent transcript) but + // `validate_auditors` rejects because slot 0 ≠ on-chain chain auditor. + let (_, wrong_ek) = generate_twisted_elgamal_keypair(); + audit_transfer_raw(&alice, &alice_dk, token, bob_addr, 100, 100, + &vector[wrong_ek], vector[]); + } + + #[test(confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, + fa = @0xfa, alice = @0xa1, bob = @0xb0)] + #[expected_failure(abort_code = 0x010006, location = confidential_asset)] + /// When an asset auditor is set, `auditor_eks` must include both the chain auditor + /// (slot 0) and the asset auditor (slot 1). Submitting only the chain auditor is + /// rejected — the prefix length check in `validate_auditors` catches it. + fun fail_transfer_if_asset_auditor_required_but_missing( + confidential_asset: signer, aptos_fx: signer, fa: signer, + alice: signer, bob: signer) + { + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + let bob_addr = signer::address_of(&bob); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + let (_, asset_aud_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_asset_auditor(&fa, token, + twisted_elgamal::pubkey_to_bytes(&asset_aud_ek)); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + // Helper auto-prepends the chain auditor ⇒ auditor_eks = [chain]. Slot 1 is missing + // even though asset auditor is set ⇒ rejected. + audit_transfer(&alice, &alice_dk, token, bob_addr, 100, 100, &vector[], vector[]); + } + + #[test(confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, + fa = @0xfa, alice = @0xa1, bob = @0xb0)] + /// Voluntary auditors at slot 2+ are accepted when no asset auditor is configured — + /// the prefix is just `[chain]`, anything after is the sender's choice. + fun success_voluntary_auditors_without_asset_auditor( + confidential_asset: signer, aptos_fx: signer, fa: signer, + alice: signer, bob: signer) + { + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (bob_dk, bob_ek) = generate_twisted_elgamal_keypair(); + let (vol1_dk, vol1_ek) = generate_twisted_elgamal_keypair(); + let (vol2_dk, vol2_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + let auditor_amounts = audit_transfer(&alice, &alice_dk, token, bob_addr, 100, 100, + &vector[vol1_ek, vol2_ek], vector[]); + + assert!(confidential_asset::verify_actual_balance(alice_addr, token, &alice_dk, 100), 1); + assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 100), 1); + // [0] = chain auditor (helper-prepended); [1] = vol1; [2] = vol2. + assert!(confidential_balance::verify_pending_balance(&auditor_amounts[1], &vol1_dk, 100), 2); + assert!(confidential_balance::verify_pending_balance(&auditor_amounts[2], &vol2_dk, 100), 3); + } + + #[test(confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, + fa = @0xfa, alice = @0xa1, bob = @0xb0)] + #[expected_failure(abort_code = 0x010006, location = confidential_asset)] + /// A proof generated under the previous chain auditor key becomes unsubmittable after + /// governance rotates the chain auditor — slot 0 no longer equals the on-chain key. + /// This is the explicit "rotation invalidates in-flight proofs" property documented + /// on `set_chain_auditor`. + fun fail_transfer_after_chain_auditor_rotation( + confidential_asset: signer, aptos_fx: signer, fa: signer, + alice: signer, bob: signer) + { + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + let bob_addr = signer::address_of(&bob); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + confidential_asset::deposit(&alice, token, 200); + confidential_asset::rollover_pending_balance(&alice, token); + + // Capture the chain auditor key in force at proof-generation time, then rotate + // before submission to simulate a governance proposal landing mid-flight. + let old_chain_ek = confidential_asset::get_chain_auditor().extract(); + let (_, new_chain_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&aptos_fx, twisted_elgamal::pubkey_to_bytes(&new_chain_ek)); + + // `audit_transfer_raw` uses the supplied list verbatim — slot 0 is the *old* key, + // which no longer matches the on-chain chain auditor. + audit_transfer_raw(&alice, &alice_dk, token, bob_addr, 100, 100, + &vector[old_chain_ek], vector[]); + } + + #[test(confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, + fa = @0xfa, alice = @0xa1, bob = @0xb0)] + /// Rotation: each rotation bumps the epoch and stamps the new epoch on subsequent + /// transfers. Off-chain auditors / gateways resolve epoch → key by indexing + /// `ChainAuditorChanged` / `AssetAuditorChanged` events. + fun success_auditor_rotation_bumps_epoch( + confidential_asset: signer, aptos_fx: signer, fa: signer, + alice: signer, bob: signer) + { + let token = set_up_for_confidential_asset_test( + &confidential_asset, &aptos_fx, &fa, &alice, &bob, 500, 500); + let alice_addr = signer::address_of(&alice); + let bob_addr = signer::address_of(&bob); + let (alice_dk, alice_ek) = generate_twisted_elgamal_keypair(); + let (_, bob_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::register_for_testing(&alice, token, twisted_elgamal::pubkey_to_bytes(&alice_ek)); + confidential_asset::register_for_testing(&bob, token, twisted_elgamal::pubkey_to_bytes(&bob_ek)); + confidential_asset::deposit(&alice, token, 300); + confidential_asset::rollover_pending_balance(&alice, token); + + // Setup installed epoch 1; rotate twice → epochs 2, 3. + assert!(confidential_asset::get_chain_auditor_epoch() == 1, 1); + let (_, ek2) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&aptos_fx, twisted_elgamal::pubkey_to_bytes(&ek2)); + assert!(confidential_asset::get_chain_auditor_epoch() == 2, 2); + let (_, ek3) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&aptos_fx, twisted_elgamal::pubkey_to_bytes(&ek3)); + assert!(confidential_asset::get_chain_auditor_epoch() == 3, 3); + + // Transfer stamps the *current* epoch (3) on the event. + transfer(&alice, &alice_dk, token, bob_addr, 100, 200, vector[]); + confidential_asset::assert_last_transferred_event_matches_state( + token, alice_addr, bob_addr, 1, vector[], 3, 0); + + // Asset auditor history is independent. Set, rotate, clear. + let (_, asset_ek1) = generate_twisted_elgamal_keypair(); + let (_, asset_ek2) = generate_twisted_elgamal_keypair(); + confidential_asset::set_asset_auditor(&fa, token, twisted_elgamal::pubkey_to_bytes(&asset_ek1)); + assert!(confidential_asset::get_asset_auditor_epoch(token) == 1, 5); + confidential_asset::set_asset_auditor(&fa, token, twisted_elgamal::pubkey_to_bytes(&asset_ek2)); + assert!(confidential_asset::get_asset_auditor_epoch(token) == 2, 6); + confidential_asset::set_asset_auditor(&fa, token, b""); + assert!(confidential_asset::get_asset_auditor_epoch(token) == 3, 7); + // After the clear there is no active asset auditor; the epoch keeps advancing. + assert!(confidential_asset::get_asset_auditor(token).is_none(), 9); + } + + // ============================================================================ + // Asset auditor authorization tests + // + // `set_asset_auditor` is gated by `object::root_owner(token) == signer::address_of(issuer)`. + // For framework-managed FAs (root = @0x1) only governance qualifies; for issuer-deployed + // FAs the account at the top of the metadata object's ownership chain qualifies — even + // if there are intermediate object owners (the USDCX-style "contract object owns FA, + // multisig owns contract" pattern). + // ============================================================================ + + /// Helper: chain auditor + module init + features, without minting or creating an FA. + /// Tests below create their own FA with custom ownership chains. + fun set_up_chain_only(confidential_asset: &signer, aptos_fx: &signer) { + chain_id::initialize_for_test(aptos_fx, 4); + confidential_asset::init_module_for_testing(confidential_asset); + features::change_feature_flags_for_testing( + aptos_fx, vector[features::get_bulletproofs_feature()], vector[] + ); + confidential_asset::set_chain_auditor_admin(aptos_fx, signer::address_of(aptos_fx)); + let (_, chain_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(aptos_fx, twisted_elgamal::pubkey_to_bytes(&chain_ek)); + } + + /// Helper: create a fresh FA owned directly by `creator_addr`. + fun create_fa_owned_by(creator_addr: address): Object { + let ctor_ref = &object::create_sticky_object(creator_addr); + primary_fungible_store::create_primary_store_enabled_fungible_asset( + ctor_ref, + option::none(), + utf8(b"MockToken"), + utf8(b"MT"), + 18, + utf8(b"https://"), + utf8(b"https://"), + ); + object::object_from_constructor_ref(ctor_ref) + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1 + )] + /// Direct-ownership case: the FA creator is the root owner of the metadata object, + /// so they can rotate the asset auditor. Mirrors the issuer-deployed FA shape where + /// no intermediate object sits between the issuer account and the FA. + fun success_set_asset_auditor_by_direct_root_owner( + confidential_asset: signer, aptos_fx: signer, fa: signer, alice: signer) + { + set_up_chain_only(&confidential_asset, &aptos_fx); + let token = create_fa_owned_by(signer::address_of(&fa)); + + // Sanity: direct owner == root owner == fa. + assert!(object::owner(token) == signer::address_of(&fa), 1); + assert!(object::root_owner(token) == signer::address_of(&fa), 2); + + let (_, ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_asset_auditor(&fa, token, twisted_elgamal::pubkey_to_bytes(&ek)); + assert!(confidential_asset::get_asset_auditor_epoch(token) == 1, 3); + assert!(confidential_asset::get_asset_auditor(token).is_some(), 4); + + // Silence unused-binding warning for `alice` (kept in the test signature so the + // address space matches sibling tests). + let _ = alice; + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1 + )] + #[expected_failure(abort_code = 0x50016, location = confidential_asset)] + /// Negative: a non-owner signer is rejected. `alice` did not create the FA and is not + /// in the ownership chain, so root_owner != alice and the call aborts with + /// `ENOT_ASSET_ISSUER` (0x16) under permission_denied (category 5). + fun fail_set_asset_auditor_if_not_root_owner( + confidential_asset: signer, aptos_fx: signer, fa: signer, alice: signer) + { + set_up_chain_only(&confidential_asset, &aptos_fx); + let token = create_fa_owned_by(signer::address_of(&fa)); + + let (_, ek) = generate_twisted_elgamal_keypair(); + // Alice is not the root owner — must abort. + confidential_asset::set_asset_auditor(&alice, token, twisted_elgamal::pubkey_to_bytes(&ek)); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + fa = @0xfa, + alice = @0xa1 + )] + #[expected_failure(abort_code = 0x50016, location = confidential_asset)] + /// Negative: governance cannot rotate the auditor of an issuer-deployed FA. This is + /// the intended authorization shift — `aptos_framework` no longer has implicit + /// authority over per-asset auditors; only the FA's root owner does. + fun fail_set_asset_auditor_by_aptos_framework_for_issuer_fa( + confidential_asset: signer, aptos_fx: signer, fa: signer, alice: signer) + { + set_up_chain_only(&confidential_asset, &aptos_fx); + let token = create_fa_owned_by(signer::address_of(&fa)); + + let (_, ek) = generate_twisted_elgamal_keypair(); + // root_owner(token) == @0xfa, signer::address_of(&aptos_fx) == @0x1 — mismatch. + confidential_asset::set_asset_auditor(&aptos_fx, token, twisted_elgamal::pubkey_to_bytes(&ek)); + + let _ = fa; + let _ = alice; + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + alice = @0xa1 + )] + /// Framework-managed FA: when the FA is created with `@aptos_framework` (= @0x1) as + /// the creator, root_owner returns @0x1 and only governance — via the + /// `aptos_framework` signer — can rotate the auditor. Models a canonical framework + /// FA like APT at @0xa. + fun success_set_asset_auditor_for_framework_fa( + confidential_asset: signer, aptos_fx: signer, alice: signer) + { + set_up_chain_only(&confidential_asset, &aptos_fx); + // Framework FA: creator is @aptos_framework, so root_owner == @0x1. + let token = create_fa_owned_by(@aptos_framework); + + assert!(object::root_owner(token) == @aptos_framework, 1); + + let (_, ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_asset_auditor(&aptos_fx, token, twisted_elgamal::pubkey_to_bytes(&ek)); + assert!(confidential_asset::get_asset_auditor_epoch(token) == 1, 2); + + let _ = alice; + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + multisig = @0x9001, + alice = @0xa1 + )] + /// Nested-ownership case (USDCX shape): a "contract object" sits between the multisig + /// issuer and the FA metadata object. `object::root_owner` walks the chain through + /// the contract object to the multisig at the top, so the multisig can call + /// `set_asset_auditor` directly without needing the contract's ExtendRef wrapper. + fun success_set_asset_auditor_via_nested_object_chain( + confidential_asset: signer, aptos_fx: signer, multisig: signer, alice: signer) + { + set_up_chain_only(&confidential_asset, &aptos_fx); + + // Build the chain: multisig -> contract_obj -> fa_metadata_obj. + let contract_ctor = object::create_sticky_object(signer::address_of(&multisig)); + let contract_signer = object::generate_signer(&contract_ctor); + let contract_addr = signer::address_of(&contract_signer); + + let token = create_fa_owned_by(contract_addr); + + // Direct owner is the contract object; root walks past it to the multisig. + assert!(object::owner(token) == contract_addr, 1); + assert!(object::root_owner(token) == signer::address_of(&multisig), 2); + + // Multisig (root) can rotate directly. + let (_, ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_asset_auditor(&multisig, token, twisted_elgamal::pubkey_to_bytes(&ek)); + assert!(confidential_asset::get_asset_auditor_epoch(token) == 1, 3); + + let _ = alice; + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + multisig = @0x9001, + alice = @0xa1 + )] + #[expected_failure(abort_code = 0x50016, location = confidential_asset)] + /// Nested-ownership negative: the *intermediate* object signer (the contract object + /// directly above the FA) is not the root owner and is rejected. Only the account at + /// the top of the chain has authority. + fun fail_set_asset_auditor_by_intermediate_object_in_chain( + confidential_asset: signer, aptos_fx: signer, multisig: signer, alice: signer) + { + set_up_chain_only(&confidential_asset, &aptos_fx); + + let contract_ctor = object::create_sticky_object(signer::address_of(&multisig)); + let contract_signer = object::generate_signer(&contract_ctor); + let contract_addr = signer::address_of(&contract_signer); + + let token = create_fa_owned_by(contract_addr); + + // The contract-object signer is the *direct* owner but not the *root* owner — + // root_owner walks past it to the multisig — so this must abort. + let (_, ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_asset_auditor( + &contract_signer, token, twisted_elgamal::pubkey_to_bytes(&ek) + ); + + let _ = alice; + } + + // ============================================================================ + // Chain-auditor admin authorization tests + // + // Movement governance does NOT directly hold rotation authority over the chain-level + // auditor key. Instead, governance designates a chain-auditor admin account via + // `set_chain_auditor_admin`, and only that account may subsequently call + // `set_chain_auditor`. + // + // The shared `set_up_for_confidential_asset_test` helper papers over this by + // designating `aptos_fx` itself as the admin so other tests don't need to know the + // detail; the tests below stand up their own state (no shared setup) to exercise the + // authorization boundary directly. + // ============================================================================ + + /// Helper: minimal init without setting the chain-auditor admin or the chain auditor + /// itself, so admin-related tests can exercise the bootstrap path explicitly. + fun set_up_chain_admin_test(confidential_asset: &signer, aptos_fx: &signer) { + chain_id::initialize_for_test(aptos_fx, 4); + confidential_asset::init_module_for_testing(confidential_asset); + features::change_feature_flags_for_testing( + aptos_fx, vector[features::get_bulletproofs_feature()], vector[] + ); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + ca_admin = @0xCA + )] + /// Happy path: governance designates a chain-auditor admin, that admin then sets the + /// chain auditor. Verifies the admin view, the emitted admin-changed event, and that + /// the chain auditor key actually lands. + fun success_set_chain_auditor_by_designated_admin( + confidential_asset: signer, aptos_fx: signer, ca_admin: signer) + { + set_up_chain_admin_test(&confidential_asset, &aptos_fx); + + // Admin starts unset. + assert!(confidential_asset::get_chain_auditor_admin().is_none(), 1); + + // Governance designates ca_admin. + let ca_admin_addr = signer::address_of(&ca_admin); + confidential_asset::set_chain_auditor_admin(&aptos_fx, ca_admin_addr); + assert!(confidential_asset::get_chain_auditor_admin() == option::some(ca_admin_addr), 2); + confidential_asset::assert_last_chain_auditor_admin_changed_event(ca_admin_addr); + + // Designated admin can install a chain auditor. + let (_, chain_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&ca_admin, twisted_elgamal::pubkey_to_bytes(&chain_ek)); + assert!(confidential_asset::get_chain_auditor_epoch() == 1, 3); + assert!(confidential_asset::get_chain_auditor().is_some(), 4); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + ca_admin = @0xCA + )] + /// Admin rotation: governance can hand the role to a successor account. The previous + /// admin loses authority; the new admin gains it. + fun success_chain_auditor_admin_rotation( + confidential_asset: signer, aptos_fx: signer, ca_admin: signer) + { + set_up_chain_admin_test(&confidential_asset, &aptos_fx); + let ca_admin_addr = signer::address_of(&ca_admin); + confidential_asset::set_chain_auditor_admin(&aptos_fx, ca_admin_addr); + + // Rotate admin to a fresh address. + let new_admin_addr = @0xCAFE; + confidential_asset::set_chain_auditor_admin(&aptos_fx, new_admin_addr); + assert!(confidential_asset::get_chain_auditor_admin() == option::some(new_admin_addr), 1); + confidential_asset::assert_last_chain_auditor_admin_changed_event(new_admin_addr); + + let _ = ca_admin; + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + not_gov = @0x9999 + )] + #[expected_failure(abort_code = 0x50003, location = aptos_framework::system_addresses)] + /// Negative: only governance can designate the chain-auditor admin. A non-governance + /// signer hits `assert_aptos_framework` and aborts with the framework's standard + /// permission_denied code (0x50003). + fun fail_set_chain_auditor_admin_by_non_governance( + confidential_asset: signer, aptos_fx: signer, not_gov: signer) + { + set_up_chain_admin_test(&confidential_asset, &aptos_fx); + confidential_asset::set_chain_auditor_admin(¬_gov, @0xCA); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework + )] + #[expected_failure(abort_code = 0x30017, location = confidential_asset)] + /// Negative bootstrap: before governance has assigned an admin, no one — not even + /// governance itself — can set the chain auditor. Aborts with + /// `ECHAIN_AUDITOR_ADMIN_NOT_SET` (0x17) under invalid_state (category 3). + fun fail_set_chain_auditor_when_admin_not_set( + confidential_asset: signer, aptos_fx: signer) + { + set_up_chain_admin_test(&confidential_asset, &aptos_fx); + + let (_, chain_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&aptos_fx, twisted_elgamal::pubkey_to_bytes(&chain_ek)); + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + ca_admin = @0xCA + )] + #[expected_failure(abort_code = 0x50018, location = confidential_asset)] + /// The separation property: once governance has designated a separate admin, + /// governance itself can no longer rotate the chain auditor. Aborts with + /// `ENOT_CHAIN_AUDITOR_ADMIN` (0x18) under permission_denied. + fun fail_set_chain_auditor_by_governance_when_admin_is_separate( + confidential_asset: signer, aptos_fx: signer, ca_admin: signer) + { + set_up_chain_admin_test(&confidential_asset, &aptos_fx); + confidential_asset::set_chain_auditor_admin(&aptos_fx, signer::address_of(&ca_admin)); + + let (_, chain_ek) = generate_twisted_elgamal_keypair(); + // aptos_fx (governance) is no longer the admin — must abort. + confidential_asset::set_chain_auditor(&aptos_fx, twisted_elgamal::pubkey_to_bytes(&chain_ek)); + + let _ = ca_admin; + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + ca_admin = @0xCA, + stranger = @0x1234 + )] + #[expected_failure(abort_code = 0x50018, location = confidential_asset)] + /// Negative: an arbitrary third party who is neither governance nor the designated + /// admin cannot rotate the chain auditor. + fun fail_set_chain_auditor_by_stranger( + confidential_asset: signer, aptos_fx: signer, ca_admin: signer, stranger: signer) + { + set_up_chain_admin_test(&confidential_asset, &aptos_fx); + confidential_asset::set_chain_auditor_admin(&aptos_fx, signer::address_of(&ca_admin)); + + let (_, chain_ek) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&stranger, twisted_elgamal::pubkey_to_bytes(&chain_ek)); + + let _ = ca_admin; + } + + #[test( + confidential_asset = @aptos_framework, + aptos_fx = @aptos_framework, + ca_admin1 = @0xCA1, + ca_admin2 = @0xCA2 + )] + #[expected_failure(abort_code = 0x50018, location = confidential_asset)] + /// Rotation revokes the prior admin: after governance moves the role from ca_admin1 + /// to ca_admin2, ca_admin1 loses authority. + fun fail_set_chain_auditor_by_revoked_admin( + confidential_asset: signer, + aptos_fx: signer, + ca_admin1: signer, + ca_admin2: signer) + { + set_up_chain_admin_test(&confidential_asset, &aptos_fx); + confidential_asset::set_chain_auditor_admin(&aptos_fx, signer::address_of(&ca_admin1)); + + // ca_admin1 sets a key successfully. + let (_, ek1) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&ca_admin1, twisted_elgamal::pubkey_to_bytes(&ek1)); + + // Governance rotates the admin role away from ca_admin1. + confidential_asset::set_chain_auditor_admin(&aptos_fx, signer::address_of(&ca_admin2)); + + // The former admin tries to rotate again — must abort. + let (_, ek2) = generate_twisted_elgamal_keypair(); + confidential_asset::set_chain_auditor(&ca_admin1, twisted_elgamal::pubkey_to_bytes(&ek2)); + + let _ = ca_admin2; + } +} diff --git a/aptos-move/framework/aptos-experimental/tests/confidential_asset/confidential_proof_tests.move b/aptos-move/framework/aptos-framework/tests/confidential_asset/confidential_proof_tests.move similarity index 53% rename from aptos-move/framework/aptos-experimental/tests/confidential_asset/confidential_proof_tests.move rename to aptos-move/framework/aptos-framework/tests/confidential_asset/confidential_proof_tests.move index 9a5ed516a71..908ea7d4812 100644 --- a/aptos-move/framework/aptos-experimental/tests/confidential_asset/confidential_proof_tests.move +++ b/aptos-move/framework/aptos-framework/tests/confidential_asset/confidential_proof_tests.move @@ -1,8 +1,17 @@ #[test_only] -module aptos_experimental::confidential_proof_tests { - use aptos_experimental::confidential_balance; - use aptos_experimental::confidential_proof; - use aptos_experimental::ristretto255_twisted_elgamal::{Self as twisted_elgamal, generate_twisted_elgamal_keypair}; +module aptos_framework::confidential_proof_tests { + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof; + use aptos_framework::ristretto255_twisted_elgamal::{Self as twisted_elgamal, generate_twisted_elgamal_keypair}; + + // Test constants for domain separation + const TEST_CHAIN_ID: u8 = 4; + const TEST_SENDER: address = @0xa1; + /// Published package account for `confidential_asset` / `confidential_proof` (matches `[addresses]` in experimental `Move.toml`). + const TEST_CONTRACT_ADDRESS: address = @aptos_framework; + // `TEST_TOKEN_ADDRESS` is declared further down in the registration-tests block (`@0xbeef`); reused here for + // every transfer/withdraw/rotation/normalization callsite so all FS transcripts are domain-separated by the same + // token address. struct WithdrawParameters has drop { ek: twisted_elgamal::CompressedPubkey, @@ -23,6 +32,7 @@ module aptos_experimental::confidential_proof_tests { recipient_amount: confidential_balance::ConfidentialBalance, auditor_eks: vector, auditor_amounts: vector, + sender_auditor_hint: vector, proof: confidential_proof::TransferProof, } @@ -62,6 +72,10 @@ module aptos_experimental::confidential_proof_tests { proof, new_balance ) = confidential_proof::prove_withdrawal( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, &dk, &ek, amount, @@ -79,10 +93,15 @@ module aptos_experimental::confidential_proof_tests { } fun transfer(): TransferParameters { - transfer_with_parameters(150, 100, 50) + transfer_with_parameters(150, 100, 50, vector[]) } - fun transfer_with_parameters(current_amount: u128, new_amount: u128, amount: u64): TransferParameters { + fun transfer_with_parameters( + current_amount: u128, + new_amount: u128, + amount: u64, + sender_auditor_hint: vector + ): TransferParameters { let (sender_dk, sender_ek) = generate_twisted_elgamal_keypair(); let (_, recipient_ek) = generate_twisted_elgamal_keypair(); @@ -104,6 +123,10 @@ module aptos_experimental::confidential_proof_tests { recipient_amount, auditor_amounts, ) = confidential_proof::prove_transfer( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, &sender_dk, &sender_ek, &recipient_ek, @@ -111,6 +134,7 @@ module aptos_experimental::confidential_proof_tests { new_amount, ¤t_balance, &auditor_eks, + sender_auditor_hint, ); TransferParameters { @@ -124,6 +148,7 @@ module aptos_experimental::confidential_proof_tests { recipient_amount, auditor_eks, auditor_amounts, + sender_auditor_hint, proof, } } @@ -145,6 +170,10 @@ module aptos_experimental::confidential_proof_tests { proof, new_balance, ) = confidential_proof::prove_rotation( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¤t_dk, &new_dk, ¤t_ek, @@ -178,6 +207,10 @@ module aptos_experimental::confidential_proof_tests { proof, new_balance ) = confidential_proof::prove_normalization( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, &dk, &ek, amount, @@ -198,6 +231,10 @@ module aptos_experimental::confidential_proof_tests { let params = withdraw(); confidential_proof::verify_withdrawal_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, params.amount, ¶ms.current_balance, @@ -211,6 +248,10 @@ module aptos_experimental::confidential_proof_tests { let params = withdraw(); confidential_proof::verify_withdrawal_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, 1000, ¶ms.current_balance, @@ -224,6 +265,10 @@ module aptos_experimental::confidential_proof_tests { let params = withdraw(); confidential_proof::verify_withdrawal_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, params.amount, &confidential_balance::new_actual_balance_from_u128( @@ -241,6 +286,10 @@ module aptos_experimental::confidential_proof_tests { let params = withdraw(); confidential_proof::verify_withdrawal_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, params.amount, ¶ms.current_balance, @@ -260,6 +309,10 @@ module aptos_experimental::confidential_proof_tests { let params = withdraw_with_params(0, max_uint128 - 1, 1); confidential_proof::verify_withdrawal_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, params.amount, ¶ms.current_balance, @@ -272,6 +325,31 @@ module aptos_experimental::confidential_proof_tests { let params = transfer(); confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.sender_ek, + ¶ms.recipient_ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.sender_amount, + ¶ms.recipient_amount, + ¶ms.auditor_eks, + ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, + ¶ms.proof); + } + + #[test] + fun success_transfer_with_non_empty_auditor_hint() { + let params = transfer_with_parameters(150, 100, 50, vector[0xabu8, 0xcdu8]); + + confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.recipient_ek, ¶ms.current_balance, @@ -280,6 +358,29 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, ¶ms.auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_transfer_if_wrong_sender_auditor_hint() { + let params = transfer_with_parameters(150, 100, 50, vector[1u8]); + + confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.sender_ek, + ¶ms.recipient_ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.sender_amount, + ¶ms.recipient_amount, + ¶ms.auditor_eks, + ¶ms.auditor_amounts, + &vector[2u8], ¶ms.proof); } @@ -289,6 +390,10 @@ module aptos_experimental::confidential_proof_tests { let params = transfer(); confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.recipient_ek, ¶ms.recipient_ek, ¶ms.current_balance, @@ -297,6 +402,7 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, ¶ms.auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -306,6 +412,10 @@ module aptos_experimental::confidential_proof_tests { let params = transfer(); confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.sender_ek, ¶ms.current_balance, @@ -314,6 +424,7 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, ¶ms.auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -323,6 +434,10 @@ module aptos_experimental::confidential_proof_tests { let params = transfer(); confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.recipient_ek, &confidential_balance::new_actual_balance_from_u128( @@ -335,6 +450,7 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, ¶ms.auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -343,9 +459,13 @@ module aptos_experimental::confidential_proof_tests { fun fail_transfer_if_negative_new_balance() { // 0 - 1 = max_uint128 let max_uint128 = 340282366920938463463374607431768211455; - let params = transfer_with_parameters(0, max_uint128 - 1, 1); + let params = transfer_with_parameters(0, max_uint128 - 1, 1, vector[]); confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.recipient_ek, ¶ms.current_balance, @@ -354,6 +474,7 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, ¶ms.auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -363,6 +484,10 @@ module aptos_experimental::confidential_proof_tests { let params = transfer(); confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.recipient_ek, ¶ms.current_balance, @@ -372,6 +497,7 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, ¶ms.auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -381,6 +507,10 @@ module aptos_experimental::confidential_proof_tests { let params = transfer(); confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.recipient_ek, ¶ms.current_balance, @@ -390,6 +520,7 @@ module aptos_experimental::confidential_proof_tests { 1000, &confidential_balance::generate_balance_randomness(), ¶ms.recipient_ek), ¶ms.auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -402,6 +533,10 @@ module aptos_experimental::confidential_proof_tests { let auditor_eks = vector[auditor_ek]; confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.recipient_ek, ¶ms.current_balance, @@ -410,6 +545,7 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, &auditor_eks, ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -427,6 +563,10 @@ module aptos_experimental::confidential_proof_tests { let auditor_amounts = vector[auditor_amount]; confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.sender_ek, ¶ms.recipient_ek, ¶ms.current_balance, @@ -435,6 +575,7 @@ module aptos_experimental::confidential_proof_tests { ¶ms.recipient_amount, ¶ms.auditor_eks, &auditor_amounts, + ¶ms.sender_auditor_hint, ¶ms.proof); } @@ -443,6 +584,10 @@ module aptos_experimental::confidential_proof_tests { let params = rotate(); confidential_proof::verify_rotation_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.current_ek, ¶ms.new_ek, ¶ms.current_balance, @@ -456,6 +601,10 @@ module aptos_experimental::confidential_proof_tests { let params = rotate(); confidential_proof::verify_rotation_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.new_ek, ¶ms.new_ek, ¶ms.current_balance, @@ -469,6 +618,10 @@ module aptos_experimental::confidential_proof_tests { let params = rotate(); confidential_proof::verify_rotation_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.current_ek, ¶ms.current_ek, ¶ms.current_balance, @@ -482,6 +635,10 @@ module aptos_experimental::confidential_proof_tests { let params = rotate(); confidential_proof::verify_rotation_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.current_ek, ¶ms.new_ek, &confidential_balance::new_actual_balance_from_u128( @@ -499,6 +656,10 @@ module aptos_experimental::confidential_proof_tests { let params = rotate(); confidential_proof::verify_rotation_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.current_ek, ¶ms.new_ek, ¶ms.current_balance, @@ -515,6 +676,10 @@ module aptos_experimental::confidential_proof_tests { let params = normalize(); confidential_proof::verify_normalization_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, ¶ms.current_balance, ¶ms.new_balance, @@ -529,6 +694,10 @@ module aptos_experimental::confidential_proof_tests { let (_, ek) = generate_twisted_elgamal_keypair(); confidential_proof::verify_normalization_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, &ek, ¶ms.current_balance, ¶ms.new_balance, @@ -541,6 +710,10 @@ module aptos_experimental::confidential_proof_tests { let params = normalize(); confidential_proof::verify_normalization_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, &confidential_balance::new_actual_balance_from_u128( 1000, @@ -557,6 +730,10 @@ module aptos_experimental::confidential_proof_tests { let params = normalize(); confidential_proof::verify_normalization_proof( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, ¶ms.ek, ¶ms.current_balance, &confidential_balance::new_actual_balance_from_u128( @@ -566,4 +743,303 @@ module aptos_experimental::confidential_proof_tests { ), ¶ms.proof); } + + // ========================================== + // Registration proof tests + // ========================================== + + const TEST_TOKEN_ADDRESS: address = @0xbeef; + + #[test] + fun success_registration() { + let (dk, ek) = generate_twisted_elgamal_keypair(); + let (commitment, response) = confidential_proof::prove_registration( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &dk, + &ek, + TEST_TOKEN_ADDRESS, + ); + + confidential_proof::verify_registration_proof_for_test( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &ek, + TEST_TOKEN_ADDRESS, + commitment, + response, + ); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_registration_if_wrong_ek() { + let (dk, ek) = generate_twisted_elgamal_keypair(); + let (commitment, response) = confidential_proof::prove_registration( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &dk, + &ek, + TEST_TOKEN_ADDRESS, + ); + + let (_, wrong_ek) = generate_twisted_elgamal_keypair(); + + confidential_proof::verify_registration_proof_for_test( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &wrong_ek, + TEST_TOKEN_ADDRESS, + commitment, + response, + ); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_registration_if_wrong_token() { + let (dk, ek) = generate_twisted_elgamal_keypair(); + let (commitment, response) = confidential_proof::prove_registration( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &dk, + &ek, + TEST_TOKEN_ADDRESS, + ); + + confidential_proof::verify_registration_proof_for_test( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &ek, + @0xdead, + commitment, + response, + ); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_registration_if_wrong_chain_id() { + let (dk, ek) = generate_twisted_elgamal_keypair(); + let (commitment, response) = confidential_proof::prove_registration( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &dk, + &ek, + TEST_TOKEN_ADDRESS, + ); + + confidential_proof::verify_registration_proof_for_test( + TEST_CHAIN_ID + 1, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &ek, + TEST_TOKEN_ADDRESS, + commitment, + response, + ); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_registration_if_wrong_sender() { + let (dk, ek) = generate_twisted_elgamal_keypair(); + let (commitment, response) = confidential_proof::prove_registration( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &dk, + &ek, + TEST_TOKEN_ADDRESS, + ); + + confidential_proof::verify_registration_proof_for_test( + TEST_CHAIN_ID, + @0xb2, + TEST_CONTRACT_ADDRESS, + &ek, + TEST_TOKEN_ADDRESS, + commitment, + response, + ); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_registration_if_wrong_contract_address() { + let (dk, ek) = generate_twisted_elgamal_keypair(); + let (commitment, response) = confidential_proof::prove_registration( + TEST_CHAIN_ID, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + &dk, + &ek, + TEST_TOKEN_ADDRESS, + ); + + confidential_proof::verify_registration_proof_for_test( + TEST_CHAIN_ID, + TEST_SENDER, + @0xc0ffee, + &ek, + TEST_TOKEN_ADDRESS, + commitment, + response, + ); + } + + // ========================================== + // Cross-chain replay rejection tests + // ========================================== + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_withdraw_if_wrong_chain_id() { + let params = withdraw(); + + confidential_proof::verify_withdrawal_proof( + TEST_CHAIN_ID + 1, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.ek, + params.amount, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_withdraw_if_wrong_sender() { + let params = withdraw(); + + confidential_proof::verify_withdrawal_proof( + TEST_CHAIN_ID, + @0xb2, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.ek, + params.amount, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_transfer_if_wrong_chain_id() { + let params = transfer(); + + confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID + 1, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.sender_ek, + ¶ms.recipient_ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.sender_amount, + ¶ms.recipient_amount, + ¶ms.auditor_eks, + ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_transfer_if_wrong_sender() { + let params = transfer(); + + confidential_proof::verify_transfer_proof( + TEST_CHAIN_ID, + @0xb2, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.sender_ek, + ¶ms.recipient_ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.sender_amount, + ¶ms.recipient_amount, + ¶ms.auditor_eks, + ¶ms.auditor_amounts, + ¶ms.sender_auditor_hint, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_rotate_if_wrong_chain_id() { + let params = rotate(); + + confidential_proof::verify_rotation_proof( + TEST_CHAIN_ID + 1, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.current_ek, + ¶ms.new_ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_rotate_if_wrong_sender() { + let params = rotate(); + + confidential_proof::verify_rotation_proof( + TEST_CHAIN_ID, + @0xb2, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.current_ek, + ¶ms.new_ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_normalize_if_wrong_chain_id() { + let params = normalize(); + + confidential_proof::verify_normalization_proof( + TEST_CHAIN_ID + 1, + TEST_SENDER, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.proof); + } + + #[test] + #[expected_failure(abort_code = 0x010001, location = confidential_proof)] + fun fail_normalize_if_wrong_sender() { + let params = normalize(); + + confidential_proof::verify_normalization_proof( + TEST_CHAIN_ID, + @0xb2, + TEST_CONTRACT_ADDRESS, + TEST_TOKEN_ADDRESS, + ¶ms.ek, + ¶ms.current_balance, + ¶ms.new_balance, + ¶ms.proof); + } } diff --git a/aptos-move/framework/aptos-framework/tests/confidential_asset/ristretto255_twisted_elgamal_tests.move b/aptos-move/framework/aptos-framework/tests/confidential_asset/ristretto255_twisted_elgamal_tests.move new file mode 100644 index 00000000000..6c8eb9d56b4 --- /dev/null +++ b/aptos-move/framework/aptos-framework/tests/confidential_asset/ristretto255_twisted_elgamal_tests.move @@ -0,0 +1,38 @@ +#[test_only] +module aptos_framework::ristretto255_twisted_elgamal_tests { + use aptos_framework::ristretto255_twisted_elgamal::{ + Self as twisted_elgamal, + generate_twisted_elgamal_keypair, + }; + + #[test] + fun new_pubkey_from_bytes_rejects_identity() { + // 32 zero bytes is the canonical compressed encoding of the Ristretto255 + // identity point. There is no scalar `sk` such that `sk^(-1) * H = identity`, + // so identity cannot correspond to any keypair and must be rejected. + let identity_bytes = x"0000000000000000000000000000000000000000000000000000000000000000"; + assert!(twisted_elgamal::new_pubkey_from_bytes(identity_bytes).is_none(), 1); + } + + #[test] + fun new_pubkey_from_bytes_accepts_real_key() { + let (_sk, ek) = generate_twisted_elgamal_keypair(); + let round_trip = twisted_elgamal::new_pubkey_from_bytes(twisted_elgamal::pubkey_to_bytes(&ek)); + assert!(round_trip.is_some(), 2); + + let round_trip_pk = round_trip.extract(); + assert!(!twisted_elgamal::is_identity_pubkey(&round_trip_pk), 3); + assert!(!twisted_elgamal::is_identity_pubkey(&ek), 4); + } + + #[test] + fun new_pubkey_from_bytes_rejects_non_canonical() { + // 31 bytes is too short. + let short = x"00000000000000000000000000000000000000000000000000000000000000"; + assert!(twisted_elgamal::new_pubkey_from_bytes(short).is_none(), 5); + + // All-ones is not a valid Ristretto255 encoding. + let bogus = x"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + assert!(twisted_elgamal::new_pubkey_from_bytes(bogus).is_none(), 6); + } +} diff --git a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs index 2cff97836bb..d8cbac18503 100644 --- a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs +++ b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs @@ -484,6 +484,31 @@ pub enum EntryFunctionCall { coin_type: TypeTag, }, + /// The same as `deposit`, but converts coins to missing FA first. + ConfidentialAssetDepositCoins { + coin_type: TypeTag, + amount: u64, + }, + + /// The same as `deposit_to`, but converts coins to missing FA first. + ConfidentialAssetDepositCoinsTo { + coin_type: TypeTag, + to: AccountAddress, + amount: u64, + }, + + /// Sets, rotates, or clears the chain-level auditor key. Pass an empty + /// `new_chain_auditor_ek` to clear (which disables all confidential transfers until a + /// successor is set). Bumps `chain_auditor_epoch` and emits [`ChainAuditorChanged`]. + /// + /// Callable only by [`GlobalConfig.chain_auditor_admin`]. Aborts with + /// [`ECHAIN_AUDITOR_ADMIN_NOT_SET`] before an admin is assigned, or + /// [`ENOT_CHAIN_AUDITOR_ADMIN`] for any other signer. Rotation invalidates pending + /// transfer proofs — see [`set_asset_auditor`]. + ConfidentialAssetSetChainAuditor { + new_chain_auditor_ek: Vec, + }, + /// Add `amount` of coins to the delegation pool `pool_address`. DelegationPoolAddStake { pool_address: AccountAddress, @@ -1624,6 +1649,17 @@ impl EntryFunctionCall { amount, } => coin_transfer(coin_type, to, amount), CoinUpgradeSupply { coin_type } => coin_upgrade_supply(coin_type), + ConfidentialAssetDepositCoins { coin_type, amount } => { + confidential_asset_deposit_coins(coin_type, amount) + }, + ConfidentialAssetDepositCoinsTo { + coin_type, + to, + amount, + } => confidential_asset_deposit_coins_to(coin_type, to, amount), + ConfidentialAssetSetChainAuditor { + new_chain_auditor_ek, + } => confidential_asset_set_chain_auditor(new_chain_auditor_ek), DelegationPoolAddStake { pool_address, amount, @@ -3357,6 +3393,65 @@ pub fn coin_upgrade_supply(coin_type: TypeTag) -> TransactionPayload { )) } +/// The same as `deposit`, but converts coins to missing FA first. +pub fn confidential_asset_deposit_coins(coin_type: TypeTag, amount: u64) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("confidential_asset").to_owned(), + ), + ident_str!("deposit_coins").to_owned(), + vec![coin_type], + vec![bcs::to_bytes(&amount).unwrap()], + )) +} + +/// The same as `deposit_to`, but converts coins to missing FA first. +pub fn confidential_asset_deposit_coins_to( + coin_type: TypeTag, + to: AccountAddress, + amount: u64, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("confidential_asset").to_owned(), + ), + ident_str!("deposit_coins_to").to_owned(), + vec![coin_type], + vec![bcs::to_bytes(&to).unwrap(), bcs::to_bytes(&amount).unwrap()], + )) +} + +/// Sets, rotates, or clears the chain-level auditor key. Pass an empty +/// `new_chain_auditor_ek` to clear (which disables all confidential transfers until a +/// successor is set). Bumps `chain_auditor_epoch` and emits [`ChainAuditorChanged`]. +/// +/// Callable only by [`GlobalConfig.chain_auditor_admin`]. Aborts with +/// [`ECHAIN_AUDITOR_ADMIN_NOT_SET`] before an admin is assigned, or +/// [`ENOT_CHAIN_AUDITOR_ADMIN`] for any other signer. Rotation invalidates pending +/// transfer proofs — see [`set_asset_auditor`]. +pub fn confidential_asset_set_chain_auditor(new_chain_auditor_ek: Vec) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("confidential_asset").to_owned(), + ), + ident_str!("set_chain_auditor").to_owned(), + vec![], + vec![bcs::to_bytes(&new_chain_auditor_ek).unwrap()], + )) +} + /// Add `amount` of coins to the delegation pool `pool_address`. pub fn delegation_pool_add_stake(pool_address: AccountAddress, amount: u64) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( @@ -6636,6 +6731,45 @@ mod decoder { } } + pub fn confidential_asset_deposit_coins( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::ConfidentialAssetDepositCoins { + coin_type: script.ty_args().get(0)?.clone(), + amount: bcs::from_bytes(script.args().get(0)?).ok()?, + }) + } else { + None + } + } + + pub fn confidential_asset_deposit_coins_to( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::ConfidentialAssetDepositCoinsTo { + coin_type: script.ty_args().get(0)?.clone(), + to: bcs::from_bytes(script.args().get(0)?).ok()?, + amount: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + + pub fn confidential_asset_set_chain_auditor( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::ConfidentialAssetSetChainAuditor { + new_chain_auditor_ek: bcs::from_bytes(script.args().get(0)?).ok()?, + }) + } else { + None + } + } + pub fn delegation_pool_add_stake(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::DelegationPoolAddStake { @@ -8362,6 +8496,18 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy, use_latest_language: bool) { +fn run_tests_for_pkg( + path_to_pkg: impl Into, + use_latest_language: bool, + additional_named_addresses: BTreeMap, +) { let pkg_path = path_in_crate(path_to_pkg); let compiler_config = CompilerConfig { known_attributes: extended_checks::get_all_attribute_names().clone(), @@ -25,6 +33,7 @@ fn run_tests_for_pkg(path_to_pkg: impl Into, use_latest_language: bool) install_dir: Some(tempdir().unwrap().path().to_path_buf()), compiler_config: compiler_config.clone(), full_model_generation: true, // Run extended checks also on test code + additional_named_addresses, ..Default::default() }; if use_latest_language { @@ -77,30 +86,37 @@ pub fn aptos_test_natives() -> NativeFunctionTable { #[test] fn move_framework_unit_tests() { - run_tests_for_pkg("aptos-framework", false); + run_tests_for_pkg("aptos-framework", false, BTreeMap::new()); } #[test] fn move_aptos_stdlib_unit_tests() { - run_tests_for_pkg("aptos-stdlib", false); + run_tests_for_pkg("aptos-stdlib", false, BTreeMap::new()); } #[test] fn move_stdlib_unit_tests() { - run_tests_for_pkg("move-stdlib", false); + run_tests_for_pkg("move-stdlib", false, BTreeMap::new()); } #[test] fn move_token_unit_tests() { - run_tests_for_pkg("aptos-token", false); + run_tests_for_pkg("aptos-token", false, BTreeMap::new()); } #[test] fn move_token_objects_unit_tests() { - run_tests_for_pkg("aptos-token-objects", false); + run_tests_for_pkg("aptos-token-objects", false, BTreeMap::new()); } #[test] fn move_experimental_unit_tests() { - run_tests_for_pkg("aptos-experimental", true); + run_tests_for_pkg( + "aptos-experimental", + true, + BTreeMap::from([( + "aptos_experimental".to_owned(), + AccountAddress::from_hex_literal("0x7").unwrap(), + )]), + ); } diff --git a/aptos-move/move-examples/confidential_asset/Move.toml b/aptos-move/move-examples/confidential_asset/Move.toml index 588bcec868d..2179f33d598 100644 --- a/aptos-move/move-examples/confidential_asset/Move.toml +++ b/aptos-move/move-examples/confidential_asset/Move.toml @@ -4,7 +4,6 @@ version = "1.0.0" [dependencies] AptosFramework = { local = "../../framework/aptos-framework" } -AptosExperimental = { local = "../../framework/aptos-experimental" } [addresses] confidential_asset_example = "_" diff --git a/aptos-move/move-examples/confidential_asset/tests/deposit_example.move b/aptos-move/move-examples/confidential_asset/tests/deposit_example.move index 791540cb63d..cd0681e5b8f 100644 --- a/aptos-move/move-examples/confidential_asset/tests/deposit_example.move +++ b/aptos-move/move-examples/confidential_asset/tests/deposit_example.move @@ -7,9 +7,9 @@ module confidential_asset_example::deposit_example { use aptos_framework::object::Object; use aptos_framework::primary_fungible_store; - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_asset_tests; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_asset; + use aptos_framework::confidential_asset_tests; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; fun deposit(bob: &signer, alice: &signer, token: Object) { let bob_addr = signer::address_of(bob); @@ -58,7 +58,7 @@ module confidential_asset_example::deposit_example { } #[test( - confidential_asset = @aptos_experimental, + confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, fa = @0xfa, bob = @0xb0, diff --git a/aptos-move/move-examples/confidential_asset/tests/normalize_example.move b/aptos-move/move-examples/confidential_asset/tests/normalize_example.move index f312dcf297e..ace143314c9 100644 --- a/aptos-move/move-examples/confidential_asset/tests/normalize_example.move +++ b/aptos-move/move-examples/confidential_asset/tests/normalize_example.move @@ -4,11 +4,11 @@ module confidential_asset_example::normalize_example { use aptos_framework::fungible_asset::Metadata; use aptos_framework::object::Object; - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_asset_tests; - use aptos_experimental::confidential_balance; - use aptos_experimental::confidential_proof; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_asset; + use aptos_framework::confidential_asset_tests; + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; fun normalize(bob: &signer, token: Object) { let bob_addr = signer::address_of(bob); @@ -43,6 +43,9 @@ module confidential_asset_example::normalize_example { proof, new_balance ) = confidential_proof::prove_normalization( + 4u8, + bob_addr, + @aptos_framework, &bob_dk, &bob_ek, bob_amount, @@ -70,7 +73,7 @@ module confidential_asset_example::normalize_example { } #[test( - confidential_asset = @aptos_experimental, + confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, fa = @0xfa, bob = @0xb0 diff --git a/aptos-move/move-examples/confidential_asset/tests/register_example.move b/aptos-move/move-examples/confidential_asset/tests/register_example.move index d8ba7f7c817..b6981b3462e 100644 --- a/aptos-move/move-examples/confidential_asset/tests/register_example.move +++ b/aptos-move/move-examples/confidential_asset/tests/register_example.move @@ -6,9 +6,9 @@ module confidential_asset_example::register_example { use aptos_framework::fungible_asset::Metadata; use aptos_framework::object::Object; - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_asset_tests; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_asset; + use aptos_framework::confidential_asset_tests; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; fun register(bob: &signer, token: Object) { let bob_addr = signer::address_of(bob); @@ -31,7 +31,7 @@ module confidential_asset_example::register_example { } #[test( - confidential_asset = @aptos_experimental, + confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, fa = @0xfa, bob = @0xb0 diff --git a/aptos-move/move-examples/confidential_asset/tests/rollover_example.move b/aptos-move/move-examples/confidential_asset/tests/rollover_example.move index f6be04e1cab..7a73ec88e36 100644 --- a/aptos-move/move-examples/confidential_asset/tests/rollover_example.move +++ b/aptos-move/move-examples/confidential_asset/tests/rollover_example.move @@ -6,9 +6,9 @@ module confidential_asset_example::rollover_example { use aptos_framework::fungible_asset::Metadata; use aptos_framework::object::Object; - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_asset_tests; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_asset; + use aptos_framework::confidential_asset_tests; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; fun rollover(bob: &signer, token: Object) { let bob_addr = signer::address_of(bob); @@ -48,7 +48,7 @@ module confidential_asset_example::rollover_example { } #[test( - confidential_asset = @aptos_experimental, + confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, fa = @0xfa, bob = @0xb0 diff --git a/aptos-move/move-examples/confidential_asset/tests/rotate_example.move b/aptos-move/move-examples/confidential_asset/tests/rotate_example.move index 617260f3320..4ee71524f78 100644 --- a/aptos-move/move-examples/confidential_asset/tests/rotate_example.move +++ b/aptos-move/move-examples/confidential_asset/tests/rotate_example.move @@ -6,11 +6,11 @@ module confidential_asset_example::rotate_example { use aptos_framework::fungible_asset::Metadata; use aptos_framework::object::Object; - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_asset_tests; - use aptos_experimental::confidential_balance; - use aptos_experimental::confidential_proof; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_asset; + use aptos_framework::confidential_asset_tests; + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; fun rotate(bob: &signer, token: Object) { let bob_addr = signer::address_of(bob); @@ -40,6 +40,9 @@ module confidential_asset_example::rotate_example { ); let (proof, new_balance) = confidential_proof::prove_rotation( + 4u8, + bob_addr, + @aptos_framework, &bob_current_dk, &bob_new_dk, &bob_current_ek, @@ -71,7 +74,7 @@ module confidential_asset_example::rotate_example { } #[test( - confidential_asset = @aptos_experimental, + confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, fa = @0xfa, bob = @0xb0 diff --git a/aptos-move/move-examples/confidential_asset/tests/transfer_example.move b/aptos-move/move-examples/confidential_asset/tests/transfer_example.move index 96594b481d4..97fd082e725 100644 --- a/aptos-move/move-examples/confidential_asset/tests/transfer_example.move +++ b/aptos-move/move-examples/confidential_asset/tests/transfer_example.move @@ -6,11 +6,11 @@ module confidential_asset_example::transfer_example { use aptos_framework::fungible_asset::Metadata; use aptos_framework::object::Object; - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_asset_tests; - use aptos_experimental::confidential_balance; - use aptos_experimental::confidential_proof; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_asset; + use aptos_framework::confidential_asset_tests; + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; fun transfer(bob: &signer, alice: &signer, token: Object) { let bob_addr = signer::address_of(bob); @@ -69,6 +69,9 @@ module confidential_asset_example::transfer_example { // It won't be stored on-chain, but an auditor can decrypt the transfer amount with its dk. auditor_amounts ) = confidential_proof::prove_transfer( + 4u8, + bob_addr, + @aptos_framework, &bob_dk, &bob_ek, &alice_ek, @@ -76,6 +79,7 @@ module confidential_asset_example::transfer_example { bob_new_amount, ¤t_balance, &auditor_eks, + vector[], ); let ( @@ -94,7 +98,8 @@ module confidential_asset_example::transfer_example { confidential_asset::serialize_auditor_amounts(&auditor_amounts), zkrp_new_balance, zkrp_transfer_amount, - sigma_proof + sigma_proof, + vector[] ); print(&utf8(b"Bob's actual balance is 250")); @@ -105,7 +110,7 @@ module confidential_asset_example::transfer_example { } #[test( - confidential_asset = @aptos_experimental, + confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, fa = @0xfa, bob = @0xb0, diff --git a/aptos-move/move-examples/confidential_asset/tests/withdraw_example.move b/aptos-move/move-examples/confidential_asset/tests/withdraw_example.move index b3f141115be..d68c5092ab7 100644 --- a/aptos-move/move-examples/confidential_asset/tests/withdraw_example.move +++ b/aptos-move/move-examples/confidential_asset/tests/withdraw_example.move @@ -7,11 +7,11 @@ module confidential_asset_example::withdraw_example { use aptos_framework::object::Object; use aptos_framework::primary_fungible_store; - use aptos_experimental::confidential_asset; - use aptos_experimental::confidential_asset_tests; - use aptos_experimental::confidential_balance; - use aptos_experimental::confidential_proof; - use aptos_experimental::ristretto255_twisted_elgamal as twisted_elgamal; + use aptos_framework::confidential_asset; + use aptos_framework::confidential_asset_tests; + use aptos_framework::confidential_balance; + use aptos_framework::confidential_proof; + use aptos_framework::ristretto255_twisted_elgamal as twisted_elgamal; fun withdraw(bob: &signer, alice: &signer, token: Object) { let bob_addr = signer::address_of(bob); @@ -48,6 +48,9 @@ module confidential_asset_example::withdraw_example { ); let (proof, new_balance) = confidential_proof::prove_withdrawal( + 4u8, + bob_addr, + @aptos_framework, &bob_dk, &bob_ek, transfer_amount, @@ -78,7 +81,7 @@ module confidential_asset_example::withdraw_example { } #[test( - confidential_asset = @aptos_experimental, + confidential_asset = @aptos_framework, aptos_fx = @aptos_framework, fa = @0xfa, bob = @0xb0, diff --git a/scripts/chain-auditor-bootstrap/Move.toml b/scripts/chain-auditor-bootstrap/Move.toml new file mode 100644 index 00000000000..663bdd8edbb --- /dev/null +++ b/scripts/chain-auditor-bootstrap/Move.toml @@ -0,0 +1,6 @@ +[package] +name = "ChainAuditorBootstrap" +version = "1.0.0" + +[dependencies] +AptosFramework = { local = "../../aptos-move/framework/aptos-framework" } diff --git a/scripts/chain-auditor-bootstrap/sources/set_chain_auditor_admin.move b/scripts/chain-auditor-bootstrap/sources/set_chain_auditor_admin.move new file mode 100644 index 00000000000..2a5d5f08f18 --- /dev/null +++ b/scripts/chain-auditor-bootstrap/sources/set_chain_auditor_admin.move @@ -0,0 +1,14 @@ +// Designates the chain-auditor admin via governance. Sender must be the core +// resources account (localnet: key in /mint.key). Pairs with the +// subsequent `confidential_asset::set_chain_auditor` entry call signed by the +// admin itself. +script { + use aptos_framework::confidential_asset; + use aptos_framework::aptos_governance; + + fun main(core_resources: &signer, new_admin: address) { + let core_signer = aptos_governance::get_signer_testnet_only(core_resources, @0x1); + let framework_signer = &core_signer; + confidential_asset::set_chain_auditor_admin(framework_signer, new_admin); + } +} diff --git a/scripts/start-localnet-confidential-assets.sh b/scripts/start-localnet-confidential-assets.sh new file mode 100755 index 00000000000..420ea722606 --- /dev/null +++ b/scripts/start-localnet-confidential-assets.sh @@ -0,0 +1,772 @@ +#!/usr/bin/env bash +# Start Movement localnet (validator REST + faucet, no Docker indexer), enable confidential-assets +# feature flag 87 (BULLETPROOFS_BATCH_NATIVES) via mint.key, then publish AptosExperimental using the +# account in .movement/config.yaml (see MOVEMENT_PROFILE / --named-addresses). +# +# From repo root: +# ./scripts/start-localnet-confidential-assets.sh +# +# If $REPO_ROOT/.movement/config.yaml is missing (no `movement init` yet), this script creates one +# automatically after the localnet REST API is up: generates an Ed25519 key and runs +# `movement init --network custom --rest-url $NODE_URL` so `move publish` can run without a prior +# manual init. Existing configs are left unchanged if `movement config show-profiles` succeeds for +# MOVEMENT_PROFILE. Set SKIP_MOVEMENT_CONFIG_INIT=1 to disable auto-init (publish will fail if no config). +# +# Ports (not the same service): +# • 8080 — fullnode REST API (ledger), what `move run-script --url` uses. Your log line +# "REST API endpoint: http://127.0.0.1:8080" is this. +# • 8070 — localnet "ready server" only: a tiny HTTP endpoint the CLI runs so clients can wait +# until configured services pass health checks (node + faucet for this stack). It is NOT +# the blockchain API. The CLI prints: Readiness endpoint: http://127.0.0.1:8070/ +# To wait only for the node REST API (8080), set: WAIT_STRATEGY=node +# +# Environment: +# MOVEMENT — movement CLI (default: $REPO_ROOT/target/release/movement, built via +# `cargo build -p movement --release` if missing). Override with +# MOVEMENT=/path/to/binary. The local build keeps the on-chain framework +# in sync with this checked-out repo. +# SKIP_MOVEMENT_BUILD=1 — do not build movement CLI; fall back to `movement` on $PATH if +# $REPO_ROOT/target/release/movement isn't already built. +# REPO_ROOT — repo root (default: parent of scripts/) +# APTOS_LOCALNET_TEST_DIR — localnet data dir (default: $REPO_ROOT/.movement/testnet) +# NODE_URL — REST base for `move run-script` (default: http://127.0.0.1:8080; refreshed from +# $TEST_DIR/0/node.yaml when that file appears) +# READY_URL — ready-server URL (default: http://127.0.0.1:8070/) when WAIT_STRATEGY=ready. +# WAIT_STRATEGY — ready | node (default: ready). "node" polls only NODE_URL/v1 (validator up). +# NODE_WAIT_TIMEOUT_SECS — max poll time (default: 120). When everything is healthy, localnet +# usually becomes ready in ~20s; raise this on slow hosts. +# SKIP_START=1 — skip starting localnet; only run the feature-flag transaction +# BACKGROUND=0 — run localnet in the foreground (blocks; run feature step separately) +# KEEP_LOCALNET — after success, keep localnet running (default: 1). Set to 0 to always stop on exit. +# On failure, localnet is always shut down if this script started it (no orphan process). +# LOCALNET_ATTACH — when KEEP_LOCALNET=1 and this script started localnet in the background (default: 1), +# block at the end on `wait` until the localnet process exits or you press Ctrl+C (which stops +# localnet via the EXIT trap). Set to 0 to return to the shell immediately while localnet keeps +# running (then stop with: kill "$(cat .movement/localnet.pid)" from REPO_ROOT). +# NODE_REST_WAIT_SECS — after the ready server, max time to wait for NODE_URL/v1 (default: 90). +# CORE_RESOURCES_ADDRESS — on-chain @core_resources address (default: 0xa550c18). Genesis creates +# this account at a fixed address then rotates its auth key to mint.key, so it is NOT the +# same as the address the CLI derives from the public key alone. We fund both via faucet, +# and pass --sender-account here so move run-script pays fees from 0xa550c18. +# FAUCET_URL — passed to fund-with-faucet (default: http://127.0.0.1:8081) +# FAUCET_WAIT_TIMEOUT_SECS — max time to keep polling for a ready faucet (default: 180). The script +# does NOT sleep a fixed 180s: it polls GET $FAUCET_URL/ every POLL_INTERVAL_SECS until the +# response body is tap:ok (official tap health), then runs fund-with-faucet immediately. +# If tap:ok never appears within this budget, the script exits with an error. +# FAUCET_HTTP_MAX_TIME — per-request curl --max-time when probing the faucet (default: 5) +# FAUCET_AMOUNT — Octas to request (default: 10000000000) +# SKIP_FAUCET=1 — skip the pre-flight fund step (otherwise python3 is used once to derive the +# pubkey-based address for the second fund-with-faucet call) +# MOVE_RUN_SCRIPT_MAX_GAS — --max-gas for move run-script (default: 2000000). On-chain +# maximum_number_of_gas_units is capped (e.g. 2_000_000 in config/global-constants for +# production genesis); higher values fail with MAX_GAS_UNITS_EXCEEDS_MAX_GAS_UNITS_BOUND. +# MOVEMENT_PROFILE — profile in $REPO_ROOT/.movement/config.yaml used to sign move publish +# (default: default). The package is published with --named-addresses +# aptos_experimental=, not Move.toml's 0x7. +# SKIP_MOVEMENT_CONFIG_INIT=1 — do not auto-create .movement/config.yaml when missing (requires +# an existing usable profile for move publish when SKIP_EXPERIMENTAL_PUBLISH=0). +# EXPERIMENTAL_PACKAGE_DIR — AptosExperimental package (default: $REPO_ROOT/aptos-move/framework/aptos-experimental) +# SKIP_EXPERIMENTAL_PUBLISH=1 — skip aptos-experimental move publish after the feature-flag script +# MOVE_PUBLISH_MAX_GAS — --max-gas for move publish (default: same as MOVE_RUN_SCRIPT_MAX_GAS) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_default_repo_root="$(cd "${SCRIPT_DIR}/.." && pwd)" +REPO_ROOT="${REPO_ROOT:-$_default_repo_root}" +# Use a locally-built movement binary so the on-chain framework matches this checked-out repo. +# Build it if missing. Override the binary path with MOVEMENT=/path/to/binary; skip the build +# (e.g. to use whatever's on $PATH) with SKIP_MOVEMENT_BUILD=1. +_local_movement="${REPO_ROOT}/target/release/movement" +SKIP_MOVEMENT_BUILD="${SKIP_MOVEMENT_BUILD:-0}" +if [[ -z "${MOVEMENT:-}" ]]; then + if [[ ! -x "$_local_movement" && "$SKIP_MOVEMENT_BUILD" != "1" ]]; then + echo "Building movement CLI (cargo build -p movement --release; first build can take 10+ minutes)..." + (cd "$REPO_ROOT" && cargo build -p movement --release) + fi + if [[ -x "$_local_movement" ]]; then + MOVEMENT="$_local_movement" + fi +fi +MOVEMENT="${MOVEMENT:-movement}" +TEST_DIR="${APTOS_LOCALNET_TEST_DIR:-$REPO_ROOT/.movement/testnet}" +LOCALNET_LOG="${REPO_ROOT}/.movement/localnet.log" +LOCALNET_PID_FILE="${REPO_ROOT}/.movement/localnet.pid" +NODE_URL="${NODE_URL:-http://127.0.0.1:8080}" +READY_URL="${READY_URL:-http://127.0.0.1:8070}" +FRAMEWORK_DIR="$REPO_ROOT/aptos-move/framework/aptos-framework" +SKIP_START="${SKIP_START:-0}" +BACKGROUND="${BACKGROUND:-1}" +NODE_WAIT_TIMEOUT_SECS="${NODE_WAIT_TIMEOUT_SECS:-120}" +MINT_KEY_WAIT_SECS="${MINT_KEY_WAIT_SECS:-60}" +POLL_INTERVAL_SECS="${POLL_INTERVAL_SECS:-0.5}" +WAIT_STRATEGY="${WAIT_STRATEGY:-ready}" +KEEP_LOCALNET="${KEEP_LOCALNET:-1}" +LOCALNET_ATTACH="${LOCALNET_ATTACH:-1}" +NODE_REST_WAIT_SECS="${NODE_REST_WAIT_SECS:-90}" +CORE_RESOURCES_ADDRESS="${CORE_RESOURCES_ADDRESS:-0xa550c18}" +FAUCET_URL="${FAUCET_URL:-http://127.0.0.1:8081}" +FAUCET_WAIT_TIMEOUT_SECS="${FAUCET_WAIT_TIMEOUT_SECS:-180}" +FAUCET_HTTP_MAX_TIME="${FAUCET_HTTP_MAX_TIME:-5}" +FAUCET_AMOUNT="${FAUCET_AMOUNT:-10000000000}" +SKIP_FAUCET="${SKIP_FAUCET:-0}" +FAUCET_FUND_CONN_TIMEOUT_SECS="${FAUCET_FUND_CONN_TIMEOUT_SECS:-45}" +MOVE_RUN_SCRIPT_MAX_GAS="${MOVE_RUN_SCRIPT_MAX_GAS:-2000000}" +MOVE_PUBLISH_MAX_GAS="${MOVE_PUBLISH_MAX_GAS:-$MOVE_RUN_SCRIPT_MAX_GAS}" +MOVEMENT_PROFILE="${MOVEMENT_PROFILE:-default}" +EXPERIMENTAL_PACKAGE_DIR="${EXPERIMENTAL_PACKAGE_DIR:-$REPO_ROOT/aptos-move/framework/aptos-experimental}" +SKIP_EXPERIMENTAL_PUBLISH="${SKIP_EXPERIMENTAL_PUBLISH:-0}" +SKIP_MOVEMENT_CONFIG_INIT="${SKIP_MOVEMENT_CONFIG_INIT:-0}" +# Set to 1 only after we nohup localnet in this shell (EXIT trap uses this). +STARTED_LOCALNET_BG=0 + +if ! command -v "$MOVEMENT" >/dev/null 2>&1; then + echo "error: '$MOVEMENT' not found. Set MOVEMENT or add it to PATH." >&2 + exit 1 +fi + +if [[ ! -d "$FRAMEWORK_DIR" ]]; then + echo "error: framework not found at $FRAMEWORK_DIR" >&2 + exit 1 +fi + +if [[ "$SKIP_EXPERIMENTAL_PUBLISH" != "1" ]] && [[ ! -d "$EXPERIMENTAL_PACKAGE_DIR" ]]; then + echo "error: experimental package not found at $EXPERIMENTAL_PACKAGE_DIR" >&2 + exit 1 +fi + +# Parse REST bind from generated validator config (host for curl). +refresh_node_url_from_node_yaml() { + local f="$TEST_DIR/0/node.yaml" + [[ -f "$f" ]] || return 1 + local addr + addr=$(awk ' + /^api:/ { in_api=1; next } + in_api && /^[a-zA-Z]/ && $0 !~ /^ / { exit } + in_api && /^ address:/ { + sub(/^ address:[[:space:]]+/, "") + gsub(/"/, "") + print + exit + } + ' "$f") + [[ -n "$addr" ]] || return 1 + addr="${addr//0.0.0.0/127.0.0.1}" + NODE_URL="http://${addr}" +} + +dump_failure_hints() { + echo "" >&2 + echo "---- last lines of $LOCALNET_LOG (nohup stdout/stderr) ----" >&2 + tail -n 40 "$LOCALNET_LOG" 2>/dev/null || echo "(no log file)" >&2 + echo "----" >&2 + echo "Per-service trace logs are often under: $TEST_DIR" >&2 + echo "Wait strategy: WAIT_STRATEGY=$WAIT_STRATEGY (ready=$READY_URL vs node=$NODE_URL/v1)" >&2 +} + +# Returns 0 when the chosen wait target responds (curl -sf). +localnet_responds() { + refresh_node_url_from_node_yaml || true + case "$WAIT_STRATEGY" in + node) + curl -sf "${NODE_URL}/v1" >/dev/null 2>&1 + ;; + ready | *) + curl -sf "$READY_URL" >/dev/null 2>&1 + ;; + esac +} + +wait_target_description() { + case "$WAIT_STRATEGY" in + node) echo "node REST ${NODE_URL}/v1" ;; + *) echo "ready server $READY_URL (node + faucet health checks)" ;; + esac +} + +# Ready on 8070 can be a stale 200 if an old process left the port open; always confirm REST /v1. +ensure_node_rest_responds() { + local start=$SECONDS + local max=$NODE_REST_WAIT_SECS + echo "Checking node REST at ${NODE_URL}/v1 (max ${max}s) ..." + while (( SECONDS - start < max )); do + refresh_node_url_from_node_yaml || true + if curl -sf "${NODE_URL}/v1" >/dev/null 2>&1; then + echo "Node REST is accepting connections." + return 0 + fi + sleep "$POLL_INTERVAL_SECS" + done + echo "error: ${NODE_URL}/v1 never responded (connection refused or timeout)." >&2 + echo " The ready server can lie if port 8070 was reused; ensure no stale localnet is bound." >&2 + return 1 +} + +shutdown_localnet_bg() { + [[ "$STARTED_LOCALNET_BG" == "1" ]] || return 0 + if [[ ! -f "$LOCALNET_PID_FILE" ]]; then + STARTED_LOCALNET_BG=0 + return 0 + fi + local pid + pid=$(cat "$LOCALNET_PID_FILE" 2>/dev/null || true) + if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then + echo "Shutting down localnet (pid $pid, SIGTERM) ..." + kill -TERM "$pid" 2>/dev/null || true + local i=0 + while kill -0 "$pid" 2>/dev/null && (( i < 120 )); do + sleep 0.5 + i=$((i + 1)) + done + if kill -0 "$pid" 2>/dev/null; then + echo "Localnet did not exit; sending SIGKILL to pid $pid" >&2 + kill -KILL "$pid" 2>/dev/null || true + fi + fi + rm -f "$LOCALNET_PID_FILE" + STARTED_LOCALNET_BG=0 +} + +cleanup_on_exit() { + local ec=$? + trap - EXIT INT TERM + # Always tear down on failure; on success, tear down only if KEEP_LOCALNET=0. + if [[ "$STARTED_LOCALNET_BG" == "1" ]]; then + if [[ "$ec" != "0" ]] || [[ "$KEEP_LOCALNET" == "0" ]]; then + shutdown_localnet_bg + fi + fi + exit "$ec" +} + +trap cleanup_on_exit EXIT INT TERM + +wait_for_localnet_ready() { + local pid=$1 + local start=$SECONDS + local deadline=$((SECONDS + NODE_WAIT_TIMEOUT_SECS)) + local next_hint=$((SECONDS + 15)) + echo "Waiting for $(wait_target_description) (max ${NODE_WAIT_TIMEOUT_SECS}s; WAIT_STRATEGY=$WAIT_STRATEGY)." + echo "REST for move run-script: $NODE_URL (refreshed from $TEST_DIR/0/node.yaml when present)." + while (( SECONDS < deadline )); do + if ! kill -0 "$pid" 2>/dev/null; then + echo "error: movement process (pid $pid) exited before localnet became ready." >&2 + dump_failure_hints + exit 1 + fi + + if localnet_responds; then + refresh_node_url_from_node_yaml || true + echo "Ready after $((SECONDS - start))s. Using NODE_URL=$NODE_URL for move run-script." + return 0 + fi + if (( SECONDS >= next_hint )); then + echo " ... still waiting ($((SECONDS - start))s / ${NODE_WAIT_TIMEOUT_SECS}s). Tip: tail -f \"$LOCALNET_LOG\"" + next_hint=$((SECONDS + 15)) + fi + sleep "$POLL_INTERVAL_SECS" + done + echo "error: timed out waiting for $(wait_target_description)." >&2 + dump_failure_hints + exit 1 +} + +# Creates $REPO_ROOT/.movement/config.yaml when missing so move publish can sign. movement init +# contacts NODE_URL; only call after localnet REST is accepting connections. +ensure_movement_cli_config_for_publish() { + if [[ "$SKIP_EXPERIMENTAL_PUBLISH" == "1" ]]; then + return 0 + fi + if [[ "$SKIP_MOVEMENT_CONFIG_INIT" == "1" ]]; then + return 0 + fi + local cfg="$REPO_ROOT/.movement/config.yaml" + if [[ -f "$cfg" ]]; then + if (cd "$REPO_ROOT" && "$MOVEMENT" config show-profiles 2>/dev/null) | python3 -c "import json,sys +raw=sys.stdin.read().strip() +if not raw: + sys.exit(1) +d=json.loads(raw) +if not isinstance(d, dict) or d.get('Error'): + sys.exit(1) +if 'Result' not in d or sys.argv[1] not in d['Result']: + sys.exit(1) +sys.exit(0)" "$MOVEMENT_PROFILE" 2>/dev/null + then + echo "Using existing Movement CLI config at $cfg (profile: $MOVEMENT_PROFILE)." + return 0 + fi + echo "error: $cfg exists but 'movement config show-profiles' does not expose profile \"$MOVEMENT_PROFILE\"." >&2 + echo " Fix the file, remove it to allow auto-init, or set SKIP_MOVEMENT_CONFIG_INIT=1 and create a profile manually." >&2 + exit 1 + fi + + echo "No Movement CLI config at $cfg; creating profile \"$MOVEMENT_PROFILE\" for this localnet (REST=$NODE_URL) ..." + mkdir -p "$REPO_ROOT/.movement" + local tmpk + tmpk=$(mktemp "$REPO_ROOT/.movement/.local-publish-key.XXXXXX") + rm -f "${tmpk}.pub" + if ! "$MOVEMENT" key generate --output-file "$tmpk" --encoding hex --assume-yes >/dev/null; then + rm -f "$tmpk" "${tmpk}.pub" + echo "error: movement key generate failed" >&2 + exit 1 + fi + if ! (cd "$REPO_ROOT" && "$MOVEMENT" init --assume-yes --network custom \ + --rest-url "$NODE_URL" \ + --faucet-url "$FAUCET_URL" \ + --skip-faucet \ + --private-key-file "$tmpk" --encoding hex \ + --profile "$MOVEMENT_PROFILE"); then + rm -f "$tmpk" "${tmpk}.pub" + echo "error: movement init failed (see messages above)" >&2 + exit 1 + fi + rm -f "$tmpk" "${tmpk}.pub" + echo "Wrote $cfg — publish signer is profile \"$MOVEMENT_PROFILE\" (re-use this file for stable module addresses)." +} + +# Address move run-script uses if you only pass --private-key-file (auth key preimage of pubkey). +mint_key_derived_address() { + local tmp pubfile addr + tmp=$(mktemp) + rm -f "${tmp}.pub" + if ! "$MOVEMENT" key extract-public-key \ + --private-key-file "$TEST_DIR/mint.key" \ + --encoding bcs \ + --output-file "$tmp" \ + --assume-yes >/dev/null 2>&1; then + echo "error: could not extract public key from $TEST_DIR/mint.key" >&2 + rm -f "$tmp" "${tmp}.pub" + return 1 + fi + pubfile="${tmp}.pub" + if [[ ! -f "$pubfile" ]]; then + echo "error: missing $pubfile after extract-public-key" >&2 + rm -f "$tmp" + return 1 + fi + addr=$(python3 -c ' +import hashlib, sys +path = sys.argv[1] +data = open(path, "rb").read() +if len(data) < 32: + sys.exit("public key file too short: %d bytes" % len(data)) +pk = data[-32:] +print("0x" + hashlib.sha3_256(pk + bytes([0])).hexdigest()) +' "$pubfile") + rm -f "$tmp" "$pubfile" + printf "%s" "$addr" +} + +# Account address for MOVEMENT_PROFILE from $REPO_ROOT/.movement/config.yaml (via CLI). +profile_account_hex() { + if ! command -v python3 >/dev/null 2>&1; then + echo "error: python3 is required to read movement profile account" >&2 + return 1 + fi + local _cfg="$REPO_ROOT/.movement/config.yaml" + # show-profiles must run from REPO_ROOT so ConfigSearchMode::CurrentDir finds .movement/config.yaml. + # Output is JSON: {\"Result\":{...}} on success, or {\"Error\":\"...\"} on failure. Some Movement builds + # may differ; we fall back to parsing config.yaml if JSON has no Result. + (cd "$REPO_ROOT" && "$MOVEMENT" config show-profiles) | python3 -c " +import json, re, sys + +def account_from_config_yaml(text, profile): + lines = text.splitlines() + in_profiles = False + in_profile = False + indent_profile = ' ' + profile + ':' + for line in lines: + if line.rstrip() == 'profiles:': + in_profiles = True + continue + if not in_profiles: + continue + if line.startswith(indent_profile): + in_profile = True + continue + if in_profile: + if re.match(r'^ [^ ].*', line) and not line.startswith(' '): + break + m = re.match(r'^\\s+account:\\s*(0x[0-9a-fA-F]+)\\s*\$', line) + if m: + return m.group(1) + return None + +profile = sys.argv[1] +cfg_path = sys.argv[2] +raw = sys.stdin.read().strip() +if not raw: + print('empty output from movement config show-profiles (run from repo root; is .movement/config.yaml present?)', file=sys.stderr) + sys.exit(1) +try: + data = json.loads(raw) +except json.JSONDecodeError as e: + print('movement config show-profiles did not return JSON:', e, file=sys.stderr) + print(raw[:1200], file=sys.stderr) + sys.exit(1) +if isinstance(data, dict) and 'Error' in data: + print('movement config show-profiles:', data['Error'], file=sys.stderr) + sys.exit(1) +acc = None +if isinstance(data, dict) and 'Result' in data and profile in data['Result']: + acc = data['Result'][profile].get('account') +if acc is None and cfg_path: + try: + text = open(cfg_path, encoding='utf-8').read() + except OSError as e: + print(f'could not read {cfg_path}: {e}', file=sys.stderr) + sys.exit(1) + acc = account_from_config_yaml(text, profile) +if acc is None or acc == '': + print('Could not resolve profile account for profile=%r (no Result.%s.account in CLI JSON and no account: in %s).' % (profile, profile, cfg_path or 'config'), file=sys.stderr) + print('CLI JSON keys: %s' % (list(data.keys()) if isinstance(data, dict) else type(data),), file=sys.stderr) + sys.exit(1) +acc = str(acc) +sys.stdout.write(acc if acc.startswith('0x') else '0x' + acc) +" "$MOVEMENT_PROFILE" "$_cfg" +} + +# Faucet serves GET / → plain text "tap:ok" when the funder is healthy (see aptos-faucet BasicApi). +wait_for_faucet_healthy() { + local base start max next_hint body + base="${FAUCET_URL%/}" + start=$SECONDS + max=$FAUCET_WAIT_TIMEOUT_SECS + next_hint=$((SECONDS + 15)) + echo "Waiting until faucet is ready (polling ${base}/ until response is tap:ok; abort after ${max}s if not) ..." + while (( SECONDS - start < max )); do + body=$(curl -sf --max-time "$FAUCET_HTTP_MAX_TIME" "${base}/" 2>/dev/null || true) + if [[ "$body" == "tap:ok" ]]; then + echo "Faucet is ready." + return 0 + fi + if (( SECONDS >= next_hint )); then + echo " ... faucet not ready yet ($((SECONDS - start))s / ${max}s). Tip: tail -f \"$LOCALNET_LOG\"" + next_hint=$((SECONDS + 15)) + fi + sleep "$POLL_INTERVAL_SECS" + done + echo "error: faucet ${base}/ never returned tap:ok (last body: ${body:-})." >&2 + echo " The node can be up before the tap; raise FAUCET_WAIT_TIMEOUT_SECS or check the faucet port in the localnet log." >&2 + exit 1 +} + +fund_mint_related_accounts() { + if [[ "$SKIP_FAUCET" == "1" ]]; then + echo "SKIP_FAUCET=1: skipping faucet fund step." + return 0 + fi + if ! command -v python3 >/dev/null 2>&1; then + echo "error: python3 is required for the second faucet target (pubkey-derived address)." >&2 + exit 1 + fi + wait_for_faucet_healthy + echo "Funding core-resources account $CORE_RESOURCES_ADDRESS via faucet ($FAUCET_URL) ..." + "$MOVEMENT" account fund-with-faucet \ + --url "$NODE_URL" \ + --faucet-url "$FAUCET_URL" \ + --connection-timeout-secs "$FAUCET_FUND_CONN_TIMEOUT_SECS" \ + --account "$CORE_RESOURCES_ADDRESS" \ + --amount "$FAUCET_AMOUNT" + local derived + derived=$(mint_key_derived_address) || exit 1 + echo "Funding pubkey-derived account $derived via faucet ($FAUCET_URL) ..." + "$MOVEMENT" account fund-with-faucet \ + --url "$NODE_URL" \ + --faucet-url "$FAUCET_URL" \ + --connection-timeout-secs "$FAUCET_FUND_CONN_TIMEOUT_SECS" \ + --account "$derived" \ + --amount "$FAUCET_AMOUNT" + if [[ "$SKIP_EXPERIMENTAL_PUBLISH" != "1" ]]; then + local prof_acct + prof_acct=$(profile_account_hex) || exit 1 + echo "Funding move publish signer ($MOVEMENT_PROFILE profile) $prof_acct via faucet ($FAUCET_URL) ..." + "$MOVEMENT" account fund-with-faucet \ + --url "$NODE_URL" \ + --faucet-url "$FAUCET_URL" \ + --connection-timeout-secs "$FAUCET_FUND_CONN_TIMEOUT_SECS" \ + --account "$prof_acct" \ + --amount "$FAUCET_AMOUNT" + fi +} + +publish_experimental_from_profile() { + if [[ "$SKIP_EXPERIMENTAL_PUBLISH" == "1" ]]; then + echo "SKIP_EXPERIMENTAL_PUBLISH=1: skipping AptosExperimental move publish." + return 0 + fi + local cfg="$REPO_ROOT/.movement/config.yaml" + if [[ ! -f "$cfg" ]]; then + echo "error: $cfg not found after init step; move publish needs a CLI profile (or set SKIP_EXPERIMENTAL_PUBLISH=1)." >&2 + exit 1 + fi + local named_addr + named_addr=$(profile_account_hex) || exit 1 + echo "Publishing AptosExperimental from profile \"$MOVEMENT_PROFILE\" with aptos_experimental=$named_addr ..." + cd "$REPO_ROOT" + "$MOVEMENT" move publish \ + --assume-yes \ + --url "$NODE_URL" \ + --profile "$MOVEMENT_PROFILE" \ + --package-dir "$EXPERIMENTAL_PACKAGE_DIR" \ + --named-addresses "aptos_experimental=${named_addr}" \ + --max-gas "$MOVE_PUBLISH_MAX_GAS" \ + --skip-fetch-latest-git-deps \ + --included-artifacts none \ + --override-size-check +} + +wait_for_mint_key() { + local deadline=$((SECONDS + MINT_KEY_WAIT_SECS)) + echo "Waiting for $TEST_DIR/mint.key ..." + while (( SECONDS < deadline )); do + if [[ -f "$TEST_DIR/mint.key" ]]; then + echo "Found mint.key." + return 0 + fi + sleep "$POLL_INTERVAL_SECS" + done + echo "error: timed out waiting for mint.key under $TEST_DIR" >&2 + exit 1 +} + +run_localnet() { + mkdir -p "$REPO_ROOT/.movement" + cd "$REPO_ROOT" + if [[ "$BACKGROUND" == "1" ]]; then + echo "Starting localnet in background (logs: $LOCALNET_LOG) ..." + nohup "$MOVEMENT" node run-localnet \ + --force-restart \ + --assume-yes \ + --do-not-delegate \ + --with-indexer-api \ + --test-dir "$TEST_DIR" \ + >"$LOCALNET_LOG" 2>&1 & + echo $! >"$LOCALNET_PID_FILE" + STARTED_LOCALNET_BG=1 + local pid + pid=$(cat "$LOCALNET_PID_FILE") + echo "PID $pid (stops on failure, or on success if KEEP_LOCALNET=0; default keeps localnet after success)" + wait_for_mint_key + wait_for_localnet_ready "$pid" + ensure_node_rest_responds + else + echo "Starting localnet in foreground; run feature script in another shell with SKIP_START=1." + exec "$MOVEMENT" node run-localnet \ + --force-restart \ + --assume-yes \ + --do-not-delegate \ + --with-indexer-api \ + --test-dir "$TEST_DIR" + fi +} + +if [[ "$SKIP_START" != "1" ]]; then + run_localnet +else + echo "SKIP_START=1: waiting for $(wait_target_description) ..." + start=$SECONDS + deadline=$((SECONDS + NODE_WAIT_TIMEOUT_SECS)) + next_hint=$((SECONDS + 15)) + while (( SECONDS < deadline )); do + if localnet_responds; then + refresh_node_url_from_node_yaml || true + echo "Ready after $((SECONDS - start))s. NODE_URL=$NODE_URL" + break + fi + if [[ -f "$LOCALNET_PID_FILE" ]]; then + pid=$(cat "$LOCALNET_PID_FILE") + if ! kill -0 "$pid" 2>/dev/null; then + echo "error: movement (pid $pid from $LOCALNET_PID_FILE) is not running." >&2 + dump_failure_hints + exit 1 + fi + fi + if (( SECONDS >= next_hint )); then + echo " ... still waiting ($((SECONDS - start))s / ${NODE_WAIT_TIMEOUT_SECS}s)" + next_hint=$((SECONDS + 15)) + fi + sleep "$POLL_INTERVAL_SECS" + done + if ! localnet_responds; then + echo "error: timed out waiting for $(wait_target_description)" >&2 + dump_failure_hints + exit 1 + fi + ensure_node_rest_responds + if [[ ! -f "$TEST_DIR/mint.key" ]]; then + echo "error: SKIP_START=1 but $TEST_DIR/mint.key missing" >&2 + exit 1 + fi +fi + +cd "$REPO_ROOT" + +ensure_movement_cli_config_for_publish + +fund_mint_related_accounts + +echo "Enabling feature flag 87 (BULLETPROOFS_BATCH_NATIVES) ..." +# Generated inline (the governance proposal repo holds the canonical copy of this script). +FEATURE_SCRIPT_DIR="$(mktemp -d)" +FEATURE_SCRIPT="$FEATURE_SCRIPT_DIR/enable-confidential-assets-feature-87.move" +cat > "$FEATURE_SCRIPT" <<'EOF' +// Enables on-chain feature flag 87 (BULLETPROOFS_BATCH_NATIVES) and reconfigures. +// Sender must be the core resources account (localnet: key in /mint.key). +script { + use aptos_framework::aptos_governance; + use std::features; + + fun main(core_resources: &signer) { + let core_signer = aptos_governance::get_signer_testnet_only(core_resources, @0x1); + let framework_signer = &core_signer; + + let enabled_blob: vector = vector[87]; + let disabled_blob: vector = vector[]; + + features::change_feature_flags_for_next_epoch(framework_signer, enabled_blob, disabled_blob); + aptos_governance::reconfigure(framework_signer); + } +} +EOF +"$MOVEMENT" move run-script \ + --assume-yes \ + --url "$NODE_URL" \ + --private-key-file "$TEST_DIR/mint.key" \ + --encoding bcs \ + --sender-account "$CORE_RESOURCES_ADDRESS" \ + --max-gas "$MOVE_RUN_SCRIPT_MAX_GAS" \ + --framework-local-dir "$FRAMEWORK_DIR" \ + --script-path "$FEATURE_SCRIPT" +rm -rf "$FEATURE_SCRIPT_DIR" + +publish_experimental_from_profile + +# Bootstrap the chain-auditor: governance (mint.key) designates the publish-profile +# account as `chain_auditor_admin`, then that admin sets a known TwistedElGamal +# pubkey as the chain auditor. Without this, every `confidential_transfer` aborts +# with ECHAIN_AUDITOR_NOT_SET because the GlobalConfig.chain_auditor_ek defaults +# to None at publish. +# +# CHAIN_AUDITOR_EK is a fixed test-only key. Re-running the script bumps the +# on-chain `chain_auditor_epoch` (harmless for tests; localnet state is wiped on +# fresh starts). Private key for this pubkey (only needed if a test ever wants +# to actually decrypt as the chain auditor): +# priv: 0xb17fdd9dd18e25a3c5f7b2004142b76c3998eab4f91372ea543830ece670b5cb +CHAIN_AUDITOR_BOOTSTRAP_DIR="$SCRIPT_DIR/chain-auditor-bootstrap" +CHAIN_AUDITOR_EK="${CHAIN_AUDITOR_EK:-0x5e7cbfdf6b100216d7541b0439704748225a90005cd4908a1ee3c90d1ab1ea00}" + +if [[ "${SKIP_EXPERIMENTAL_PUBLISH:-0}" != "1" ]]; then + if [[ ! -d "$CHAIN_AUDITOR_BOOTSTRAP_DIR" ]]; then + echo "error: missing $CHAIN_AUDITOR_BOOTSTRAP_DIR" >&2 + exit 1 + fi + admin_addr=$(profile_account_hex) || exit 1 + + # `move run-script` does not accept --named-addresses, so compile the bootstrap + # script package separately and feed the resulting bytecode in via + # --compiled-script-path. + echo "Compiling chain-auditor bootstrap script (aptos_experimental=$admin_addr) ..." + "$MOVEMENT" move compile-script \ + --package-dir "$CHAIN_AUDITOR_BOOTSTRAP_DIR" \ + --named-addresses "aptos_experimental=$admin_addr" \ + --output-file "$CHAIN_AUDITOR_BOOTSTRAP_DIR/build/set_chain_auditor_admin.mv" + + echo "Designating $admin_addr (profile \"$MOVEMENT_PROFILE\") as chain_auditor_admin ..." + "$MOVEMENT" move run-script \ + --assume-yes \ + --url "$NODE_URL" \ + --private-key-file "$TEST_DIR/mint.key" \ + --encoding bcs \ + --sender-account "$CORE_RESOURCES_ADDRESS" \ + --max-gas "$MOVE_RUN_SCRIPT_MAX_GAS" \ + --compiled-script-path "$CHAIN_AUDITOR_BOOTSTRAP_DIR/build/set_chain_auditor_admin.mv" \ + --args "address:$admin_addr" + + echo "Setting chain auditor encryption key to $CHAIN_AUDITOR_EK ..." + "$MOVEMENT" move run \ + --assume-yes \ + --url "$NODE_URL" \ + --profile "$MOVEMENT_PROFILE" \ + --max-gas "$MOVE_RUN_SCRIPT_MAX_GAS" \ + --function-id "0x1::confidential_asset::set_chain_auditor" \ + --args "hex:$CHAIN_AUDITOR_EK" +fi + +# Set the asset-specific auditor key for the MOVE fungible asset (metadata object @0xa). +# `set_asset_auditor` must be signed by `object::root_owner(token)`, which for the MOVE/APT +# FA at 0xa is @aptos_framework (0x1). So this runs as a governance script: mint.key gets the +# @0x1 signer via `get_signer_testnet_only`, then calls 0x1::confidential_asset::set_asset_auditor. +# Pass an empty ASSET_AUDITOR_EK to skip (the entry function would clear the key on empty input, +# so we just don't run the step). Re-running bumps the on-chain asset_auditor_epoch. +ASSET_AUDITOR_EK="${ASSET_AUDITOR_EK:-0x5e7cbfdf6b100216d7541b0439704748225a90005cd4908a1ee3c90d1ab1ea00}" +SKIP_ASSET_AUDITOR="${SKIP_ASSET_AUDITOR:-0}" + +if [[ "$SKIP_ASSET_AUDITOR" != "1" ]]; then + echo "Setting MOVE (FA @0xa) asset auditor encryption key to $ASSET_AUDITOR_EK ..." + ASSET_AUDITOR_SCRIPT_DIR="$(mktemp -d)" + ASSET_AUDITOR_SCRIPT="$ASSET_AUDITOR_SCRIPT_DIR/set-move-asset-auditor.move" + cat > "$ASSET_AUDITOR_SCRIPT" <<'EOF' +// Sets the asset-specific auditor key for the MOVE fungible asset (metadata object @0xa). +// Sender must be the core resources account (localnet: key in /mint.key); the script +// obtains the @0x1 framework signer (root owner of the MOVE FA) via governance. +script { + use aptos_framework::aptos_governance; + use aptos_framework::confidential_asset; + use aptos_framework::object; + use aptos_framework::fungible_asset::Metadata; + + fun main(core_resources: &signer, auditor_ek: vector) { + let core_signer = aptos_governance::get_signer_testnet_only(core_resources, @0x1); + let framework_signer = &core_signer; + + let move_metadata = object::address_to_object( + @0x000000000000000000000000000000000000000000000000000000000000000a + ); + + confidential_asset::set_asset_auditor(framework_signer, move_metadata, auditor_ek); + } +} +EOF + "$MOVEMENT" move run-script \ + --assume-yes \ + --url "$NODE_URL" \ + --private-key-file "$TEST_DIR/mint.key" \ + --encoding bcs \ + --sender-account "$CORE_RESOURCES_ADDRESS" \ + --max-gas "$MOVE_RUN_SCRIPT_MAX_GAS" \ + --framework-local-dir "$FRAMEWORK_DIR" \ + --script-path "$ASSET_AUDITOR_SCRIPT" \ + --args "hex:$ASSET_AUDITOR_EK" + rm -rf "$ASSET_AUDITOR_SCRIPT_DIR" +fi + +echo "Done — feature flag and (if enabled) publish finished." +echo "REST: $NODE_URL/v1 (ready probe: $READY_URL — set WAIT_STRATEGY=node to wait only on REST)" + +# Hold this shell so localnet is not an invisible background process (default). Ctrl+C stops localnet. +if [[ "$STARTED_LOCALNET_BG" == "1" ]] && [[ "$KEEP_LOCALNET" == "1" ]] && [[ "${LOCALNET_ATTACH:-1}" == "1" ]]; then + if [[ -f "$LOCALNET_PID_FILE" ]]; then + pid=$(cat "$LOCALNET_PID_FILE" 2>/dev/null || true) + if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then + echo "" + echo "━━━━━━━━ Localnet is running — this terminal stays attached ━━━━━━━━" + echo " REST API: ${NODE_URL}/v1" + echo " Ready check: $READY_URL" + echo " Process pid: $pid (also in $LOCALNET_PID_FILE)" + echo " Live logs: tail -f \"$LOCALNET_LOG\"" + echo " Stop: Ctrl+C here, or in another shell: kill $pid" + echo " Detach next time (return to prompt while localnet runs): LOCALNET_ATTACH=0" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # Ctrl+C yields non-zero exit; EXIT trap runs shutdown_localnet_bg. + wait "$pid" || true + shutdown_localnet_bg + fi + fi +elif [[ "$STARTED_LOCALNET_BG" == "1" ]] && [[ "$KEEP_LOCALNET" == "1" ]] && [[ "${LOCALNET_ATTACH:-1}" != "1" ]]; then + if [[ -f "$LOCALNET_PID_FILE" ]]; then + pid=$(cat "$LOCALNET_PID_FILE" 2>/dev/null || true) + echo "Localnet left running in the background (pid ${pid:-?}). Stop from repo root: kill \"\$(cat .movement/localnet.pid)\"" + fi +fi diff --git a/testsuite/module-publish/src/main.rs b/testsuite/module-publish/src/main.rs index 64ce282fcba..07775d0d82c 100644 --- a/testsuite/module-publish/src/main.rs +++ b/testsuite/module-publish/src/main.rs @@ -26,11 +26,6 @@ fn additional_packages() -> Vec<(&'static str, &'static str, bool)> { "src/packages/framework_usecases", false, ), - ( - "experimental_usecases", - "src/packages/experimental_usecases", - true, - ), ("complex", "src/packages/complex", false), ( "ambassador_token", diff --git a/testsuite/module-publish/src/packages/experimental_usecases/Move.toml b/testsuite/module-publish/src/packages/experimental_usecases/Move.toml deleted file mode 100644 index 6bcfeea82c8..00000000000 --- a/testsuite/module-publish/src/packages/experimental_usecases/Move.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "ExperimentalUsecases" -version = "0.0.0" - -[dependencies] -AptosFramework = { local = "../../../../../aptos-move/framework/aptos-framework" } -AptosExperimental = { local = "../../../../../aptos-move/framework/aptos-experimental" } -AptosToken = { local = "../../../../../aptos-move/framework/aptos-token" } -AptosTokenObjects = { local = "../../../../../aptos-move/framework/aptos-token-objects" } - -# testing exchanging of constant address works as well. -[addresses] -publisher_address = "0xABCD" diff --git a/testsuite/module-publish/src/packages/experimental_usecases/sources/order_book_example.move b/testsuite/module-publish/src/packages/experimental_usecases/sources/order_book_example.move deleted file mode 100644 index ec9ce4c966b..00000000000 --- a/testsuite/module-publish/src/packages/experimental_usecases/sources/order_book_example.move +++ /dev/null @@ -1,83 +0,0 @@ -module 0xABCD::order_book_example { - use std::signer; - use std::error; - use std::option; - - use aptos_experimental::active_order_book::{Self, ActiveOrderBook}; - use aptos_experimental::order_book::{Self, OrderBook}; - use aptos_experimental::order_book_types; - - const ENOT_AUTHORIZED: u64 = 1; - // Resource being modified doesn't exist - const EDEX_RESOURCE_NOT_PRESENT: u64 = 2; - - struct Empty has store, copy, drop {} - - struct ActiveOnly has key { - active_only: ActiveOrderBook, - } - - struct Dex has key { - order_book: OrderBook, - } - - // Create the global `Dex`. - // Stored under the module publisher address. - fun init_module(publisher: &signer) { - assert!( - signer::address_of(publisher) == @publisher_address, - ENOT_AUTHORIZED, - ); - - move_to( - publisher, - ActiveOnly { active_only: active_order_book::new_active_order_book() } - ); - - move_to( - publisher, - Dex { order_book: order_book::new_order_book() } - ); - } - - public entry fun place_active_post_only_order(sender: address, account_order_id: u64, bid_price: u64, volume: u64, is_buy: bool) acquires ActiveOnly { - assert!(exists(@publisher_address), error::invalid_argument(EDEX_RESOURCE_NOT_PRESENT)); - let active_only = borrow_global_mut(@publisher_address); - - let order_id = order_book_types::new_order_id_type(sender, account_order_id); - // TODO change from random to monothonically increasing value - let unique_priority_idx = order_book_types::generate_unique_idx_fifo_tiebraker(); - - active_only.active_only.place_maker_order( - order_id, - bid_price, - unique_priority_idx, - volume, - is_buy - ); - } - - public entry fun place_order(sender: address, account_order_id: u64, bid_price: u64, volume: u64, is_buy: bool) acquires Dex { - assert!(exists(@publisher_address), error::invalid_argument(EDEX_RESOURCE_NOT_PRESENT)); - let dex = borrow_global_mut(@publisher_address); - dex.order_book.place_order_and_get_matches( - order_book::new_order_request( - sender, // account - account_order_id, - option::none(), // unique_priority_idx, - bid_price, - volume, - volume, - is_buy, - option::none(), // trigger_condition - Empty {}, //metadata - ) - ); - } - - public entry fun cancel_order(account_order_id: u64) acquires Dex { - assert!(exists(@publisher_address), error::invalid_argument(EDEX_RESOURCE_NOT_PRESENT)); - let order_book = borrow_global_mut(@publisher_address); - order_book.order_book.cancel_order(@publisher_address, account_order_id); - } -}