diff --git a/crates/confidential-assets/Cargo.toml b/crates/confidential-assets/Cargo.toml index b7ff11e..57a9101 100644 --- a/crates/confidential-assets/Cargo.toml +++ b/crates/confidential-assets/Cargo.toml @@ -18,6 +18,10 @@ e2e = [] name = "simple_ca_transfer" required-features = ["e2e"] +[[example]] +name = "set_auditor_example" +required-features = ["e2e"] + [dependencies] movement-sdk = { workspace = true, features = ["twisted_ed25519", "ed25519", "faucet"] } aptos-bcs = { workspace = true } diff --git a/crates/confidential-assets/examples/set_auditor_example.rs b/crates/confidential-assets/examples/set_auditor_example.rs new file mode 100644 index 0000000..1e19466 --- /dev/null +++ b/crates/confidential-assets/examples/set_auditor_example.rs @@ -0,0 +1,239 @@ +// Example: Set (and clear) chain-level and per-asset auditors on confidential assets +// +// Demonstrates two independent auditor flows: +// +// A. Chain auditor — applies to every confidential transfer on the network: +// 1. Set the chain auditor EK (signed by the chain auditor admin) +// 2. Read it back from chain to verify +// 3. Clear it (signed by the chain auditor admin) +// +// B. Asset auditor — applies only to transfers of a specific FA token (optional): +// 1. Set the asset auditor EK (signed by the FA root owner / issuer) +// 2. Read it back from chain to verify +// 3. Clear it (signed by the issuer) +// +// Section B is skipped when TOKEN_ADDRESS is not set. It requires a user-created +// FA token whose root owner you control — the built-in MOVE FA at 0xa is owned +// by 0x1 (@aptos_framework) and cannot be used here. +// To create your own FA, deploy a Move module that calls +// `fungible_asset::add_fungibility`, then pass its metadata address as TOKEN_ADDRESS. +// +// Prerequisites: +// - A. The chain auditor admin must already be assigned via the governance Move script +// (see deploy/scripts/setup_chain_auditor.move). Provide that admin's private key +// in CHAIN_AUDITOR_ADMIN_PRIVATE_KEY. +// - B. (optional) A user-created FA token whose root owner you control. +// +// Environment variables: +// CHAIN_AUDITOR_ADMIN_PRIVATE_KEY — hex Ed25519 private key of the chain auditor admin (required for A) +// TOKEN_ADDRESS — hex address of your FA metadata object (optional, enables B) +// ISSUER_PRIVATE_KEY — hex Ed25519 private key of the FA root owner (required when TOKEN_ADDRESS is set) +// ISSUER_ADDRESS — on-chain address of the issuer when it differs from the key-derived +// address (e.g. a genesis account whose address is fixed at node init) +// MOVEMENT_NETWORK — TESTNET | MAINNET | LOCAL (optional, default LOCAL) +// MOVEMENT_RPC_URL — custom RPC endpoint, used when MOVEMENT_NETWORK is not set +// CONFIDENTIAL_MODULE_ADDRESS — defaults to 0x1 +// +// Run (section A only): +// CHAIN_AUDITOR_ADMIN_PRIVATE_KEY= \ +// cargo run -p confidential-assets --example set_auditor_example --features confidential-assets/e2e +// +// Run (sections A + B): +// CHAIN_AUDITOR_ADMIN_PRIVATE_KEY= TOKEN_ADDRESS=0x ISSUER_PRIVATE_KEY= \ +// cargo run -p confidential-assets --example set_auditor_example --features confidential-assets/e2e + +use confidential_assets::api::ConfidentialAsset; +use confidential_assets::crypto::twisted_ed25519::TwistedEd25519PrivateKey; +use movement_sdk::{ + Movement, MovementConfig, MovementError, MovementResult, account::Ed25519Account, + types::AccountAddress, +}; +use std::env; + +#[tokio::main] +async fn main() -> MovementResult<()> { + println!("=== Set Auditor Example ===\n"); + + // ── 1. Build the Movement client ───────────────────────────────────────── + let config = match env::var("MOVEMENT_NETWORK") { + Ok(network) => match network.to_uppercase().as_str() { + "TESTNET" => MovementConfig::testnet(), + "MAINNET" => MovementConfig::mainnet(), + _ => MovementConfig::local(), + }, + Err(_) => match env::var("MOVEMENT_RPC_URL") { + Ok(url) => MovementConfig::custom(&url)?, + Err(_) => MovementConfig::local(), + }, + }; + let movement = Movement::new(config)?; + println!("Connected to Movement network.\n"); + + let module_address = + env::var("CONFIDENTIAL_MODULE_ADDRESS").unwrap_or_else(|_| "0x1".to_string()); + let ca = ConfidentialAsset::new(&movement, Some(&module_address))?; + + // ── 2. Generate a shared auditor key pair ───────────────────────────────── + // The same dk/ek pair is reused for both the chain and asset auditor sections. + let auditor_dk = TwistedEd25519PrivateKey::generate(); + let auditor_ek = auditor_dk.public_key(); + println!( + "Generated auditor decryption key (dk): {}", + hex::encode(auditor_dk.to_bytes()) + ); + println!( + "Generated auditor encryption key (ek): {}", + hex::encode(auditor_ek.to_bytes()) + ); + println!(); + + // ════════════════════════════════════════════════════════════════════════ + // A. Chain auditor + // ════════════════════════════════════════════════════════════════════════ + + let chain_admin_key_hex = env::var("CHAIN_AUDITOR_ADMIN_PRIVATE_KEY").map_err(|_| { + MovementError::Internal( + "CHAIN_AUDITOR_ADMIN_PRIVATE_KEY is required (hex Ed25519 key of the chain auditor admin)".to_string(), + ) + })?; + let chain_admin = Ed25519Account::from_private_key_hex(&chain_admin_key_hex).map_err(|e| { + MovementError::Internal(format!("invalid CHAIN_AUDITOR_ADMIN_PRIVATE_KEY: {e}")) + })?; + + println!("── A. Chain auditor ─────────────────────────────────────────────"); + println!("Chain auditor admin: {}", chain_admin.address()); + + let current_chain = ca.get_chain_auditor_encryption_key().await?; + println!( + "Current chain auditor EK: {}", + current_chain + .as_ref() + .map(|ek| hex::encode(ek.to_bytes())) + .unwrap_or_else(|| "(none)".to_string()) + ); + + // Set + println!("Setting chain auditor EK …"); + let payload = ca.set_chain_auditor(Some(&auditor_ek))?; + movement + .sign_submit_and_wait(&chain_admin, payload, None) + .await?; + + let stored_chain = ca + .get_chain_auditor_encryption_key() + .await? + .ok_or_else(|| { + MovementError::Internal("Chain auditor EK not found after set".to_string()) + })?; + assert_eq!( + stored_chain.to_bytes(), + auditor_ek.to_bytes(), + "Stored chain auditor EK does not match" + ); + println!( + " Verified — stored EK: {}", + hex::encode(stored_chain.to_bytes()) + ); + + // Clear + println!("Clearing chain auditor EK …"); + let payload = ca.set_chain_auditor(None)?; + movement + .sign_submit_and_wait(&chain_admin, payload, None) + .await?; + + assert!( + ca.get_chain_auditor_encryption_key().await?.is_none(), + "Chain auditor EK should be None after clearing" + ); + println!(" Verified — chain auditor EK is cleared."); + println!(); + + // ════════════════════════════════════════════════════════════════════════ + // B. Asset auditor (optional — skipped when TOKEN_ADDRESS is not set) + // ════════════════════════════════════════════════════════════════════════ + + let token_raw = match env::var("TOKEN_ADDRESS") { + Ok(v) => v, + Err(_) => { + println!("── B. Asset auditor ─────────────────────────────────────────────"); + println!(" Skipped — TOKEN_ADDRESS not set."); + println!(" To run this section, deploy a Move module that creates a fungible asset,"); + println!( + " then re-run with TOKEN_ADDRESS=0x ISSUER_PRIVATE_KEY=." + ); + println!(); + println!("Section A complete!"); + return Ok(()); + } + }; + + let token = AccountAddress::from_hex(&token_raw) + .map_err(|e| MovementError::Internal(format!("invalid TOKEN_ADDRESS: {e}")))?; + + let issuer_key_hex = env::var("ISSUER_PRIVATE_KEY").map_err(|_| { + MovementError::Internal( + "ISSUER_PRIVATE_KEY is required when TOKEN_ADDRESS is set (hex Ed25519 key of the FA root owner)".to_string(), + ) + })?; + let mut issuer = Ed25519Account::from_private_key_hex(&issuer_key_hex) + .map_err(|e| MovementError::Internal(format!("invalid ISSUER_PRIVATE_KEY: {e}")))?; + if let Ok(addr_hex) = env::var("ISSUER_ADDRESS") { + let addr = AccountAddress::from_hex(&addr_hex) + .map_err(|e| MovementError::Internal(format!("invalid ISSUER_ADDRESS: {e}")))?; + issuer = issuer.with_address(addr); + } + + println!("── B. Asset auditor ─────────────────────────────────────────────"); + println!("Issuer : {}", issuer.address()); + println!("Token : {token}"); + + let current_asset = ca.get_asset_auditor_encryption_key(&token).await?; + println!( + "Current asset auditor EK: {}", + current_asset + .as_ref() + .map(|ek| hex::encode(ek.to_bytes())) + .unwrap_or_else(|| "(none)".to_string()) + ); + + // Set + println!("Setting asset auditor EK …"); + let payload = ca.set_asset_auditor(&token, Some(&auditor_ek))?; + movement + .sign_submit_and_wait(&issuer, payload, None) + .await?; + + let stored_asset = ca + .get_asset_auditor_encryption_key(&token) + .await? + .ok_or_else(|| { + MovementError::Internal("Asset auditor EK not found after set".to_string()) + })?; + assert_eq!( + stored_asset.to_bytes(), + auditor_ek.to_bytes(), + "Stored asset auditor EK does not match" + ); + println!( + " Verified — stored EK: {}", + hex::encode(stored_asset.to_bytes()) + ); + + // Clear + println!("Clearing asset auditor EK …"); + let payload = ca.set_asset_auditor(&token, None)?; + movement + .sign_submit_and_wait(&issuer, payload, None) + .await?; + + assert!( + ca.get_asset_auditor_encryption_key(&token).await?.is_none(), + "Asset auditor EK should be None after clearing" + ); + println!(" Verified — asset auditor EK is cleared."); + println!(); + + println!("All assertions passed. Example complete!"); + Ok(()) +} diff --git a/crates/confidential-assets/examples/simple_ca_transfer.rs b/crates/confidential-assets/examples/simple_ca_transfer.rs index 50ba3df..3324086 100644 --- a/crates/confidential-assets/examples/simple_ca_transfer.rs +++ b/crates/confidential-assets/examples/simple_ca_transfer.rs @@ -147,7 +147,33 @@ async fn main() -> MovementResult<()> { ); println!(); - // ── 7. Confidential transfer: Alice → Bob ──────────────────────────────── + // ── 7. Check active auditors ───────────────────────────────────────────── + // The SDK fetches chain and asset auditors from chain and includes them in + // the ZK proof automatically — they do not need to be passed explicitly. + // We read them here to give a clear error before attempting the transfer if + // the chain auditor (required by the Move contract) is not configured. + let chain_auditor_ek = ca.get_chain_auditor_encryption_key().await?; + let asset_auditor_ek = ca.get_asset_auditor_encryption_key(&token).await?; + + println!("Active auditors:"); + match &chain_auditor_ek { + Some(_) => println!(" Chain auditor : set (included in transfer proof automatically)"), + None => { + return Err(MovementError::Internal( + "No chain auditor is configured on this network. \ + Confidential transfers require a chain auditor — \ + run the set_auditor_example to configure one first." + .to_string(), + )); + } + } + match &asset_auditor_ek { + Some(_) => println!(" Asset auditor : set (included in transfer proof automatically)"), + None => println!(" Asset auditor : not set"), + } + println!(); + + // ── 8. Confidential transfer: Alice → Bob ──────────────────────────────── println!("Transferring {TRANSFER_AMOUNT} tokens from Alice to Bob (confidential) …"); let payload = ca .transfer( @@ -156,14 +182,14 @@ async fn main() -> MovementResult<()> { &token, TRANSFER_AMOUNT, &alice_dk, - &[], // no additional auditors - &[], // no auditor hint + &[], // chain and asset auditors above are included automatically + &[], // no sender auditor hint ) .await?; movement.sign_submit_and_wait(&alice, payload, None).await?; println!(); - // ── 8. Verify balances after transfer ──────────────────────────────────── + // ── 9. Verify balances after transfer ──────────────────────────────────── let alice_bal = ca.get_balance(&alice.address(), &token, &alice_dk).await?; let bob_bal = ca.get_balance(&bob.address(), &token, &bob_dk).await?; @@ -218,7 +244,7 @@ async fn main() -> MovementResult<()> { ); println!(); - // ── 11. Normalize Bob's available balance ───────────────────────────────── + // ── 12. Normalize Bob's available balance ──────────────────────────────── println!("Normalizing Bob's balance …"); let payload = ca .normalize_balance(&bob.address(), &bob_dk, &token) diff --git a/crates/confidential-assets/src/api/confidential_asset.rs b/crates/confidential-assets/src/api/confidential_asset.rs index f6e9b03..d592f30 100644 --- a/crates/confidential-assets/src/api/confidential_asset.rs +++ b/crates/confidential-assets/src/api/confidential_asset.rs @@ -389,6 +389,37 @@ impl<'a> ConfidentialAsset<'a> { .await } + /// Build a `set_chain_auditor` transaction. + /// + /// Must be signed by the chain auditor admin assigned via `set_chain_auditor_admin`. + /// Pass `None` to clear the chain auditor (disables all confidential transfers). + pub fn set_chain_auditor( + &self, + new_chain_auditor_ek: Option<&TwistedEd25519PublicKey>, + ) -> Result { + self.transaction.set_chain_auditor(new_chain_auditor_ek) + } + + /// Get the global chain auditor encryption key, if set. + pub async fn get_chain_auditor_encryption_key( + &self, + ) -> Result, MovementError> { + self.transaction.get_chain_auditor_encryption_key().await + } + + /// Build a `set_asset_auditor` transaction. + /// + /// Must be signed by the FA metadata object's root owner (the issuer). + /// Pass `None` for `new_auditor_ek` to clear the per-asset auditor. + pub fn set_asset_auditor( + &self, + token_address: &AccountAddress, + new_auditor_ek: Option<&TwistedEd25519PublicKey>, + ) -> Result { + self.transaction + .set_asset_auditor(token_address, new_auditor_ek) + } + /// Get the asset auditor encryption key for a token. pub async fn get_asset_auditor_encryption_key( &self, diff --git a/crates/confidential-assets/src/internal/transaction_builder.rs b/crates/confidential-assets/src/internal/transaction_builder.rs index 346e936..08ab7a5 100644 --- a/crates/confidential-assets/src/internal/transaction_builder.rs +++ b/crates/confidential-assets/src/internal/transaction_builder.rs @@ -469,6 +469,49 @@ impl<'a> ConfidentialAssetTransactionBuilder<'a> { .into()) } + /// Build a `set_chain_auditor` entry function payload. + /// + /// Must be signed by the account previously assigned via `set_chain_auditor_admin`. + /// Pass `None` to clear the chain auditor (disables all confidential transfers). + pub fn set_chain_auditor( + &self, + new_chain_auditor_ek: Option<&TwistedEd25519PublicKey>, + ) -> Result { + let ek_bytes: Vec = match new_chain_auditor_ek { + Some(ek) => ek.to_bytes().to_vec(), + None => vec![], + }; + Ok(EntryFunction::new( + module_id(self.contract_address), + "set_chain_auditor", + vec![], + vec![serialize_vector_u8(&ek_bytes)], + ) + .into()) + } + + /// Build a `set_asset_auditor` entry function payload. + /// + /// The issuer (FA metadata object root owner) sets or clears the per-asset auditor EK. + /// Pass `None` to clear the auditor (empty `vector` on-chain). + pub fn set_asset_auditor( + &self, + token_address: &AccountAddress, + new_auditor_ek: Option<&TwistedEd25519PublicKey>, + ) -> Result { + let ek_bytes: Vec = match new_auditor_ek { + Some(ek) => ek.to_bytes().to_vec(), + None => vec![], + }; + Ok(EntryFunction::new( + module_id(self.contract_address), + "set_asset_auditor", + vec![], + vec![bcs_addr(token_address), serialize_vector_u8(&ek_bytes)], + ) + .into()) + } + /// Get the global chain auditor encryption key for a token, if set. pub async fn get_chain_auditor_encryption_key( &self, diff --git a/crates/movement-sdk/src/account/ed25519.rs b/crates/movement-sdk/src/account/ed25519.rs index e74ceaa..5c1d751 100644 --- a/crates/movement-sdk/src/account/ed25519.rs +++ b/crates/movement-sdk/src/account/ed25519.rs @@ -82,6 +82,17 @@ impl Ed25519Account { Ok(Self::from_private_key(private_key)) } + /// Overrides the account address while keeping the same signing key. + /// + /// Use this when the on-chain account address differs from the address derived + /// from the public key — for example, framework accounts like `@core_resources` + /// whose address is fixed at genesis regardless of the authentication key. + #[must_use] + pub fn with_address(mut self, address: AccountAddress) -> Self { + self.address = address; + self + } + /// Creates an account from a BIP-39 mnemonic phrase. /// /// Uses the standard Movement derivation path: `m/44'/637'/0'/0'/index'`