1 of 52

Confidential assets:

A cryptographic deep dive

Alin Tomescu

Head of Cryptography, Aptos Labs

@alinush407

March 24th, 2026

2 of 52

First: a moment of silence for "veiled coins"...

2

3 of 52

Outline

  1. What?
  2. Why?
  3. Demo
  4. How? ⚠️

3

4 of 52

Public transactions (the status quo)

4

🌍

💰 250

🌍

💰 100

🔏 "Send 💰 20 to Bob"

🌍

Everybody sees balances and TXN amounts.

Everybody sees sender & recipient.

💰 -20

💰 +20

5 of 52

Public transactions (the status quo)

5

🌍

💰 230

🌍

💰 120

Everybody sees balances and TXN amounts.

Everybody sees sender & recipient.

6 of 52

6

🌍

💰 230

🌍

💰 120

💰 500

💰 700

Nobody sees balances nor TXN amounts.

🔏 "Send 💰 50 to Bob"

🌍

Confidential transactions as an option

💰 -50

💰 +50

7 of 52

7

🌍

💰 230

🌍

💰 120

Nobody sees balances nor TXN amounts.

Everybody still sees sender & recipient.

💰 550

💰 650

🔏 "Send 💰 50 to Bob"

🌍

Confidential transactions as an option

Except auditor(s).

8 of 52

Outline

  • What?
  • Why?
  • Demo
  • How? ⚠️

8

9 of 52

Why?

9

Regulatory compliance:

  • 3rd party auditors can see
  • detect abuse

Confidentiality = user safety

  • hide balances and amounts ⇒
  • protects users (e.g., see here)

New use cases:

  • on-chain salaries
  • on-chain RSU/token grants

Human dignity ⊇ ability to choose what information you reveal about yourself. -- Hester Pierce, SEC Commissioner (video)

10 of 52

Outline

  • What?
  • Why?
  • Demo
  • How? ⚠️

10

11 of 52

Demo: Confidential payments (confidential.aptoslabs.com)

11

Keyless (sign in with your Google):

  • No 🔑 management needed
  • No wallet installation needed (!)

Venmo-like:

  • Send payments to usernames
  • Secured via Aptos Names

12 of 52

Outline

  • What?
  • Why?
  • Demo
  • How? ⚠️

12

13 of 52

Before we dig in… What are the next steps?

  1. Mainnet deployment in v1.43
  2. Enable APT token
  3. Native support in Petra for confidential balances & transfers

We can pause for some early Q&A, if any 🙋

13

14 of 52

Outline

  • What?
  • Why?
  • Demo
  • How? ⚠️
    • Familiarity with elliptic curve groups of prime order p, with generator G
    • Familiarity with ElGamal encryption
    • Familiarity with Σ-protocols for linear relations among group elements
      • Mostly, familiarity with zero-knowledge proofs for NP relations

14

15 of 52

Comparison to previous account model protocols

15

16 of 52

Comparison to previous account model protocols

16

17 of 52

Comparison to previous account model protocols

17

18 of 52

Comparison to previous account model protocols

18

19 of 52

Comparison to previous account model protocols

19

20 of 52

Comparison to previous account model protocols

20

21 of 52

Comparison to previous account model protocols

21

22 of 52

Deposits: enroll for confidentiality

22

23 of 52

Prereq 1: Chunked'n'twisted ElGamal encryption 🔐

KGen(1λ) → (dk, ek)

dkRp

ek dk-1 H

E(ek, v1, …, v; r1, ..., r) → c

Ri ri ek, ∀i ∈ [ℓ]

Pivi G + riH, ∀i ∈ [ℓ]

c (Pi, Ri)i ∈ []

23

i.e., Pedersen commitment to vi using randomness ri and commitment key (G, H)

Bolded, underlined variables denote vectors (e.g., c).

Convention: We will use c.Ped := (Pi)i ∈ [] to denote the Pedersen components of the ciphertext.

Randomness; important!

24 of 52

Prereq 2: Chunking

For Aptos, we will work with:

  • 128-bit balances a, split into = 8 chunks a = [a1, a2, …, a8] of b = 16 bits each
  • 64-bit amounts v, split into n = 4 chunks v = [v1, v2, …, v4] of b = 16 bits each

i.e., ToChunks returns u’s base-2b representation:

  • a = ToChunks(a, b, ), such that a = Σi∈ [] (216)i-1 ai = Unchunk(a, b, )
  • v = ToChunks(v, b, n), such that v = Σi∈ [n] (216)i-1 vi = Unchunk(v, b, n)

24

Convention: u = ToChunks(u, b, m) splits u into m chunks of b bits each & returns them as u.

u = Unchunk(u, b, m) does the opposite such that Unchunk( ToChunks(u, b, m), b, m ) = u.

25 of 52

25

pubS: $10

Sherry (ekS)

A'S E(ekS, v; 0)

AS: empty / unset

i.e.: ToChunks(v, b, )

2

set new

ava. bal.

TXN

1. public amount v

// to deposit for Sherry

1

Note: Deposited amount is public ⇒ No privacy just yet. (Must send or receive a transfer for that.)

🎉 Sherry can deposit confidential coins into her account!

26 of 52

Withdrawal: dispose of confidentiality

26

27 of 52

Prereq 3: Zero-knowledge proofs (ZKP) for NP relations

𝓡(x; w) = 1

27

A ZKP for x 𝓡

convinces verifier that prover knows

a secret witness w such that 𝓡(x; w) = 1

Public statement: Both prover & verifier have it.

Private witness: Only prover has it.

Goal: Hide it from verifier.

Any publicly-known algorithm, henceforth called a relation

28 of 52

Prereq 3’: Zero-knowledge proofs (ZKP) (e.g.: Sudoku)

Sudoku(puzzle; sol) = 1

28

A ZKP for puzzle Sudoku

convinces verifier that prover knows

a secret sol such that Sudoku(puzzle; sol) = 1

A publicly-known algorithm for checking if sol is a valid solution to puzzle.

29 of 52

ZK range proof relation for Pedersen-committed values

𝓡b((Pi)i ∈ []; (vi, ri)i ∈ []) = 1

29

∀i ∈ []:

Pi = vi G + riH

vi ∈ [0, 2b),

30 of 52

Prereq 4: Chunked'n'twisted ElGamal decryption 🔐

Dec(c; dk) → (v1G, …, vG)

(Pi, Ri)i ∈ [ℓ] c

vi G Pi - dk Ri, ∀i ∈ [ℓ]

Dec*(c; dk) → v G

(v1G, …, vG) ← Dec(dk, c)

vG Σi∈ [](216)i (vi G)

30

= (Pi) - dk (Ri)

= (vi G + riH) - dk (ri ek)

= (vi G + riH) - dk (ri dk-1 H)

= (vi G + riH) - ri H

= vi G

Discrete logarithm problem (DLP):

given viG, compute vi.

Fast for 32-bit vi’s (e.g., ~10ms in Rust)

Note: Fix b and . Then, Dec*( E(ek, ToChunks(u, b, ); r); dk ) = u G, for all u, r, ek = dk-1 ᐧ H

31 of 52

31

TXN

1

pubS: $10

Sherry (ekS)

AS: E(ekS, aS; )

i.e.: ToChunks(aS, b, )

2. A'S // Sherry’s new balance

3. πwithdraw for (ekS, v, AS, A'S) 𝓡withdraw

1. public amount v

// to withdraw from Sherry

A'SE(ekS, as; r)

2

set new

ava. bal.

i.e.: ToChunks(aS - v, b, )

🎉 Sherry can withdraw confidential coins back into her public!

𝓡withdraw(ekS, v, AS, A'S; dkS, r, as)

a. ekS = dkS-1 ᐧ H // dk is 👌

b. A'S = E(ekS, aS; r)

c. Unchunk( as, b, ) + v ᐧ G = D*(dkS, AS)

Infinite money glitch: aS = 0 and v = 1 ⇒ aS= aS - v = -1 (mod p) = p - 1 (mod p)

Why? 𝓡withdraw does not check that v aS.

4. π for (A'S.Ped) 𝓡b

That’s why we need a range proof!

32 of 52

Transfers: leverage confidentiality

32

33 of 52

Prereq 5: Chunked'n'twisted ElGamal batch encryption 🔐

E(ek1, …, ekm, v1, …, v; r1, ..., r) → c

Ri, j ri ekj, ∀i ∈ [ℓ], ∀j ∈ [m]

Pivi G + riH, ∀i ∈ [ℓ]

c (Pi, Ri, 1, …, Ri, m)i ∈ []

33

Convention: As before, c.Ped := (Pi)i ∈ [] denotes the Pedersen components of the batch ciphertext.

(m+1)ℓ-sized vs. 2mℓ, if done naively via E(ekj, v1, …, v; r1,j, ..., rℓ, j), ∀j ∈ [m]

34 of 52

Prereq 6: Chunked'n'twisted ElGamal homomorphism 🔐

Rai ri ek

Paiai G + riH

34

E(ek, a; r) + E(ek, b; r’) = E(ek, (a + b) mod p; r + r’)

E(ek, a; r) + En(ek, v; r) = E(ek, (a + v) mod p; r + r’)

Convention: When adding a size-n ciphertext with a larger size-one, we implicitly pad with 0s.

Note: All ciphertexts are under the same ek

+

Rbi r’i ek

Pbibi G + r’iH

=

RiRai + Rbi = (ri+r’i) ek

PiPai + Pbi= (ai+bi) G + (ri+r’i)H

Food for thought: If a and b were base-2b, when combined like this will the result remain base-2b?

35 of 52

35

1

TXN

pubR: $60

Rob (ekR)

AR: E(ekR, aR; )

2. A'S // Sherry’s new balance

3. πtxfer for (ekS, ekR, c, AS, A'S) 𝓡txfer

4. π for (c.Ped), (A'S.Ped) 𝓡b

1. c = En(ekS, ekR, v; s)

// to withdraw from Sherry and deposit for Rob

i.e.: ToChunks(v, b, n)

3

hom. upd.

A'SE(ekS, as; r)

2

set new

ava. bal.

This update can invalidate Rob’s ZKP w.r.t. AR

pubS: $10

Sherry (ekS)

AS: E(ekS, aS; )

𝓡txfer(ekS, ekR, c, AS, A'S; dkS, v, r, as)

a. 1 = 𝓡withdraw(ekS, v, AS, A'S; dkS, r, as)

b. c = En(ekS, ekR, ToChunks(v, b, n); s)

Why? If v = p - 1 ⇒ aS= aS - v = aS + 1 ⇒ (A'S.Ped) 𝓡b will pass

A'RAR+ c =

= E(ekR, aR+ v; )

🎉 Sherry can transfer confidential coins to anyone!

36 of 52

36

1

TXN

pubR: $60

Rob (ekR)

AR: E(ekR, aR; )

2. A'S // Sherry’s new balance

3. πtxfer for (ekS, ekR, c, AS, A'S) 𝓡txfer

4. π for (c.Ped), (A'S.Ped) 𝓡b

1. c = En(ekS, ekR, v; s)

// to withdraw from Sherry and deposit for Rob

i.e.: ToChunks(v, b, n)

3

hom. upd.

A'SE(ekS, as; r)

2

set new

ava. bal.

This update can invalidate Rob’s ZKP w.r.t. AR

pubS: $10

Sherry (ekS)

AS: E(ekS, aS; )

𝓡txfer(ekS, ekR, c, AS, A'S; dkS, v, r, as)

a. ekS = dkS-1 ᐧ H // dk is 👌

b. A'S = E(ekS, aS; r)

c. Unchunk( as, b, ) + v ᐧ G = D*(dkS, AS)

b. c = En(ekS, ekR, ToChunks(v, b, n); s)

Why? If v = p - 1 ⇒ aS= aS - v = aS + 1 ⇒ (A'S.Ped) 𝓡b will pass

A'RAR+ c =

= E(ekR, aR+ v; )

37 of 52

37

A'SE(ekS, as; r)

TXN

2. A'S // Sherry’s new balance

3. πtxfer for (ekS, ekR, c, AS, A'S) 𝓡txfer

4. π for (c.Ped), (A'S.Ped) 𝓡b

1. c = En(ekS, ekR, v; s)

// to withdraw from Sherry and deposit for Rob

pubS: $10

Sherry (ekS)

pubR: $60

Rob (ekR)

AR: E(ekR, aR; )

1

PR: En(ekR, pR; )

# recv’d: 35

# recv’dR: 21

AS: E(ekS, aS; )

PS: En(ekS, ps; )

P'RPR+ c =

= E(ekR, pR+ v; )

3

hom. upd.

# recv’dR21 + 1

2

set new

ava. bal.

Total bal.: aS + pS

i.e.: ToChunks(v, b, n)

38 of 52

38

pubS: $10

Sherry (ekS)

pubR: $60

Rob (ekR)

AR: E(ekR, ekA, aR; )

1

TXN

PR: En(ekR, pR; )

2. A'S // Sherry’s new (normalized) balance

3. π for (c.Ped), (A'S.Ped) 𝓡b

4. πtxfer for (ekS, ekR, ekA, c, AS, A'S) 𝓡txfer

1. c = En(ekS, ekR, ekA, v; )

// for debiting Sherry, crediting Rob, and 👀 auditor

# recv’d: 35

# recv’dR: 21

AS: E(ekS, ekA, aS; )

PS: En(ekS, ps; )

A'SE(ekS, ekA, as; r)

Added auditor ekA!

TXN

2. A'S // Sherry’s new balance

3. πtxfer for (ekS, ekR, ekA, c, AS, A'S) 𝓡txfer

4. π for (c.Ped), (A'S.Ped) 𝓡b

1. c = En(ekS, ekR, ekA, v; s)

// to withdraw from Sherry and deposit for Rob + 👀 auditor

P'RPR+ c =

= E(ekR, pR+ v; )

3

hom. upd.

2

set new

ava. bal.

# recv’dR← 21 + 1

i.e.: ToChunks(v, b, n)

Too many tx’fers ⇒ chunks get large ⇒ expensive DLs.

Solution: Roll over into available balance after 2b in-transfers.

39 of 52

39

# recv’dR← 0

2

hom. upd.

pubR: $60

Rob (ekR)

AR: E(ekR, ekA, aR; r)

PR: En(ekR, pR; t)

# recv’dR: 22

A'RAR+ PR =

= E(ekR, aR+ pR; r + t)

1

TXN

“Roll over!”

After 2b in-transfers, pending chunks will become < 22b

⇒ after rollover, available balance chunks may be ≤ 2b bit-wide.

Solution: Normalize” to b-bit by withdrawing 0 from your account!

40 of 52

A lot (like a lot) more details can be found at…

alinush.org/confidential-assets:

  • Gas costs on Aptos
  • TBSGS-k discrete log algorithm + benchmarks
  • Global vs. asset-specific auditor (who is the effective auditor)
  • Sender-picked voluntary auditors that learn the amount

40

41 of 52

That's it folks! 👋👋

Resources:

Time for Q&A (part II) 🙋

41

42 of 52

Appendix

42

43 of 52

Part 4a: Move APIs

At a high-level, the public entry Move functions are:

  1. register( ownersigner, asset_type, ek, zk-proof ); Note: ek stands for "encryption key"
  2. deposit ( sendersigner, asset_type, pub_amt ); Note: pub_amt is the "public amount"
  3. withdraw( ownersigner, asset_type, pub_amt, zk-proof );
  4. transfer( sendersigner, recipientaddress, asset_type, zk-proof );
  5. rotate_key( ownersigner, asset_type, new_ek, zk-proof );
  6. balance( owneraddress, asset_type ) → secret_bal Note: See next slide for more details

Compatible with:

  • Any Aptos account types: Keyless, traditional (e.g., ECDSA, Ed25519), etc.
  • Any Aptos Fungible Asset (FA) (e.g., USDC, USDT, etc.)

43

Actually, a bit more complicated: see next slide

44 of 52

Part 4b: Move APIs (additional complexity)

At a lower-level, there are some extra public entry Move functions:

  • pending_balance( owneraddress, asset_type ) → secret_pnd_bal
  • available_balance( owneraddress, asset_type ) → secret_avl_bal
    1. Note: Think of the previous slide’s balance( ) as returning a tuple of both the pending and the available balance
  • roll_over( ownersigner, asset_type )
  • normalize( ownersigner, asset_type, zk-proof );
  • set_asset_specific_auditor( apt_govsigner, asset_type, ekauditor, zk-proof );
  • set_global_auditor( apt_govsigner, ekauditor, zk-proof );
  • pause_incoming_txfers( ownersigner );
  • resume_incoming_txfers( ownersigner );

44

45 of 52

Part 4c: SDK APIs (TODO: update extra/normalization/withdraw auditors)

At a high-level, the TypeScript SDK APIs exposed are:

  • gen_encryption_key_pair( ) → dk, ek
  • gen_withdraw_proof(secret_bal, dk, asset_type, pub_amt) → proof
  • gen_transfer_proof(secret_bal, dk, ekrecipient, [ ekauditor,]* asset_type, secret_amt) → proof
  • gen_normalization_proof(secret_bal, dk, asset_type) → new_secret_bal, proof
  • gen_key_rotation_proof(secret_bal, dkold, dknew) → proof
  • decrypt(secret_bal[/amt], dk) → pub_bal[/amt]
  • + APIs for calling all the public entry Move functions from previous slide

45

46 of 52

Base-2b vs. radix-2b representation

Definition 2: v = [v1, …, v] is a size- base-2b representation of v iff.:

  1. v is a radix-2b representation of v
  2. vi ∈ [0, 2b), for all i ∈ [] ⇒ v < (2b)

Definition 1: v = [v1, …, v] is a size- radix-2b representation of v iff. v = ∑i ∈ [] (2b)i-1 vi

46

Does NOT restrict the sizes of the vi's at all, nor the size of v!

e.g., v = [1, 1, -1], and v' = [0, 0, 5] are size-3 radix-2 representations of v = 5

e,g., v = [1, 0, 1] is the unique size-3 base-2 representation of v = 5

47 of 52

Size- base-2b representations

47

ToChunks(v, b, ) → v = [ v1, v2, …, vm , …, v]

Returns the size- base-2b representation v of v: i.e., v = ∑i ∈ [] (2b)i-1 vi

log2(v) / b chunks

could be larger than this

v1 is the least-significant chunk (LSC)

v is the most-significant chunk (MSC)

48 of 52

128-bit ± 64-bit integer arithmetic mod (~256-bit) prime q

Recall that when we "subtract" two encryptions of bal ∈ [0, 2128) and v ∈ [0, 264), we get an encryption of (bal - v) mod q.

Q: How can the validators check that the encrypted values satisfy bal v?

A: Check that v ∈ [0, 264) and bal - v ∈ [0, 2128) or, otherwise see what happens 👇

48

Attack 1 (works when you only check v ∈ [0, 264)):

  • Set v ← (bal + 1) transferred amount > the balance! 😱
  • Validators "subtract" and get encryption of:

(bal - v) mod q =

(bal - (bal + 1)) mod q =

(-1) mod q =

(q - 1) mod q ∉ [0, 2128)

new balance > the old balance! 😱

Attack 2 (works when you only check bal - v ∈ [0, 2128)):

  • Set v ← (q - c), for any c ≥ 1 s.t. v ∉ [0, 264)

transferred amount likely > the balance! 😱

  • Validators "subtract" and get encryption of:

(bal - v) mod q =

(bal - (q - c)) mod q =

(bal + c) mod q

new balance > the old balance! 😱

49 of 52

In general: k-bit ± n-bit integer arithmetic mod prime q

Recall that when we "subtract" two encryptions of bal ∈ [0, 2n) and v ∈ [0, 2k), we get an encryption of (bal - v) mod q.

Q: How can the validators check that the encrypted values satisfy bal v?

A: Check that v ∈ [0, 2k) and bal - v ∈ [0, 2n), assuming that 2n + 2k q,

49

Attack 1 (works when you only check v ∈ [0, 2k)):

  • Set v ← (bal + 1) transferred amount > the balance! 😱
  • Validators "subtract" and get encryption of:

(bal - v) mod q =

(bal - (bal + 1)) mod q =

(-1) mod q =

(q - 1) mod q ∉ [0, 2n)

new balance > the old balance! 😱

Attack 2 (works when you only check bal - v ∈ [0, 2n)):

  • Set v ← (q - c), for any c ≥ 1 s.t. v ∉ [0, 2k)

transferred amount likely > the balance! 😱

  • Validators "subtract" and get encryption of:

(bal - v) mod q =

(bal - (q - c)) mod q =

(bal + c) mod q

new balance > the old balance! 😱

i.e., as long as bal' + v does not "overflow" in the field ℤq = {0, 1, …, q-1}; o/w, receiver's balance will wrap around.

50 of 52

Part 1: k-bit ± n-bit integer arithmetic modulo a prime q

Goal: TXN seeks to prove that the encrypted integers v, balS, balR ∈ [0, q) satisfy:

  1. v balS (i.e., sender has enough balance)
  2. balR + v < q (i.e., sender does not overflow recipient's balance)

Assumption 1: ElGamal ciphertext encrypts numbers in {0, 1, …, q-1} = ℤq

Assumption 2: We maintain an encrypted balance invariant (EBI) that balS, balR ∈ [0, 2n)

Assumption 3: TXN includes ZKPs for:

  1. v ∈ [0, 2k)
  2. balS - v mod q ∈ [0, 2n)

50

mod q, because that's the only thing range proofs guarantee.

51 of 52

Part 2: k-bit ± n-bit integer arithmetic modulo a prime q

Observation 1: If balR ∈ [0, 2n) (from EBI) and v ∈ [0, 2k) (from TXN ZKP) hold, then balR + v < 2n + 2k.

So, if we assume 2n + 2k q, then balR + v < q also holds!

Theorem 1: balR ∈ [0, 2n) & v ∈ [0, 2k) & 2n + 2k qbalR + v < q

51

Proof is sketched above ☝

52 of 52

Part 3: k-bit ± n-bit integer arithmetic modulo a prime q

Theorem 2: If k, n, q ∈ ℕ* and integers balS, balR, v satisfy:

  1. balS ∈ [0, 2n) (from EBI)
  2. v ∈ [0, 2k) (from TXN ZKP)
  3. balS - v mod q ∈ [0, 2n) (from TXN ZKP)
  4. 2n + 2k q (from Theorem 1)

Then, v balS (as integers, no mod q anymore!) and, of course, balR + v < q (restating Theorem 1)

Proof: Suppose v > balS. Let δ = balS - v. Then δ < 0. We know that balS - v mod q ∈ [0, 2n) ⇔ 0 ≤ δ mod q < 2n ⇔ ∃q s.t. 0 ≤ δ + q · q < 2n. Note that if q = 0 ⇒ δ ≥ 0 ⇒ contradiction. So, q ≠ 0. Next, suppose q 2.

Lemma 1: Since δ = balS - v, and (1) + (2) hold ⇒ δ ∈ (-2k, 0) b.c. min balS = 0 and max v = 2k - 1δ -(2k-1).

Applying Lemma 1 and (4), we can lower bound: δ + q · q -(2k-1) + 2(2n + 2k) = 2n+1 + 2k + 1. But we know from before that δ + q · q < 2n. And 2n+1 + 2k + 1 exceeds that. Contradiction. So, q can only be 1.�

Finally, since q = 1, we know δ + q · q < 2n δ + q < 2n. But q ≥ 2n + 2k and δ -(2k-1) ⇒ δ + q 2n + 2k - (2k-1) = 2n + 1. This is a contradiction. Original, v > balS assumption must be false ⇒ v balS. QED.

52