Skip to content

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.

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.

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.

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)
  • G is the Ristretto255 base point; H is the hash-to-point base.
  • The holder decrypts by computing v_i · G = P_i − DK · R_i and solving the discrete log.
  • Because encryption is additively homomorphic, new deposits can be added directly to the ciphertext without decrypting.
Balance typeChunksTotal bitsMax value
pending_balance4 × 16-bit642⁶⁴ − 1
available_balance8 × 16-bit1282¹²⁸ − 1

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.

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.

Every state-modifying operation (except deposit) requires a ZKP. Two kinds are used:

Four NP relations, each proved with a Schnorr-style Σ-protocol:

OperationNP relationWhat is proved
register_balanceR_regEK = DK⁻¹ · H (user knows DK)
withdraw_toR_withdrawWithdrawal amount matches ciphertext; balance stays non-negative
confidential_transferR_transferSent amount matches recipient ciphertext; balance conservation
rotate_encryption_keyR_keyrotNew EK = δ · old EK; R components re-encrypted correctly

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_bulletproofs native module.

The auditor system lets designated parties read confidential amounts. Every transfer includes encrypted amount ciphertexts for the applicable auditors; these are verified on-chain.

PriorityTypeDescription
HighestAsset auditorSet per-token by the asset creator; overrides the global auditor
DefaultGlobal auditorSet by Aptos governance; applies to all tokens without an asset auditor
OptionalVoluntary auditorsAdded 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.

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.

SettingWho controlsEffect
Allow-listGovernanceOnly allow-listed FA types can be used with CA; bypassed if allow-list is empty
Emergency pauseGovernanceHalts all operations except deposits and withdrawals
Global auditorGovernanceSets/changes the mandatory global auditor EK
Asset auditorFA creatorSets/changes the mandatory per-token auditor EK

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.

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
)

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,
)

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
)

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
)

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>,
)

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
)

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>,
)
FunctionReturnsDescription
get_encryption_keyOption<vector<u8>>User's current EK for a token
has_user_registeredboolWhether a confidential store exists for (user, token)
get_pending_balanceraw bytesEncrypted pending balance
get_available_balanceraw bytesEncrypted available balance
get_pending_balance_countu32Number of pending deposits since last rollover
is_balance_normalizedboolWhether available balance is in 16-bit normal form
is_incoming_pausedboolWhether incoming transfers are paused
get_global_auditorOption<vector<u8>>Global auditor EK (if set by governance)
get_asset_auditorOption<vector<u8>>Asset-specific auditor EK (if set by creator)
is_emergency_pausedboolWhether governance has engaged the emergency pause
get_total_confidential_supplyu128Total tokens currently locked in all confidential stores
  • 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_supply reveals how many tokens are currently in confidential mode.