Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cbf04a2
add README and TODO
alinush Jan 23, 2026
80a92ec
fix decryption time benchmarks
alinush Jan 28, 2026
238fc53
add BSGS TS benches
alinush Jan 29, 2026
a6f89bb
add unified WASM
alinush Feb 3, 2026
b5ce910
breaking changes to on-chain structs, rewrite key rotation proof, gen…
alinush Feb 14, 2026
464e153
remove legacy sigma protocol code
alinush Feb 26, 2026
1c5b537
rename rotate_encryption_key* funcs
alinush Feb 26, 2026
21b2565
keep up with Move refactoring of entry function interface and proof s…
alinush Feb 27, 2026
c9cb762
add global auditor
alinush Feb 27, 2026
f99f8f0
do not start localnet automatically for confidential assets (TODO: fo…
alinush Mar 2, 2026
7bb8f6b
add functions to fetch the auditor epoch
alinush Mar 2, 2026
76de85c
fix transfer e2e tests
alinush Mar 3, 2026
a0c29b7
simplify normalization
alinush Mar 4, 2026
d126610
rename variable for all auditor amount ciphertexts
alinush Mar 4, 2026
245204d
add contract address to Fiat-Shamir DST and remove unnecessary tests …
alinush Mar 4, 2026
39a542d
add chain ID as domain separator
alinush Mar 4, 2026
7ab21cc
update SHA2-512 hashing in FS transform
alinush Mar 5, 2026
b2ca636
include proof type name in FS transcript
alinush Mar 5, 2026
d2f28c6
rename 'extra auditors' to 'voluntary auditors'
alinush Mar 5, 2026
a1ee60d
oops, fixed SDK DST issue and update README
alinush Mar 6, 2026
15e01f4
update SDK to handle new EffectiveAuditorConfig (untested though)
alinush Mar 18, 2026
04e45a4
add corresponding getEffectiveAuditorHint function
alinush Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions confidential-assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Confidential Assets SDK

## WASM Dependencies

This package uses a unified WebAssembly module for cryptographic operations:
- **Discrete log solver**: TBSGS-k32 algorithm for decryption (~512 KiB table)
- **Range proofs**: Bulletproofs for range proof generation/verification

### How WASM Loading Works

The WASM binary is **not bundled** with the SDK. Instead, it is loaded dynamically at runtime when needed. This is intentional:

**Why not bundle the WASM?**
- The `.wasm` binary is large (~774 KiB for the unified module)
- Bundling would bloat every app using the SDK, even if they never use confidential assets
- WASM binaries don't tree-shake - you'd pay the full size cost even if the feature is unused

**What the npm dependency provides:**
- TypeScript type definitions
- JavaScript glue code (thin wrappers that call into WASM)
- These are small and get bundled normally with the SDK

**What gets loaded at runtime:**
- The actual `.wasm` binary file
- Loaded via `fetch()` + `WebAssembly.instantiate()` only when `initializeWasm()`, `initializeSolver()`, or range proof functions are called
- **Single initialization**: Both discrete log and range proof functionality share the same WASM module, so it only needs to be loaded once

**Environment-specific loading:**

In **browser environments**, WASM is fetched from unpkg.com CDN.

In **Node.js environments** (e.g., running tests), the code automatically detects Node.js and loads WASM from local `node_modules`. This avoids network requests and ensures tests work offline.

### WASM Initialization

The SDK provides unified WASM initialization:

```typescript
import { initializeWasm, isWasmInitialized } from "@aptos-labs/confidential-assets";

// Initialize once - shared between discrete log and range proofs
await initializeWasm();

// Check if initialized
if (isWasmInitialized()) {
// Both discrete log and range proof functions are ready
}
```

For convenience, the SDK auto-initializes when you call any function that needs WASM. Manual initialization is only needed if you want to control when the WASM download happens.

### Setting Up Local WASM for Development

If you want to use locally-built WASM bindings (e.g., for development or testing changes):

1. Clone and build the WASM bindings:
```bash
cd ~/repos
git clone https://github.com/aptos-labs/confidential-asset-wasm-bindings
cd confidential-asset-wasm-bindings
./scripts/build-all.sh
```

2. Update `package.json` to use the local path:
```json
"@aptos-labs/confidential-asset-wasm-bindings": "file:../../confidential-asset-wasm-bindings/aptos-confidential-asset-wasm-bindings"
```

3. Install dependencies:
```bash
# Use --force if you've made some local changes to the DL algorithm; otherwise the version remains the same and this does nothing
pnpm install
```

Now tests will use your locally-built WASM.

---

# Testing

To test against a modified `aptos-core` repo:

First, run a local node from your modified `aptos-core` branch:
```
ulimit -n unlimited
cargo run -p aptos -- node run-localnet --with-indexer-api --assume-yes --force-restart
```

Second, run the SDK test of your choosing; e.g.:
```
pnpm test tests/e2e/confidentialAsset.test.ts

pnpm test tests/e2e/

pnpm test decryption

pnpm test tests/e2e/confidentialAsset.test.ts -t "rotate Alice" --runInBand
```

Or, run all tests:
```
pnpm test
```

## Useful tests to know about

### Discrete log / decryption benchmarks

```bash
pnpm test tests/units/discrete-log.test.ts
```

### Range proof tests

```bash
pnpm test tests/units/confidentialProofs.test.ts
```
2 changes: 1 addition & 1 deletion confidential-assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@aptos-labs/ts-sdk": "^5.2.1 || ^6.1.0"
},
"dependencies": {
"@aptos-labs/confidential-asset-wasm-bindings": "^0.0.2",
"@aptos-labs/confidential-asset-wasm-bindings": "^0.0.3",
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0"
},
Expand Down
10 changes: 5 additions & 5 deletions confidential-assets/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 64 additions & 34 deletions confidential-assets/src/api/confidentialAsset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import {
Account,
AccountAddress,
AccountAddressInput,
AnyNumber,
AptosConfig,
Expand All @@ -17,13 +18,15 @@ import {
ConfidentialAssetTransactionBuilder,
ConfidentialBalance,
getBalance,
getEffectiveAuditorHint,
getEncryptionKey,
hasUserRegistered,
isBalanceNormalized,
isPendingBalanceFrozen,
isIncomingTransfersPaused,
} from "../internal";

// Constants
import { DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS, MODULE_NAME } from "../consts";
import { DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS } from "../consts";

// Base param types
type ConfidentialAssetSubmissionParams = {
Expand Down Expand Up @@ -54,7 +57,7 @@ type TransferParams = WithdrawParams & {

type RolloverParams = ConfidentialAssetSubmissionParams & {
senderDecryptionKey?: TwistedEd25519PrivateKey;
withFreezeBalance?: boolean;
withPauseIncoming?: boolean;
};

type RotateKeyParams = ConfidentialAssetSubmissionParams & {
Expand Down Expand Up @@ -206,7 +209,7 @@ export class ConfidentialAsset {
*
* @param args.signer - The address of the sender of the transaction
* @param args.tokenAddress - The token address of the asset to roll over
* @param args.withFreezeBalance - Whether to freeze the balance after rolling over. Default is false.
* @param args.withPauseIncoming - Whether to pause incoming transfers after rolling over. Default is false.
* @param args.checkNormalized - Whether to check if the balance is normalized before rolling over. Default is true.
* @param args.withFeePayer - Whether to use the fee payer for the transaction
* @returns A SimpleTransaction to roll over the balance
Expand Down Expand Up @@ -256,17 +259,7 @@ export class ConfidentialAsset {
tokenAddress: AccountAddressInput;
options?: LedgerVersionArg;
}): Promise<TwistedEd25519PublicKey | undefined> {
const [{ vec: globalAuditorPubKey }] = await this.client().view<[{ vec: Uint8Array }]>({
options: args.options,
payload: {
function: `${this.moduleAddress()}::${MODULE_NAME}::get_auditor`,
functionArguments: [args.tokenAddress],
},
});
if (globalAuditorPubKey.length === 0) {
return undefined;
}
return new TwistedEd25519PublicKey(globalAuditorPubKey);
return this.transaction.getAssetAuditorEncryptionKey(args);
}

/**
Expand Down Expand Up @@ -334,24 +327,24 @@ export class ConfidentialAsset {
}

/**
* Check if a user's balance is frozen.
* Check if a user's incoming transfers are paused.
*
* A user's balance would likely be frozen if they plan to rotate their encryption key after a rollover. Rotating the encryption key requires
* the pending balance to be empty so a user may want to freeze their balance to prevent others from transferring into their pending balance
* A user's incoming transfers would likely be paused if they plan to rotate their encryption key after a rollover. Rotating the encryption key requires
* the pending balance to be empty so a user may want to pause incoming transfers to prevent others from transferring into their pending balance
* which would interfere with the rotation, as it would require a user to rollover their pending balance.
*
* @param args.accountAddress - The account address to check
* @param args.tokenAddress - The token address of the asset to check
* @param args.options.ledgerVersion - The ledger version to use for the view call
* @returns A boolean indicating if the user's balance is frozen
* @returns A boolean indicating if the user's incoming transfers are paused
* @throws {AptosApiError} If the there is no registered confidential balance for token address on the account
*/
async isPendingBalanceFrozen(args: {
async isIncomingTransfersPaused(args: {
accountAddress: AccountAddressInput;
tokenAddress: AccountAddressInput;
options?: LedgerVersionArg;
}): Promise<boolean> {
return isPendingBalanceFrozen({
return isIncomingTransfersPaused({
client: this.client(),
moduleAddress: this.moduleAddress(),
...args,
Expand All @@ -361,8 +354,8 @@ export class ConfidentialAsset {
/**
* Rotate the encryption key for a confidential asset balance.
*
* This will check if the pending balance is empty and roll it over if needed. It also checks if the balance
* is frozen and will unfreeze it if necessary.
* This will check if the pending balance is empty and roll it over if needed. It also checks if incoming
* transfers are paused and will unpause them if necessary.
*
* @param args.signer - The account that will sign the transaction
* @param args.senderDecryptionKey - The current decryption key
Expand All @@ -389,10 +382,17 @@ export class ConfidentialAsset {
tokenAddress,
decryptionKey: senderDecryptionKey,
});
if (balance.pendingBalance() > 0n) {

// The on-chain rotate_encryption_key_raw requires incoming transfers to be paused.
// If pending > 0, rollover + pause handles both. If pending == 0, we still need to pause.
const isPaused = await this.isIncomingTransfersPaused({
accountAddress: signer.accountAddress,
tokenAddress,
});
if (balance.pendingBalance() > 0n || !isPaused) {
const rolloverTxs = await this.rolloverPendingBalance({
...args,
withFreezeBalance: true,
withPauseIncoming: true,
});
results.push(...rolloverTxs);
}
Expand Down Expand Up @@ -428,16 +428,11 @@ export class ConfidentialAsset {
tokenAddress: AccountAddressInput;
options?: LedgerVersionArg;
}): Promise<boolean> {
const [isRegistered] = await this.client().view<[boolean]>({
payload: {
function: `${this.moduleAddress()}::${MODULE_NAME}::has_confidential_asset_store`,
typeArguments: [],
functionArguments: [args.accountAddress, args.tokenAddress],
},
options: args.options,
return hasUserRegistered({
client: this.client(),
moduleAddress: this.moduleAddress(),
...args,
});

return isRegistered;
}

/**
Expand Down Expand Up @@ -484,6 +479,27 @@ export class ConfidentialAsset {
});
}

/**
* Get the effective auditor hint for a user's confidential store.
* Indicates which auditor (global vs asset-specific) and epoch the balance ciphertext is encrypted for.
*
* @param args.accountAddress - The account address to query
* @param args.tokenAddress - The token address of the asset
* @param args.options - Optional ledger version for the view call
* @returns The auditor hint, or undefined if no auditor hint is set
*/
async getEffectiveAuditorHint(args: {
accountAddress: AccountAddressInput;
tokenAddress: AccountAddressInput;
options?: LedgerVersionArg;
}): Promise<{ isGlobal: boolean; epoch: bigint } | undefined> {
return getEffectiveAuditorHint({
client: this.client(),
moduleAddress: this.moduleAddress(),
...args,
});
}

/**
* Normalize a user's balance.
*
Expand All @@ -506,9 +522,23 @@ export class ConfidentialAsset {
useCachedValue: true,
});

// Resolve addresses to 32-byte arrays
const senderAddr = AccountAddress.from(signer.accountAddress);
const tokenAddr = AccountAddress.from(tokenAddress);

// Get chain ID for domain separation
const chainId = await this.transaction.getChainId();

// Get the auditor public key for the token
const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress });

const confidentialNormalization = await ConfidentialNormalization.create({
decryptionKey: senderDecryptionKey,
unnormalizedAvailableBalance: available,
senderAddress: senderAddr.toUint8Array(),
tokenAddress: tokenAddr.toUint8Array(),
chainId,
auditorEncryptionKey: effectiveAuditorPubKey,
});

const transaction = await confidentialNormalization.createTransaction({
Expand Down
8 changes: 0 additions & 8 deletions confidential-assets/src/consts.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
export const PROOF_CHUNK_SIZE = 32; // bytes

export const SIGMA_PROOF_WITHDRAW_SIZE = PROOF_CHUNK_SIZE * 21; // bytes

export const SIGMA_PROOF_TRANSFER_SIZE = PROOF_CHUNK_SIZE * 33; // bytes

export const SIGMA_PROOF_KEY_ROTATION_SIZE = PROOF_CHUNK_SIZE * 23; // bytes

export const SIGMA_PROOF_NORMALIZATION_SIZE = PROOF_CHUNK_SIZE * 21; // bytes

/** For now we only deploy to devnet as part of aptos-experimental, which lives at 0x7. */
export const DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS = "0x7";
export const MODULE_NAME = "confidential_asset";
Loading
Loading