Confidential Asset (CA)
The Confidential Asset (CA) standard is a privacy-focused protocol for Fungible Assets (FA) on Aptos. It lets users transfer and hold token amounts that are hidden on-chain, while keeping sender and recipient addresses publicly visible.
Any non-dispatchable FA can be wrapped into a confidential balance. The protocol supports transfers up to 2⁶⁴ − 1 and balances up to 2¹²⁸ − 1.
Key Concepts
Section titled “Key Concepts”Encryption Keypair
Section titled “Encryption Keypair”For each (user, token) pair, the user generates a standalone keypair:
- Decryption key (DK) — a private scalar kept only by the user; never leaves the client.
- Encryption key (EK) — derived from DK and stored on-chain; used by others to encrypt amounts for this user.
The relationship is EK = DK⁻¹ · H where H is the Ristretto255 hash-to-point base.
These keys are entirely independent from the user's Aptos account signing key.
Pending and Available Balances
Section titled “Pending and Available Balances”Every confidential store holds two encrypted balances:
pending_balance— accumulates incoming deposits and transfers.available_balance— the spendable balance; used for outgoing transfers and withdrawals.
Funds land in pending_balance first. The user must call rollover_pending_balance to move them into available_balance before spending.
Twisted ElGamal Encryption
Section titled “Twisted ElGamal Encryption”Each balance is split into 16-bit chunks and each chunk is encrypted with Twisted ElGamal on Ristretto255:
P_i = v_i · G + r_i · H (commitment)R_i = r_i · EK (key ciphertext)Gis the Ristretto255 base point;His the hash-to-point base.- The holder decrypts by computing
v_i · G = P_i − DK · R_iand solving the discrete log. - Because encryption is additively homomorphic, new deposits can be added directly to the ciphertext without decrypting.
Balance Chunking
Section titled “Balance Chunking”| Balance type | Chunks | Total bits | Max value |
|---|---|---|---|
pending_balance | 4 × 16-bit | 64 | 2⁶⁴ − 1 |
available_balance | 8 × 16-bit | 128 | 2¹²⁸ − 1 |
Normalization
Section titled “Normalization”Homomorphic additions can cause chunk values to exceed 16 bits. Normalization re-packs an available_balance so
each chunk is back in [0, 2¹⁶). It is required before rolling over a pending balance, and is provably correct via a
ZKP submitted on-chain.
On-Chain State
Section titled “On-Chain State”enum ConfidentialStore has key { V1 { pending_balance: CompressedBalance<Pending>, available_balance: CompressedBalance<Available>, ek: CompressedRistretto, // encryption key (public) pending_balance_count: u32, // rolls over at MAX_TRANSFERS_BEFORE_ROLLOVER pause_incoming: bool, // pauses incoming transfers (needed for key rotation) }}MAX_TRANSFERS_BEFORE_ROLLOVER = 65536: if pending_balance_count hits this limit, the user must call
rollover_pending_balance before the next incoming transfer is accepted.
Zero-Knowledge Proofs
Section titled “Zero-Knowledge Proofs”Every state-modifying operation (except deposit) requires a ZKP. Two kinds are used:
Sigma-Protocol Proofs (Schnorr-style)
Section titled “Sigma-Protocol Proofs (Schnorr-style)”Four NP relations, each proved with a Schnorr-style Σ-protocol:
| Operation | NP relation | What is proved |
|---|---|---|
register_balance | R_reg | EK = DK⁻¹ · H (user knows DK) |
withdraw_to | R_withdraw | Withdrawal amount matches ciphertext; balance stays non-negative |
confidential_transfer | R_transfer | Sent amount matches recipient ciphertext; balance conservation |
rotate_encryption_key | R_keyrot | New EK = δ · old EK; R components re-encrypted correctly |
Bulletproof Range Proofs
Section titled “Bulletproof Range Proofs”Every spend/normalize operation includes a Bulletproof range proof proving each balance chunk ∈ [0, 2¹⁶).
- DST:
b"AptosConfidentialAsset/BulletproofRangeProof" - Batch-verified on-chain using the
ristretto255_bulletproofsnative module.
Auditor System
Section titled “Auditor System”The auditor system lets designated parties read confidential amounts. Every transfer includes encrypted amount ciphertexts for the applicable auditors; these are verified on-chain.
Auditor Priority
Section titled “Auditor Priority”| Priority | Type | Description |
|---|---|---|
| Highest | Asset auditor | Set per-token by the asset creator; overrides the global auditor |
| Default | Global auditor | Set by Aptos governance; applies to all tokens without an asset auditor |
| Optional | Voluntary auditors | Added per-transfer by the sender; supplementary, any number |
The effective auditor (global or asset-specific) is mandatory when configured. The on-chain proof verifier enforces that the ciphertext for the effective auditor is correctly formed.
Auditor Epoch Tracking
Section titled “Auditor Epoch Tracking”The available_balance stores an R_aud component encrypted under the auditor's EK at the time of the last
rollover. When the auditor key changes (epoch bump), users must rollover to refresh this component; the on-chain
verifier enforces epoch consistency.
Governance
Section titled “Governance”| Setting | Who controls | Effect |
|---|---|---|
| Allow-list | Governance | Only allow-listed FA types can be used with CA; bypassed if allow-list is empty |
| Emergency pause | Governance | Halts all operations except deposits and withdrawals |
| Global auditor | Governance | Sets/changes the mandatory global auditor EK |
| Asset auditor | FA creator | Sets/changes the mandatory per-token auditor EK |
Entry Functions
Section titled “Entry Functions”All entry functions are in aptos_framework::confidential_asset. The _raw suffix indicates they accept
raw byte vectors (for proof data) rather than typed structs—this is the current stable API.
register_balance_raw
Section titled “register_balance_raw”Creates a ConfidentialStore for (signer, token) and stores the EK on-chain.
public entry fun register_balance_raw( sender: &signer, token: Object<Metadata>, ek: vector<u8>, // 32-byte compressed Ristretto255 point proof_bytes: vector<u8>, // serialized sigma-proof for R_reg)deposit
Section titled “deposit”Moves tokens from the signer's public FA store into pending_balance. No ZKP required.
public entry fun deposit( sender: &signer, token: Object<Metadata>, to: address, amount: u64,)rollover_pending_balance
Section titled “rollover_pending_balance”Homomorphically adds pending_balance into available_balance and resets pending_balance to zero.
The available balance must be normalized first.
public entry fun rollover_pending_balance( sender: &signer, token: Object<Metadata>, new_balance: ConfidentialAvailableBalance, // re-encrypted available balance proof: SigmaProofWithdraw, // proves the rollover is correct zkrp: RangeProof, // proves new available chunks are 16-bit pause_incoming: bool, // set true before key rotation)withdraw_to_raw
Section titled “withdraw_to_raw”Decrypts an amount from available_balance and sends it to a public FA store.
public entry fun withdraw_to_raw( sender: &signer, token: Object<Metadata>, to: address, amount: u64, new_balance: vector<vector<u8>>, // remaining available balance bytes proof_bytes: vector<u8>, // R_withdraw sigma proof zkrp_bytes: vector<u8>, // Bulletproof range proof)normalize_raw
Section titled “normalize_raw”Re-packs available_balance chunks into 16-bit bounds, proved by a ZKP.
public entry fun normalize_raw( sender: &signer, token: Object<Metadata>, new_balance: vector<vector<u8>>, proof_bytes: vector<u8>, zkrp_bytes: vector<u8>,)confidential_transfer_raw
Section titled “confidential_transfer_raw”Transfers a secret amount from the sender's available_balance to the recipient's pending_balance.
The amount is never revealed on-chain.
public entry fun confidential_transfer_raw( sender: &signer, token: Object<Metadata>, recipient: address, new_balance: vector<vector<u8>>, transfer_amount: vector<vector<u8>>, // ciphertext of amount under recipient EK proof_bytes: vector<u8>, // R_transfer sigma proof zkrp_bytes: vector<u8>, // Bulletproof range proof memo: vector<u8>, // optional; max 256 bytes; not encrypted)rotate_encryption_key_raw
Section titled “rotate_encryption_key_raw”Replaces the stored EK with a new one and re-encrypts the R components of available_balance under the new key.
Requires pause_incoming = true and zero pending_balance.
public entry fun rotate_encryption_key_raw( sender: &signer, token: Object<Metadata>, new_ek: vector<u8>, new_balance: vector<vector<u8>>, proof_bytes: vector<u8>, zkrp_bytes: vector<u8>,)View Functions
Section titled “View Functions”| Function | Returns | Description |
|---|---|---|
get_encryption_key | Option<vector<u8>> | User's current EK for a token |
has_user_registered | bool | Whether a confidential store exists for (user, token) |
get_pending_balance | raw bytes | Encrypted pending balance |
get_available_balance | raw bytes | Encrypted available balance |
get_pending_balance_count | u32 | Number of pending deposits since last rollover |
is_balance_normalized | bool | Whether available balance is in 16-bit normal form |
is_incoming_paused | bool | Whether incoming transfers are paused |
get_global_auditor | Option<vector<u8>> | Global auditor EK (if set by governance) |
get_asset_auditor | Option<vector<u8>> | Asset-specific auditor EK (if set by creator) |
is_emergency_paused | bool | Whether governance has engaged the emergency pause |
get_total_confidential_supply | u128 | Total tokens currently locked in all confidential stores |
Limitations
Section titled “Limitations”- Dispatchable FA not supported. Only non-dispatchable Fungible Assets can be used with CA. Dispatchable FAs
(those with a custom transfer dispatch function) are rejected at registration via
is_safe_for_confidentiality. - Addresses are public. Sender and recipient addresses appear in plaintext in every transaction.
- Total supply is public.
get_total_confidential_supplyreveals how many tokens are currently in confidential mode.