diff --git a/confidential-assets/README.md b/confidential-assets/README.md new file mode 100644 index 000000000..42646df92 --- /dev/null +++ b/confidential-assets/README.md @@ -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 +``` diff --git a/confidential-assets/package.json b/confidential-assets/package.json index d5513a311..c789112b3 100644 --- a/confidential-assets/package.json +++ b/confidential-assets/package.json @@ -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" }, diff --git a/confidential-assets/pnpm-lock.yaml b/confidential-assets/pnpm-lock.yaml index 7fec32d90..c798119ae 100644 --- a/confidential-assets/pnpm-lock.yaml +++ b/confidential-assets/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: .: dependencies: '@aptos-labs/confidential-asset-wasm-bindings': - specifier: ^0.0.2 - version: 0.0.2 + specifier: ^0.0.3 + version: 0.0.3 '@aptos-labs/ts-sdk': specifier: ^5.2.1 || ^6.1.0 version: 6.1.0(got@13.0.0) @@ -70,8 +70,8 @@ packages: peerDependencies: got: ^11.8.6 - '@aptos-labs/confidential-asset-wasm-bindings@0.0.2': - resolution: {integrity: sha512-xfgRVc4WX4N7hjSHP91a3o686gC4307PGa9eDT7eWhp2VN9YgYhDIKRFRITpKT5JC7IXbuh6pGq1MqrqTjBokA==} + '@aptos-labs/confidential-asset-wasm-bindings@0.0.3': + resolution: {integrity: sha512-vU1C+IHl3gZbVZhJodyyr1sJt5+hUdUn5H2qGVFxmUWgW8z3+VUN12wBWmgQ9swIQwioThxssB05l3AywEtiCQ==} '@aptos-labs/ts-sdk@6.1.0': resolution: {integrity: sha512-YjtEXivGj0xuf3eKzrnKOfjZcYIWUR1E2zNJ4JempgMgoM+VVPbp7b1Ubu24UcOYxRYLb8/kKavOUg62o8zElw==} @@ -1634,7 +1634,7 @@ snapshots: dependencies: got: 13.0.0 - '@aptos-labs/confidential-asset-wasm-bindings@0.0.2': {} + '@aptos-labs/confidential-asset-wasm-bindings@0.0.3': {} '@aptos-labs/ts-sdk@6.1.0(got@13.0.0)': dependencies: diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index 519e8c538..e308159e1 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -3,6 +3,7 @@ import { Account, + AccountAddress, AccountAddressInput, AnyNumber, AptosConfig, @@ -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 = { @@ -54,7 +57,7 @@ type TransferParams = WithdrawParams & { type RolloverParams = ConfidentialAssetSubmissionParams & { senderDecryptionKey?: TwistedEd25519PrivateKey; - withFreezeBalance?: boolean; + withPauseIncoming?: boolean; }; type RotateKeyParams = ConfidentialAssetSubmissionParams & { @@ -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 @@ -256,17 +259,7 @@ export class ConfidentialAsset { tokenAddress: AccountAddressInput; options?: LedgerVersionArg; }): Promise { - 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); } /** @@ -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 { - return isPendingBalanceFrozen({ + return isIncomingTransfersPaused({ client: this.client(), moduleAddress: this.moduleAddress(), ...args, @@ -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 @@ -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); } @@ -428,16 +428,11 @@ export class ConfidentialAsset { tokenAddress: AccountAddressInput; options?: LedgerVersionArg; }): Promise { - 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; } /** @@ -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. * @@ -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({ diff --git a/confidential-assets/src/consts.ts b/confidential-assets/src/consts.ts index 0076fb33e..b0a010839 100644 --- a/confidential-assets/src/consts.ts +++ b/confidential-assets/src/consts.ts @@ -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"; diff --git a/confidential-assets/src/crypto/bsgs.ts b/confidential-assets/src/crypto/bsgs.ts new file mode 100644 index 000000000..f774282e3 --- /dev/null +++ b/confidential-assets/src/crypto/bsgs.ts @@ -0,0 +1,197 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Baby-Step Giant-Step (BSGS) algorithm for solving discrete logarithms. + * + * Given a point P = x * G, finds x in O(sqrt(n)) time using O(sqrt(n)) space, + * where n = 2^bitWidth is the search space size. + */ + +import { RistrettoPoint } from "@noble/curves/ed25519"; +import { RistPoint } from "./twistedEd25519"; + +/** + * BSGS table for a specific bit width. + */ +export interface BsgsTable { + /** The bit width this table was created for */ + bitWidth: number; + /** m = 2^(bitWidth/2), the number of baby steps */ + m: bigint; + /** The baby-step table: maps point (as bigint) to index j */ + babySteps: Map; + /** Precomputed -m * G for giant steps */ + giantStep: RistPoint; +} + +/** + * Convert bytes to a BigInt key for fast Map lookups. + * Uses little-endian interpretation of the full 32 bytes. + */ +function bytesToBigInt(bytes: Uint8Array): bigint { + let result = 0n; + for (let i = bytes.length - 1; i >= 0; i--) { + result = (result << 8n) | BigInt(bytes[i]); + } + return result; +} + +/** + * Creates a BSGS table for solving DLPs up to the given bit width. + * + * Time complexity: O(2^(bitWidth/2)) + * Space complexity: O(2^(bitWidth/2)) + * + * @param bitWidth - Maximum bit width of discrete logs to solve (must be even) + * @returns The BSGS table + */ +export function createBsgsTable(bitWidth: number): BsgsTable { + if (bitWidth <= 0) { + throw new Error("bitWidth must be positive"); + } + if (bitWidth % 2 !== 0) { + throw new Error("bitWidth must be even for BSGS"); + } + + const m = 1n << BigInt(bitWidth / 2); + const babySteps = new Map(); + + // Baby steps: compute j * G for j = 0, 1, ..., m-1 using only additions + let current = RistrettoPoint.ZERO; + const G = RistrettoPoint.BASE; + + for (let j = 0n; j < m; j++) { + const key = bytesToBigInt(current.toRawBytes()); + babySteps.set(key, j); + current = current.add(G); + } + + // After the loop, current = m * G (no need to recompute with multiply) + const giantStep = current.negate(); + + return { + bitWidth, + m, + babySteps, + giantStep, + }; +} + +/** + * Solves the discrete log problem using BSGS. + * + * Given P = x * G, finds x where 0 <= x < 2^bitWidth. + * + * Time complexity: O(2^(bitWidth/2)) + * + * @param point - The point P = x * G (as Uint8Array serialization) + * @param table - The precomputed BSGS table + * @returns The discrete log x, or null if not found + */ +export function solveDlpBsgs(point: Uint8Array, table: BsgsTable): bigint | null { + // Handle zero point (identity) - check if in baby steps first + const pointKey = bytesToBigInt(point); + if (table.babySteps.has(pointKey)) { + return table.babySteps.get(pointKey)!; + } + + const P = RistrettoPoint.fromHex(point); + const { m, babySteps, giantStep } = table; + + // Giant steps: for i = 1, 2, ..., m-1 + // Compute gamma = P + i * giantStep = P - i * m * G + // If gamma is in babySteps at index j, then x = i * m + j + let gamma = P.add(giantStep); // Start with i = 1 + + for (let i = 1n; i < m; i++) { + const gammaKey = bytesToBigInt(gamma.toBytes()); + + if (babySteps.has(gammaKey)) { + const j = babySteps.get(gammaKey)!; + return i * m + j; + } + + gamma = gamma.add(giantStep); + } + + // Not found in search space + return null; +} + +/** + * BSGS solver class that manages multiple tables for different bit widths. + */ +export class BsgsSolver { + private tables: Map = new Map(); + private initPromise: Promise | undefined; + + /** + * Initialize tables for the specified bit widths. + * @param bitWidths - Array of bit widths to precompute tables for (must be even) + */ + async initialize(bitWidths: number[]): Promise { + if (this.initPromise) { + return this.initPromise; + } + + this.initPromise = (async () => { + for (const bitWidth of bitWidths) { + if (!this.tables.has(bitWidth)) { + //console.log(`BSGS: Creating table for ${bitWidth}-bit DLPs...`); + const startTime = performance.now(); + const table = createBsgsTable(bitWidth); + const elapsed = performance.now() - startTime; + //console.log(`BSGS: ${bitWidth}-bit table created in ${elapsed.toFixed(2)}ms (${table.babySteps.size} entries)`); + this.tables.set(bitWidth, table); + } + } + })(); + + return this.initPromise; + } + + /** + * Solve DLP using the appropriate table. + * Tries tables from smallest to largest bit width. + * + * @param point - The point P = x * G (as Uint8Array) + * @returns The discrete log x + * @throws If no solution found in any table + */ + solve(point: Uint8Array): bigint { + // Sort tables by bit width (smallest first for efficiency) + const sortedTables = [...this.tables.entries()].sort((a, b) => a[0] - b[0]); + + for (const [bitWidth, table] of sortedTables) { + const result = solveDlpBsgs(point, table); + if (result !== null) { + return result; + } + } + + throw new Error("BSGS: No solution found in search space"); + } + + /** + * Check if a table exists for the given bit width. + */ + hasTable(bitWidth: number): boolean { + return this.tables.has(bitWidth); + } + + /** + * Get the table for a specific bit width. + */ + getTable(bitWidth: number): BsgsTable | undefined { + return this.tables.get(bitWidth); + } + + /** + * Clear all tables to free memory. + */ + clear(): void { + this.tables.clear(); + this.initPromise = undefined; + } +} diff --git a/confidential-assets/src/crypto/confidentialKeyRotation.ts b/confidential-assets/src/crypto/confidentialKeyRotation.ts index cd4671886..1bfbe847a 100644 --- a/confidential-assets/src/crypto/confidentialKeyRotation.ts +++ b/confidential-assets/src/crypto/confidentialKeyRotation.ts @@ -1,343 +1,308 @@ -import { bytesToNumberLE, concatBytes, numberToBytesLE } from "@noble/curves/abstract/utils"; +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Key rotation proof using the new generic Sigma protocol framework. + * + * The NP relation (from `sigma_protocol_key_rotation.move`): + * + * H = dk * ek + * new_ek = delta * ek + * ek = delta_inv * new_ek + * new_D_i = delta * old_D_i, for all i in [num_chunks] + * + * where: + * - H is the encryption key basepoint (= hash_to_point_base) + * - ek is the old encryption key + * - new_ek is the new encryption key + * - dk is the old decryption key + * - delta = old_dk * new_dk^{-1} (since ek = dk^{-1} * H) + * - delta_inv = new_dk * old_dk^{-1} + * + * The homomorphism psi(dk, delta, delta_inv) outputs: + * [dk * ek, delta * ek, delta_inv * new_ek, delta * old_D_i for each i] + * + * The transformation function f outputs: + * [H, new_ek, ek, new_D_i for each i] + */ + +import { bytesToNumberLE } from "@noble/curves/abstract/utils"; import { utf8ToBytes } from "@noble/hashes/utils"; -import { PROOF_CHUNK_SIZE, SIGMA_PROOF_KEY_ROTATION_SIZE } from "../consts"; -import { genFiatShamirChallenge } from "../helpers"; -import { RangeProofExecutor } from "./rangeProof"; -import { TwistedEd25519PrivateKey, RistrettoPoint, H_RISTRETTO, TwistedEd25519PublicKey } from "."; -import { TwistedElGamalCiphertext } from "./twistedElGamal"; -import { ed25519GenListOfRandom, ed25519GenRandom, ed25519modN, ed25519InvertN } from "../utils"; +import { TwistedEd25519PrivateKey, TwistedEd25519PublicKey, RistrettoPoint, H_RISTRETTO } from "."; +import type { RistPoint } from "."; +import { ed25519InvertN, ed25519modN } from "../utils"; import { EncryptedAmount } from "./encryptedAmount"; -import { AVAILABLE_BALANCE_CHUNK_COUNT, CHUNK_BITS, CHUNK_BITS_BIG_INT } from "./chunkedAmount"; - -export type ConfidentialKeyRotationSigmaProof = { - alpha1List: Uint8Array[]; - alpha2: Uint8Array; - alpha3: Uint8Array; - alpha4: Uint8Array; - alpha5List: Uint8Array[]; - X1: Uint8Array; - X2: Uint8Array; - X3: Uint8Array; - X4List: Uint8Array[]; - X5List: Uint8Array[]; -}; +import type { TwistedElGamalCiphertext } from "./twistedElGamal"; +import { AVAILABLE_BALANCE_CHUNK_COUNT } from "./chunkedAmount"; +import { + sigmaProtocolProve, + sigmaProtocolVerify, + bcsSerializeKeyRotationSession, + APTOS_EXPERIMENTAL_ADDRESS, + type DomainSeparator, + type SigmaProtocolStatement, + type SigmaProtocolProof, + type PsiFunction, + type TransformationFunction, +} from "./sigmaProtocol"; + +/** Protocol ID matching the Move constant */ +const PROTOCOL_ID = "AptosConfidentialAsset/KeyRotationV1"; + +/** Fully-qualified Move type name for the phantom marker type, matching `type_info::type_name()` */ +const TYPE_NAME = "0x7::sigma_protocol_key_rotation::KeyRotation"; + +/** Statement point indices (matching Move constants) */ +const IDX_H = 0; +const IDX_EK = 1; +const IDX_EK_NEW = 2; +const START_IDX_OLD_D = 3; + +/** Helper to get the starting index for new_D values */ +function getStartIdxForNewD(numChunks: number): number { + return START_IDX_OLD_D + numChunks; +} + +/** + * Build the homomorphism psi for key rotation. + * + * psi(dk, delta, delta_inv) = [dk*ek, delta*ek, delta_inv*new_ek, delta*old_D_i for each i] + */ +function makeKeyRotationPsi(numChunks: number): PsiFunction { + return (s: SigmaProtocolStatement, w: bigint[]): RistPoint[] => { + const dk_w = w[0]; + const delta_w = w[1]; + const deltaInv_w = w[2]; + + const ek = s.points[IDX_EK]; + const new_ek = s.points[IDX_EK_NEW]; + + const result: RistPoint[] = [ + ek.multiply(dk_w), // dk * ek + ek.multiply(delta_w), // delta * ek + new_ek.multiply(deltaInv_w), // delta_inv * new_ek + ]; + + for (let i = 0; i < numChunks; i++) { + result.push(s.points[START_IDX_OLD_D + i].multiply(delta_w)); + } + + return result; + }; +} + +/** + * Build the transformation function f for key rotation. + * + * f(stmt) = [H, new_ek, ek, new_D_i for each i] + */ +function makeKeyRotationF(numChunks: number): TransformationFunction { + return (s: SigmaProtocolStatement): RistPoint[] => { + const idxNewDStart = getStartIdxForNewD(numChunks); + + const result: RistPoint[] = [ + s.points[IDX_H], // H + s.points[IDX_EK_NEW], // new_ek + s.points[IDX_EK], // ek + ]; + + for (let i = 0; i < numChunks; i++) { + result.push(s.points[idxNewDStart + i]); + } + + return result; + }; +} export type CreateConfidentialKeyRotationOpArgs = { senderDecryptionKey: TwistedEd25519PrivateKey; newSenderDecryptionKey: TwistedEd25519PrivateKey; currentEncryptedAvailableBalance: EncryptedAmount; - randomness?: bigint[]; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte token/metadata object address */ + tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; +}; + +export type KeyRotationProof = { + /** New encryption key (32 bytes, compressed Ristretto point) */ + newEkBytes: Uint8Array; + /** Re-encrypted D components (one 32-byte array per chunk) */ + newDBytes: Uint8Array[]; + /** Sigma protocol proof */ + proof: SigmaProtocolProof; }; export class ConfidentialKeyRotation { - randomness: bigint[]; + private currentDecryptionKey: TwistedEd25519PrivateKey; - currentDecryptionKey: TwistedEd25519PrivateKey; + private newDecryptionKey: TwistedEd25519PrivateKey; - newDecryptionKey: TwistedEd25519PrivateKey; + private currentEncryptedAvailableBalance: EncryptedAmount; - currentEncryptedAvailableBalance: EncryptedAmount; + private senderAddress: Uint8Array; + + private tokenAddress: Uint8Array; - newEncryptedAvailableBalance: EncryptedAmount; + private chainId: number; constructor(args: { - randomness: bigint[]; currentDecryptionKey: TwistedEd25519PrivateKey; newDecryptionKey: TwistedEd25519PrivateKey; currentEncryptedAvailableBalance: EncryptedAmount; - newEncryptedAvailableBalance: EncryptedAmount; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; }) { - this.randomness = args.randomness; this.currentDecryptionKey = args.currentDecryptionKey; this.newDecryptionKey = args.newDecryptionKey; this.currentEncryptedAvailableBalance = args.currentEncryptedAvailableBalance; - this.newEncryptedAvailableBalance = args.newEncryptedAvailableBalance; + this.senderAddress = args.senderAddress; + this.tokenAddress = args.tokenAddress; + this.chainId = args.chainId; } - static FIAT_SHAMIR_SIGMA_DST = "AptosConfidentialAsset/RotationProofFiatShamir"; - - static async create(args: CreateConfidentialKeyRotationOpArgs) { - const { - randomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), - currentEncryptedAvailableBalance, - senderDecryptionKey, - newSenderDecryptionKey, - } = args; - - const newEncryptedAvailableBalance = EncryptedAmount.fromAmountAndPublicKey({ - amount: currentEncryptedAvailableBalance.getAmount(), - publicKey: newSenderDecryptionKey.publicKey(), - randomness, - }); - + static create(args: CreateConfidentialKeyRotationOpArgs): ConfidentialKeyRotation { return new ConfidentialKeyRotation({ - currentDecryptionKey: senderDecryptionKey, - newDecryptionKey: newSenderDecryptionKey, - currentEncryptedAvailableBalance, - newEncryptedAvailableBalance, - randomness, + currentDecryptionKey: args.senderDecryptionKey, + newDecryptionKey: args.newSenderDecryptionKey, + currentEncryptedAvailableBalance: args.currentEncryptedAvailableBalance, + senderAddress: args.senderAddress, + tokenAddress: args.tokenAddress, + chainId: args.chainId, }); } - static serializeSigmaProof(sigmaProof: ConfidentialKeyRotationSigmaProof): Uint8Array { - return concatBytes( - ...sigmaProof.alpha1List, - sigmaProof.alpha2, - sigmaProof.alpha3, - sigmaProof.alpha4, - ...sigmaProof.alpha5List, - sigmaProof.X1, - sigmaProof.X2, - sigmaProof.X3, - ...sigmaProof.X4List, - ...sigmaProof.X5List, - ); - } + /** + * Generate the key rotation proof and re-encrypted balance components. + * + * Returns everything needed to call the `rotate_encryption_key_raw` entry function. + */ + authorizeKeyRotation(): KeyRotationProof { + const numChunks = AVAILABLE_BALANCE_CHUNK_COUNT; + const oldDk = bytesToNumberLE(this.currentDecryptionKey.toUint8Array()); + const newDk = bytesToNumberLE(this.newDecryptionKey.toUint8Array()); + + // delta = old_dk * new_dk^{-1} (since ek = dk^{-1} * H, new_ek = delta * old_ek) + const newDkInv = ed25519InvertN(newDk); + const delta = ed25519modN(oldDk * newDkInv); + const deltaInv = ed25519InvertN(delta); + + // Get old encryption key (compressed Ristretto point) + const oldEkBytes = this.currentDecryptionKey.publicKey().toUint8Array(); + const oldEk = RistrettoPoint.fromHex(oldEkBytes); + + // H = encryption key basepoint (hash_to_point_base) + const H = H_RISTRETTO; + const compressedH = H.toRawBytes(); + + // new_ek = delta * old_ek + const newEk = oldEk.multiply(delta); + const compressedNewEk = newEk.toRawBytes(); + + // Get old D components from the current balance + const oldCipherTexts = this.currentEncryptedAvailableBalance.getCipherText(); + const oldD: RistPoint[] = oldCipherTexts.map((ct) => ct.D); + const compressedOldD: Uint8Array[] = oldD.map((d) => d.toRawBytes()); + + // Compute new_D = delta * old_D for each chunk + const newD: RistPoint[] = oldD.map((d) => d.multiply(delta)); + const compressedNewD: Uint8Array[] = newD.map((d) => d.toRawBytes()); + + // Build statement: points = [H, ek, new_ek, old_D_0..old_D_{n-1}, new_D_0..new_D_{n-1}] + const stmtPoints: RistPoint[] = [H, oldEk, newEk, ...oldD, ...newD]; + const stmtCompressedPoints: Uint8Array[] = [compressedH, oldEkBytes, compressedNewEk, ...compressedOldD, ...compressedNewD]; + + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressedPoints, + scalars: [], // key rotation has no public scalars + }; - static deserializeSigmaProof(sigmaProof: Uint8Array): ConfidentialKeyRotationSigmaProof { - if (sigmaProof.length !== SIGMA_PROOF_KEY_ROTATION_SIZE) { - throw new Error( - `Invalid sigma proof length of confidential key rotation: got ${sigmaProof.length}, expected ${SIGMA_PROOF_KEY_ROTATION_SIZE}`, - ); - } + // Build witness: [dk, delta, delta_inv] + const witness: bigint[] = [oldDk, delta, deltaInv]; - const proofArr: Uint8Array[] = []; - for (let i = 0; i < SIGMA_PROOF_KEY_ROTATION_SIZE; i += PROOF_CHUNK_SIZE) { - proofArr.push(sigmaProof.subarray(i, i + PROOF_CHUNK_SIZE)); - } + // Build domain separator + const sessionId = bcsSerializeKeyRotationSession(this.senderAddress, this.tokenAddress, numChunks); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId: this.chainId, + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; - const alpha1List = proofArr.slice(0, 3); - const alpha2 = proofArr[3]; - const alpha3 = proofArr[4]; - const alpha4 = proofArr[5]; - const alpha5List = proofArr.slice(6, 6 + AVAILABLE_BALANCE_CHUNK_COUNT); - const X1 = proofArr[6 + AVAILABLE_BALANCE_CHUNK_COUNT]; - const X2 = proofArr[7 + AVAILABLE_BALANCE_CHUNK_COUNT]; - const X3 = proofArr[8 + AVAILABLE_BALANCE_CHUNK_COUNT]; - const X4List = proofArr.slice(8 + AVAILABLE_BALANCE_CHUNK_COUNT, 8 + 2 * AVAILABLE_BALANCE_CHUNK_COUNT); - const X5List = proofArr.slice(8 + 2 * AVAILABLE_BALANCE_CHUNK_COUNT); + // Generate the proof + const proof: SigmaProtocolProof = sigmaProtocolProve(dst, TYPE_NAME, makeKeyRotationPsi(numChunks), stmt, witness); return { - alpha1List, - alpha2, - alpha3, - alpha4, - alpha5List, - X1, - X2, - X3, - X4List, - X5List, + newEkBytes: compressedNewEk, + newDBytes: compressedNewD, + proof, }; } - async genSigmaProof(): Promise { - if (this.randomness && this.randomness.length !== AVAILABLE_BALANCE_CHUNK_COUNT) { - throw new Error("Invalid length list of randomness"); + /** + * Verify a key rotation sigma protocol proof. + * + * @param args.oldEk - The old encryption key (32 bytes compressed) + * @param args.newEk - The new encryption key (32 bytes compressed) + * @param args.oldD - The old D components from the ciphertext (one per chunk) + * @param args.newD - The new D components after re-encryption (one per chunk) + * @param args.senderAddress - 32-byte sender address + * @param args.tokenAddress - 32-byte token/metadata address + * @param args.proof - The sigma protocol proof to verify + * @returns true if the proof verifies, false otherwise + */ + static verify(args: { + oldEk: Uint8Array; + newEk: Uint8Array; + oldD: Uint8Array[]; + newD: Uint8Array[]; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + proof: SigmaProtocolProof; + }): boolean { + const { oldEk, newEk, oldD, newD, senderAddress, tokenAddress, chainId, proof } = args; + const numChunks = oldD.length; + + if (newD.length !== numChunks) { + return false; } - const x1List = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); - const x2 = ed25519GenRandom(); - const x3 = ed25519GenRandom(); - const x4 = ed25519GenRandom(); - - const x5List = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - x1List.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const x1i = el * coef; - - return acc + x1i; - }, 0n), - ), - ).add( - this.currentEncryptedAvailableBalance - .getCipherText() - .reduce((acc, el, i) => acc.add(el.D.multiply(2n ** (BigInt(i) * CHUNK_BITS_BIG_INT))), RistrettoPoint.ZERO) - .multiply(x2), - ); - const X2 = H_RISTRETTO.multiply(x3); - const X3 = H_RISTRETTO.multiply(x4); - const X4List = x1List.map((el, index) => { - const x1iG = RistrettoPoint.BASE.multiply(el); - const x5iH = H_RISTRETTO.multiply(x5List[index]); - - return x1iG.add(x5iH); - }); - const X5List = x5List.map((el) => { - const Pnew = RistrettoPoint.fromHex(this.newEncryptedAvailableBalance.publicKey.toUint8Array()); - return Pnew.multiply(el); - }); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialKeyRotation.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - this.currentEncryptedAvailableBalance.publicKey.toUint8Array(), - this.newEncryptedAvailableBalance.publicKey.toUint8Array(), - this.currentEncryptedAvailableBalance.getCipherTextBytes(), - this.newEncryptedAvailableBalance.getCipherTextBytes(), - X1.toRawBytes(), - X2.toRawBytes(), - X3.toRawBytes(), - ...X4List.map((el) => el.toRawBytes()), - ...X5List.map((el) => el.toRawBytes()), - ); - - const oldSLE = bytesToNumberLE(this.currentDecryptionKey.toUint8Array()); - const invertOldSLE = ed25519InvertN(oldSLE); - const newSLE = bytesToNumberLE(this.newDecryptionKey.toUint8Array()); - const invertNewSLE = ed25519InvertN(newSLE); - - const alpha1List = x1List.map((el, i) => { - const pChunk = ed25519modN(p * this.currentEncryptedAvailableBalance.getAmountChunks()[i]); - - return ed25519modN(el - pChunk); - }); - const alpha2 = ed25519modN(x2 - p * oldSLE); - const alpha3 = ed25519modN(x3 - p * invertOldSLE); - const alpha4 = ed25519modN(x4 - p * invertNewSLE); - const alpha5List = x5List.map((el, i) => { - const pri = ed25519modN(p * this.randomness[i]); - - return ed25519modN(el - pri); - }); + // Build statement points + const H = H_RISTRETTO; + const ek = RistrettoPoint.fromHex(oldEk); + const new_ek = RistrettoPoint.fromHex(newEk); + const oldDPoints = oldD.map((d) => RistrettoPoint.fromHex(d)); + const newDPoints = newD.map((d) => RistrettoPoint.fromHex(d)); + + const stmtPoints: RistPoint[] = [H, ek, new_ek, ...oldDPoints, ...newDPoints]; + const stmtCompressedPoints: Uint8Array[] = [ + H.toRawBytes(), + oldEk, + newEk, + ...oldD, + ...newD, + ]; - return { - alpha1List: alpha1List.map((el) => numberToBytesLE(el, 32)), - alpha2: numberToBytesLE(alpha2, 32), - alpha3: numberToBytesLE(alpha3, 32), - alpha4: numberToBytesLE(alpha4, 32), - alpha5List: alpha5List.map((el) => numberToBytesLE(el, 32)), - X1: X1.toRawBytes(), - X2: X2.toRawBytes(), - X3: X3.toRawBytes(), - X4List: X4List.map((el) => el.toRawBytes()), - X5List: X5List.map((el) => el.toRawBytes()), + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressedPoints, + scalars: [], }; - } - static verifySigmaProof(opts: { - sigmaProof: ConfidentialKeyRotationSigmaProof; - currPublicKey: TwistedEd25519PublicKey; - newPublicKey: TwistedEd25519PublicKey; - currEncryptedBalance: TwistedElGamalCiphertext[]; - newEncryptedBalance: TwistedElGamalCiphertext[]; - }) { - const alpha1LEList = opts.sigmaProof.alpha1List.map(bytesToNumberLE); - const alpha2LE = bytesToNumberLE(opts.sigmaProof.alpha2); - const alpha3LE = bytesToNumberLE(opts.sigmaProof.alpha3); - const alpha4LE = bytesToNumberLE(opts.sigmaProof.alpha4); - const alpha5LEList = opts.sigmaProof.alpha5List.map(bytesToNumberLE); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialKeyRotation.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - opts.currPublicKey.toUint8Array(), - opts.newPublicKey.toUint8Array(), - ...opts.currEncryptedBalance.map((el) => el.serialize()).flat(), - ...opts.newEncryptedBalance.map((el) => el.serialize()).flat(), - opts.sigmaProof.X1, - opts.sigmaProof.X2, - opts.sigmaProof.X3, - ...opts.sigmaProof.X4List, - ...opts.sigmaProof.X5List, - ); - - const pkOldRist = RistrettoPoint.fromHex(opts.currPublicKey.toUint8Array()); - const pkNewRist = RistrettoPoint.fromHex(opts.newPublicKey.toUint8Array()); - - const { DOldSum, COldSum } = opts.currEncryptedBalance.reduce( - (acc, { C, D }, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - return { - DOldSum: acc.DOldSum.add(D.multiply(coef)), - COldSum: acc.COldSum.add(C.multiply(coef)), - }; - }, - { DOldSum: RistrettoPoint.ZERO, COldSum: RistrettoPoint.ZERO }, - ); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - alpha1LEList.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const a1i = el * coef; - - return acc + a1i; - }, 0n), - ), - ) - .add(DOldSum.multiply(alpha2LE)) - .add(COldSum.multiply(p)); - const X2 = H_RISTRETTO.multiply(alpha3LE).add(pkOldRist.multiply(p)); - const X3 = H_RISTRETTO.multiply(alpha4LE).add(pkNewRist.multiply(p)); - const X4List = alpha1LEList.map((el, i) => { - const a1iG = RistrettoPoint.BASE.multiply(el); - const a5iH = H_RISTRETTO.multiply(alpha5LEList[i]); - const pC = opts.newEncryptedBalance[i].C.multiply(p); - - return a1iG.add(a5iH).add(pC); - }); - const X5List = alpha5LEList.map((el, i) => { - const a5iPnew = pkNewRist.multiply(el); - const pDnew = opts.newEncryptedBalance[i].D.multiply(p); - return a5iPnew.add(pDnew); - }); - - return ( - X1.equals(RistrettoPoint.fromHex(opts.sigmaProof.X1)) && - X2.equals(RistrettoPoint.fromHex(opts.sigmaProof.X2)) && - X3.equals(RistrettoPoint.fromHex(opts.sigmaProof.X3)) && - X4List.every((X4, i) => X4.equals(RistrettoPoint.fromHex(opts.sigmaProof.X4List[i]))) && - X5List.every((X5, i) => X5.equals(RistrettoPoint.fromHex(opts.sigmaProof.X5List[i]))) - ); - } - - async genRangeProof(): Promise { - const rangeProof = await RangeProofExecutor.genBatchRangeZKP({ - v: this.currentEncryptedAvailableBalance.getAmountChunks(), - rs: this.randomness.map((chunk) => numberToBytesLE(chunk, 32)), - val_base: RistrettoPoint.BASE.toRawBytes(), - rand_base: H_RISTRETTO.toRawBytes(), - num_bits: CHUNK_BITS, - }); - - return rangeProof.proof; - } - - async authorizeKeyRotation(): Promise< - [ - { - sigmaProof: ConfidentialKeyRotationSigmaProof; - rangeProof: Uint8Array; - }, - EncryptedAmount, - ] - > { - const sigmaProof = await this.genSigmaProof(); - - const rangeProof = await this.genRangeProof(); - - return [ - { - sigmaProof, - rangeProof, - }, - this.newEncryptedAvailableBalance, - ]; - } + // Build domain separator + const sessionId = bcsSerializeKeyRotationSession(senderAddress, tokenAddress, numChunks); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; - static async verifyRangeProof(opts: { rangeProof: Uint8Array; newEncryptedBalance: TwistedElGamalCiphertext[] }) { - return RangeProofExecutor.verifyBatchRangeZKP({ - proof: opts.rangeProof, - comm: opts.newEncryptedBalance.map((el) => el.C.toRawBytes()), - val_base: RistrettoPoint.BASE.toRawBytes(), - rand_base: H_RISTRETTO.toRawBytes(), - num_bits: CHUNK_BITS, - }); + return sigmaProtocolVerify(dst, TYPE_NAME, makeKeyRotationPsi(numChunks), makeKeyRotationF(numChunks), stmt, proof); } } diff --git a/confidential-assets/src/crypto/confidentialNormalization.ts b/confidential-assets/src/crypto/confidentialNormalization.ts index 1123e3ebc..ae75218ff 100644 --- a/confidential-assets/src/crypto/confidentialNormalization.ts +++ b/confidential-assets/src/crypto/confidentialNormalization.ts @@ -1,29 +1,26 @@ import { RistrettoPoint } from "@noble/curves/ed25519"; -import { utf8ToBytes } from "@noble/hashes/utils"; -import { bytesToNumberLE, concatBytes, numberToBytesLE } from "@noble/curves/abstract/utils"; -import { MODULE_NAME, PROOF_CHUNK_SIZE, SIGMA_PROOF_NORMALIZATION_SIZE } from "../consts"; -import { genFiatShamirChallenge } from "../helpers"; +import { numberToBytesLE } from "@noble/curves/abstract/utils"; +import { MODULE_NAME } from "../consts"; import { RangeProofExecutor } from "./rangeProof"; import { TwistedEd25519PrivateKey, H_RISTRETTO, TwistedEd25519PublicKey } from "."; -import { ed25519GenListOfRandom, ed25519GenRandom, ed25519modN, ed25519InvertN } from "../utils"; +import { ed25519GenListOfRandom } from "../utils"; import { EncryptedAmount } from "./encryptedAmount"; -import { AVAILABLE_BALANCE_CHUNK_COUNT, CHUNK_BITS, CHUNK_BITS_BIG_INT, ChunkedAmount } from "./chunkedAmount"; +import { AVAILABLE_BALANCE_CHUNK_COUNT, CHUNK_BITS } from "./chunkedAmount"; import { Aptos, SimpleTransaction, AccountAddressInput, InputGenerateTransactionOptions } from "@aptos-labs/ts-sdk"; - -export type ConfidentialNormalizationSigmaProof = { - alpha1List: Uint8Array[]; - alpha2: Uint8Array; - alpha3: Uint8Array; - alpha4List: Uint8Array[]; - X1: Uint8Array; - X2: Uint8Array; - X3List: Uint8Array[]; - X4List: Uint8Array[]; -}; +import type { SigmaProtocolProof } from "./sigmaProtocol"; +import { proveWithdrawal } from "./sigmaProtocolWithdraw"; export type CreateConfidentialNormalizationOpArgs = { decryptionKey: TwistedEd25519PrivateKey; unnormalizedAvailableBalance: EncryptedAmount; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte token address */ + tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; + /** Optional auditor encryption key */ + auditorEncryptionKey?: TwistedEd25519PublicKey; randomness?: bigint[]; }; @@ -34,16 +31,37 @@ export class ConfidentialNormalization { normalizedEncryptedAvailableBalance: EncryptedAmount; + /** Optional: normalized balance encrypted under auditor key */ + auditorEncryptedNormalizedBalance?: EncryptedAmount; + randomness: bigint[]; + senderAddress: Uint8Array; + + tokenAddress: Uint8Array; + + auditorEncryptionKey?: TwistedEd25519PublicKey; + + chainId: number; + constructor(args: { decryptionKey: TwistedEd25519PrivateKey; unnormalizedEncryptedAvailableBalance: EncryptedAmount; normalizedEncryptedAvailableBalance: EncryptedAmount; + auditorEncryptedNormalizedBalance?: EncryptedAmount; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + auditorEncryptionKey?: TwistedEd25519PublicKey; }) { this.decryptionKey = args.decryptionKey; this.unnormalizedEncryptedAvailableBalance = args.unnormalizedEncryptedAvailableBalance; this.normalizedEncryptedAvailableBalance = args.normalizedEncryptedAvailableBalance; + this.auditorEncryptedNormalizedBalance = args.auditorEncryptedNormalizedBalance; + this.senderAddress = args.senderAddress; + this.tokenAddress = args.tokenAddress; + this.chainId = args.chainId; + this.auditorEncryptionKey = args.auditorEncryptionKey; const randomness = this.normalizedEncryptedAvailableBalance.getRandomness(); if (!randomness) { throw new Error("Randomness is not set"); @@ -52,7 +70,14 @@ export class ConfidentialNormalization { } static async create(args: CreateConfidentialNormalizationOpArgs) { - const { decryptionKey, randomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT) } = args; + const { + decryptionKey, + randomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), + senderAddress, + tokenAddress, + chainId, + auditorEncryptionKey, + } = args; const unnormalizedEncryptedAvailableBalance = args.unnormalizedAvailableBalance; @@ -61,217 +86,64 @@ export class ConfidentialNormalization { publicKey: decryptionKey.publicKey(), randomness, }); + + // If auditor is set, encrypt the normalized balance under the auditor key with the same randomness + let auditorEncryptedNormalizedBalance: EncryptedAmount | undefined; + if (auditorEncryptionKey) { + auditorEncryptedNormalizedBalance = EncryptedAmount.fromAmountAndPublicKey({ + amount: unnormalizedEncryptedAvailableBalance.getAmount(), + publicKey: auditorEncryptionKey, + randomness, + }); + } + return new ConfidentialNormalization({ decryptionKey, unnormalizedEncryptedAvailableBalance, normalizedEncryptedAvailableBalance, + auditorEncryptedNormalizedBalance, + senderAddress, + tokenAddress, + chainId, + auditorEncryptionKey, }); } - static FIAT_SHAMIR_SIGMA_DST = "AptosConfidentialAsset/NormalizationProofFiatShamir"; - - static serializeSigmaProof(sigmaProof: ConfidentialNormalizationSigmaProof): Uint8Array { - return concatBytes( - ...sigmaProof.alpha1List, - sigmaProof.alpha2, - sigmaProof.alpha3, - ...sigmaProof.alpha4List, - sigmaProof.X1, - sigmaProof.X2, - ...sigmaProof.X3List, - ...sigmaProof.X4List, - ); - } - - static deserializeSigmaProof(sigmaProof: Uint8Array): ConfidentialNormalizationSigmaProof { - if (sigmaProof.length !== SIGMA_PROOF_NORMALIZATION_SIZE) { - throw new Error( - `Invalid sigma proof length of confidential normalization: got ${sigmaProof.length}, expected ${SIGMA_PROOF_NORMALIZATION_SIZE}`, - ); - } - - const proofArr: Uint8Array[] = []; - for (let i = 0; i < SIGMA_PROOF_NORMALIZATION_SIZE; i += PROOF_CHUNK_SIZE) { - proofArr.push(sigmaProof.subarray(i, i + PROOF_CHUNK_SIZE)); - } - - const alpha1List = proofArr.slice(0, 3); - const alpha2 = proofArr[3]; - const alpha3 = proofArr[4]; - const alpha4List = proofArr.slice(5, 5 + AVAILABLE_BALANCE_CHUNK_COUNT); - const X1 = proofArr[5 + 2 * AVAILABLE_BALANCE_CHUNK_COUNT]; - const X2 = proofArr[5 + 2 * AVAILABLE_BALANCE_CHUNK_COUNT + 1]; - const X3List = proofArr.slice(5 + 2 * AVAILABLE_BALANCE_CHUNK_COUNT + 2, 5 + 3 * AVAILABLE_BALANCE_CHUNK_COUNT + 2); - const X4List = proofArr.slice(5 + 3 * AVAILABLE_BALANCE_CHUNK_COUNT + 2, 5 + 4 * AVAILABLE_BALANCE_CHUNK_COUNT + 2); - - return { - alpha1List, - alpha2, - alpha3, - alpha4List, - X1, - X2, - X3List, - X4List, - }; - } - - async genSigmaProof(): Promise { - if (this.randomness && this.randomness.length !== AVAILABLE_BALANCE_CHUNK_COUNT) { - throw new Error("Invalid length list of randomness"); + /** + * Generate the sigma protocol proof for normalization. + * Normalization is the same as withdrawal with v = 0. + */ + genSigmaProof(): SigmaProtocolProof { + const oldCipherTexts = this.unnormalizedEncryptedAvailableBalance.getCipherText(); + const newCipherTexts = this.normalizedEncryptedAvailableBalance.getCipherText(); + + const oldBalanceC = oldCipherTexts.map((ct) => ct.C); + const oldBalanceD = oldCipherTexts.map((ct) => ct.D); + const newBalanceC = newCipherTexts.map((ct) => ct.C); + const newBalanceD = newCipherTexts.map((ct) => ct.D); + + let auditorEncryptionKey: TwistedEd25519PublicKey | undefined; + let newBalanceDAud: import(".").RistPoint[] | undefined; + if (this.auditorEncryptionKey && this.auditorEncryptedNormalizedBalance) { + auditorEncryptionKey = this.auditorEncryptionKey; + newBalanceDAud = this.auditorEncryptedNormalizedBalance.getCipherText().map((ct) => ct.D); } - const x1List = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); - const x2 = ed25519GenRandom(); - const x3 = ed25519GenRandom(); - - const x4List = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - x1List.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const x1i = el * coef; - - return acc + x1i; - }, 0n), - ), - ).add( - this.unnormalizedEncryptedAvailableBalance - .getCipherText() - .reduce( - (acc, ciphertext, i) => acc.add(ciphertext.D.multiply(2n ** (BigInt(i) * CHUNK_BITS_BIG_INT))), - RistrettoPoint.ZERO, - ) - .multiply(x2), - ); - const X2 = H_RISTRETTO.multiply(x3); - const X3List = x1List.map((el, index) => { - const x1iG = RistrettoPoint.BASE.multiply(el); - - const x4iH = H_RISTRETTO.multiply(x4List[index]); - - return x1iG.add(x4iH); - }); - const X4List = x4List.map((el) => - RistrettoPoint.fromHex(this.decryptionKey.publicKey().toUint8Array()).multiply(el), - ); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialNormalization.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - this.decryptionKey.publicKey().toUint8Array(), - this.unnormalizedEncryptedAvailableBalance.getCipherTextBytes(), - this.normalizedEncryptedAvailableBalance.getCipherTextBytes(), - X1.toRawBytes(), - X2.toRawBytes(), - ...X3List.map((X3) => X3.toRawBytes()), - ...X4List.map((X4) => X4.toRawBytes()), - ); - - const sLE = bytesToNumberLE(this.decryptionKey.toUint8Array()); - const invertSLE = ed25519InvertN(sLE); - - const ps = ed25519modN(p * sLE); - const psInvert = ed25519modN(p * invertSLE); - - const alpha1List = x1List.map((x1, i) => { - const pChunk = ed25519modN(p * this.normalizedEncryptedAvailableBalance.getAmountChunks()[i]); - - return ed25519modN(x1 - pChunk); - }); - const alpha2 = ed25519modN(x2 - ps); - const alpha3 = ed25519modN(x3 - psInvert); - const alpha4List = x4List.map((el, i) => { - const pri = ed25519modN(p * this.randomness[i]); - - return ed25519modN(el - pri); + return proveWithdrawal({ + dk: this.decryptionKey, + senderAddress: this.senderAddress, + tokenAddress: this.tokenAddress, + chainId: this.chainId, + amount: 0n, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks: this.normalizedEncryptedAvailableBalance.getAmountChunks(), + newRandomness: this.randomness, + auditorEncryptionKey, + newBalanceDAud, }); - - return { - alpha1List: alpha1List.map((alpha1) => numberToBytesLE(alpha1, 32)), - alpha2: numberToBytesLE(alpha2, 32), - alpha3: numberToBytesLE(alpha3, 32), - alpha4List: alpha4List.map((alpha4) => numberToBytesLE(alpha4, 32)), - X1: X1.toRawBytes(), - X2: X2.toRawBytes(), - X3List: X3List.map((X3) => X3.toRawBytes()), - X4List: X4List.map((X4) => X4.toRawBytes()), - }; - } - - static verifySigmaProof(opts: { - publicKey: TwistedEd25519PublicKey; - sigmaProof: ConfidentialNormalizationSigmaProof; - unnormalizedEncryptedBalance: EncryptedAmount; - normalizedEncryptedBalance: EncryptedAmount; - }): boolean { - const publicKeyU8 = opts.publicKey.toUint8Array(); - - const alpha1LEList = opts.sigmaProof.alpha1List.map((a) => bytesToNumberLE(a)); - const alpha2LE = bytesToNumberLE(opts.sigmaProof.alpha2); - const alpha3LE = bytesToNumberLE(opts.sigmaProof.alpha3); - const alpha4LEList = opts.sigmaProof.alpha4List.map((a) => bytesToNumberLE(a)); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialNormalization.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - publicKeyU8, - opts.unnormalizedEncryptedBalance.getCipherTextBytes(), - opts.normalizedEncryptedBalance.getCipherTextBytes(), - opts.sigmaProof.X1, - opts.sigmaProof.X2, - ...opts.sigmaProof.X3List, - ...opts.sigmaProof.X4List, - ); - const alpha2D = opts.unnormalizedEncryptedBalance - .getCipherText() - .reduce((acc, { D }, i) => acc.add(D.multiply(2n ** (BigInt(i) * CHUNK_BITS_BIG_INT))), RistrettoPoint.ZERO) - .multiply(alpha2LE); - const pBalOld = opts.unnormalizedEncryptedBalance - .getCipherText() - .reduce((acc, ciphertext, i) => { - const chunk = ciphertext.C.multiply(2n ** (BigInt(i) * CHUNK_BITS_BIG_INT)); - return acc.add(chunk); - }, RistrettoPoint.ZERO) - .multiply(p); - - const alpha3H = H_RISTRETTO.multiply(alpha3LE); - const pP = RistrettoPoint.fromHex(publicKeyU8).multiply(p); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - alpha1LEList.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const alpha1i = el * coef; - return acc + alpha1i; - }, 0n), - ), - ) - .add(alpha2D) - .add(pBalOld); - const X2 = alpha3H.add(pP); - const X3List = alpha1LEList.map((el, i) => { - const a1iG = RistrettoPoint.BASE.multiply(el); - const a4iH = H_RISTRETTO.multiply(alpha4LEList[i]); - const pC = opts.normalizedEncryptedBalance.getCipherText()[i].C.multiply(p); - return a1iG.add(a4iH).add(pC); - }); - const X4List = alpha4LEList.map((el, i) => { - const a4iP = RistrettoPoint.fromHex(publicKeyU8).multiply(el); - const pDnew = opts.normalizedEncryptedBalance.getCipherText()[i].D.multiply(p); - - return a4iP.add(pDnew); - }); - - return ( - X1.equals(RistrettoPoint.fromHex(opts.sigmaProof.X1)) && - X2.equals(RistrettoPoint.fromHex(opts.sigmaProof.X2)) && - X3List.every((X3, i) => X3.equals(RistrettoPoint.fromHex(opts.sigmaProof.X3List[i]))) && - X4List.every((X4, i) => X4.equals(RistrettoPoint.fromHex(opts.sigmaProof.X4List[i]))) - ); } async genRangeProof(): Promise { @@ -300,12 +172,20 @@ export class ConfidentialNormalization { } async authorizeNormalization(): Promise< - [{ sigmaProof: ConfidentialNormalizationSigmaProof; rangeProof: Uint8Array }, EncryptedAmount] + [ + { sigmaProof: SigmaProtocolProof; rangeProof: Uint8Array }, + EncryptedAmount, + EncryptedAmount | undefined, + ] > { - const sigmaProof = await this.genSigmaProof(); + const sigmaProof = this.genSigmaProof(); const rangeProof = await this.genRangeProof(); - return [{ sigmaProof, rangeProof }, this.normalizedEncryptedAvailableBalance]; + return [ + { sigmaProof, rangeProof }, + this.normalizedEncryptedAvailableBalance, + this.auditorEncryptedNormalizedBalance, + ]; } async createTransaction(args: { @@ -316,17 +196,25 @@ export class ConfidentialNormalization { withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { - const [{ sigmaProof, rangeProof }, normalizedCB] = await this.authorizeNormalization(); + const [{ sigmaProof, rangeProof }, normalizedCB, auditorCB] = await this.authorizeNormalization(); + + // Build auditor A components (D points encrypted under auditor key) + const newBalanceA = auditorCB + ? auditorCB.getCipherText().map((ct) => ct.D.toRawBytes()) + : ([] as Uint8Array[]); return args.client.transaction.build.simple({ ...args, data: { - function: `${args.confidentialAssetModuleAddress}::${MODULE_NAME}::normalize`, + function: `${args.confidentialAssetModuleAddress}::${MODULE_NAME}::normalize_raw`, functionArguments: [ args.tokenAddress, - normalizedCB.getCipherTextBytes(), + normalizedCB.getCipherText().map((ct) => ct.C.toRawBytes()), // new_balance_C + normalizedCB.getCipherText().map((ct) => ct.D.toRawBytes()), // new_balance_D + newBalanceA, // new_balance_A rangeProof, - ConfidentialNormalization.serializeSigmaProof(sigmaProof), + sigmaProof.commitment, + sigmaProof.response, ], }, options: args.options, diff --git a/confidential-assets/src/crypto/confidentialTransfer.ts b/confidential-assets/src/crypto/confidentialTransfer.ts index 4ce86a278..2909fe51b 100644 --- a/confidential-assets/src/crypto/confidentialTransfer.ts +++ b/confidential-assets/src/crypto/confidentialTransfer.ts @@ -1,38 +1,19 @@ -import { bytesToNumberLE, concatBytes, numberToBytesLE } from "@noble/curves/abstract/utils"; +import { numberToBytesLE } from "@noble/curves/abstract/utils"; import { RistrettoPoint } from "@noble/curves/ed25519"; -import { utf8ToBytes } from "@noble/hashes/utils"; -import { PROOF_CHUNK_SIZE, SIGMA_PROOF_TRANSFER_SIZE } from "../consts"; -import { genFiatShamirChallenge } from "../helpers"; import { AVAILABLE_BALANCE_CHUNK_COUNT, CHUNK_BITS, - CHUNK_BITS_BIG_INT, ChunkedAmount, TRANSFER_AMOUNT_CHUNK_COUNT, } from "./chunkedAmount"; -import { AnyNumber, HexInput } from "@aptos-labs/ts-sdk"; +import { AnyNumber } from "@aptos-labs/ts-sdk"; import { RangeProofExecutor } from "./rangeProof"; import { TwistedEd25519PrivateKey, TwistedEd25519PublicKey, H_RISTRETTO } from "."; import { TwistedElGamalCiphertext } from "./twistedElGamal"; -import { ed25519GenListOfRandom, ed25519GenRandom, ed25519modN, ed25519InvertN } from "../utils"; +import { ed25519GenListOfRandom } from "../utils"; import { EncryptedAmount } from "./encryptedAmount"; - -export type ConfidentialTransferSigmaProof = { - alpha1List: Uint8Array[]; - alpha2: Uint8Array; - alpha3List: Uint8Array[]; - alpha4List: Uint8Array[]; - alpha5: Uint8Array; - alpha6List: Uint8Array[]; - X1: Uint8Array; - X2List: Uint8Array[]; - X3List: Uint8Array[]; - X4List: Uint8Array[]; - X5: Uint8Array; - X6List: Uint8Array[]; - X7List?: Uint8Array[]; - X8List: Uint8Array[]; -}; +import type { SigmaProtocolProof } from "./sigmaProtocol"; +import { proveTransfer } from "./sigmaProtocolTransfer"; export type ConfidentialTransferRangeProof = { rangeProofAmount: Uint8Array; @@ -44,8 +25,22 @@ export type CreateConfidentialTransferOpArgs = { senderAvailableBalanceCipherText: TwistedElGamalCiphertext[]; amount: AnyNumber; recipientEncryptionKey: TwistedEd25519PublicKey; + /** + * Whether the last element in auditorEncryptionKeys is the effective (asset-level / global) auditor. + * Extras are all preceding elements. This affects the sigma protocol statement layout and + * domain separator. + */ + hasEffectiveAuditor?: boolean; auditorEncryptionKeys?: TwistedEd25519PublicKey[]; transferAmountRandomness?: bigint[]; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte recipient address */ + recipientAddress: Uint8Array; + /** 32-byte token address */ + tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; }; export class ConfidentialTransfer { @@ -53,6 +48,8 @@ export class ConfidentialTransfer { recipientEncryptionKey: TwistedEd25519PublicKey; + hasEffectiveAuditor: boolean; + auditorEncryptionKeys: TwistedEd25519PublicKey[]; transferAmountEncryptedByAuditors: EncryptedAmount[]; @@ -91,20 +88,38 @@ export class ConfidentialTransfer { */ newBalanceRandomness: bigint[]; + /** Optional: new balance encrypted under each auditor key */ + auditorEncryptedBalancesAfterTransfer: EncryptedAmount[]; + + senderAddress: Uint8Array; + + recipientAddress: Uint8Array; + + tokenAddress: Uint8Array; + + chainId: number; + private constructor(args: { senderDecryptionKey: TwistedEd25519PrivateKey; recipientEncryptionKey: TwistedEd25519PublicKey; amount: bigint; + hasEffectiveAuditor: boolean; auditorEncryptionKeys: TwistedEd25519PublicKey[]; senderEncryptedAvailableBalance: EncryptedAmount; transferAmountEncryptedBySender: EncryptedAmount; transferAmountEncryptedByRecipient: EncryptedAmount; transferAmountEncryptedByAuditors: EncryptedAmount[]; senderEncryptedAvailableBalanceAfterTransfer: EncryptedAmount; + auditorEncryptedBalancesAfterTransfer: EncryptedAmount[]; + senderAddress: Uint8Array; + recipientAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; }) { const { senderDecryptionKey, recipientEncryptionKey, + hasEffectiveAuditor, auditorEncryptionKeys, senderEncryptedAvailableBalance, amount, @@ -112,9 +127,15 @@ export class ConfidentialTransfer { transferAmountEncryptedByRecipient, transferAmountEncryptedByAuditors, senderEncryptedAvailableBalanceAfterTransfer, + auditorEncryptedBalancesAfterTransfer, + senderAddress, + recipientAddress, + tokenAddress, + chainId, } = args; this.senderDecryptionKey = senderDecryptionKey; this.recipientEncryptionKey = recipientEncryptionKey; + this.hasEffectiveAuditor = hasEffectiveAuditor; this.auditorEncryptionKeys = auditorEncryptionKeys; this.senderEncryptedAvailableBalance = senderEncryptedAvailableBalance; if (amount < 0n) { @@ -130,6 +151,7 @@ export class ConfidentialTransfer { this.transferAmountEncryptedByRecipient = transferAmountEncryptedByRecipient; this.transferAmountEncryptedByAuditors = transferAmountEncryptedByAuditors; this.senderEncryptedAvailableBalanceAfterTransfer = senderEncryptedAvailableBalanceAfterTransfer; + this.auditorEncryptedBalancesAfterTransfer = auditorEncryptedBalancesAfterTransfer; const transferAmountRandomness = transferAmountEncryptedBySender.getRandomness(); if (!transferAmountRandomness) { @@ -142,6 +164,11 @@ export class ConfidentialTransfer { throw new Error("New balance randomness is not set"); } this.newBalanceRandomness = newBalanceRandomness; + + this.senderAddress = senderAddress; + this.recipientAddress = recipientAddress; + this.tokenAddress = tokenAddress; + this.chainId = chainId; } static async create(args: CreateConfidentialTransferOpArgs) { @@ -149,8 +176,13 @@ export class ConfidentialTransfer { senderAvailableBalanceCipherText, senderDecryptionKey, recipientEncryptionKey, + hasEffectiveAuditor = false, auditorEncryptionKeys = [], transferAmountRandomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), + senderAddress, + recipientAddress, + tokenAddress, + chainId, } = args; const amount = BigInt(args.amount); const newBalanceRandomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); @@ -188,9 +220,20 @@ export class ConfidentialTransfer { }), ); + // Encrypt the new balance under each auditor key with the same randomness + const auditorEncryptedBalancesAfterTransfer = auditorEncryptionKeys.map( + (encryptionKey) => + EncryptedAmount.fromAmountAndPublicKey({ + amount: remainingBalance, + publicKey: encryptionKey, + randomness: newBalanceRandomness, + }), + ); + return new ConfidentialTransfer({ senderDecryptionKey, recipientEncryptionKey, + hasEffectiveAuditor, auditorEncryptionKeys, senderEncryptedAvailableBalance, amount, @@ -198,452 +241,64 @@ export class ConfidentialTransfer { transferAmountEncryptedByRecipient, transferAmountEncryptedByAuditors, senderEncryptedAvailableBalanceAfterTransfer, + auditorEncryptedBalancesAfterTransfer, + senderAddress, + recipientAddress, + tokenAddress, + chainId, }); } - static FIAT_SHAMIR_SIGMA_DST = "AptosConfidentialAsset/TransferProofFiatShamir"; - - static serializeSigmaProof(sigmaProof: ConfidentialTransferSigmaProof): Uint8Array { - return concatBytes( - ...sigmaProof.alpha1List, - sigmaProof.alpha2, - ...sigmaProof.alpha3List, - ...sigmaProof.alpha4List, - sigmaProof.alpha5, - ...sigmaProof.alpha6List, - sigmaProof.X1, - ...sigmaProof.X2List, - ...sigmaProof.X3List, - ...sigmaProof.X4List, - sigmaProof.X5, - ...sigmaProof.X6List, - ...(sigmaProof.X7List ?? []), - ...sigmaProof.X8List, - ); - } - - static deserializeSigmaProof(sigmaProof: Uint8Array): ConfidentialTransferSigmaProof { - if (sigmaProof.length % PROOF_CHUNK_SIZE !== 0) { - throw new Error(`Invalid sigma proof length: the length must be a multiple of ${PROOF_CHUNK_SIZE}`); - } - - if (sigmaProof.length < SIGMA_PROOF_TRANSFER_SIZE) { - throw new Error( - `Invalid sigma proof length of confidential transfer: got ${sigmaProof.length}, expected minimum ${SIGMA_PROOF_TRANSFER_SIZE}`, - ); - } - - const baseProof = sigmaProof.slice(0, SIGMA_PROOF_TRANSFER_SIZE); - - const X7List: Uint8Array[] = []; - const baseProofArray: Uint8Array[] = []; - - for (let i = 0; i < SIGMA_PROOF_TRANSFER_SIZE; i += PROOF_CHUNK_SIZE) { - baseProofArray.push(baseProof.subarray(i, i + PROOF_CHUNK_SIZE)); - } - - if (sigmaProof.length > SIGMA_PROOF_TRANSFER_SIZE) { - const auditorsPartLength = sigmaProof.length - SIGMA_PROOF_TRANSFER_SIZE; - const auditorsPart = sigmaProof.slice(SIGMA_PROOF_TRANSFER_SIZE); - - for (let i = 0; i < auditorsPartLength; i += PROOF_CHUNK_SIZE) { - X7List.push(auditorsPart.subarray(i, i + PROOF_CHUNK_SIZE)); - } - } - - const half = TRANSFER_AMOUNT_CHUNK_COUNT; - - const alpha1List = baseProofArray.slice(0, half); - const alpha2 = baseProofArray[half]; - const alpha3List = baseProofArray.slice(half + 1, half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT); - const alpha4List = baseProofArray.slice( - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT, - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT + AVAILABLE_BALANCE_CHUNK_COUNT, - ); - const alpha5 = baseProofArray[half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT + AVAILABLE_BALANCE_CHUNK_COUNT]; - const alpha6List = baseProofArray.slice( - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT + AVAILABLE_BALANCE_CHUNK_COUNT + 1, - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 2 + 1, - ); - - const X1 = baseProofArray[half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 2 + 1]; - const X2List = baseProofArray.slice( - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 2 + 1 + 1, - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 3 + 1, - ); - const X3List = baseProofArray.slice( - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 3 + 1, - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 4 + 1, - ); - const X4List = baseProofArray.slice( - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 4 + 1, - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 5 + 1, - ); - const X5 = baseProofArray[half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 5 + 1]; - const X6List = baseProofArray.slice( - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 5 + 1 + 1, - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 6 + 1, - ); - const X8List = baseProofArray.slice( - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 6 + 1, - half + 1 + AVAILABLE_BALANCE_CHUNK_COUNT * 7 + 1, - ); - - return { - alpha1List, - alpha2, - alpha3List, - alpha4List, - alpha5, - alpha6List, - X1, - X2List, - X3List, - X4List, - X5, - X6List, - X7List, - X8List, - }; - } - - async genSigmaProof(): Promise { - if (this.transferAmountRandomness && this.transferAmountRandomness.length !== AVAILABLE_BALANCE_CHUNK_COUNT) - throw new TypeError("Invalid length list of randomness"); - - if (this.transferAmountEncryptedBySender.getAmount() > 2n ** (2n * CHUNK_BITS_BIG_INT) - 1n) - throw new TypeError(`Amount must be less than 2n**${CHUNK_BITS_BIG_INT * 2n}`); - - const senderPKRistretto = RistrettoPoint.fromHex(this.senderDecryptionKey.publicKey().toUint8Array()); - const recipientPKRistretto = RistrettoPoint.fromHex(this.recipientEncryptionKey.toUint8Array()); - - // Prover selects random x1, x2, x3i[], x4j[], x5, x6i[], where i in {0, 3} and j in {0, 1} - const i = AVAILABLE_BALANCE_CHUNK_COUNT; - const j = TRANSFER_AMOUNT_CHUNK_COUNT; - - const x1List = ed25519GenListOfRandom(i); - const x2 = ed25519GenRandom(); - const x3List = ed25519GenListOfRandom(j); - const x4List = ed25519GenListOfRandom(j); - const x5 = ed25519GenRandom(); - const x6List = ed25519GenListOfRandom(i); - - // const lastHalfIndexesOfChunksCount = Array.from( - // { length: ChunkedAmount.CHUNKS_COUNT_HALF }, - // // eslint-disable-next-line @typescript-eslint/no-shadow - // (_, i) => i + ChunkedAmount.CHUNKS_COUNT_HALF, - // ); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - x1List.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const x1i = el * coef; - - return acc + x1i; - }, 0n), - ), - ) - .add( - H_RISTRETTO.multiply( - ed25519modN( - x6List.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const x6i = el * coef; - - return acc + x6i; - }, 0n), - ), - ).subtract( - H_RISTRETTO.multiply( - ed25519modN( - x3List.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const x3i = el * coef; - - return acc + x3i; - }, 0n), - ), - ), - ), - ) - .add( - this.senderEncryptedAvailableBalance - .getCipherText() - .reduce( - (acc, { D }, idx) => acc.add(D.multiply(2n ** (BigInt(idx) * CHUNK_BITS_BIG_INT))), - RistrettoPoint.ZERO, - ) - .multiply(x2), - ) - .subtract( - this.senderEncryptedAvailableBalanceAfterTransfer - .getCipherText() - .reduce( - (acc, { D }, idx) => acc.add(D.multiply(2n ** (BigInt(idx) * CHUNK_BITS_BIG_INT))), - RistrettoPoint.ZERO, - ) - .multiply(x2), - ) - // .add( - // lastHalfIndexesOfChunksCount.reduce( - // (acc, curr) => - // acc.add( - // H_RISTRETTO.multiply(x3List[curr]).multiply(2n ** (CHUNK_BITS_BI * BigInt(curr))), - // ), - // RistrettoPoint.ZERO, - // ), - // ) - .toRawBytes(); - const X2List = x6List.map((el) => senderPKRistretto.multiply(el).toRawBytes()); - const X3List = x3List.slice(0, j).map((x3) => recipientPKRistretto.multiply(x3).toRawBytes()); - const X4List = x4List - .slice(0, j) - .map((x4, idx) => RistrettoPoint.BASE.multiply(x4).add(H_RISTRETTO.multiply(x3List[idx])).toRawBytes()); - const X5 = H_RISTRETTO.multiply(x5).toRawBytes(); - const X6List = x1List.map((el, idx) => { - const x1iG = RistrettoPoint.BASE.multiply(el); - const x6iH = H_RISTRETTO.multiply(x6List[idx]); - - return x1iG.add(x6iH).toRawBytes(); - }); - const X7List = - this.auditorEncryptionKeys - .map((pk) => pk.toUint8Array()) - .map((pk) => x3List.slice(0, j).map((el) => RistrettoPoint.fromHex(pk).multiply(el).toRawBytes())) ?? []; - const X8List = x3List.map((el) => senderPKRistretto.multiply(el).toRawBytes()); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialTransfer.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - this.senderDecryptionKey.publicKey().toUint8Array(), - this.recipientEncryptionKey.toUint8Array(), - ...this.auditorEncryptionKeys.map((pk) => pk.toUint8Array()), - this.senderEncryptedAvailableBalance.getCipherTextBytes(), - this.transferAmountEncryptedByRecipient.getCipherTextBytes(), - ...this.transferAmountEncryptedByAuditors.map((el) => el.getCipherTextDPointBytes()).flat(), - this.transferAmountEncryptedBySender.getCipherTextDPointBytes(), - this.senderEncryptedAvailableBalanceAfterTransfer.getCipherTextBytes(), - X1, - ...X2List, - ...X3List, - ...X4List, - X5, - ...X6List, - ...X7List.flat(), - ...X8List, - ); - - const sLE = bytesToNumberLE(this.senderDecryptionKey.toUint8Array()); - const invertSLE = ed25519InvertN(sLE); - - const alpha1List = x1List.map((x1, idx) => - ed25519modN(x1 - ed25519modN(p * this.senderEncryptedAvailableBalanceAfterTransfer.getAmountChunks()[idx])), - ); - const alpha2 = ed25519modN(x2 - p * sLE); - const alpha3List = x3List.map((el, idx) => - ed25519modN(BigInt(el) - p * BigInt(this.transferAmountRandomness[idx])), - ); - const alpha4List = x4List - .slice(0, j) - .map((el, idx) => ed25519modN(el - p * this.transferAmountEncryptedBySender.getAmountChunks()[idx])); - const alpha5 = ed25519modN(x5 - p * invertSLE); - const alpha6List = x6List.map((el, idx) => ed25519modN(el - p * this.newBalanceRandomness[idx])); - - return { - alpha1List: alpha1List.map((a) => numberToBytesLE(a, 32)), - alpha2: numberToBytesLE(alpha2, 32), - alpha3List: alpha3List.map((a) => numberToBytesLE(a, 32)), - alpha4List: alpha4List.map((a) => numberToBytesLE(a, 32)), - alpha5: numberToBytesLE(alpha5, 32), - alpha6List: alpha6List.map((a) => numberToBytesLE(a, 32)), - X1, - X2List, - X3List, - X4List, - X5, - X6List, - X7List: X7List.flat(), - X8List, - }; - } - - static verifySigmaProof(opts: { - senderPrivateKey: TwistedEd25519PrivateKey; - recipientPublicKey: TwistedEd25519PublicKey; - encryptedActualBalance: TwistedElGamalCiphertext[]; - encryptedActualBalanceAfterTransfer: EncryptedAmount; - encryptedTransferAmountByRecipient: EncryptedAmount; - encryptedTransferAmountBySender: EncryptedAmount; - sigmaProof: ConfidentialTransferSigmaProof; - auditors?: { - publicKeys: TwistedEd25519PublicKey[]; - auditorsCBList: TwistedElGamalCiphertext[][]; - }; - }): boolean { - const auditorPKs = opts?.auditors?.publicKeys.map((pk) => pk.toUint8Array()) ?? []; - const proofX7List = opts.sigmaProof.X7List ?? []; - - const alpha1LEList = opts.sigmaProof.alpha1List.map((a) => bytesToNumberLE(a)); - const alpha2LE = bytesToNumberLE(opts.sigmaProof.alpha2); - const alpha3LEList = opts.sigmaProof.alpha3List.map((a) => bytesToNumberLE(a)); - const alpha4LEList = opts.sigmaProof.alpha4List.map((a) => bytesToNumberLE(a)); - const alpha5LE = bytesToNumberLE(opts.sigmaProof.alpha5); - const alpha6LEList = opts.sigmaProof.alpha6List.map((a) => bytesToNumberLE(a)); - - const senderPublicKeyU8 = opts.senderPrivateKey.publicKey().toUint8Array(); - const recipientPublicKeyU8 = opts.recipientPublicKey.toUint8Array(); - const senderPKRistretto = RistrettoPoint.fromHex(senderPublicKeyU8); - const recipientPKRistretto = RistrettoPoint.fromHex(recipientPublicKeyU8); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialTransfer.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - senderPublicKeyU8, - recipientPublicKeyU8, - ...auditorPKs, - ...opts.encryptedActualBalance.map((el) => el.serialize()).flat(), - opts.encryptedTransferAmountByRecipient.getCipherTextBytes(), - ...(opts.auditors?.auditorsCBList?.flat().map(({ D }) => D.toRawBytes()) || []), - opts.encryptedTransferAmountBySender.getCipherTextDPointBytes(), - opts.encryptedActualBalanceAfterTransfer.getCipherTextBytes(), - opts.sigmaProof.X1, - ...opts.sigmaProof.X2List, - ...opts.sigmaProof.X3List, - ...opts.sigmaProof.X4List, - opts.sigmaProof.X5, - ...opts.sigmaProof.X6List, - ...proofX7List, - ...opts.sigmaProof.X8List, - ); - - const { oldDSum, oldCSum } = opts.encryptedActualBalance.reduce( - (acc, { C, D }, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - return { - oldDSum: acc.oldDSum.add(D.multiply(coef)), - oldCSum: acc.oldCSum.add(C.multiply(coef)), - }; - }, - { oldDSum: RistrettoPoint.ZERO, oldCSum: RistrettoPoint.ZERO }, - ); - - const newDSum = opts.encryptedActualBalanceAfterTransfer.getCipherText().reduce((acc, { D }, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - return acc.add(D.multiply(coef)); - }, RistrettoPoint.ZERO); - - const j = TRANSFER_AMOUNT_CHUNK_COUNT; - - // const lastHalfIndexesOfChunksCount = Array.from( - // { length: ChunkedAmount.CHUNKS_COUNT_HALF }, - // (_, i) => i + ChunkedAmount.CHUNKS_COUNT_HALF, - // ); - - const amountCSum = opts.encryptedTransferAmountByRecipient - .getCipherText() - .slice(0, j) - .reduce((acc, { C }, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - return acc.add(C.multiply(coef)); - }, RistrettoPoint.ZERO); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - alpha1LEList.reduce((acc, curr, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const a1i = curr * coef; - - return acc + a1i; - }, 0n), - ), - ) - .add( - H_RISTRETTO.multiply( - ed25519modN( - alpha6LEList.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const a6i = el * coef; - - return acc + a6i; - }, 0n), - ), - ).subtract( - H_RISTRETTO.multiply( - ed25519modN( - alpha3LEList.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const a3i = el * coef; - - return acc + a3i; - }, 0n), - ), - ), - ), - ) - .add(oldDSum.multiply(alpha2LE)) - .subtract(newDSum.multiply(alpha2LE)) - .add(oldCSum.multiply(p)) - .subtract(amountCSum.multiply(p)); - // .add( - // lastHalfIndexesOfChunksCount.reduce( - // (acc, curr) => - // acc.add( - // H_RISTRETTO.multiply(alpha3LEList[curr]).multiply( - // 2n ** (CHUNK_BITS_BI * BigInt(curr)), - // ), - // ), - // RistrettoPoint.ZERO, - // ), - // ) - const X2List = alpha6LEList.map((el, i) => - senderPKRistretto.multiply(el).add(opts.encryptedActualBalanceAfterTransfer.getCipherText()[i].D.multiply(p)), - ); - const X3List = alpha3LEList - .slice(0, j) - .map((a3, i) => - recipientPKRistretto.multiply(a3).add(opts.encryptedTransferAmountByRecipient.getCipherText()[i].D.multiply(p)), - ); - const X4List = alpha4LEList.slice(0, j).map((a4, i) => { - const a4G = RistrettoPoint.BASE.multiply(a4); - const a3H = H_RISTRETTO.multiply(alpha3LEList[i]); - const pC = opts.encryptedTransferAmountByRecipient.getCipherText()[i].C.multiply(p); - return a4G.add(a3H).add(pC); - }); - const X5 = H_RISTRETTO.multiply(alpha5LE).add(senderPKRistretto.multiply(p)); - const X6List = alpha1LEList.map((el, i) => { - const a1iG = RistrettoPoint.BASE.multiply(el); - const a6iH = H_RISTRETTO.multiply(alpha6LEList[i]); - const pC = opts.encryptedActualBalanceAfterTransfer.getCipherText()[i].C.multiply(p); - return a1iG.add(a6iH).add(pC); - }); - const X7List = auditorPKs.map((auPk, auPubKIdx) => - alpha3LEList - .slice(0, j) - .map((a3, idxJ) => - RistrettoPoint.fromHex(auPk) - .multiply(a3) - .add(RistrettoPoint.fromHex(opts.auditors!.auditorsCBList[auPubKIdx][idxJ].D.toRawBytes()).multiply(p)), - ), - ); - const X8List = alpha3LEList.map((el, i) => { - const a3P = senderPKRistretto.multiply(el); - const pD = opts.encryptedTransferAmountBySender.getCipherText()[i].D.multiply(p); - return a3P.add(pD); + /** + * Generate the sigma protocol proof for transfer. + */ + genSigmaProof(): SigmaProtocolProof { + const oldCipherTexts = this.senderEncryptedAvailableBalance.getCipherText(); + const newCipherTexts = this.senderEncryptedAvailableBalanceAfterTransfer.getCipherText(); + const senderTransferCipherTexts = this.transferAmountEncryptedBySender.getCipherText(); + const recipientTransferCipherTexts = this.transferAmountEncryptedByRecipient.getCipherText(); + + const oldBalanceC = oldCipherTexts.map((ct) => ct.C); + const oldBalanceD = oldCipherTexts.map((ct) => ct.D); + const newBalanceC = newCipherTexts.map((ct) => ct.C); + const newBalanceD = newCipherTexts.map((ct) => ct.D); + const transferAmountC = senderTransferCipherTexts.map((ct) => ct.C); + const transferAmountDSender = senderTransferCipherTexts.map((ct) => ct.D); + const transferAmountDRecipient = recipientTransferCipherTexts.map((ct) => ct.D); + + // Auditor data + const auditorEncryptionKeys = this.auditorEncryptionKeys.length > 0 ? this.auditorEncryptionKeys : undefined; + const newBalanceDAud = this.auditorEncryptedBalancesAfterTransfer.length > 0 + ? this.auditorEncryptedBalancesAfterTransfer.map((ea) => ea.getCipherText().map((ct) => ct.D)) + : undefined; + const transferAmountDAud = this.transferAmountEncryptedByAuditors.length > 0 + ? this.transferAmountEncryptedByAuditors.map((ea) => ea.getCipherText().map((ct) => ct.D)) + : undefined; + + return proveTransfer({ + dk: this.senderDecryptionKey, + senderAddress: this.senderAddress, + recipientAddress: this.recipientAddress, + tokenAddress: this.tokenAddress, + chainId: this.chainId, + senderEncryptionKey: this.senderDecryptionKey.publicKey(), + recipientEncryptionKey: this.recipientEncryptionKey, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks: this.senderEncryptedAvailableBalanceAfterTransfer.getAmountChunks(), + newRandomness: this.newBalanceRandomness, + transferAmountC, + transferAmountDSender, + transferAmountDRecipient, + transferAmountChunks: this.transferAmountEncryptedBySender.getAmountChunks(), + transferRandomness: this.transferAmountRandomness.slice(0, TRANSFER_AMOUNT_CHUNK_COUNT), + hasEffectiveAuditor: this.hasEffectiveAuditor, + auditorEncryptionKeys, + newBalanceDAud, + transferAmountDAud, }); - - return ( - X1.equals(RistrettoPoint.fromHex(opts.sigmaProof.X1)) && - X2List.every((X2, i) => X2.equals(RistrettoPoint.fromHex(opts.sigmaProof.X2List[i]))) && - X3List.every((X3, i) => X3.equals(RistrettoPoint.fromHex(opts.sigmaProof.X3List[i]))) && - X4List.every((X4, i) => X4.equals(RistrettoPoint.fromHex(opts.sigmaProof.X4List[i]))) && - X5.equals(RistrettoPoint.fromHex(opts.sigmaProof.X5)) && - X6List.every((X6, i) => X6.equals(RistrettoPoint.fromHex(opts.sigmaProof.X6List[i]))) && - X7List.flat().every((X7, i) => X7.equals(RistrettoPoint.fromHex(proofX7List[i]))) && - X8List.every((X8, i) => X8.equals(RistrettoPoint.fromHex(opts.sigmaProof.X8List[i]))) - ); } async genRangeProof(): Promise { @@ -671,14 +326,14 @@ export class ConfidentialTransfer { async authorizeTransfer(): Promise< [ - { sigmaProof: ConfidentialTransferSigmaProof; rangeProof: ConfidentialTransferRangeProof }, + { sigmaProof: SigmaProtocolProof; rangeProof: ConfidentialTransferRangeProof }, EncryptedAmount, EncryptedAmount, EncryptedAmount[], + EncryptedAmount[], ] > { - const sigmaProof = await this.genSigmaProof(); - + const sigmaProof = this.genSigmaProof(); const rangeProof = await this.genRangeProof(); return [ @@ -689,6 +344,7 @@ export class ConfidentialTransfer { this.senderEncryptedAvailableBalanceAfterTransfer, this.transferAmountEncryptedByRecipient, this.transferAmountEncryptedByAuditors, + this.auditorEncryptedBalancesAfterTransfer, ]; } diff --git a/confidential-assets/src/crypto/confidentialWithdraw.ts b/confidential-assets/src/crypto/confidentialWithdraw.ts index e38f4bb6f..e5acbb5b0 100644 --- a/confidential-assets/src/crypto/confidentialWithdraw.ts +++ b/confidential-assets/src/crypto/confidentialWithdraw.ts @@ -1,37 +1,31 @@ -import { bytesToNumberLE, concatBytes, numberToBytesLE } from "@noble/curves/abstract/utils"; +import { numberToBytesLE } from "@noble/curves/abstract/utils"; import { RistrettoPoint } from "@noble/curves/ed25519"; -import { utf8ToBytes } from "@noble/hashes/utils"; -import { genFiatShamirChallenge } from "../helpers"; -import { PROOF_CHUNK_SIZE, SIGMA_PROOF_WITHDRAW_SIZE } from "../consts"; -import { ed25519GenListOfRandom, ed25519GenRandom, ed25519modN, ed25519InvertN } from "../utils"; +import { ed25519GenListOfRandom } from "../utils"; import { AVAILABLE_BALANCE_CHUNK_COUNT, CHUNK_BITS, - CHUNK_BITS_BIG_INT, - ChunkedAmount, - TRANSFER_AMOUNT_CHUNK_COUNT, RangeProofExecutor, TwistedEd25519PrivateKey, + TwistedEd25519PublicKey, H_RISTRETTO, TwistedElGamalCiphertext, EncryptedAmount, } from "."; - -export type ConfidentialWithdrawSigmaProof = { - alpha1List: Uint8Array[]; - alpha2: Uint8Array; - alpha3: Uint8Array; - alpha4List: Uint8Array[]; - X1: Uint8Array; - X2: Uint8Array; - X3List: Uint8Array[]; - X4List: Uint8Array[]; -}; +import type { SigmaProtocolProof } from "./sigmaProtocol"; +import { proveWithdrawal } from "./sigmaProtocolWithdraw"; export type CreateConfidentialWithdrawOpArgs = { decryptionKey: TwistedEd25519PrivateKey; senderAvailableBalanceCipherText: TwistedElGamalCiphertext[]; amount: bigint; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte token address */ + tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; + /** Optional auditor encryption key */ + auditorEncryptionKey?: TwistedEd25519PublicKey; randomness?: bigint[]; }; @@ -40,18 +34,34 @@ export class ConfidentialWithdraw { senderEncryptedAvailableBalance: EncryptedAmount; - amount: ChunkedAmount; + amount: bigint; senderEncryptedAvailableBalanceAfterWithdrawal: EncryptedAmount; + /** Optional: new balance encrypted under auditor key */ + auditorEncryptedBalanceAfterWithdrawal?: EncryptedAmount; + randomness: bigint[]; + senderAddress: Uint8Array; + + tokenAddress: Uint8Array; + + auditorEncryptionKey?: TwistedEd25519PublicKey; + + chainId: number; + constructor(args: { decryptionKey: TwistedEd25519PrivateKey; senderEncryptedAvailableBalance: EncryptedAmount; amount: bigint; senderEncryptedAvailableBalanceAfterWithdrawal: EncryptedAmount; + auditorEncryptedBalanceAfterWithdrawal?: EncryptedAmount; randomness: bigint[]; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + auditorEncryptionKey?: TwistedEd25519PublicKey; }) { const { decryptionKey, @@ -59,6 +69,11 @@ export class ConfidentialWithdraw { amount, randomness, senderEncryptedAvailableBalanceAfterWithdrawal, + auditorEncryptedBalanceAfterWithdrawal, + senderAddress, + tokenAddress, + chainId, + auditorEncryptionKey, } = args; if (amount < 0n) { throw new Error("Amount to withdraw must not be negative"); @@ -77,15 +92,27 @@ export class ConfidentialWithdraw { ); } - this.amount = ChunkedAmount.createTransferAmount(amount); + this.amount = amount; this.decryptionKey = decryptionKey; this.senderEncryptedAvailableBalance = senderEncryptedAvailableBalance; this.randomness = randomness; this.senderEncryptedAvailableBalanceAfterWithdrawal = senderEncryptedAvailableBalanceAfterWithdrawal; + this.auditorEncryptedBalanceAfterWithdrawal = auditorEncryptedBalanceAfterWithdrawal; + this.senderAddress = senderAddress; + this.tokenAddress = tokenAddress; + this.chainId = chainId; + this.auditorEncryptionKey = auditorEncryptionKey; } static async create(args: CreateConfidentialWithdrawOpArgs) { - const { amount, randomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT) } = args; + const { + amount, + randomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), + senderAddress, + tokenAddress, + chainId, + auditorEncryptionKey, + } = args; const senderEncryptedAvailableBalance = await EncryptedAmount.fromCipherTextAndPrivateKey( args.senderAvailableBalanceCipherText, @@ -97,226 +124,64 @@ export class ConfidentialWithdraw { randomness, }); + // If auditor is set, encrypt the new balance under the auditor key with the same randomness + let auditorEncryptedBalanceAfterWithdrawal: EncryptedAmount | undefined; + if (auditorEncryptionKey) { + auditorEncryptedBalanceAfterWithdrawal = EncryptedAmount.fromAmountAndPublicKey({ + amount: senderEncryptedAvailableBalance.getAmount() - amount, + publicKey: auditorEncryptionKey, + randomness, + }); + } + return new ConfidentialWithdraw({ decryptionKey: args.decryptionKey, amount, senderEncryptedAvailableBalance, senderEncryptedAvailableBalanceAfterWithdrawal, + auditorEncryptedBalanceAfterWithdrawal, randomness, + senderAddress, + tokenAddress, + chainId, + auditorEncryptionKey, }); } - static FIAT_SHAMIR_SIGMA_DST = "AptosConfidentialAsset/WithdrawalProofFiatShamir"; - - static serializeSigmaProof(sigmaProof: ConfidentialWithdrawSigmaProof): Uint8Array { - return concatBytes( - ...sigmaProof.alpha1List, - sigmaProof.alpha2, - sigmaProof.alpha3, - ...sigmaProof.alpha4List, - sigmaProof.X1, - sigmaProof.X2, - ...sigmaProof.X3List, - ...sigmaProof.X4List, - ); - } - - static deserializeSigmaProof(sigmaProof: Uint8Array): ConfidentialWithdrawSigmaProof { - if (sigmaProof.length !== SIGMA_PROOF_WITHDRAW_SIZE) { - throw new Error( - `Invalid sigma proof length of confidential withdraw: got ${sigmaProof.length}, expected ${SIGMA_PROOF_WITHDRAW_SIZE}`, - ); - } - - const proofArr: Uint8Array[] = []; - for (let i = 0; i < SIGMA_PROOF_WITHDRAW_SIZE; i += PROOF_CHUNK_SIZE) { - proofArr.push(sigmaProof.subarray(i, i + PROOF_CHUNK_SIZE)); + /** + * Generate the sigma protocol proof for withdrawal. + */ + genSigmaProof(): SigmaProtocolProof { + const oldCipherTexts = this.senderEncryptedAvailableBalance.getCipherText(); + const newCipherTexts = this.senderEncryptedAvailableBalanceAfterWithdrawal.getCipherText(); + + const oldBalanceC = oldCipherTexts.map((ct) => ct.C); + const oldBalanceD = oldCipherTexts.map((ct) => ct.D); + const newBalanceC = newCipherTexts.map((ct) => ct.C); + const newBalanceD = newCipherTexts.map((ct) => ct.D); + + let auditorEncryptionKey: TwistedEd25519PublicKey | undefined; + let newBalanceDAud: import(".").RistPoint[] | undefined; + if (this.auditorEncryptionKey && this.auditorEncryptedBalanceAfterWithdrawal) { + auditorEncryptionKey = this.auditorEncryptionKey; + newBalanceDAud = this.auditorEncryptedBalanceAfterWithdrawal.getCipherText().map((ct) => ct.D); } - const alpha1List = proofArr.slice(0, 3); - const alpha2 = proofArr[3]; - const alpha3 = proofArr[4]; - const alpha4List = proofArr.slice(5, 5 + AVAILABLE_BALANCE_CHUNK_COUNT); - const X1 = proofArr[11]; - const X2 = proofArr[12]; - const X3List = proofArr.slice(13, 13 + AVAILABLE_BALANCE_CHUNK_COUNT); - const X4List = proofArr.slice(13 + AVAILABLE_BALANCE_CHUNK_COUNT, 13 + 2 * AVAILABLE_BALANCE_CHUNK_COUNT); - - return { - alpha1List, - alpha2, - alpha3, - alpha4List, - X1, - X2, - X3List, - X4List, - }; - } - - async genSigmaProof(): Promise { - if (this.randomness && this.randomness.length !== AVAILABLE_BALANCE_CHUNK_COUNT) { - throw new Error("Invalid length list of randomness"); - } - - const x1List = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); - const x2 = ed25519GenRandom(); - const x3 = ed25519GenRandom(); - - const x4List = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); - // const x5List = ed25519GenListOfRandom(ChunkedAmount.CHUNKS_COUNT); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - x1List.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const x1i = el * coef; - - return acc + x1i; - }, 0n), - ), - ).add( - this.senderEncryptedAvailableBalance.getCipherText().reduce((acc, el, i) => { - const { D } = el; - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - - const DCoef = D.multiply(coef); - - const DCoefX2 = DCoef.multiply(x2); - - return acc.add(DCoefX2); - }, RistrettoPoint.ZERO), - ); - const X2 = H_RISTRETTO.multiply(x3); - const X3List = x1List.map((item, idx) => RistrettoPoint.BASE.multiply(item).add(H_RISTRETTO.multiply(x4List[idx]))); - const X4List = x4List.map((item) => - RistrettoPoint.fromHex(this.decryptionKey.publicKey().toUint8Array()).multiply(item), - ); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialWithdraw.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - this.decryptionKey.publicKey().toUint8Array(), - concatBytes(...this.amount.amountChunks.slice(0, TRANSFER_AMOUNT_CHUNK_COUNT).map((a) => numberToBytesLE(a, 32))), - concatBytes( - ...this.senderEncryptedAvailableBalance - .getCipherText() - .map((el) => el.serialize()) - .flat(), - ), - X1.toRawBytes(), - X2.toRawBytes(), - ...X3List.map((el) => el.toRawBytes()), - ...X4List.map((el) => el.toRawBytes()), - ); - - const sLE = bytesToNumberLE(this.decryptionKey.toUint8Array()); - const invertSLE = ed25519InvertN(sLE); - - const ps = ed25519modN(p * sLE); - const psInvert = ed25519modN(p * invertSLE); - - const alpha1List = x1List.map((el, i) => { - const pChunk = ed25519modN(p * this.senderEncryptedAvailableBalanceAfterWithdrawal.getAmountChunks()[i]); - return ed25519modN(el - pChunk); - }); - const alpha2 = ed25519modN(x2 - ps); - const alpha3 = ed25519modN(x3 - psInvert); - const alpha4List = x4List.map((el, i) => { - const rChunk = ed25519modN(p * this.randomness[i]); - return ed25519modN(el - rChunk); - }); - - return { - alpha1List: alpha1List.map((el) => numberToBytesLE(el, 32)), - alpha2: numberToBytesLE(alpha2, 32), - alpha3: numberToBytesLE(alpha3, 32), - alpha4List: alpha4List.map((el) => numberToBytesLE(el, 32)), - X1: X1.toRawBytes(), - X2: X2.toRawBytes(), - X3List: X3List.map((el) => el.toRawBytes()), - X4List: X4List.map((el) => el.toRawBytes()), - }; - } - - static verifySigmaProof(opts: { - sigmaProof: ConfidentialWithdrawSigmaProof; - senderEncryptedAvailableBalance: EncryptedAmount; - senderEncryptedAvailableBalanceAfterWithdrawal: EncryptedAmount; - amountToWithdraw: bigint; - }): boolean { - const publicKeyU8 = opts.senderEncryptedAvailableBalance.publicKey.toUint8Array(); - const confidentialAmountToWithdraw = ChunkedAmount.fromAmount(opts.amountToWithdraw, { - chunksCount: TRANSFER_AMOUNT_CHUNK_COUNT, + return proveWithdrawal({ + dk: this.decryptionKey, + senderAddress: this.senderAddress, + tokenAddress: this.tokenAddress, + chainId: this.chainId, + amount: this.amount, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks: this.senderEncryptedAvailableBalanceAfterWithdrawal.getAmountChunks(), + newRandomness: this.randomness, + auditorEncryptionKey, + newBalanceDAud, }); - - const alpha1LEList = opts.sigmaProof.alpha1List.map((a) => bytesToNumberLE(a)); - const alpha2LE = bytesToNumberLE(opts.sigmaProof.alpha2); - const alpha3LE = bytesToNumberLE(opts.sigmaProof.alpha3); - const alpha4LEList = opts.sigmaProof.alpha4List.map((a) => bytesToNumberLE(a)); - - const p = genFiatShamirChallenge( - utf8ToBytes(ConfidentialWithdraw.FIAT_SHAMIR_SIGMA_DST), - RistrettoPoint.BASE.toRawBytes(), - H_RISTRETTO.toRawBytes(), - publicKeyU8, - ...confidentialAmountToWithdraw.amountChunks - .slice(0, TRANSFER_AMOUNT_CHUNK_COUNT) - .map((a) => numberToBytesLE(a, 32)), - opts.senderEncryptedAvailableBalance.getCipherTextBytes(), - opts.sigmaProof.X1, - opts.sigmaProof.X2, - ...opts.sigmaProof.X3List, - ...opts.sigmaProof.X4List, - ); - - const { DOldSum, COldSum } = opts.senderEncryptedAvailableBalance.getCipherText().reduce( - (acc, { C, D }, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - return { - DOldSum: acc.DOldSum.add(D.multiply(coef)), - COldSum: acc.COldSum.add(C.multiply(coef)), - }; - }, - { DOldSum: RistrettoPoint.ZERO, COldSum: RistrettoPoint.ZERO }, - ); - - const X1 = RistrettoPoint.BASE.multiply( - ed25519modN( - alpha1LEList.reduce((acc, el, i) => { - const coef = 2n ** (BigInt(i) * CHUNK_BITS_BIG_INT); - const elCoef = el * coef; - - return acc + elCoef; - }, 0n), - ), - ) - .add(DOldSum.multiply(alpha2LE)) - .add(COldSum.multiply(p)) - .subtract(RistrettoPoint.BASE.multiply(p).multiply(confidentialAmountToWithdraw.amount)); - const X2 = H_RISTRETTO.multiply(alpha3LE).add(RistrettoPoint.fromHex(publicKeyU8).multiply(p)); - - const X3List = alpha1LEList.map((el, i) => { - const a1iG = RistrettoPoint.BASE.multiply(el); - const a4iH = H_RISTRETTO.multiply(alpha4LEList[i]); - const pC = opts.senderEncryptedAvailableBalanceAfterWithdrawal.getCipherText()[i].C.multiply(p); - return a1iG.add(a4iH).add(pC); - }); - const X4List = alpha4LEList.map((el, i) => { - const a4iP = RistrettoPoint.fromHex(publicKeyU8).multiply(el); - - const pDNew = opts.senderEncryptedAvailableBalanceAfterWithdrawal.getCipherText()[i].D.multiply(p); - - return a4iP.add(pDNew); - }); - - return ( - X1.equals(RistrettoPoint.fromHex(opts.sigmaProof.X1)) && - X2.equals(RistrettoPoint.fromHex(opts.sigmaProof.X2)) && - X3List.every((X3, i) => X3.equals(RistrettoPoint.fromHex(opts.sigmaProof.X3List[i]))) && - X4List.every((X4, i) => X4.equals(RistrettoPoint.fromHex(opts.sigmaProof.X4List[i]))) - ); } async genRangeProof() { @@ -334,16 +199,21 @@ export class ConfidentialWithdraw { async authorizeWithdrawal(): Promise< [ { - sigmaProof: ConfidentialWithdrawSigmaProof; + sigmaProof: SigmaProtocolProof; rangeProof: Uint8Array; }, EncryptedAmount, + EncryptedAmount | undefined, ] > { - const sigmaProof = await this.genSigmaProof(); + const sigmaProof = this.genSigmaProof(); const rangeProof = await this.genRangeProof(); - return [{ sigmaProof, rangeProof }, this.senderEncryptedAvailableBalanceAfterWithdrawal]; + return [ + { sigmaProof, rangeProof }, + this.senderEncryptedAvailableBalanceAfterWithdrawal, + this.auditorEncryptedBalanceAfterWithdrawal, + ]; } static async verifyRangeProof(opts: { diff --git a/confidential-assets/src/crypto/index.ts b/confidential-assets/src/crypto/index.ts index dc6a1b1b6..116f4caf5 100644 --- a/confidential-assets/src/crypto/index.ts +++ b/confidential-assets/src/crypto/index.ts @@ -1,8 +1,14 @@ export * from "./twistedEd25519"; export * from "./twistedElGamal"; +export * from "./bsgs"; export * from "./rangeProof"; +export { initializeWasm, isWasmInitialized, ensureWasmInitialized } from "./wasmLoader"; export * from "./chunkedAmount"; export * from "./encryptedAmount"; +export * from "./sigmaProtocol"; +export * from "./sigmaProtocolRegistration"; +export * from "./sigmaProtocolWithdraw"; +export * from "./sigmaProtocolTransfer"; export * from "./confidentialKeyRotation"; export * from "./confidentialNormalization"; export * from "./confidentialTransfer"; diff --git a/confidential-assets/src/crypto/rangeProof.ts b/confidential-assets/src/crypto/rangeProof.ts index 97a408a2a..ecf440281 100644 --- a/confidential-assets/src/crypto/rangeProof.ts +++ b/confidential-assets/src/crypto/rangeProof.ts @@ -1,15 +1,23 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 -import initWasm, { - range_proof as rangeProof, - verify_proof as verifyProof, - batch_range_proof as batchRangeProof, - batch_verify_proof as batchVerifyProof, -} from "@aptos-labs/confidential-asset-wasm-bindings/range-proofs"; - -const RANGE_PROOF_WASM_URL = - "https://unpkg.com/@aptos-labs/confidential-asset-wasm-bindings@0.0.2/range-proofs/aptos_rp_wasm_bg.wasm"; +import { + ensureWasmInitialized, + initializeWasm, + rangeProofWasm, + verifyProofWasm, + batchRangeProofWasm, + batchVerifyProofWasm, +} from "./wasmLoader"; + +/** + * Initialize range proof WASM module. + * @param wasmSource - Optional WASM source: URL string, or Buffer/ArrayBuffer for Node.js + * @deprecated Use initializeWasm() from wasmLoader instead for unified initialization + */ +export async function initializeRangeProofWasm(wasmSource?: string | BufferSource) { + await initializeWasm(wasmSource); +} export interface RangeProofInputs { v: bigint; @@ -54,9 +62,9 @@ export class RangeProofExecutor { * @param opts.bits Bits size of value to create the range proof */ static async generateRangeZKP(opts: RangeProofInputs): Promise<{ proof: Uint8Array; commitment: Uint8Array }> { - await initWasm({ module_or_path: RANGE_PROOF_WASM_URL }); + await ensureWasmInitialized(); - const proof = rangeProof(opts.v, opts.r, opts.valBase, opts.randBase, opts.bits ?? 32); + const proof = rangeProofWasm(opts.v, opts.r, opts.valBase, opts.randBase, opts.bits ?? 32); return { proof: proof.proof(), @@ -74,9 +82,9 @@ export class RangeProofExecutor { * @param opts.bits Bits size of the value for range proof */ static async verifyRangeZKP(opts: VerifyRangeProofInputs): Promise { - await initWasm({ module_or_path: RANGE_PROOF_WASM_URL }); + await ensureWasmInitialized(); - return verifyProof(opts.proof, opts.commitment, opts.valBase, opts.randBase, opts.bits ?? 32); + return verifyProofWasm(opts.proof, opts.commitment, opts.valBase, opts.randBase, opts.bits ?? 32); } /** @@ -91,9 +99,15 @@ export class RangeProofExecutor { static async genBatchRangeZKP( opts: BatchRangeProofInputs, ): Promise<{ proof: Uint8Array; commitments: Uint8Array[] }> { - await initWasm({ module_or_path: RANGE_PROOF_WASM_URL }); + await ensureWasmInitialized(); - const proof = batchRangeProof(new BigUint64Array(opts.v), opts.rs, opts.val_base, opts.rand_base, opts.num_bits); + const proof = batchRangeProofWasm( + new BigUint64Array(opts.v), + opts.rs, + opts.val_base, + opts.rand_base, + opts.num_bits, + ); return { proof: proof.proof(), @@ -111,8 +125,8 @@ export class RangeProofExecutor { * @param opts.num_bits Bits size of values to create the range proof */ static async verifyBatchRangeZKP(opts: BatchVerifyRangeProofInputs): Promise { - await initWasm({ module_or_path: RANGE_PROOF_WASM_URL }); + await ensureWasmInitialized(); - return batchVerifyProof(opts.proof, opts.comm, opts.val_base, opts.rand_base, opts.num_bits); + return batchVerifyProofWasm(opts.proof, opts.comm, opts.val_base, opts.rand_base, opts.num_bits); } } diff --git a/confidential-assets/src/crypto/sigmaProtocol.ts b/confidential-assets/src/crypto/sigmaProtocol.ts new file mode 100644 index 000000000..8961d482d --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocol.ts @@ -0,0 +1,351 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Generic Sigma protocol prover matching the Move verifier in `sigma_protocol.move`. + * + * The Move verifier uses BCS-serialized `FiatShamirInputs` for the Fiat-Shamir challenge, + * so the prover must produce the exact same serialization. + */ + +import { sha512 } from "@noble/hashes/sha512"; +import { bytesToNumberLE, numberToBytesLE } from "@noble/curves/abstract/utils"; +import { Serializer, U64, Serializable, FixedBytes } from "@aptos-labs/ts-sdk"; +import { ed25519modN, ed25519GenListOfRandom } from "../utils"; +import { utf8ToBytes } from "@noble/hashes/utils"; +import { RistrettoPoint } from "."; +import type { RistPoint } from "."; + +// ============================================================================= +// Domain Separator & Session +// ============================================================================= + +/** + * The `@aptos_experimental` contract address (0x7) as 32 raw bytes. + * Used in domain separators for defense-in-depth binding to the deployed contract. + */ +export const APTOS_EXPERIMENTAL_ADDRESS = (() => { + const addr = new Uint8Array(32); + addr[31] = 0x07; + return addr; +})(); + +/** + * Matches the Move `DomainSeparator` struct: + * ```move + * enum DomainSeparator { V1 { contract_address: address, chain_id: u8, protocol_id: vector, session_id: vector } } + * ``` + */ +export interface DomainSeparator { + contractAddress: Uint8Array; + chainId: number; + protocolId: Uint8Array; + sessionId: Uint8Array; +} + +/** + * BCS-serializable DomainSeparator (enum with V1 variant). + */ +class BcsDomainSeparator extends Serializable { + constructor(public readonly dst: DomainSeparator) { + super(); + } + + serialize(serializer: Serializer): void { + serializer.serializeU32AsUleb128(0); // V1 variant index + serializer.serialize(new FixedBytes(this.dst.contractAddress)); + serializer.serializeU8(this.dst.chainId); + serializer.serializeBytes(this.dst.protocolId); + serializer.serializeBytes(this.dst.sessionId); + } +} + +/** + * Serialize a KeyRotationSession to BCS bytes (to be used as sessionId in DomainSeparator). + * + * Matches the Move struct: + * ```move + * struct KeyRotationSession { sender: address, token_type: Object, num_chunks: u64 } + * ``` + */ +export function bcsSerializeKeyRotationSession( + senderAddress: Uint8Array, + tokenTypeAddress: Uint8Array, + numChunks: number, +): Uint8Array { + const serializer = new Serializer(); + // address is 32 raw bytes (FixedBytes, no length prefix) + serializer.serialize(new FixedBytes(senderAddress)); + // Object is 32 raw bytes (FixedBytes, no length prefix) + serializer.serialize(new FixedBytes(tokenTypeAddress)); + // u64 is 8 bytes LE + serializer.serialize(new U64(numChunks)); + return serializer.toUint8Array(); +} + +// ============================================================================= +// Statement +// ============================================================================= + +/** + * A public statement for a Sigma protocol, matching `sigma_protocol_statement::Statement`. + * Contains points (RistrettoPoints), their compressed forms, and optional scalars. + */ +export interface SigmaProtocolStatement { + /** The decompressed points in the statement */ + points: RistPoint[]; + /** The compressed (serialized) points (32 bytes each) */ + compressedPoints: Uint8Array[]; + /** Optional public scalars in the statement (empty for key rotation) */ + scalars: Uint8Array[]; +} + +// ============================================================================= +// Fiat-Shamir Challenge +// ============================================================================= + +/** + * BCS-serializable `FiatShamirInputs` struct matching the Move definition: + * ```move + * struct FiatShamirInputs { + * dst: DomainSeparator, + * type_name: String, + * k: u64, + * stmt_X: vector, + * stmt_x: vector, + * proof_A: vector, + * } + * ``` + */ +class BcsFiatShamirInputs extends Serializable { + constructor( + public readonly dst: DomainSeparator, + public readonly typeName: string, + public readonly k: number, + public readonly stmtX: Uint8Array[], + public readonly stmtx: Uint8Array[], + public readonly proofA: Uint8Array[], + ) { + super(); + } + + serialize(serializer: Serializer): void { + serializer.serialize(new BcsDomainSeparator(this.dst)); + // String in Move is { bytes: vector }, BCS = ULEB128(len) || utf8_bytes + serializer.serializeBytes(utf8ToBytes(this.typeName)); + serializer.serialize(new U64(this.k)); + // vector where CompressedRistretto = { data: vector } + serializer.serializeU32AsUleb128(this.stmtX.length); + for (const p of this.stmtX) { + serializer.serializeBytes(p); + } + // vector where Scalar = { data: vector } + serializer.serializeU32AsUleb128(this.stmtx.length); + for (const s of this.stmtx) { + serializer.serializeBytes(s); + } + // vector + serializer.serializeU32AsUleb128(this.proofA.length); + for (const a of this.proofA) { + serializer.serializeBytes(a); + } + } +} + +/** + * Converts 64 hash bytes to a scalar using the same method as Move's + * `ristretto255::new_scalar_uniform_from_64_bytes`: interpret as 512-bit LE integer, reduce mod l. + */ +function scalarFromUniform64Bytes(hash: Uint8Array): bigint { + return ed25519modN(bytesToNumberLE(hash)); +} + +/** + * Compute the Fiat-Shamir challenge matching Move's `sigma_protocol_fiat_shamir::fiat_shamir`. + * + * @param typeName - The fully-qualified Move type name of the phantom marker type `P` in `Statement

`. + * E.g., `"0x7::sigma_protocol_registration::Registration"`. Must match `type_info::type_name

()` on-chain. + * + * Returns `{ e, betas }` where `e` is the challenge scalar and `betas = [1, beta, beta^2, ...]`. + */ +export function sigmaProtocolFiatShamir( + dst: DomainSeparator, + typeName: string, + stmt: SigmaProtocolStatement, + compressedA: Uint8Array[], + k: number, +): { e: bigint; betas: bigint[] } { + const m = compressedA.length; + if (m === 0) throw new Error("Proof commitment must not be empty"); + + const fiatShamirInputs = new BcsFiatShamirInputs( + dst, + typeName, + k, + stmt.compressedPoints, + stmt.scalars, + compressedA, + ); + const bytes = fiatShamirInputs.bcsToBytes(); + + // seed = SHA2-512(BCS(inputs)) + const seed = sha512(bytes); + + // e = scalar_from(SHA2-512(seed || 0x00)) + const eInput = new Uint8Array(seed.length + 1); + eInput.set(seed); + eInput[seed.length] = 0x00; + const eHash = sha512(eInput); + + // beta = scalar_from(SHA2-512(seed || 0x01)) + eInput[seed.length] = 0x01; + const betaHash = sha512(eInput); + + const e = scalarFromUniform64Bytes(eHash); + const beta = scalarFromUniform64Bytes(betaHash); + + const betas: bigint[] = [1n]; // beta^0 = 1 + let prevBeta = 1n; + for (let i = 1; i < m; i++) { + prevBeta = ed25519modN(prevBeta * beta); + betas.push(prevBeta); + } + + return { e, betas }; +} + +// ============================================================================= +// Generic Sigma Protocol Prover +// ============================================================================= + +/** + * A homomorphism function psi: given witness scalars and statement points, + * returns a vector of m RistrettoPoints. + */ +export type PsiFunction = (stmt: SigmaProtocolStatement, witness: bigint[]) => RistPoint[]; + +/** + * A transformation function f: given a statement, returns the target vector + * that psi(witness) should equal. + */ +export type TransformationFunction = (stmt: SigmaProtocolStatement) => RistPoint[]; + +/** + * The result of a Sigma protocol proof. + */ +export interface SigmaProtocolProof { + /** Compressed commitment points A (one per output of psi) */ + commitment: Uint8Array[]; + /** Response scalars sigma = alpha + e * w (one per witness element) */ + response: Uint8Array[]; +} + +/** + * Generic Sigma protocol prover. + * + * Given: + * - A domain separator `dst` + * - A homomorphism `psi` mapping witness scalars to group elements + * - A statement `stmt` containing the public points/scalars + * - A witness `w` (vector of secret scalars) + * + * Produces a proof (A, sigma) where: + * - A = psi(alpha) for random alpha + * - e = FiatShamir(dst, stmt, A, k) + * - sigma = alpha + e * w + */ +export function sigmaProtocolProve( + dst: DomainSeparator, + typeName: string, + psi: PsiFunction, + stmt: SigmaProtocolStatement, + witness: bigint[], +): SigmaProtocolProof { + const k = witness.length; + + // Step 1: Pick random alpha in F^k + const alpha = ed25519GenListOfRandom(k); + + // Step 2: A = psi(alpha) + const _A = psi(stmt, alpha); + + // Step 3: Compress A + const compressedA = _A.map((p) => p.toRawBytes()); + + // Step 4: Derive challenge e via Fiat-Shamir + const { e } = sigmaProtocolFiatShamir(dst, typeName, stmt, compressedA, k); + + // Step 5: sigma_i = alpha_i + e * w_i (mod l) + const sigma = witness.map((w_i, i) => ed25519modN(alpha[i] + e * w_i)); + + return { + commitment: compressedA, + response: sigma.map((s) => numberToBytesLE(s, 32)), + }; +} + +// ============================================================================= +// Generic Sigma Protocol Verifier +// ============================================================================= + +/** + * Generic Sigma protocol verifier. + * + * Verifies a proof (A, sigma) by checking: + * psi(sigma) == A + e * f(stmt) + * + * For each output index i: + * psi(stmt, sigma)[i] == A[i] + e * f(stmt)[i] + * + * @param dst - Domain separator for Fiat-Shamir + * @param psi - Homomorphism function mapping scalars to group elements + * @param f - Transformation function that extracts target points from statement + * @param stmt - The public statement + * @param proof - The proof to verify (commitment A and response sigma) + * @returns true if the proof verifies, false otherwise + */ +export function sigmaProtocolVerify( + dst: DomainSeparator, + typeName: string, + psi: PsiFunction, + f: TransformationFunction, + stmt: SigmaProtocolStatement, + proof: SigmaProtocolProof, +): boolean { + const { commitment, response } = proof; + const m = commitment.length; + const k = response.length; + + if (m === 0) return false; + + // Convert response bytes back to bigints + const sigma = response.map((r) => bytesToNumberLE(r)); + + // Recompute the challenge e + const { e } = sigmaProtocolFiatShamir(dst, typeName, stmt, commitment, k); + + // Compute psi(sigma) - evaluating the homomorphism on the response + const psiSigma = psi(stmt, sigma); + + // Compute f(stmt) - the transformation function output + const fStmt = f(stmt); + + if (psiSigma.length !== m || fStmt.length !== m) { + return false; + } + + // Decompress commitment points A + const _A = commitment.map((c) => RistrettoPoint.fromHex(c)); + + // Check: psi(sigma)[i] == A[i] + e * f(stmt)[i] for all i + for (let i = 0; i < m; i++) { + // RHS = A[i] + e * f(stmt)[i] + const rhs = _A[i].add(fStmt[i].multiply(e)); + + if (!psiSigma[i].equals(rhs)) { + return false; + } + } + + return true; +} diff --git a/confidential-assets/src/crypto/sigmaProtocolRegistration.ts b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts new file mode 100644 index 000000000..51209abea --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts @@ -0,0 +1,150 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Sigma protocol proof for confidential asset registration. + * + * The NP relation (from `sigma_protocol_registration.move`): + * + * H = dk * ek + * + * where: + * - H is the encryption key basepoint (= hash_to_point_base) + * - ek is the encryption key being registered + * - dk is the secret decryption key + * + * The homomorphism psi(dk) outputs: [dk * ek] + * The transformation function f outputs: [H] + */ + +import { bytesToNumberLE } from "@noble/curves/abstract/utils"; +import { utf8ToBytes } from "@noble/hashes/utils"; +import { RistrettoPoint, H_RISTRETTO, TwistedEd25519PrivateKey } from "."; +import type { RistPoint } from "."; +import { + sigmaProtocolProve, + sigmaProtocolVerify, + APTOS_EXPERIMENTAL_ADDRESS, + type DomainSeparator, + type SigmaProtocolStatement, + type SigmaProtocolProof, + type PsiFunction, + type TransformationFunction, +} from "./sigmaProtocol"; +import { Serializer, FixedBytes } from "@aptos-labs/ts-sdk"; + +/** Protocol ID matching the Move constant */ +const PROTOCOL_ID = "AptosConfidentialAsset/RegistrationV1"; + +/** Fully-qualified Move type name for the phantom marker type, matching `type_info::type_name()` */ +const TYPE_NAME = "0x7::sigma_protocol_registration::Registration"; + +/** Statement point indices */ +const IDX_H = 0; +const IDX_EK = 1; + +/** + * BCS-serialize a RegistrationSession matching the Move struct: + * ```move + * struct RegistrationSession { sender: address, asset_type: Object } + * ``` + */ +export function bcsSerializeRegistrationSession( + senderAddress: Uint8Array, + tokenTypeAddress: Uint8Array, +): Uint8Array { + const serializer = new Serializer(); + serializer.serialize(new FixedBytes(senderAddress)); + serializer.serialize(new FixedBytes(tokenTypeAddress)); + return serializer.toUint8Array(); +} + +/** + * Build the homomorphism psi for registration. + * + * psi(dk) = [dk * ek] + */ +function makeRegistrationPsi(): PsiFunction { + return (s: SigmaProtocolStatement, w: bigint[]): RistPoint[] => { + const dk = w[0]; + const ek = s.points[IDX_EK]; + return [ek.multiply(dk)]; + }; +} + +/** + * Build the transformation function f for registration. + * + * f(stmt) = [H] + */ +function makeRegistrationF(): TransformationFunction { + return (s: SigmaProtocolStatement): RistPoint[] => { + return [s.points[IDX_H]]; + }; +} + +/** + * Prove knowledge of dk such that ek = dk^{-1} * H. + */ +export function proveRegistration(args: { + dk: TwistedEd25519PrivateKey; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; +}): SigmaProtocolProof { + const { dk, senderAddress, tokenAddress, chainId } = args; + const dkBigint = bytesToNumberLE(dk.toUint8Array()); + + const ekBytes = dk.publicKey().toUint8Array(); + const ek = RistrettoPoint.fromHex(ekBytes); + const H = H_RISTRETTO; + + const stmt: SigmaProtocolStatement = { + points: [H, ek], + compressedPoints: [H.toRawBytes(), ekBytes], + scalars: [], + }; + + const witness = [dkBigint]; + + const sessionId = bcsSerializeRegistrationSession(senderAddress, tokenAddress); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolProve(dst, TYPE_NAME, makeRegistrationPsi(), stmt, witness); +} + +/** + * Verify a registration sigma protocol proof. + */ +export function verifyRegistration(args: { + ek: Uint8Array; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + proof: SigmaProtocolProof; +}): boolean { + const { ek: ekBytes, senderAddress, tokenAddress, chainId, proof } = args; + const ek = RistrettoPoint.fromHex(ekBytes); + const H = H_RISTRETTO; + + const stmt: SigmaProtocolStatement = { + points: [H, ek], + compressedPoints: [H.toRawBytes(), ekBytes], + scalars: [], + }; + + const sessionId = bcsSerializeRegistrationSession(senderAddress, tokenAddress); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolVerify(dst, TYPE_NAME, makeRegistrationPsi(), makeRegistrationF(), stmt, proof); +} diff --git a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts new file mode 100644 index 000000000..2a71971a6 --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts @@ -0,0 +1,587 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Sigma protocol proof for confidential asset transfer. + * + * The NP relation (from `sigma_protocol_transfer.move`): + * + * Combines a "veiled withdrawal" on the sender side with an "equality" proof linking + * the sender's transfer amount ciphertexts to the recipient's. + * + * Statement points: + * Base: [G, H, ek_sid, ek_rid, old_P[ell], old_R[ell], new_P[ell], new_R[ell], P[n], R_sid[n], R_rid[n]] + * If has_effective_auditor: + [ek_aud_eff, new_R_aud_eff[ell], R_aud_eff[n]] + * For each voluntary auditor: + [ek_volun, R_volun[n]] + * + * Witness: [dk, new_a[ell], new_r[ell], v[n], r[n]] + */ + +import { bytesToNumberLE, numberToBytesLE } from "@noble/curves/abstract/utils"; +import { utf8ToBytes } from "@noble/hashes/utils"; +import { ed25519 } from "@noble/curves/ed25519"; +import { RistrettoPoint, H_RISTRETTO, TwistedEd25519PrivateKey, TwistedEd25519PublicKey } from "."; +import type { RistPoint } from "."; +import { ed25519modN } from "../utils"; +import { + sigmaProtocolProve, + sigmaProtocolVerify, + APTOS_EXPERIMENTAL_ADDRESS, + type DomainSeparator, + type SigmaProtocolStatement, + type SigmaProtocolProof, + type PsiFunction, + type TransformationFunction, +} from "./sigmaProtocol"; +import { Serializer, FixedBytes, U64 } from "@aptos-labs/ts-sdk"; + +const PROTOCOL_ID = "AptosConfidentialAsset/TransferV1"; + +/** Fully-qualified Move type name for the phantom marker type, matching `type_info::type_name()` */ +const TYPE_NAME = "0x7::sigma_protocol_transfer::Transfer"; + +/** + * BCS-serialize a TransferSession matching the Move struct: + * ```move + * struct TransferSession { + * sender: address, + * recipient: address, + * asset_type: Object, + * num_avail_chunks: u64, + * num_transfer_chunks: u64, + * has_effective_auditor: bool, + * num_volun_auditors: u64, + * } + * ``` + */ +export function bcsSerializeTransferSession( + senderAddress: Uint8Array, + recipientAddress: Uint8Array, + tokenTypeAddress: Uint8Array, + numAvailChunks: number, + numTransferChunks: number, + hasEffectiveAuditor: boolean, + numVolunAuditors: number, +): Uint8Array { + const serializer = new Serializer(); + serializer.serialize(new FixedBytes(senderAddress)); + serializer.serialize(new FixedBytes(recipientAddress)); + serializer.serialize(new FixedBytes(tokenTypeAddress)); + serializer.serialize(new U64(numAvailChunks)); + serializer.serialize(new U64(numTransferChunks)); + serializer.serializeBool(hasEffectiveAuditor); + serializer.serialize(new U64(numVolunAuditors)); + return serializer.toUint8Array(); +} + +/** + * Compute the chunk base powers: [1, 2^16, 2^32, ...] mod l. + */ +function computeBPowers(count: number): bigint[] { + const B = 1n << 16n; + const powers: bigint[] = [1n]; + for (let i = 1; i < count; i++) { + powers.push(ed25519modN(powers[i - 1] * B)); + } + return powers; +} + +/** + * Statement point layout: + * + * Base (always): + * [G, H, ek_sid, ek_rid, old_P[ell], old_R[ell], new_P[ell], new_R[ell], P[n], R_sid[n], R_rid[n]] + * → 4 + 4*ell + 3*n points + * + * If has_effective_auditor: + * + [ek_aud_eff, new_R_aud_eff[ell], R_aud_eff[n]] + * → +1 + ell + n points + * + * For each voluntary auditor i ∈ [num_volun]: + * + [ek_volun_i, R_volun_i[n]] + * → +(1 + n) points per voluntary auditor + */ +const IDX_G = 0; +const IDX_H = 1; +const IDX_EK_SID = 2; +const IDX_EK_RID = 3; +const START_IDX_OLD_P = 4; + +function getStartIdxOldR(ell: number): number { + return START_IDX_OLD_P + ell; +} +function getStartIdxNewP(ell: number): number { + return START_IDX_OLD_P + 2 * ell; +} +function getStartIdxNewR(ell: number): number { + return START_IDX_OLD_P + 3 * ell; +} +function getStartIdxP(ell: number): number { + return START_IDX_OLD_P + 4 * ell; +} +function getStartIdxRSid(ell: number, n: number): number { + return START_IDX_OLD_P + 4 * ell + n; +} +function getStartIdxRRid(ell: number, n: number): number { + return START_IDX_OLD_P + 4 * ell + 2 * n; +} +/** Start of the effective auditor section (if present). */ +function getIdxEkAudEff(ell: number, n: number): number { + return START_IDX_OLD_P + 4 * ell + 3 * n; +} +/** Start of the voluntary auditors section. */ +function getStartIdxVolun(ell: number, n: number, hasEffective: boolean): number { + return START_IDX_OLD_P + 4 * ell + 3 * n + (hasEffective ? 1 + ell + n : 0); +} + +// Note: the old single-auditor makeTransferPsi/makeTransferF have been removed. +// All auditor logic is handled by makeTransferPsi/makeTransferF below. + +export type TransferProofArgs = { + /** The sender's decryption key */ + dk: TwistedEd25519PrivateKey; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte recipient address */ + recipientAddress: Uint8Array; + /** 32-byte token address */ + tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; + /** Sender's encryption key */ + senderEncryptionKey: TwistedEd25519PublicKey; + /** Recipient's encryption key */ + recipientEncryptionKey: TwistedEd25519PublicKey; + /** Old sender balance C (commitment) points, one per chunk (ell chunks) */ + oldBalanceC: RistPoint[]; + /** Old sender balance D (ciphertext) points, one per chunk (ell chunks) */ + oldBalanceD: RistPoint[]; + /** New sender balance C points (ell chunks) */ + newBalanceC: RistPoint[]; + /** New sender balance D points (ell chunks) */ + newBalanceD: RistPoint[]; + /** New balance amount chunks (plaintext values per chunk, ell chunks) */ + newAmountChunks: bigint[]; + /** New balance randomness (ell values) */ + newRandomness: bigint[]; + /** Transfer amount C (commitment) points (n chunks) */ + transferAmountC: RistPoint[]; + /** Transfer amount D for sender (n chunks) */ + transferAmountDSender: RistPoint[]; + /** Transfer amount D for recipient (n chunks) */ + transferAmountDRecipient: RistPoint[]; + /** Transfer amount chunks (plaintext values, n chunks) */ + transferAmountChunks: bigint[]; + /** Transfer amount randomness (n values) */ + transferRandomness: bigint[]; + /** + * Whether an effective (asset-level or global) auditor is present. + * If true, the LAST element in auditorEncryptionKeys / newBalanceDAud / transferAmountDAud + * is the effective auditor; all preceding elements are voluntary auditors. + */ + hasEffectiveAuditor: boolean; + /** Auditor encryption keys: voluntary first, then effective (if hasEffectiveAuditor) */ + auditorEncryptionKeys?: TwistedEd25519PublicKey[]; + /** New balance D points encrypted under each auditor key (only effective auditor's is used in sigma proof) */ + newBalanceDAud?: RistPoint[][]; + /** Transfer amount D points encrypted under each auditor key (all used in sigma proof) */ + transferAmountDAud?: RistPoint[][]; +}; + +/** + * Prove a confidential transfer. + * + * The sigma proof covers all auditors in a single proof. The statement layout + * distinguishes between the effective auditor (sees balance + transfer amount) + * and voluntary auditors (see only transfer amount). + * + * Convention: if hasEffectiveAuditor is true, the LAST element in auditorEncryptionKeys + * (and newBalanceDAud / transferAmountDAud) is the effective auditor. All preceding + * elements are voluntary. + */ +export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { + const { + dk, + senderAddress, + recipientAddress, + tokenAddress, + chainId, + senderEncryptionKey, + recipientEncryptionKey, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks, + newRandomness, + transferAmountC, + transferAmountDSender, + transferAmountDRecipient, + transferAmountChunks, + transferRandomness, + hasEffectiveAuditor, + auditorEncryptionKeys = [], + newBalanceDAud = [], + transferAmountDAud = [], + } = args; + + const ell = oldBalanceC.length; + const n = transferAmountC.length; + const numVolun = hasEffectiveAuditor + ? auditorEncryptionKeys.length - 1 + : auditorEncryptionKeys.length; + const dkBigint = bytesToNumberLE(dk.toUint8Array()); + + const G = RistrettoPoint.BASE; + const H = H_RISTRETTO; + const ekSidBytes = senderEncryptionKey.toUint8Array(); + const ekSid = RistrettoPoint.fromHex(ekSidBytes); + const ekRidBytes = recipientEncryptionKey.toUint8Array(); + const ekRid = RistrettoPoint.fromHex(ekRidBytes); + + // Build statement points — base + const stmtPoints: RistPoint[] = [G, H, ekSid, ekRid]; + const stmtCompressed: Uint8Array[] = [G.toRawBytes(), H.toRawBytes(), ekSidBytes, ekRidBytes]; + + const pushPoint = (p: RistPoint) => { stmtPoints.push(p); stmtCompressed.push(p.toRawBytes()); }; + const pushPointBytes = (p: RistPoint, bytes: Uint8Array) => { stmtPoints.push(p); stmtCompressed.push(bytes); }; + + for (let i = 0; i < ell; i++) pushPoint(oldBalanceC[i]); // old_P + for (let i = 0; i < ell; i++) pushPoint(oldBalanceD[i]); // old_R + for (let i = 0; i < ell; i++) pushPoint(newBalanceC[i]); // new_P + for (let i = 0; i < ell; i++) pushPoint(newBalanceD[i]); // new_R + for (let j = 0; j < n; j++) pushPoint(transferAmountC[j]); // P + for (let j = 0; j < n; j++) pushPoint(transferAmountDSender[j]); // R_sid + for (let j = 0; j < n; j++) pushPoint(transferAmountDRecipient[j]); // R_rid + + // Effective auditor: [ek_eff, new_R_aud_eff[ell], R_aud_eff[n]] + if (hasEffectiveAuditor) { + const effIdx = auditorEncryptionKeys.length - 1; + const ekEffBytes = auditorEncryptionKeys[effIdx].toUint8Array(); + pushPointBytes(RistrettoPoint.fromHex(ekEffBytes), ekEffBytes); + for (let i = 0; i < ell; i++) pushPoint(newBalanceDAud[effIdx][i]); + for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[effIdx][j]); + } + + // Voluntary auditors: for each, [ek_volun, R_volun[n]] + for (let a = 0; a < numVolun; a++) { + const ekVolunBytes = auditorEncryptionKeys[a].toUint8Array(); + pushPointBytes(RistrettoPoint.fromHex(ekVolunBytes), ekVolunBytes); + for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[a][j]); + } + + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressed, + scalars: [], + }; + + // Witness: [dk, new_a[ell], new_r[ell], v[n], r[n]] + const witness: bigint[] = [dkBigint, ...newAmountChunks, ...newRandomness, ...transferAmountChunks, ...transferRandomness]; + + // Domain separator + const sessionId = bcsSerializeTransferSession( + senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numVolun, + ); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolProve(dst, TYPE_NAME, makeTransferPsi(ell, n, hasEffectiveAuditor, numVolun), stmt, witness); +} + +/** + * Build the homomorphism psi for the transfer relation. + * + * Matches the Move implementation ordering: + * 1. dk * ek_sid + * 2. new_a[i]*G + new_r[i]*H, ∀i ∈ [ℓ] + * 3. new_r[i]*ek_sid, ∀i ∈ [ℓ] + * 3b. new_r[i]*ek_aud_eff, ∀i ∈ [ℓ] (effective auditor only) + * 4. dk*⟨B,old_R⟩ + (⟨B,new_a⟩ + ⟨B,v⟩)*G + * 5. v[j]*G + r[j]*H, ∀j ∈ [n] + * 6. r[j]*ek_sid, ∀j ∈ [n] + * 7. r[j]*ek_rid, ∀j ∈ [n] + * 7b. r[j]*ek_aud_eff, ∀j ∈ [n] (effective auditor only) + * 7c. r[j]*ek_volun_t, ∀j ∈ [n], ∀t ∈ [T] (voluntary auditors) + */ +function makeTransferPsi(ell: number, n: number, hasEffective: boolean, numVolun: number): PsiFunction { + return (s: SigmaProtocolStatement, w: bigint[]): RistPoint[] => { + const dk = w[0]; + const newA = w.slice(1, 1 + ell); + const newR = w.slice(1 + ell, 1 + 2 * ell); + const vChunks = w.slice(1 + 2 * ell, 1 + 2 * ell + n); + const rTransfer = w.slice(1 + 2 * ell + n, 1 + 2 * ell + 2 * n); + + const G = s.points[IDX_G]; + const H = s.points[IDX_H]; + const ekSid = s.points[IDX_EK_SID]; + const ekRid = s.points[IDX_EK_RID]; + + const result: RistPoint[] = []; + + // 1. dk * ek_sid + result.push(ekSid.multiply(dk)); + + // 2. new_a[i]*G + new_r[i]*H + for (let i = 0; i < ell; i++) { + result.push(G.multiply(newA[i]).add(H.multiply(newR[i]))); + } + + // 3. new_r[i]*ek_sid + for (let i = 0; i < ell; i++) { + result.push(ekSid.multiply(newR[i])); + } + + // 3b. (effective auditor only) new_r[i]*ek_aud_eff + if (hasEffective) { + const ekAudEff = s.points[getIdxEkAudEff(ell, n)]; + for (let i = 0; i < ell; i++) { + result.push(ekAudEff.multiply(newR[i])); + } + } + + // 4. Balance equation: dk*⟨B,old_R⟩ + (⟨B,new_a⟩ + ⟨B,v⟩)*G + const bPowersEll = computeBPowers(ell); + const bPowersN = computeBPowers(n); + let balanceResult = RistrettoPoint.ZERO; + const startOldR = getStartIdxOldR(ell); + for (let i = 0; i < ell; i++) { + balanceResult = balanceResult.add(s.points[startOldR + i].multiply(ed25519modN(dk * bPowersEll[i]))); + } + for (let i = 0; i < ell; i++) { + balanceResult = balanceResult.add(G.multiply(ed25519modN(newA[i] * bPowersEll[i]))); + } + for (let j = 0; j < n; j++) { + balanceResult = balanceResult.add(G.multiply(ed25519modN(vChunks[j] * bPowersN[j]))); + } + result.push(balanceResult); + + // 5. v[j]*G + r[j]*H + for (let j = 0; j < n; j++) { + result.push(G.multiply(vChunks[j]).add(H.multiply(rTransfer[j]))); + } + + // 6. r[j]*ek_sid + for (let j = 0; j < n; j++) { + result.push(ekSid.multiply(rTransfer[j])); + } + + // 7. r[j]*ek_rid + for (let j = 0; j < n; j++) { + result.push(ekRid.multiply(rTransfer[j])); + } + + // 7b. (effective auditor only) r[j]*ek_aud_eff + if (hasEffective) { + const ekAudEff = s.points[getIdxEkAudEff(ell, n)]; + for (let j = 0; j < n; j++) { + result.push(ekAudEff.multiply(rTransfer[j])); + } + } + + // 7c. (voluntary auditors) r[j]*ek_volun_t + const volunStart = getStartIdxVolun(ell, n, hasEffective); + for (let t = 0; t < numVolun; t++) { + const ekVolunIdx = volunStart + t * (1 + n); + const ekVolun = s.points[ekVolunIdx]; + for (let j = 0; j < n; j++) { + result.push(ekVolun.multiply(rTransfer[j])); + } + } + + return result; + }; +} + +/** + * Build the transformation function f for the transfer relation. + * + * Matches the Move implementation ordering (mirrors psi with statement points). + */ +function makeTransferF(ell: number, n: number, hasEffective: boolean, numVolun: number): TransformationFunction { + return (s: SigmaProtocolStatement): RistPoint[] => { + const result: RistPoint[] = []; + + // 1. H + result.push(s.points[IDX_H]); + + // 2. new_P[i] + const startNewP = getStartIdxNewP(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewP + i]); + } + + // 3. new_R[i] + const startNewR = getStartIdxNewR(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewR + i]); + } + + // 3b. (effective auditor only) new_R_aud_eff[i] + if (hasEffective) { + const newRAudStart = getIdxEkAudEff(ell, n) + 1; + for (let i = 0; i < ell; i++) { + result.push(s.points[newRAudStart + i]); + } + } + + // 4. Balance equation target: ⟨B,old_P⟩ + const bPowersEll = computeBPowers(ell); + let balanceTarget = RistrettoPoint.ZERO; + for (let i = 0; i < ell; i++) { + balanceTarget = balanceTarget.add(s.points[START_IDX_OLD_P + i].multiply(bPowersEll[i])); + } + result.push(balanceTarget); + + // 5. P[j] + const startP = getStartIdxP(ell); + for (let j = 0; j < n; j++) { + result.push(s.points[startP + j]); + } + + // 6. R_sid[j] + const startRSid = getStartIdxRSid(ell, n); + for (let j = 0; j < n; j++) { + result.push(s.points[startRSid + j]); + } + + // 7. R_rid[j] + const startRRid = getStartIdxRRid(ell, n); + for (let j = 0; j < n; j++) { + result.push(s.points[startRRid + j]); + } + + // 7b. (effective auditor only) R_aud_eff[j] + if (hasEffective) { + const rAudEffStart = getIdxEkAudEff(ell, n) + 1 + ell; + for (let j = 0; j < n; j++) { + result.push(s.points[rAudEffStart + j]); + } + } + + // 7c. (voluntary auditors) R_volun_t[j] + const volunStart = getStartIdxVolun(ell, n, hasEffective); + for (let t = 0; t < numVolun; t++) { + const rVolunStart = volunStart + t * (1 + n) + 1; + for (let j = 0; j < n; j++) { + result.push(s.points[rVolunStart + j]); + } + } + + return result; + }; +} + +/** + * Verify a confidential transfer proof. + * + * Convention: if hasEffectiveAuditor, the last element in auditorEkBytes / newBalanceDAud / + * transferAmountDAud is the effective auditor; preceding elements are voluntary. + */ +export function verifyTransfer(args: { + senderAddress: Uint8Array; + recipientAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + ekSidBytes: Uint8Array; + ekRidBytes: Uint8Array; + oldBalanceC: RistPoint[]; + oldBalanceD: RistPoint[]; + newBalanceC: RistPoint[]; + newBalanceD: RistPoint[]; + transferAmountC: RistPoint[]; + transferAmountDSender: RistPoint[]; + transferAmountDRecipient: RistPoint[]; + hasEffectiveAuditor: boolean; + auditorEkBytes?: Uint8Array[]; + newBalanceDAud?: RistPoint[][]; + transferAmountDAud?: RistPoint[][]; + proof: SigmaProtocolProof; +}): boolean { + const { + senderAddress, + recipientAddress, + tokenAddress, + chainId, + ekSidBytes, + ekRidBytes, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + transferAmountC, + transferAmountDSender, + transferAmountDRecipient, + hasEffectiveAuditor, + auditorEkBytes = [], + newBalanceDAud = [], + transferAmountDAud = [], + proof, + } = args; + + const ell = oldBalanceC.length; + const n = transferAmountC.length; + const numVolun = hasEffectiveAuditor ? auditorEkBytes.length - 1 : auditorEkBytes.length; + + const G = RistrettoPoint.BASE; + const H = H_RISTRETTO; + const ekSid = RistrettoPoint.fromHex(ekSidBytes); + const ekRid = RistrettoPoint.fromHex(ekRidBytes); + + const stmtPoints: RistPoint[] = [G, H, ekSid, ekRid]; + const stmtCompressed: Uint8Array[] = [G.toRawBytes(), H.toRawBytes(), ekSidBytes, ekRidBytes]; + + const pushPoint = (p: RistPoint) => { stmtPoints.push(p); stmtCompressed.push(p.toRawBytes()); }; + const pushPointBytes = (p: RistPoint, bytes: Uint8Array) => { stmtPoints.push(p); stmtCompressed.push(bytes); }; + + for (let i = 0; i < ell; i++) pushPoint(oldBalanceC[i]); + for (let i = 0; i < ell; i++) pushPoint(oldBalanceD[i]); + for (let i = 0; i < ell; i++) pushPoint(newBalanceC[i]); + for (let i = 0; i < ell; i++) pushPoint(newBalanceD[i]); + for (let j = 0; j < n; j++) pushPoint(transferAmountC[j]); + for (let j = 0; j < n; j++) pushPoint(transferAmountDSender[j]); + for (let j = 0; j < n; j++) pushPoint(transferAmountDRecipient[j]); + + // Effective auditor: [ek_eff, new_R_aud_eff[ell], R_aud_eff[n]] + if (hasEffectiveAuditor) { + const effIdx = auditorEkBytes.length - 1; + pushPointBytes(RistrettoPoint.fromHex(auditorEkBytes[effIdx]), auditorEkBytes[effIdx]); + for (let i = 0; i < ell; i++) pushPoint(newBalanceDAud[effIdx][i]); + for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[effIdx][j]); + } + + // Voluntary auditors: [ek_volun, R_volun[n]] + for (let a = 0; a < numVolun; a++) { + pushPointBytes(RistrettoPoint.fromHex(auditorEkBytes[a]), auditorEkBytes[a]); + for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[a][j]); + } + + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressed, + scalars: [], + }; + + const sessionId = bcsSerializeTransferSession( + senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numVolun, + ); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolVerify( + dst, + TYPE_NAME, + makeTransferPsi(ell, n, hasEffectiveAuditor, numVolun), + makeTransferF(ell, n, hasEffectiveAuditor, numVolun), + stmt, + proof, + ); +} diff --git a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts new file mode 100644 index 000000000..4eb3db6f8 --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts @@ -0,0 +1,502 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Sigma protocol proof for confidential asset withdrawal and normalization. + * + * The NP relation (from `sigma_protocol_withdraw.move`): + * + * Given old balance ciphertexts (old_P, old_R) and new balance ciphertexts (new_P, new_R), + * proves knowledge of dk, new_a[], new_r[] such that: + * + * 1. H = dk * ek (knowledge of dk) + * 2. new_P[i] = new_a[i] * G + new_r[i] * H (new commitments are well-formed) + * 3. new_R[i] = new_r[i] * ek (new ciphertext consistency) + * 4. - v*G = dk * + * G (balance equation) + * + * where B = (1, 2^16, 2^32, ...) are the chunk base powers. + * + * For normalization, v = 0 (same protocol ID). + * + * When an auditor is present, additional outputs prove new_R_aud[i] = new_r[i] * ek_aud. + */ + +import { bytesToNumberLE, numberToBytesLE } from "@noble/curves/abstract/utils"; +import { utf8ToBytes } from "@noble/hashes/utils"; +import { ed25519 } from "@noble/curves/ed25519"; +import { RistrettoPoint, H_RISTRETTO, TwistedEd25519PrivateKey, TwistedEd25519PublicKey } from "."; +import type { RistPoint } from "."; +import { ed25519modN } from "../utils"; +import { + sigmaProtocolProve, + sigmaProtocolVerify, + APTOS_EXPERIMENTAL_ADDRESS, + type DomainSeparator, + type SigmaProtocolStatement, + type SigmaProtocolProof, + type PsiFunction, + type TransformationFunction, +} from "./sigmaProtocol"; +import { Serializer, FixedBytes, U64 } from "@aptos-labs/ts-sdk"; + +const PROTOCOL_ID_WITHDRAWAL = "AptosConfidentialAsset/WithdrawalV1"; + +/** Fully-qualified Move type name for the phantom marker type, matching `type_info::type_name()` */ +const TYPE_NAME = "0x7::sigma_protocol_withdraw::Withdrawal"; + +/** + * BCS-serialize a WithdrawSession matching the Move struct: + * ```move + * struct WithdrawSession { sender: address, asset_type: Object, num_chunks: u64, has_auditor: bool } + * ``` + */ +export function bcsSerializeWithdrawSession( + senderAddress: Uint8Array, + tokenTypeAddress: Uint8Array, + numChunks: number, + hasAuditor: boolean, +): Uint8Array { + const serializer = new Serializer(); + serializer.serialize(new FixedBytes(senderAddress)); + serializer.serialize(new FixedBytes(tokenTypeAddress)); + serializer.serialize(new U64(numChunks)); + serializer.serializeBool(hasAuditor); + return serializer.toUint8Array(); +} + +/** + * Compute the chunk base powers: [1, 2^16, 2^32, ...] mod l. + */ +function computeBPowers(count: number): bigint[] { + const B = 1n << 16n; + const powers: bigint[] = [1n]; + for (let i = 1; i < count; i++) { + powers.push(ed25519modN(powers[i - 1] * B)); + } + return powers; +} + +/** Statement point layout (auditorless): + * [G, H, ek, old_P[0..ell-1], old_R[0..ell-1], new_P[0..ell-1], new_R[0..ell-1]] + * total = 3 + 4*ell + * + * With auditor: + * [..., ek_aud, new_R_aud[0..ell-1]] + * total = 4 + 5*ell + */ +const IDX_G = 0; +const IDX_H = 1; +const IDX_EK = 2; +const START_IDX_OLD_P = 3; + +function getStartIdxOldR(ell: number): number { + return START_IDX_OLD_P + ell; +} +function getStartIdxNewP(ell: number): number { + return START_IDX_OLD_P + 2 * ell; +} +function getStartIdxNewR(ell: number): number { + return START_IDX_OLD_P + 3 * ell; +} +function getIdxEkAud(ell: number): number { + return START_IDX_OLD_P + 4 * ell; +} +function getStartIdxNewRAud(ell: number): number { + return START_IDX_OLD_P + 4 * ell + 1; +} + +/** + * Build the homomorphism psi for withdrawal/normalization. + * + * Witness layout: [dk, new_a[0..ell-1], new_r[0..ell-1]] + * + * psi outputs (auditorless, m = 2 + 2*ell): + * 0: dk * ek + * 1..ell: new_a[i] * G + new_r[i] * H + * ell+1..2*ell: new_r[i] * ek + * 2*ell+1: dk * + * G (balance equation) + * + * With auditor (m = 2 + 3*ell): + * Insert new_r[i] * ek_aud between the ek outputs and the balance equation. + */ +function makeWithdrawPsi(ell: number, hasAuditor: boolean): PsiFunction { + return (s: SigmaProtocolStatement, w: bigint[]): RistPoint[] => { + const dk = w[0]; + const newA = w.slice(1, 1 + ell); + const newR = w.slice(1 + ell, 1 + 2 * ell); + + const G = s.points[IDX_G]; + const H = s.points[IDX_H]; + const ek = s.points[IDX_EK]; + + const result: RistPoint[] = []; + + // Output 0: dk * ek + result.push(ek.multiply(dk)); + + // Outputs 1..ell: new_a[i] * G + new_r[i] * H + for (let i = 0; i < ell; i++) { + result.push(G.multiply(newA[i]).add(H.multiply(newR[i]))); + } + + // Outputs ell+1..2*ell: new_r[i] * ek + for (let i = 0; i < ell; i++) { + result.push(ek.multiply(newR[i])); + } + + // If auditor, outputs 2*ell+1..3*ell: new_r[i] * ek_aud + if (hasAuditor) { + const ekAud = s.points[getIdxEkAud(ell)]; + for (let i = 0; i < ell; i++) { + result.push(ekAud.multiply(newR[i])); + } + } + + // Balance equation: dk * + * G + const bPowers = computeBPowers(ell); + let balanceResult = RistrettoPoint.ZERO; + const startOldR = getStartIdxOldR(ell); + for (let i = 0; i < ell; i++) { + balanceResult = balanceResult.add(s.points[startOldR + i].multiply(ed25519modN(dk * bPowers[i]))); + } + for (let i = 0; i < ell; i++) { + balanceResult = balanceResult.add(G.multiply(ed25519modN(newA[i] * bPowers[i]))); + } + result.push(balanceResult); + + return result; + }; +} + +/** + * Build the transformation function f for withdrawal/normalization. + * + * f outputs (auditorless): + * 0: H + * 1..ell: new_P[i] + * ell+1..2*ell: new_R[i] + * 2*ell+1: - v*G + * + * With auditor: + * Insert new_R_aud[i] between the new_R outputs and the balance equation target. + */ +function makeWithdrawF(ell: number, hasAuditor: boolean, v: bigint): TransformationFunction { + return (s: SigmaProtocolStatement): RistPoint[] => { + const G = s.points[IDX_G]; + const result: RistPoint[] = []; + + // Output 0: H + result.push(s.points[IDX_H]); + + // Outputs 1..ell: new_P[i] + const startNewP = getStartIdxNewP(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewP + i]); + } + + // Outputs ell+1..2*ell: new_R[i] + const startNewR = getStartIdxNewR(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewR + i]); + } + + // If auditor, outputs 2*ell+1..3*ell: new_R_aud[i] + if (hasAuditor) { + const startNewRAud = getStartIdxNewRAud(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewRAud + i]); + } + } + + // Balance equation target: - v*G + const bPowers = computeBPowers(ell); + let balanceTarget = RistrettoPoint.ZERO; + for (let i = 0; i < ell; i++) { + balanceTarget = balanceTarget.add(s.points[START_IDX_OLD_P + i].multiply(bPowers[i])); + } + // Subtract v*G: add (-v)*G (skip when v = 0 to avoid multiply-by-zero error) + const vMod = ed25519modN(v); + if (vMod !== 0n) { + const negV = ed25519modN(ed25519.CURVE.n - vMod); + balanceTarget = balanceTarget.add(G.multiply(negV)); + } + result.push(balanceTarget); + + return result; + }; +} + +export type WithdrawProofArgs = { + /** The sender's decryption key */ + dk: TwistedEd25519PrivateKey; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte token address */ + tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; + /** The withdrawal amount (0 for normalization) */ + amount: bigint; + /** Old balance C (commitment) points, one per chunk */ + oldBalanceC: RistPoint[]; + /** Old balance D (ciphertext) points, one per chunk */ + oldBalanceD: RistPoint[]; + /** New balance C points */ + newBalanceC: RistPoint[]; + /** New balance D points */ + newBalanceD: RistPoint[]; + /** New balance amount chunks (plaintext values per chunk) */ + newAmountChunks: bigint[]; + /** New balance randomness, one per chunk */ + newRandomness: bigint[]; + /** Optional auditor encryption key */ + auditorEncryptionKey?: TwistedEd25519PublicKey; + /** Optional new balance D points encrypted under auditor key */ + newBalanceDAud?: RistPoint[]; +}; + +/** + * Build the statement and witness for a withdrawal or normalization proof, then prove it. + */ +function proveWithdrawInternal( + protocolId: string, + args: WithdrawProofArgs, +): SigmaProtocolProof { + const { + dk, + senderAddress, + tokenAddress, + chainId, + amount, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks, + newRandomness, + auditorEncryptionKey, + newBalanceDAud, + } = args; + + const ell = oldBalanceC.length; + const hasAuditor = auditorEncryptionKey !== undefined; + const dkBigint = bytesToNumberLE(dk.toUint8Array()); + const ekBytes = dk.publicKey().toUint8Array(); + const ek = RistrettoPoint.fromHex(ekBytes); + + const G = RistrettoPoint.BASE; + const H = H_RISTRETTO; + + // Build statement points + const stmtPoints: RistPoint[] = [G, H, ek]; + const stmtCompressed: Uint8Array[] = [G.toRawBytes(), H.toRawBytes(), ekBytes]; + + // old_P (old balance C = commitments) + for (let i = 0; i < ell; i++) { + stmtPoints.push(oldBalanceC[i]); + stmtCompressed.push(oldBalanceC[i].toRawBytes()); + } + // old_R (old balance D = ciphertext D components) + for (let i = 0; i < ell; i++) { + stmtPoints.push(oldBalanceD[i]); + stmtCompressed.push(oldBalanceD[i].toRawBytes()); + } + // new_P (new balance C) + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceC[i]); + stmtCompressed.push(newBalanceC[i].toRawBytes()); + } + // new_R (new balance D) + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceD[i]); + stmtCompressed.push(newBalanceD[i].toRawBytes()); + } + + // Auditor points + if (hasAuditor) { + const ekAudBytes = auditorEncryptionKey.toUint8Array(); + const ekAud = RistrettoPoint.fromHex(ekAudBytes); + stmtPoints.push(ekAud); + stmtCompressed.push(ekAudBytes); + + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceDAud![i]); + stmtCompressed.push(newBalanceDAud![i].toRawBytes()); + } + } + + // Statement scalars: [v] as 32-byte LE + const vScalar = numberToBytesLE(ed25519modN(amount), 32); + + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressed, + scalars: [vScalar], + }; + + // Build witness: [dk, new_a[0..ell-1], new_r[0..ell-1]] + const witness: bigint[] = [dkBigint, ...newAmountChunks, ...newRandomness]; + + // Build domain separator + const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell, hasAuditor); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, + protocolId: utf8ToBytes(protocolId), + sessionId, + }; + + return sigmaProtocolProve(dst, TYPE_NAME, makeWithdrawPsi(ell, hasAuditor), stmt, witness); +} + +/** + * Prove a confidential withdrawal. + */ +export function proveWithdrawal(args: WithdrawProofArgs): SigmaProtocolProof { + return proveWithdrawInternal(PROTOCOL_ID_WITHDRAWAL, args); +} + +/** + * Prove a confidential normalization (same as withdrawal with v = 0). + * @deprecated Use `proveWithdrawal` instead — normalization is just withdrawal with amount = 0. + */ +export function proveNormalization(args: WithdrawProofArgs): SigmaProtocolProof { + return proveWithdrawInternal(PROTOCOL_ID_WITHDRAWAL, args); +} + +/** + * Verify a confidential withdrawal proof. + */ +export function verifyWithdrawal(args: { + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + amount: bigint; + ekBytes: Uint8Array; + oldBalanceC: RistPoint[]; + oldBalanceD: RistPoint[]; + newBalanceC: RistPoint[]; + newBalanceD: RistPoint[]; + auditorEkBytes?: Uint8Array; + newBalanceDAud?: RistPoint[]; + proof: SigmaProtocolProof; +}): boolean { + return verifyWithdrawInternal(PROTOCOL_ID_WITHDRAWAL, args); +} + +/** + * Verify a confidential normalization proof. + * @deprecated Use `verifyWithdrawal` instead — normalization is just withdrawal with amount = 0. + */ +export function verifyNormalization(args: { + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + amount: bigint; + ekBytes: Uint8Array; + oldBalanceC: RistPoint[]; + oldBalanceD: RistPoint[]; + newBalanceC: RistPoint[]; + newBalanceD: RistPoint[]; + auditorEkBytes?: Uint8Array; + newBalanceDAud?: RistPoint[]; + proof: SigmaProtocolProof; +}): boolean { + return verifyWithdrawInternal(PROTOCOL_ID_WITHDRAWAL, args); +} + +function verifyWithdrawInternal( + protocolId: string, + args: { + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + chainId: number; + amount: bigint; + ekBytes: Uint8Array; + oldBalanceC: RistPoint[]; + oldBalanceD: RistPoint[]; + newBalanceC: RistPoint[]; + newBalanceD: RistPoint[]; + auditorEkBytes?: Uint8Array; + newBalanceDAud?: RistPoint[]; + proof: SigmaProtocolProof; + }, +): boolean { + const { + senderAddress, + tokenAddress, + chainId, + amount, + ekBytes, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + auditorEkBytes, + newBalanceDAud, + proof, + } = args; + + const ell = oldBalanceC.length; + const hasAuditor = auditorEkBytes !== undefined; + const ek = RistrettoPoint.fromHex(ekBytes); + const G = RistrettoPoint.BASE; + const H = H_RISTRETTO; + + // Build statement points + const stmtPoints: RistPoint[] = [G, H, ek]; + const stmtCompressed: Uint8Array[] = [G.toRawBytes(), H.toRawBytes(), ekBytes]; + + for (let i = 0; i < ell; i++) { + stmtPoints.push(oldBalanceC[i]); + stmtCompressed.push(oldBalanceC[i].toRawBytes()); + } + for (let i = 0; i < ell; i++) { + stmtPoints.push(oldBalanceD[i]); + stmtCompressed.push(oldBalanceD[i].toRawBytes()); + } + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceC[i]); + stmtCompressed.push(newBalanceC[i].toRawBytes()); + } + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceD[i]); + stmtCompressed.push(newBalanceD[i].toRawBytes()); + } + + if (hasAuditor) { + const ekAud = RistrettoPoint.fromHex(auditorEkBytes); + stmtPoints.push(ekAud); + stmtCompressed.push(auditorEkBytes); + + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceDAud![i]); + stmtCompressed.push(newBalanceDAud![i].toRawBytes()); + } + } + + const vScalar = numberToBytesLE(ed25519modN(amount), 32); + + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressed, + scalars: [vScalar], + }; + + const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell, hasAuditor); + const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, + protocolId: utf8ToBytes(protocolId), + sessionId, + }; + + return sigmaProtocolVerify( + dst, + TYPE_NAME, + makeWithdrawPsi(ell, hasAuditor), + makeWithdrawF(ell, hasAuditor, amount), + stmt, + proof, + ); +} diff --git a/confidential-assets/src/crypto/twistedElGamal.ts b/confidential-assets/src/crypto/twistedElGamal.ts index 22bd5a486..6cd386ff9 100644 --- a/confidential-assets/src/crypto/twistedElGamal.ts +++ b/confidential-assets/src/crypto/twistedElGamal.ts @@ -6,17 +6,7 @@ import { bytesToNumberLE } from "@noble/curves/abstract/utils"; import { H_RISTRETTO, RistPoint, TwistedEd25519PrivateKey, TwistedEd25519PublicKey } from "./twistedEd25519"; import { ed25519GenRandom, ed25519modN } from "../utils"; import { HexInput } from "@aptos-labs/ts-sdk"; -import { create_kangaroo, WASMKangaroo } from "@aptos-labs/confidential-asset-wasm-bindings/pollard-kangaroo"; -import initWasm from "@aptos-labs/confidential-asset-wasm-bindings/pollard-kangaroo"; - -const POLLARD_KANGAROO_WASM_URL = - "https://unpkg.com/@aptos-labs/confidential-asset-wasm-bindings@0.0.2/pollard-kangaroo/aptos_pollard_kangaroo_wasm_bg.wasm"; - -export async function createKangaroo(secret_size: number) { - await initWasm({ module_or_path: POLLARD_KANGAROO_WASM_URL }); - - return create_kangaroo(secret_size); -} +import { DiscreteLogSolver, ensureWasmInitialized, initializeWasm, isWasmInitialized } from "./wasmLoader"; class AsyncLock { private locks: Map> = new Map(); @@ -125,88 +115,51 @@ export class TwistedElGamal { return new TwistedElGamalCiphertext(C.toRawBytes(), RistrettoPoint.ZERO.toRawBytes()); } - static tablePreloadPromise: Promise | undefined; - static tablesPreloaded = false; + static initPromise: Promise | undefined; + static initialized = false; private static initializationLock = new AsyncLock(); - static kangaroo16: WASMKangaroo; - static kangaroo32: WASMKangaroo; - static kangaroo48: WASMKangaroo; + static solver: DiscreteLogSolver; - static decryptionFn: ((pk: Uint8Array) => Promise) | undefined; - - static async initializeKangaroos() { - return this.initializationLock.acquire("kangaroo-init", async () => { + /** + * Initialize the discrete log solver with precomputed tables. + * Supports 16-bit (O(1) lookup) and 32-bit (~12ms with TBSGS-k32) secrets. + * + * @param wasmSource - Optional WASM source: URL string, or Buffer/ArrayBuffer for Node.js + */ + static async initializeSolver(wasmSource?: string | BufferSource) { + return this.initializationLock.acquire("solver-init", async () => { try { - if (TwistedElGamal.tablesPreloaded && TwistedElGamal.decryptionFn !== undefined) { + if (TwistedElGamal.initialized) { return; } - if (!TwistedElGamal.tablePreloadPromise) { - const createKangaroos = async () => { + if (!TwistedElGamal.initPromise) { + const createSolver = async () => { try { - TwistedElGamal.kangaroo16 = await createKangaroo(16); - TwistedElGamal.kangaroo32 = await createKangaroo(32); - TwistedElGamal.kangaroo48 = await createKangaroo(48); + await initializeWasm(wasmSource); + TwistedElGamal.solver = new DiscreteLogSolver(); } catch (error) { // Reset state on failure - TwistedElGamal.tablePreloadPromise = undefined; - TwistedElGamal.tablesPreloaded = false; + TwistedElGamal.initPromise = undefined; + TwistedElGamal.initialized = false; throw error; } }; - TwistedElGamal.tablePreloadPromise = createKangaroos(); + TwistedElGamal.initPromise = createSolver(); } - await TwistedElGamal.tablePreloadPromise; - - if (!TwistedElGamal.decryptionFn) { - TwistedElGamal.setDecryptionFn(async (pk) => { - if (bytesToNumberLE(pk) === 0n) return 0n; - try { - let result = TwistedElGamal.kangaroo16.solve_dlp(pk, 30n); - if (!result) { - result = TwistedElGamal.kangaroo32.solve_dlp(pk, 120n); - } - if (!result) { - // Exponential backoff - const maxRetries = 3; - const baseTimeout = 2000n; - - for (let attempt = 0; attempt < maxRetries; attempt++) { - const timeout = baseTimeout * 2n ** BigInt(attempt); // 2000, 4000, 8000 - result = TwistedElGamal.kangaroo48.solve_dlp(pk, timeout); - if (result) return result; - - if (attempt < maxRetries - 1) { - console.warn(`decryption attempt ${attempt + 1} failed, retrying with timeout ${timeout}...`); - } - } - } - if (!result) throw new TypeError("Decryption failed. Timed out."); - return result; - } catch (e) { - console.error("Decryption failed:", e); - throw e; - } - }); - } - - TwistedElGamal.tablesPreloaded = true; + await TwistedElGamal.initPromise; + TwistedElGamal.initialized = true; } catch (error) { // Reset state on any initialization failure - TwistedElGamal.tablePreloadPromise = undefined; - TwistedElGamal.tablesPreloaded = false; - TwistedElGamal.decryptionFn = undefined; + TwistedElGamal.initPromise = undefined; + TwistedElGamal.initialized = false; throw error; } }); } - static setDecryptionFn(fn: (pk: Uint8Array) => Promise) { - this.decryptionFn = fn; - } - static calculateCiphertextMG(ciphertext: TwistedElGamalCiphertext, privateKey: TwistedEd25519PrivateKey): RistPoint { const { C, D } = ciphertext; const modS = ed25519modN(bytesToNumberLE(privateKey.toUint8Array())); @@ -216,11 +169,33 @@ export class TwistedElGamal { return mG; } + /** + * Solves the discrete log problem to recover the encrypted amount. + * Tries 16-bit first (O(1) lookup), then falls back to 32-bit. + */ + private static async decryptAmount(pk: Uint8Array): Promise { + if (bytesToNumberLE(pk) === 0n) return 0n; + try { + // Try 16-bit first (O(1) lookup) + try { + return TwistedElGamal.solver.solve(pk, 16); + } catch { + // Fall through to 32-bit + } + // Try 32-bit (~12ms with TBSGS-k32) + return TwistedElGamal.solver.solve(pk, 32); + } catch (e) { + console.error("Decryption failed:", e); + throw new TypeError("Decryption failed. Value may be out of 32-bit range."); + } + } + /** * Decrypts the amount with Twisted ElGamal * @param ciphertext сiphertext points encrypted by Twisted ElGamal * @param privateKey Twisted ElGamal Ed25519 private key. - * @param decryptionRange The range of amounts to be used in decryption + * + * TODO: rename WithPK to WithDK? */ static async decryptWithPK( ciphertext: TwistedElGamalCiphertext, @@ -229,7 +204,7 @@ export class TwistedElGamal { await TwistedElGamal.ensureInitialized(); const mG = TwistedElGamal.calculateCiphertextMG(ciphertext, privateKey); - return TwistedElGamal.decryptionFn!(mG.toRawBytes()); + return TwistedElGamal.decryptAmount(mG.toRawBytes()); } /** @@ -275,32 +250,30 @@ export class TwistedElGamal { } static async cleanup() { - return this.initializationLock.acquire("kangaroo-cleanup", async () => { + return this.initializationLock.acquire("solver-cleanup", async () => { try { - if (TwistedElGamal.kangaroo16) TwistedElGamal.kangaroo16.free(); - if (TwistedElGamal.kangaroo32) TwistedElGamal.kangaroo32.free(); - if (TwistedElGamal.kangaroo48) TwistedElGamal.kangaroo48.free(); + if (TwistedElGamal.solver) TwistedElGamal.solver.free(); } finally { - TwistedElGamal.tablePreloadPromise = undefined; - TwistedElGamal.tablesPreloaded = false; - TwistedElGamal.decryptionFn = undefined; + TwistedElGamal.initPromise = undefined; + TwistedElGamal.initialized = false; } }); } static isInitialized(): boolean { - return ( - TwistedElGamal.tablesPreloaded && - TwistedElGamal.decryptionFn !== undefined && - TwistedElGamal.kangaroo16 !== undefined && - TwistedElGamal.kangaroo32 !== undefined && - TwistedElGamal.kangaroo48 !== undefined - ); + return TwistedElGamal.initialized && TwistedElGamal.solver !== undefined && isWasmInitialized(); + } + + /** + * Returns the algorithm name used by the discrete log solver. + */ + static getAlgorithmName(): string { + return TwistedElGamal.solver?.algorithm() ?? "not initialized"; } private static async ensureInitialized() { if (!this.isInitialized()) { - await this.initializeKangaroos(); + await this.initializeSolver(); } } } diff --git a/confidential-assets/src/crypto/wasmLoader.ts b/confidential-assets/src/crypto/wasmLoader.ts new file mode 100644 index 000000000..af431822b --- /dev/null +++ b/confidential-assets/src/crypto/wasmLoader.ts @@ -0,0 +1,109 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/** + * Unified WASM loader for confidential assets. + * + * This module provides a single initialization point for the unified WASM + * that contains both discrete log and range proof functionality. + */ + +import initWasm, { + DiscreteLogSolver, + range_proof as rangeProofWasm, + verify_proof as verifyProofWasm, + batch_range_proof as batchRangeProofWasm, + batch_verify_proof as batchVerifyProofWasm, +} from "@aptos-labs/confidential-asset-wasm-bindings"; + +// Unified WASM URL +const UNIFIED_WASM_URL = + "https://unpkg.com/@aptos-labs/confidential-asset-wasm-bindings@0.0.3/aptos_confidential_asset_wasm_bg.wasm"; + +let initPromise: Promise | undefined; +let initialized = false; + +/** + * Get WASM source - for Node.js, try local file first + */ +async function getWasmSource(): Promise { + // In Node.js, try to load from local node_modules + if (typeof process !== "undefined" && process.versions?.node) { + try { + // Dynamic import for Node.js fs module + const fs = await import("fs"); + const path = await import("path"); + + // Try to find the WASM file in node_modules + const possiblePaths = [ + path.resolve( + process.cwd(), + "node_modules/@aptos-labs/confidential-asset-wasm-bindings/aptos_confidential_asset_wasm_bg.wasm", + ), + path.resolve( + __dirname, + "../../node_modules/@aptos-labs/confidential-asset-wasm-bindings/aptos_confidential_asset_wasm_bg.wasm", + ), + ]; + + for (const wasmPath of possiblePaths) { + if (fs.existsSync(wasmPath)) { + return fs.readFileSync(wasmPath); + } + } + } catch { + // Fall through to URL + } + } + return UNIFIED_WASM_URL; +} + +/** + * Initialize the unified confidential asset WASM module. + * This is shared between discrete log and range proof functionality. + * + * @param wasmSource - Optional WASM source: URL string, or Buffer/ArrayBuffer for Node.js + */ +export async function initializeWasm(wasmSource?: string | BufferSource): Promise { + if (initialized) return; + + if (!initPromise) { + initPromise = (async () => { + try { + const source = wasmSource ?? (await getWasmSource()); + await initWasm({ module_or_path: source }); + initialized = true; + } catch (error) { + initPromise = undefined; + throw error; + } + })(); + } + + await initPromise; +} + +/** + * Check if the WASM module is initialized. + */ +export function isWasmInitialized(): boolean { + return initialized; +} + +/** + * Ensure WASM is initialized before use. + */ +export async function ensureWasmInitialized(): Promise { + if (!initialized) { + await initializeWasm(); + } +} + +// Re-export WASM functions and classes +export { + DiscreteLogSolver, + rangeProofWasm, + verifyProofWasm, + batchRangeProofWasm, + batchVerifyProofWasm, +}; diff --git a/confidential-assets/src/helpers.ts b/confidential-assets/src/helpers.ts index 4e7628134..7f1658608 100644 --- a/confidential-assets/src/helpers.ts +++ b/confidential-assets/src/helpers.ts @@ -1,14 +1,2 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 - -import { sha512 } from "@noble/hashes/sha512"; -import { bytesToNumberLE, concatBytes } from "@noble/curves/abstract/utils"; -import { ed25519modN } from "./utils"; - -/* - * Generate Fiat-Shamir challenge - */ -export function genFiatShamirChallenge(...arrays: Uint8Array[]): bigint { - const hash = sha512(concatBytes(...arrays)); - return ed25519modN(bytesToNumberLE(hash)); -} diff --git a/confidential-assets/src/index.ts b/confidential-assets/src/index.ts index b2dacfe91..315d9808c 100644 --- a/confidential-assets/src/index.ts +++ b/confidential-assets/src/index.ts @@ -5,7 +5,6 @@ export * from "./crypto/confidentialTransfer"; export * from "./crypto/confidentialWithdraw"; export * from "./consts"; export * from "./crypto/encryptedAmount"; -export * from "./helpers"; export * from "./api/confidentialAsset"; export * from "./crypto"; export * from "./utils"; diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index f5311a99e..1600dff47 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -1,7 +1,8 @@ -// Copyright © Aptos Foundation +// Copyright (c) Aptos Foundation // SPDX-License-Identifier: Apache-2.0 import { + AccountAddress, AccountAddressInput, AnyNumber, Aptos, @@ -10,7 +11,6 @@ import { LedgerVersionArg, SimpleTransaction, } from "@aptos-labs/ts-sdk"; -import { concatBytes } from "@noble/hashes/utils"; import { TwistedElGamal, ConfidentialNormalization, @@ -20,8 +20,9 @@ import { TwistedEd25519PublicKey, TwistedEd25519PrivateKey, } from "../crypto"; +import { proveRegistration } from "../crypto/sigmaProtocolRegistration"; import { DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS, MODULE_NAME } from "../consts"; -import { getBalance, getEncryptionKey, isBalanceNormalized, isPendingBalanceFrozen } from "./viewFunctions"; +import { getBalance, getEncryptionKey, isBalanceNormalized, isIncomingTransfersPaused } from "./viewFunctions"; /** * A class to handle creating transactions for confidential asset operations @@ -30,10 +31,19 @@ export class ConfidentialAssetTransactionBuilder { readonly client: Aptos; readonly confidentialAssetModuleAddress: string; + private _chainId?: number; + + async getChainId(): Promise { + if (!this._chainId) { + this._chainId = await this.client.getChainId(); + } + return this._chainId; + } + constructor(config: AptosConfig, confidentialAssetModuleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS) { this.client = new Aptos(config); this.confidentialAssetModuleAddress = confidentialAssetModuleAddress; - TwistedElGamal.initializeKangaroos(); + TwistedElGamal.initializeSolver(); } /** @@ -53,25 +63,45 @@ export class ConfidentialAssetTransactionBuilder { withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { - const { tokenAddress, decryptionKey } = args; + const { sender, tokenAddress, decryptionKey } = args; + + // Resolve addresses to 32-byte arrays + const senderAddr = AccountAddress.from(sender); + const tokenAddr = AccountAddress.from(tokenAddress); + + // Get chain ID for domain separation + const chainId = await this.getChainId(); + + // Generate the registration sigma proof + const sigmaProof = proveRegistration({ + dk: decryptionKey, + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + chainId, + }); + return this.client.transaction.build.simple({ ...args, data: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::register`, - functionArguments: [tokenAddress, decryptionKey.publicKey().toUint8Array()], + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::register_raw`, + functionArguments: [ + tokenAddress, + decryptionKey.publicKey().toUint8Array(), + sigmaProof.commitment, + sigmaProof.response, + ], }, }); } /** - * Deposit an amount from a non-confidential asset balance into a confidential asset balance. + * Deposit an amount from a non-confidential asset balance into the sender's own confidential asset balance. * * This can be used by an account to convert their own non-confidential asset balance into a confidential asset balance if they have * already registered a balance for the token. * * @param args.tokenAddress - The token address of the asset to deposit to * @param args.amount - The amount to deposit - * @param args.recipient - The account address to deposit to. This is the senders address if not set. * @param args.withFeePayer - Whether to use the fee payer for the transaction * @param args.options - Optional transaction options * @returns A SimpleTransaction to deposit the amount @@ -80,12 +110,10 @@ export class ConfidentialAssetTransactionBuilder { sender: AccountAddressInput; tokenAddress: AccountAddressInput; amount: AnyNumber; - /** If not set we will use the sender's address. */ - recipient?: AccountAddressInput; withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { - const { tokenAddress, amount, recipient = args.sender } = args; + const { tokenAddress, amount } = args; validateAmount({ amount }); const amountString = String(amount); @@ -93,8 +121,8 @@ export class ConfidentialAssetTransactionBuilder { return this.client.transaction.build.simple({ ...args, data: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::deposit_to`, - functionArguments: [tokenAddress, recipient, amountString], + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::deposit`, + functionArguments: [tokenAddress, amountString], }, }); } @@ -126,6 +154,16 @@ export class ConfidentialAssetTransactionBuilder { const { sender, tokenAddress, amount, senderDecryptionKey, recipient = args.sender, options } = args; validateAmount({ amount }); + // Resolve addresses to 32-byte arrays + const senderAddr = AccountAddress.from(sender); + const tokenAddr = AccountAddress.from(tokenAddress); + + // Get chain ID for domain separation + const chainId = await this.getChainId(); + + // Get the auditor public key for the token + const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + // Get the sender's available balance from the chain const { available: senderEncryptedAvailableBalance } = await getBalance({ client: this.client, @@ -139,21 +177,34 @@ export class ConfidentialAssetTransactionBuilder { decryptionKey: senderDecryptionKey, senderAvailableBalanceCipherText: senderEncryptedAvailableBalance.getCipherText(), amount: BigInt(amount), + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + chainId, + auditorEncryptionKey: effectiveAuditorPubKey, }); - const [{ sigmaProof, rangeProof }, encryptedAmountAfterWithdraw] = await confidentialWithdraw.authorizeWithdrawal(); + const [{ sigmaProof, rangeProof }, encryptedAmountAfterWithdraw, auditorEncryptedBalance] = + await confidentialWithdraw.authorizeWithdrawal(); + + // Build auditor A components (D points encrypted under auditor key) + const newBalanceA = auditorEncryptedBalance + ? auditorEncryptedBalance.getCipherText().map((ct) => ct.D.toRawBytes()) + : ([] as Uint8Array[]); return this.client.transaction.build.simple({ ...args, data: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::withdraw_to`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::withdraw_to_raw`, functionArguments: [ tokenAddress, recipient, String(amount), - encryptedAmountAfterWithdraw.getCipherTextBytes(), + encryptedAmountAfterWithdraw.getCipherText().map((ct) => ct.C.toRawBytes()), // new_balance_C + encryptedAmountAfterWithdraw.getCipherText().map((ct) => ct.D.toRawBytes()), // new_balance_D + newBalanceA, // new_balance_A rangeProof, - ConfidentialWithdraw.serializeSigmaProof(sigmaProof), + sigmaProof.commitment, + sigmaProof.response, ], }, options, @@ -165,7 +216,7 @@ export class ConfidentialAssetTransactionBuilder { * * @param args.sender - 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 @@ -174,12 +225,12 @@ export class ConfidentialAssetTransactionBuilder { async rolloverPendingBalance(args: { sender: AccountAddressInput; tokenAddress: AccountAddressInput; - withFreezeBalance?: boolean; + withPauseIncoming?: boolean; withFeePayer?: boolean; checkNormalized?: boolean; options?: InputGenerateTransactionOptions; }): Promise { - const { checkNormalized = true, withFreezeBalance = false } = args; + const { checkNormalized = true, withPauseIncoming = false } = args; if (checkNormalized) { const isNormalized = await isBalanceNormalized({ client: this.client, @@ -192,7 +243,7 @@ export class ConfidentialAssetTransactionBuilder { } } - const functionName = withFreezeBalance ? "rollover_pending_balance_and_freeze" : "rollover_pending_balance"; + const functionName = withPauseIncoming ? "rollover_pending_balance_and_pause" : "rollover_pending_balance"; return this.client.transaction.build.simple({ ...args, @@ -217,17 +268,25 @@ export class ConfidentialAssetTransactionBuilder { tokenAddress: AccountAddressInput; options?: LedgerVersionArg; }): Promise { - const [{ vec: globalAuditorPubKey }] = await this.client.view<[{ vec: Uint8Array }]>({ + // EffectiveAuditorConfig::V1 { is_global: bool, config: AuditorConfig::V1 { ek: Option, epoch: u64 } } + type EffectiveAuditorConfigResponse = { + is_global: boolean; + config: { + ek: { vec: { data: string }[] }; + epoch: string; + }; + }; + const [{ config }] = await this.client.view<[EffectiveAuditorConfigResponse]>({ options: args.options, payload: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_auditor`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_effective_auditor_config`, functionArguments: [args.tokenAddress], }, }); - if (globalAuditorPubKey.length === 0) { + if (config.ek.vec.length === 0) { return undefined; } - return new TwistedEd25519PublicKey(globalAuditorPubKey); + return new TwistedEd25519PublicKey(config.ek.vec[0].data); } /** @@ -258,11 +317,19 @@ export class ConfidentialAssetTransactionBuilder { withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { - const { senderDecryptionKey, recipient, tokenAddress, amount, additionalAuditorEncryptionKeys = [] } = args; + const { sender, senderDecryptionKey, recipient, tokenAddress, amount, additionalAuditorEncryptionKeys = [] } = args; validateAmount({ amount }); + // Resolve addresses to 32-byte arrays + const senderAddr = AccountAddress.from(sender); + const recipientAddr = AccountAddress.from(recipient); + const tokenAddr = AccountAddress.from(tokenAddress); + + // Get chain ID for domain separation + const chainId = await this.getChainId(); + // Get the auditor public key for the token - const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ + const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress, }); @@ -277,14 +344,14 @@ export class ConfidentialAssetTransactionBuilder { } catch (e) { throw new Error(`Failed to get encryption key for recipient - ${e}`); } - const isFrozen = await isPendingBalanceFrozen({ + const isPaused = await isIncomingTransfersPaused({ client: this.client, moduleAddress: this.confidentialAssetModuleAddress, accountAddress: recipient, tokenAddress, }); - if (isFrozen) { - throw new Error("Recipient balance is frozen"); + if (isPaused) { + throw new Error("Recipient's incoming transfers are paused"); } // Get the sender's available balance from the chain const { available: senderEncryptedAvailableBalance } = await getBalance({ @@ -295,16 +362,25 @@ export class ConfidentialAssetTransactionBuilder { decryptionKey: senderDecryptionKey, }); + // Build the full auditor list for proof generation: [...voluntary, global (if set)] + // The contract will append the global auditor itself, so we only send voluntary auditor EKs on-chain. + const allAuditorEncryptionKeys = [ + ...additionalAuditorEncryptionKeys, + ...(effectiveAuditorPubKey ? [effectiveAuditorPubKey] : []), + ]; + // Create the confidential transfer object const confidentialTransfer = await ConfidentialTransfer.create({ senderDecryptionKey, senderAvailableBalanceCipherText: senderEncryptedAvailableBalance.getCipherText(), amount, recipientEncryptionKey, - auditorEncryptionKeys: [ - ...(globalAuditorPubKey ? [globalAuditorPubKey] : []), - ...additionalAuditorEncryptionKeys, - ], + hasEffectiveAuditor: !!effectiveAuditorPubKey, + auditorEncryptionKeys: allAuditorEncryptionKeys, + senderAddress: senderAddr.toUint8Array(), + recipientAddress: recipientAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + chainId, }); const [ @@ -314,28 +390,51 @@ export class ConfidentialAssetTransactionBuilder { }, encryptedAmountAfterTransfer, encryptedAmountByRecipient, - auditorsCBList, + allAuditorAmountCiphertexts, + auditorNewBalanceList, ] = await confidentialTransfer.authorizeTransfer(); - const auditorEncryptionKeys = confidentialTransfer.auditorEncryptionKeys.map((pk) => pk.toUint8Array()); - const auditorBalances = auditorsCBList.map((el) => el.getCipherTextBytes()); + // Only send voluntary auditor EKs on-chain (not the global auditor, which the contract fetches itself) + const volunAuditorEncryptionKeys = additionalAuditorEncryptionKeys.map((pk) => pk.toUint8Array()); + + // Only send D components for recipient and auditors (C components are shared with sender_amount) + const recipientDPoints = encryptedAmountByRecipient.getCipherText().map((ct) => ct.D.toRawBytes()); + // Split auditor D points into effective (last, if present) and voluntary (remaining) + const effectiveAuditorDPoints = effectiveAuditorPubKey + ? allAuditorAmountCiphertexts[allAuditorAmountCiphertexts.length - 1].getCipherText().map((ct) => ct.D.toRawBytes()) + : []; + const volunAuditorDPoints = (effectiveAuditorPubKey + ? allAuditorAmountCiphertexts.slice(0, -1) + : allAuditorAmountCiphertexts + ).map((cb) => cb.getCipherText().map((ct) => ct.D.toRawBytes())); + + // Build R_aud components for new balance (D points encrypted under the effective auditor key, i.e., the last one) + // Only populated when there IS an effective auditor — voluntary auditors don't get new balance R components. + const newBalanceA = effectiveAuditorPubKey + ? auditorNewBalanceList[auditorNewBalanceList.length - 1].getCipherText().map((ct) => ct.D.toRawBytes()) + : []; return this.client.transaction.build.simple({ ...args, withFeePayer: args.withFeePayer, data: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::confidential_transfer`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::confidential_transfer_raw`, functionArguments: [ tokenAddress, recipient, - encryptedAmountAfterTransfer.getCipherTextBytes(), - confidentialTransfer.transferAmountEncryptedBySender.getCipherTextBytes(), - encryptedAmountByRecipient.getCipherTextBytes(), - concatBytes(...auditorEncryptionKeys), - concatBytes(...auditorBalances), + encryptedAmountAfterTransfer.getCipherText().map((ct) => ct.C.toRawBytes()), // new_balance_C + encryptedAmountAfterTransfer.getCipherText().map((ct) => ct.D.toRawBytes()), // new_balance_D + newBalanceA, // new_balance_A + confidentialTransfer.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.C.toRawBytes()), // sender_amount_C + confidentialTransfer.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.D.toRawBytes()), // sender_amount_D + recipientDPoints, + effectiveAuditorDPoints, + volunAuditorEncryptionKeys, + volunAuditorDPoints, rangeProofNewBalance, rangeProofAmount, - ConfidentialTransfer.serializeSigmaProof(sigmaProof), + sigmaProof.commitment, + sigmaProof.response, ], }, }); @@ -344,18 +443,15 @@ export class ConfidentialAssetTransactionBuilder { /** * Rotate the encryption key for a confidential asset balance. * - * This will by default check if the pending balance is empty and throw an error if it is not. It also checks if the balance is frozen and - * will unfreeze it if it is. - * - * TODO: Parallelize the view calls + * This will by default check if the pending balance is empty and throw an error if it is not. + * The new entry function uses the Sigma protocol for key rotation proofs and supports an `unpause` flag. * * @param args.sender - The address of the sender of the transaction who's encryption key is being rotated * @param args.senderDecryptionKey - The decryption key of the sender - * @param args.newDecryptionKey - The new decryption key + * @param args.newSenderDecryptionKey - The new decryption key * @param args.tokenAddress - The token address of the asset to rotate the encryption key for - * @param args.checkPendingBalanceEmpty - Whether to check if the pending balance is empty before rotating the encryption key. Default is true. - * @param args.withUnfreezeBalance - Whether to unfreeze the balance after rotating the encryption key. By default it will check the chain to - * see if the balance is frozen and if so, will unfreeze it. + * @param args.checkPendingBalanceEmpty - Whether to check if the pending balance is empty before rotating. Default is true. + * @param args.unpause - Whether to unpause incoming transfers after rotation. Default is true. * @param args.withFeePayer - Whether to use the fee payer for the transaction * @returns A SimpleTransaction to rotate the encryption key * @throws {Error} If the pending balance is not 0 before rotating the encryption key, unless checkPendingBalanceEmpty is false. @@ -366,7 +462,7 @@ export class ConfidentialAssetTransactionBuilder { newSenderDecryptionKey: TwistedEd25519PrivateKey; tokenAddress: AccountAddressInput; checkPendingBalanceEmpty?: boolean; - withUnfreezePendingBalance?: boolean; + unpause?: boolean; withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { @@ -376,12 +472,7 @@ export class ConfidentialAssetTransactionBuilder { newSenderDecryptionKey, checkPendingBalanceEmpty = true, tokenAddress, - withUnfreezePendingBalance = await isPendingBalanceFrozen({ - client: this.client, - moduleAddress: this.confidentialAssetModuleAddress, - accountAddress: sender, - tokenAddress, - }), + unpause = true, } = args; // Get the sender's balance from the chain @@ -399,33 +490,38 @@ export class ConfidentialAssetTransactionBuilder { } } - // Create the confidential key rotation object - const confidentialKeyRotation = await ConfidentialKeyRotation.create({ + // Resolve the sender and token addresses to 32-byte arrays + const senderAddr = AccountAddress.from(sender); + const tokenAddr = AccountAddress.from(tokenAddress); + + // Get chain ID for domain separation + const chainId = await this.getChainId(); + + // Create the confidential key rotation object and generate the proof + const confidentialKeyRotation = ConfidentialKeyRotation.create({ senderDecryptionKey, newSenderDecryptionKey, currentEncryptedAvailableBalance, + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + chainId, }); - // Create the sigma proof and range proof - const [{ sigmaProof, rangeProof }, newEncryptedAvailableBalance] = - await confidentialKeyRotation.authorizeKeyRotation(); - - const newPublicKeyBytes = args.newSenderDecryptionKey.publicKey().toUint8Array(); - - const method = withUnfreezePendingBalance ? "rotate_encryption_key_and_unfreeze" : "rotate_encryption_key"; + const { newEkBytes, newDBytes, proof } = confidentialKeyRotation.authorizeKeyRotation(); return this.client.transaction.build.simple({ ...args, withFeePayer: args.withFeePayer, sender: args.sender, data: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::${method}`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::rotate_encryption_key_raw`, functionArguments: [ args.tokenAddress, - newPublicKeyBytes, - newEncryptedAvailableBalance.getCipherTextBytes(), - rangeProof, - ConfidentialKeyRotation.serializeSigmaProof(sigmaProof), + newEkBytes, + unpause, + newDBytes, + proof.commitment, + proof.response, ], }, options: args.options, @@ -451,6 +547,17 @@ export class ConfidentialAssetTransactionBuilder { options?: InputGenerateTransactionOptions; }): Promise { const { sender, senderDecryptionKey, tokenAddress, withFeePayer, options } = args; + + // Resolve addresses to 32-byte arrays + const senderAddr = AccountAddress.from(sender); + const tokenAddr = AccountAddress.from(tokenAddress); + + // Get chain ID for domain separation + const chainId = await this.getChainId(); + + // Get the auditor public key for the token + const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + const { available } = await getBalance({ client: this.client, moduleAddress: this.confidentialAssetModuleAddress, @@ -462,6 +569,10 @@ export class ConfidentialAssetTransactionBuilder { const confidentialNormalization = await ConfidentialNormalization.create({ decryptionKey: senderDecryptionKey, unnormalizedAvailableBalance: available, + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + chainId, + auditorEncryptionKey: effectiveAuditorPubKey, }); return confidentialNormalization.createTransaction({ diff --git a/confidential-assets/src/internal/viewFunctions.ts b/confidential-assets/src/internal/viewFunctions.ts index 737621174..034293fad 100644 --- a/confidential-assets/src/internal/viewFunctions.ts +++ b/confidential-assets/src/internal/viewFunctions.ts @@ -26,10 +26,8 @@ type ViewFunctionParams = { }; export type ConfidentialBalanceResponse = { - chunks: { - left: { data: string }; - right: { data: string }; - }[]; + P: { data: string }[]; + R: { data: string }[]; }[]; /** @@ -172,7 +170,7 @@ async function getBalanceCipherText(args: ViewFunctionParams): Promise<{ const [[chunkedPendingBalance], [chunkedActualBalances]] = await Promise.all([ client.view({ payload: { - function: `${moduleAddress}::${MODULE_NAME}::pending_balance`, + function: `${moduleAddress}::${MODULE_NAME}::get_pending_balance`, typeArguments: [], functionArguments: [accountAddress, tokenAddress], }, @@ -180,7 +178,7 @@ async function getBalanceCipherText(args: ViewFunctionParams): Promise<{ }), client.view({ payload: { - function: `${moduleAddress}::${MODULE_NAME}::actual_balance`, + function: `${moduleAddress}::${MODULE_NAME}::get_available_balance`, typeArguments: [], functionArguments: [accountAddress, tokenAddress], }, @@ -189,11 +187,11 @@ async function getBalanceCipherText(args: ViewFunctionParams): Promise<{ ]); return { - pending: chunkedPendingBalance.chunks.map( - (el) => new TwistedElGamalCiphertext(el.left.data.slice(2), el.right.data.slice(2)), + pending: chunkedPendingBalance.P.map( + (p, i) => new TwistedElGamalCiphertext(p.data.slice(2), chunkedPendingBalance.R[i].data.slice(2)), ), - available: chunkedActualBalances.chunks.map( - (el) => new TwistedElGamalCiphertext(el.left.data.slice(2), el.right.data.slice(2)), + available: chunkedActualBalances.P.map( + (p, i) => new TwistedElGamalCiphertext(p.data.slice(2), chunkedActualBalances.R[i].data.slice(2)), ), }; } @@ -211,17 +209,17 @@ export async function isBalanceNormalized(args: ViewFunctionParams): Promise { - const [isFrozen] = await args.client.view<[boolean]>({ +export async function isIncomingTransfersPaused(args: ViewFunctionParams): Promise { + const [isPaused] = await args.client.view<[boolean]>({ options: args.options, payload: { - function: `${args.moduleAddress}::${MODULE_NAME}::is_frozen`, + function: `${args.moduleAddress}::${MODULE_NAME}::incoming_transfers_paused`, typeArguments: [], functionArguments: [args.accountAddress, args.tokenAddress], }, }); - return isFrozen; + return isPaused; } /** @@ -237,7 +235,7 @@ export async function isPendingBalanceFrozen(args: ViewFunctionParams): Promise< export async function hasUserRegistered(args: ViewFunctionParams): Promise { const [isRegistered] = await args.client.view<[boolean]>({ payload: { - function: `${args.moduleAddress}::${MODULE_NAME}::has_confidential_asset_store`, + function: `${args.moduleAddress}::${MODULE_NAME}::has_confidential_store`, typeArguments: [], functionArguments: [args.accountAddress, args.tokenAddress], }, @@ -259,6 +257,44 @@ export async function hasUserRegistered(args: ViewFunctionParams): Promise`. + */ +export type EffectiveAuditorHintResponse = { + vec: { is_global: boolean; epoch: string }[]; +}; + +/** + * 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. + * + * @returns The auditor hint, or undefined if no auditor hint is set (e.g., balance is zero or no auditor was active). + */ +export async function getEffectiveAuditorHint( + args: ViewFunctionParams, +): Promise<{ isGlobal: boolean; epoch: bigint } | undefined> { + const { + client, + accountAddress, + tokenAddress, + options, + moduleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS, + } = args; + const [hint] = await client.view<[EffectiveAuditorHintResponse]>({ + options, + payload: { + function: `${moduleAddress}::${MODULE_NAME}::get_effective_auditor_hint`, + typeArguments: [], + functionArguments: [accountAddress, tokenAddress], + }, + }); + if (hint.vec.length === 0) { + return undefined; + } + return { isGlobal: hint.vec[0].is_global, epoch: BigInt(hint.vec[0].epoch) }; +} + export async function getEncryptionKey( args: ViewFunctionParams & { useCachedValue?: boolean; @@ -268,14 +304,14 @@ export async function getEncryptionKey( try { return await memoizeAsync( async () => { - const [{ point }] = await args.client.view<[{ point: { data: string } }]>({ + const [{ data }] = await args.client.view<[{ data: string }]>({ options, payload: { - function: `${args.moduleAddress}::${MODULE_NAME}::encryption_key`, + function: `${args.moduleAddress}::${MODULE_NAME}::get_encryption_key`, functionArguments: [accountAddress, tokenAddress], }, }); - return new TwistedEd25519PublicKey(point.data); + return new TwistedEd25519PublicKey(data); }, `${accountAddress}-encryption-key-for-${tokenAddress}-${args.client.config.network}`, 1000 * 60 * 60, // 1 hour cache duration @@ -285,3 +321,4 @@ export async function getEncryptionKey( throw error; } } + diff --git a/confidential-assets/tests/e2e/confidentialAsset.test.ts b/confidential-assets/tests/e2e/confidentialAsset.test.ts index 0bc9bbc44..975400a5a 100644 --- a/confidential-assets/tests/e2e/confidentialAsset.test.ts +++ b/confidential-assets/tests/e2e/confidentialAsset.test.ts @@ -29,6 +29,7 @@ describe("Confidential Asset Sender API", () => { const aliceConfidential = getTestConfidentialAccount(alice); const bob = Account.generate(); + const bobConfidential = getTestConfidentialAccount(bob); async function getPublicTokenBalance(accountAddress: AccountAddressInput) { return await aptos.getAccountCoinAmount({ @@ -52,6 +53,21 @@ describe("Confidential Asset Sender API", () => { expect(confidentialBalance.pendingBalance()).toBe(BigInt(expectedPending)); } + async function checkBobDecryptedBalance( + expectedAvailable: AnyNumber, + expectedPending: AnyNumber, + ) { + const confidentialBalance = await confidentialAsset.getBalance({ + accountAddress: bob.accountAddress, + tokenAddress: TOKEN_ADDRESS, + decryptionKey: bobConfidential, + useCachedValue: false, + }); + + expect(confidentialBalance.availableBalance()).toBe(BigInt(expectedAvailable)); + expect(confidentialBalance.pendingBalance()).toBe(BigInt(expectedPending)); + } + async function checkAliceNormalizedBalanceStatus(expectedStatus: boolean) { const isNormalized = await confidentialAsset.isBalanceNormalized({ accountAddress: alice.accountAddress, @@ -61,12 +77,12 @@ describe("Confidential Asset Sender API", () => { expect(isNormalized).toBe(expectedStatus); } - async function checkAliceBalanceFrozenStatus(expectedStatus: boolean) { - const isFrozen = await confidentialAsset.isPendingBalanceFrozen({ + async function checkAliceIncomingTransfersPausedStatus(expectedStatus: boolean) { + const isPaused = await confidentialAsset.isIncomingTransfersPaused({ accountAddress: alice.accountAddress, tokenAddress: TOKEN_ADDRESS, }); - expect(isFrozen).toBe(expectedStatus); + expect(isPaused).toBe(expectedStatus); } beforeAll(async () => { await aptos.fundAccount({ @@ -285,6 +301,19 @@ describe("Confidential Asset Sender API", () => { longTestTimeout, ); + test( + "it should register Bob's confidential balance", + async () => { + const registerBobTx = await confidentialAsset.registerBalance({ + signer: bob, + tokenAddress: TOKEN_ADDRESS, + decryptionKey: bobConfidential, + }); + expect(registerBobTx.success).toBeTruthy(); + }, + longTestTimeout, + ); + test( "it should throw if transferring more than the available balance", async () => { @@ -306,7 +335,7 @@ describe("Confidential Asset Sender API", () => { tokenAddress: TOKEN_ADDRESS, senderDecryptionKey: aliceConfidential, amount: confidentialBalance.availableBalance() + BigInt(1), // This is more than the available balance - recipient: alice.accountAddress, + recipient: bob.accountAddress, }), ).rejects.toThrow("Insufficient balance"); }, @@ -336,19 +365,19 @@ describe("Confidential Asset Sender API", () => { tokenAddress: TOKEN_ADDRESS, senderDecryptionKey: aliceConfidential, amount: transferAmount, - recipient: alice.accountAddress, + recipient: bob.accountAddress, }); await checkAliceDecryptedBalance( confidentialBalance.availableBalance() + confidentialBalance.pendingBalance() - transferAmount, - transferAmount, + 0, ); }, longTestTimeout, ); test( - "it should transfer Alice's tokens to Alice's pending balance without auditor", + "it should transfer Alice's tokens to Bob's pending balance without auditor", async () => { const confidentialBalance = await confidentialAsset.getBalance({ accountAddress: alice.accountAddress, @@ -361,22 +390,26 @@ describe("Confidential Asset Sender API", () => { amount: TRANSFER_AMOUNT, signer: alice, tokenAddress: TOKEN_ADDRESS, - recipient: alice.accountAddress, + recipient: bob.accountAddress, }); expect(transferTx.success).toBeTruthy(); // Verify the confidential balance has been updated correctly await checkAliceDecryptedBalance( confidentialBalance.availableBalance() - TRANSFER_AMOUNT, - confidentialBalance.pendingBalance() + TRANSFER_AMOUNT, + confidentialBalance.pendingBalance(), ); }, longTestTimeout, ); - const AUDITOR = TwistedEd25519PrivateKey.generate(); + // --- Auditor configuration tests --- + + const VOLUNTARY_AUDITOR = TwistedEd25519PrivateKey.generate(); + const EFFECTIVE_AUDITOR = TwistedEd25519PrivateKey.generate(); + test( - "it should transfer Alice's tokens to Alice's confidential balance with auditor", + "it should transfer with voluntary auditor only (no effective auditor on-chain)", async () => { const confidentialBalance = await confidentialAsset.getBalance({ accountAddress: alice.accountAddress, @@ -389,43 +422,50 @@ describe("Confidential Asset Sender API", () => { amount: TRANSFER_AMOUNT, signer: alice, tokenAddress: TOKEN_ADDRESS, - recipient: alice.accountAddress, - additionalAuditorEncryptionKeys: [AUDITOR.publicKey()], + recipient: bob.accountAddress, + additionalAuditorEncryptionKeys: [VOLUNTARY_AUDITOR.publicKey()], }); expect(transferTx.success).toBeTruthy(); - // Verify the confidential balance has been updated correctly await checkAliceDecryptedBalance( confidentialBalance.availableBalance() - TRANSFER_AMOUNT, - confidentialBalance.pendingBalance() + TRANSFER_AMOUNT, + confidentialBalance.pendingBalance(), ); }, longTestTimeout, ); + // Effective-auditor e2e tests require calling set_asset_specific_auditor as the @aptos_framework (0x1) + // signer, which we can't do on localnet without the framework private key. The effective-auditor + // sigma protocol is fully covered by Move unit tests (all 6 configs) and SDK unit tests. + // TODO: once an `aptos` CLI profile for 0x1 is available on localnet, add e2e tests for: + // - "effective auditor only" (set global auditor, transfer with no voluntary auditors) + // - "effective + voluntary auditors" (set global auditor, transfer with voluntary auditors) + test( - "it should check is Alice's balance not frozen", + "it should check that Alice's incoming transfers are not paused", async () => { - const isFrozen = await confidentialAsset.isPendingBalanceFrozen({ + const isPaused = await confidentialAsset.isIncomingTransfersPaused({ accountAddress: alice.accountAddress, tokenAddress: TOKEN_ADDRESS, }); - expect(isFrozen).toBeFalsy(); + expect(isPaused).toBeFalsy(); }, longTestTimeout, ); test( - "it should throw if checking is Bob's balance not frozen", + "it should throw if checking whether an unregistered account's incoming transfers are paused", async () => { + const unregistered = Account.generate(); await expect( - confidentialAsset.isPendingBalanceFrozen({ - accountAddress: bob.accountAddress, + confidentialAsset.isIncomingTransfersPaused({ + accountAddress: unregistered.accountAddress, tokenAddress: TOKEN_ADDRESS, }), - ).rejects.toThrow("ECA_STORE_NOT_PUBLISHED"); + ).rejects.toThrow("E_CONFIDENTIAL_STORE_NOT_REGISTERED"); }, longTestTimeout, ); @@ -496,12 +536,13 @@ describe("Confidential Asset Sender API", () => { test( "it should throw if checking if account balance is normalized and the account has not registered a balance", async () => { + const unregistered = Account.generate(); await expect( confidentialAsset.isBalanceNormalized({ tokenAddress: TOKEN_ADDRESS, - accountAddress: bob.accountAddress, + accountAddress: unregistered.accountAddress, }), - ).rejects.toThrow("ECA_STORE_NOT_PUBLISHED"); + ).rejects.toThrow("E_CONFIDENTIAL_STORE_NOT_REGISTERED"); }, longTestTimeout, ); @@ -565,7 +606,7 @@ describe("Confidential Asset Sender API", () => { amount: TRANSFER_AMOUNT, signer: alice, tokenAddress: TOKEN_ADDRESS, - recipient: alice.accountAddress, + recipient: bob.accountAddress, }); expect(transferTx.success).toBeTruthy(); @@ -593,20 +634,19 @@ describe("Confidential Asset Sender API", () => { decryptionKey: aliceConfidential, }); - // This should unfreeze the balance even though withUnfreezeBalance is unset as it will check the - // chain for frozen state. - const keyRotationAndUnfreezeTx = await confidentialAsset.rotateEncryptionKey({ + // This will unpause incoming transfers after rotation (unpause defaults to true). + const keyRotationAndUnpauseTx = await confidentialAsset.rotateEncryptionKey({ signer: alice, senderDecryptionKey: aliceConfidential, newSenderDecryptionKey: ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, tokenAddress: TOKEN_ADDRESS, }); - for (const tx of keyRotationAndUnfreezeTx) { + for (const tx of keyRotationAndUnpauseTx) { expect(tx.success).toBeTruthy(); } - // Check that the balance is unfrozen - await checkAliceBalanceFrozenStatus(false); + // Check that incoming transfers are unpaused + await checkAliceIncomingTransfersPausedStatus(false); // If this decrypts correctly, then the key rotation worked. await checkAliceDecryptedBalance( diff --git a/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts b/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts deleted file mode 100644 index 0ac32dabd..000000000 --- a/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts +++ /dev/null @@ -1,557 +0,0 @@ -// Copyright © Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - -import { Account, AccountAddressInput, AnyNumber } from "@aptos-labs/ts-sdk"; -import { TwistedEd25519PrivateKey } from "../../src"; -import { - getTestAccount, - getTestConfidentialAccount, - aptos, - TOKEN_ADDRESS, - sendAndWaitTx, - longTestTimeout, - confidentialAsset, -} from "../helpers"; - -describe.skip("Confidential balance api", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - const transactionBuilder = confidentialAsset.transaction; - - const bob = Account.generate(); - - async function getPublicTokenBalance(accountAddress: AccountAddressInput) { - return await aptos.getAccountCoinAmount({ - accountAddress, - faMetadataAddress: TOKEN_ADDRESS, - }); - } - async function checkAliceDecryptedBalance( - expectedAvailable: AnyNumber, - expectedPending: AnyNumber, - decryptionKey?: TwistedEd25519PrivateKey, - ) { - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: decryptionKey || aliceConfidential, - }); - - expect(confidentialBalance.availableBalance()).toBe(BigInt(expectedAvailable)); - expect(confidentialBalance.pendingBalance()).toBe(BigInt(expectedPending)); - } - - async function checkAliceNormalizedBalanceStatus(expectedStatus: boolean) { - const isNormalized = await confidentialAsset.isBalanceNormalized({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - expect(isNormalized).toBe(expectedStatus); - } - - async function checkAliceBalanceFrozenStatus(expectedStatus: boolean) { - const isFrozen = await confidentialAsset.isPendingBalanceFrozen({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - expect(isFrozen).toBe(expectedStatus); - } - beforeAll(async () => { - await aptos.fundAccount({ - accountAddress: alice.accountAddress, - amount: 100000000, - }); - await aptos.fundAccount({ - accountAddress: bob.accountAddress, - amount: 100000000, - }); - - console.log("Funded accounts"); - - const isAliceRegistered = await confidentialAsset.hasUserRegistered({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - expect(isAliceRegistered).toBeFalsy(); - const registerBalanceTx = await transactionBuilder.registerBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - const registerBalanceTxResp = await sendAndWaitTx(registerBalanceTx, alice); - - expect(registerBalanceTxResp.success).toBeTruthy(); - - await checkAliceDecryptedBalance(0, 0); - }, longTestTimeout); - - test( - "it should check Alice encrypted confidential balances", - async () => { - const [aliceConfidentialBalances] = await Promise.all([ - confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }), - ]); - expect(aliceConfidentialBalances.pendingBalance()).toBe(0n); - expect(aliceConfidentialBalances.availableBalance()).toBe(100n); - }, - longTestTimeout, - ); - - const DEPOSIT_AMOUNT = 5; - test( - "it should deposit Alice's balance of fungible token to her confidential balance and check the balance", - async () => { - const depositTx = await transactionBuilder.deposit({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - amount: DEPOSIT_AMOUNT, - }); - const resp = await sendAndWaitTx(depositTx, alice); - - expect(resp.success).toBeTruthy(); - - // Verify the confidential balance has been updated correctly. - await checkAliceDecryptedBalance(0, DEPOSIT_AMOUNT); - }, - longTestTimeout, - ); - - test( - "it should rollover Alice's confidential balance and check the balance", - async () => { - const rolloverTx = await transactionBuilder.rolloverPendingBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - const txResp = await sendAndWaitTx(rolloverTx, alice); - expect(txResp.success).toBeTruthy(); - - // This should be false after a rollover - checkAliceNormalizedBalanceStatus(false); - - // Verify the confidential balance has been updated correctly - await checkAliceDecryptedBalance(DEPOSIT_AMOUNT, 0); - }, - longTestTimeout, - ); - - test( - "it should throw error if rollover is attempted but Alice's confidential balance is not normalized", - async () => { - // Verify the current balance is not normalized - checkAliceNormalizedBalanceStatus(false); - - // Attempting to rollover should throw an error as the balance is not normalized - await expect( - transactionBuilder.rolloverPendingBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }), - ).rejects.toThrow("Balance must be normalized before rollover"); - - // We can force creation of the transaction by setting checkNormalized to false - const rolloverTx = await transactionBuilder.rolloverPendingBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - checkNormalized: false, - }); - - // Sending the transaction should result in an error thrown as the chain will fail the transaction - // since the balance is not normalized - await expect(sendAndWaitTx(rolloverTx, alice)).rejects.toThrow( - "The operation requires the actual balance to be normalized.", - ); - }, - longTestTimeout, - ); - - const WITHDRAW_AMOUNT = 1; - test( - "it should withdraw Alice's confidential balance and check the balance", - async () => { - // Get the current public token balance of Alice - const aliceTokenBalance = await aptos.getAccountCoinAmount({ - accountAddress: alice.accountAddress, - faMetadataAddress: TOKEN_ADDRESS, - }); - - // Withdraw the amount from the confidential balance to the public balance - const withdrawTx = await transactionBuilder.withdraw({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - senderDecryptionKey: aliceConfidential, - amount: WITHDRAW_AMOUNT, - }); - const txResp = await sendAndWaitTx(withdrawTx, alice); - - expect(txResp.success).toBeTruthy(); - - // Verify the confidential balance has been updated correctly - await checkAliceDecryptedBalance(DEPOSIT_AMOUNT - WITHDRAW_AMOUNT, 0); - - // Verify the public token balance has been updated correctly - const aliceNewTokenBalance = await aptos.getAccountCoinAmount({ - accountAddress: alice.accountAddress, - faMetadataAddress: TOKEN_ADDRESS, - }); - - expect(aliceNewTokenBalance).toBe(aliceTokenBalance + WITHDRAW_AMOUNT); - - // Verify the balance is normalized after the withdrawal - checkAliceNormalizedBalanceStatus(true); - }, - longTestTimeout, - ); - - test( - "it should throw if withdrawing more than the available balance", - async () => { - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - - // Withdraw the amount from the confidential balance to the public balance - await expect( - transactionBuilder.withdraw({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - senderDecryptionKey: aliceConfidential, - amount: confidentialBalance.availableBalance() + BigInt(1), - }), - ).rejects.toThrow("Insufficient balance"); - }, - longTestTimeout, - ); - - // TODO: Add this back in once the test setup sets up the auditor correctly. - test.skip( - "it should get global auditor", - async () => { - const globalAuditor = await transactionBuilder.getAssetAuditorEncryptionKey({ - tokenAddress: TOKEN_ADDRESS, - }); - - expect(globalAuditor).toBeDefined(); - }, - longTestTimeout, - ); - - const TRANSFER_AMOUNT = 2n; - test( - "it should throw if transfering to another account that has not registered a balance", - async () => { - // Check that Bob has not registered a balance - const isBobRegistered = await confidentialAsset.hasUserRegistered({ - accountAddress: bob.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - expect(isBobRegistered).toBeFalsy(); - - await expect( - transactionBuilder.transfer({ - senderDecryptionKey: aliceConfidential, - amount: TRANSFER_AMOUNT, - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - recipient: bob.accountAddress, - }), - ).rejects.toThrow("Failed to get encryption key for recipient"); - }, - longTestTimeout, - ); - - test( - "it should throw if transferring more than the available balance", - async () => { - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - - // Withdraw the amount from the confidential balance to the public balance - await expect( - transactionBuilder.transfer({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - senderDecryptionKey: aliceConfidential, - amount: confidentialBalance.availableBalance() + BigInt(1), - recipient: alice.accountAddress, - }), - ).rejects.toThrow("Insufficient balance"); - }, - longTestTimeout, - ); - - test( - "it should transfer Alice's tokens to Alice's pending balance without auditor", - async () => { - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - - const transferTx = await transactionBuilder.transfer({ - senderDecryptionKey: aliceConfidential, - amount: TRANSFER_AMOUNT, - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - recipient: alice.accountAddress, - }); - const txResp = await sendAndWaitTx(transferTx, alice); - - expect(txResp.success).toBeTruthy(); - - // Verify the confidential balance has been updated correctly - await checkAliceDecryptedBalance( - confidentialBalance.availableBalance() - TRANSFER_AMOUNT, - confidentialBalance.pendingBalance() + TRANSFER_AMOUNT, - ); - }, - longTestTimeout, - ); - - const AUDITOR = TwistedEd25519PrivateKey.generate(); - test( - "it should transfer Alice's tokens to Alice's confidential balance with auditor", - async () => { - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - - const transferTx = await transactionBuilder.transfer({ - senderDecryptionKey: aliceConfidential, - amount: TRANSFER_AMOUNT, - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - recipient: alice.accountAddress, - additionalAuditorEncryptionKeys: [AUDITOR.publicKey()], - }); - const txResp = await sendAndWaitTx(transferTx, alice); - - expect(txResp.success).toBeTruthy(); - - // Verify the confidential balance has been updated correctly - await checkAliceDecryptedBalance( - confidentialBalance.availableBalance() - TRANSFER_AMOUNT, - confidentialBalance.pendingBalance() + TRANSFER_AMOUNT, - ); - }, - longTestTimeout, - ); - - test( - "it should check is Alice's balance not frozen", - async () => { - const isFrozen = await confidentialAsset.isPendingBalanceFrozen({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - expect(isFrozen).toBeFalsy(); - }, - longTestTimeout, - ); - - test( - "it should throw if checking is Bob's balance not frozen", - async () => { - await expect( - confidentialAsset.isPendingBalanceFrozen({ - accountAddress: bob.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }), - ).rejects.toThrow("ECA_STORE_NOT_PUBLISHED"); - }, - longTestTimeout, - ); - - test( - "it should normalize Alice's confidential balance", - async () => { - const rolloverTx = await transactionBuilder.rolloverPendingBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - const txResp = await sendAndWaitTx(rolloverTx, alice); - expect(txResp.success).toBeTruthy(); - - // This should be false after a rollover - checkAliceNormalizedBalanceStatus(false); - - const normalizeTx = await transactionBuilder.normalizeBalance({ - tokenAddress: TOKEN_ADDRESS, - senderDecryptionKey: aliceConfidential, - sender: alice.accountAddress, - }); - - const normalizeTxResp = await sendAndWaitTx(normalizeTx, alice); - expect(normalizeTxResp.success).toBeTruthy(); - - // This should be true after normalization - checkAliceNormalizedBalanceStatus(true); - }, - longTestTimeout, - ); - - test( - "it should throw if Bob's balance is normalized", - async () => { - await expect( - confidentialAsset.isBalanceNormalized({ - tokenAddress: TOKEN_ADDRESS, - accountAddress: bob.accountAddress, - }), - ).rejects.toThrow("ECA_STORE_NOT_PUBLISHED"); - }, - longTestTimeout, - ); - - test( - "it throw if withdraw to another account", - async () => { - // Get the current public token balance of Bob - const bobTokenBalance = await getPublicTokenBalance(bob.accountAddress); - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - - // Withdraw the amount from the confidential balance to the public balance - const withdrawTx = await transactionBuilder.withdraw({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - senderDecryptionKey: aliceConfidential, - amount: WITHDRAW_AMOUNT, - recipient: bob.accountAddress, - }); - const txResp = await sendAndWaitTx(withdrawTx, alice); - expect(txResp.success).toBeTruthy(); - - const bobNewTokenBalance = await getPublicTokenBalance(bob.accountAddress); - expect(bobNewTokenBalance).toBe(bobTokenBalance + WITHDRAW_AMOUNT); - - // Verify the confidential balance has been updated correctly - await checkAliceDecryptedBalance( - confidentialBalance.availableBalance() - BigInt(WITHDRAW_AMOUNT), - confidentialBalance.pendingBalance(), - ); - }, - longTestTimeout, - ); - - test( - "it should check that transferring normalizes Alice's confidential balance", - async () => { - const depositTx = await transactionBuilder.deposit({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - amount: DEPOSIT_AMOUNT, - }); - const resp = await sendAndWaitTx(depositTx, alice); - expect(resp.success).toBeTruthy(); - - const rolloverTx = await transactionBuilder.rolloverPendingBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - const txResp = await sendAndWaitTx(rolloverTx, alice); - expect(txResp.success).toBeTruthy(); - - // This should be false after normalization - checkAliceNormalizedBalanceStatus(false); - - const transferTx = await transactionBuilder.transfer({ - senderDecryptionKey: aliceConfidential, - amount: TRANSFER_AMOUNT, - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - recipient: alice.accountAddress, - }); - const transferTxResp = await sendAndWaitTx(transferTx, alice); - expect(transferTxResp.success).toBeTruthy(); - - // This should be true after normalization - checkAliceNormalizedBalanceStatus(true); - }, - longTestTimeout, - ); - - const ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY = TwistedEd25519PrivateKey.generate(); - test( - "it should rotate Alice's confidential balance key", - async () => { - const depositTx = await transactionBuilder.deposit({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - amount: DEPOSIT_AMOUNT, - }); - let txResp = await sendAndWaitTx(depositTx, alice); - expect(txResp.success).toBeTruthy(); - - await expect( - transactionBuilder.rotateEncryptionKey({ - sender: alice.accountAddress, - - senderDecryptionKey: aliceConfidential, - newSenderDecryptionKey: ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, - withUnfreezePendingBalance: true, - tokenAddress: TOKEN_ADDRESS, - }), - ).rejects.toThrow("Pending balance must be 0 before rotating encryption key"); - - const rolloverTx = await transactionBuilder.rolloverPendingBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - withFreezeBalance: true, - }); - txResp = await sendAndWaitTx(rolloverTx, alice); - expect(txResp.success).toBeTruthy(); - - await checkAliceBalanceFrozenStatus(true); - - // Get the current balance before rotation - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - - // This should unfreeze the balance even though withUnfreezeBalance is unset as it will check the - // chain for frozen state. - const keyRotationAndUnfreezeTx = await transactionBuilder.rotateEncryptionKey({ - sender: alice.accountAddress, - senderDecryptionKey: aliceConfidential, - newSenderDecryptionKey: ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, - tokenAddress: TOKEN_ADDRESS, - }); - txResp = await sendAndWaitTx(keyRotationAndUnfreezeTx, alice); - expect(txResp.success).toBeTruthy(); - - // Check that the balance is unfrozen - await checkAliceBalanceFrozenStatus(false); - - // If this decrypts correctly, then the key rotation worked. - await checkAliceDecryptedBalance( - confidentialBalance.availableBalance(), - confidentialBalance.pendingBalance(), - ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, - ); - }, - longTestTimeout, - ); -}); diff --git a/confidential-assets/tests/units/api/checkBalances.test.ts b/confidential-assets/tests/units/api/checkBalances.test.ts deleted file mode 100644 index a694a2332..000000000 --- a/confidential-assets/tests/units/api/checkBalances.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { preloadTables } from "../../../src/preloadKangarooTables"; -import { getBalances, getTestAccount, getTestConfidentialAccount, longTestTimeout } from "../../helpers"; - -describe("Check balance", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - it("should check balance", async () => { - const balances = await getBalances(aliceConfidential, alice.accountAddress); - - console.log({ - pending: { - encrypted: balances.pendingBalanceCipherText().map((el) => el.serialize()), - amount: balances.pendingBalance(), - amountChunks: balances.pending.getAmountChunks().map((chunk) => chunk.toString()), - }, - actual: { - encrypted: balances.availableBalanceCipherText().map((el) => el.serialize()), - amount: balances.availableBalance(), - amountChunks: balances.available.getAmountChunks().map((chunk) => chunk.toString()), - }, - }); - - expect(balances.pendingBalance()).toBeDefined(); - expect(balances.availableBalance()).toBeDefined(); - }); -}); diff --git a/confidential-assets/tests/units/api/checkIsFrozen.test.ts b/confidential-assets/tests/units/api/checkIsFrozen.test.ts deleted file mode 100644 index 12e0ad329..000000000 --- a/confidential-assets/tests/units/api/checkIsFrozen.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { confidentialAsset, getTestAccount, TOKEN_ADDRESS } from "../../helpers"; - -describe("should check if user confidential account is frozen", () => { - const alice = getTestAccount(); - - it("should check if user confidential account is frozen", async () => { - const isFrozen = await confidentialAsset.isBalanceFrozen({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - console.log(`${alice.accountAddress.toString()} frozen status is:`, isFrozen); - - expect(isFrozen).toBeDefined(); - }); -}); diff --git a/confidential-assets/tests/units/api/checkIsNormalized.test.ts b/confidential-assets/tests/units/api/checkIsNormalized.test.ts deleted file mode 100644 index d1e2209f0..000000000 --- a/confidential-assets/tests/units/api/checkIsNormalized.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { confidentialAsset, getTestAccount, TOKEN_ADDRESS } from "../../helpers"; - -describe("Check is normalized", () => { - const alice = getTestAccount(); - - it("should check if user confidential balance is normalized", async () => { - const isAliceBalanceNormalized = await confidentialAsset.isBalanceNormalized({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - expect(isAliceBalanceNormalized).toBeDefined(); - }); -}); diff --git a/confidential-assets/tests/units/api/checkIsRegistered.test.ts b/confidential-assets/tests/units/api/checkIsRegistered.test.ts deleted file mode 100644 index 97e39fad1..000000000 --- a/confidential-assets/tests/units/api/checkIsRegistered.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { confidentialAsset, getTestAccount, TOKEN_ADDRESS } from "../../helpers"; - -describe("Check Registration status", () => { - const alice = getTestAccount(); - - it("should return true if the user is registered", async () => { - const isAliceRegistered = await confidentialAsset.hasUserRegistered({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - expect(isAliceRegistered).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/api/deposit.test.ts b/confidential-assets/tests/units/api/deposit.test.ts deleted file mode 100644 index 4c21aea42..000000000 --- a/confidential-assets/tests/units/api/deposit.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { aptos, confidentialAsset, getTestAccount, TOKEN_ADDRESS } from "../../helpers"; - -describe("Deposit", () => { - const alice = getTestAccount(); - - const DEPOSIT_AMOUNT = 5n; - it("it should deposit Alice's balance of fungible token to her confidential balance", async () => { - await aptos.fundAccount({ - accountAddress: alice.accountAddress, - amount: 100000000, - }); - - const depositTx = await confidentialAsset.deposit({ - signer: alice, - tokenAddress: TOKEN_ADDRESS, - amount: DEPOSIT_AMOUNT, - }); - - console.log("gas used:", depositTx.gas_used); - - expect(depositTx.success).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/api/getAssetAuditor.test.ts b/confidential-assets/tests/units/api/getAssetAuditor.test.ts deleted file mode 100644 index 70cd70c13..000000000 --- a/confidential-assets/tests/units/api/getAssetAuditor.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { confidentialAsset, TOKEN_ADDRESS } from "../../helpers"; - -describe("Global auditor", () => { - it("it should get global auditor", async () => { - const globalAuditorPubKey = await confidentialAsset.getAssetAuditorEncryptionKey({ - tokenAddress: TOKEN_ADDRESS, - }); - - console.log(globalAuditorPubKey); - - expect(globalAuditorPubKey).toBeDefined(); - }); -}); diff --git a/confidential-assets/tests/units/api/negativeNormalize.test.ts b/confidential-assets/tests/units/api/negativeNormalize.test.ts deleted file mode 100644 index 13998cf0e..000000000 --- a/confidential-assets/tests/units/api/negativeNormalize.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Account } from "@aptos-labs/ts-sdk"; -import { - aptos, - confidentialAsset, - getBalances, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, -} from "../../helpers"; -import { numberToBytesLE, bytesToNumberLE } from "@noble/curves/abstract/utils"; -import { bytesToHex } from "@noble/hashes/utils"; -import { ed25519modN } from "../../../src"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Transfer", () => { - const alice = Account.generate(); - const aliceConfidential = getTestConfidentialAccount(alice); - - const coinType = "0x1::aptos_coin::AptosCoin"; - const tokenAddress = "0x000000000000000000000000000000000000000000000000000000000000000a"; - const fundAmount = 1 * 10 ** 8; - const depositAmount = 0.5 * 10 ** 8; - - console.log("pk", alice.privateKey.toString()); - console.log("dk", aliceConfidential.toString()); - console.log("ek", aliceConfidential.publicKey().toString()); - console.log( - "dk(move)", - bytesToHex(numberToBytesLE(ed25519modN(bytesToNumberLE(aliceConfidential.toUint8Array())), 32)), - ); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - it("should fund Alice's account", async () => { - await aptos.fundAccount({ - accountAddress: alice.accountAddress, - amount: fundAmount, - }); - }); - - it("should register Alice's balance", async () => { - const aliceRegisterVBTxBody = await confidentialAsset.registerBalance({ - sender: alice.accountAddress, - tokenAddress: tokenAddress, - decryptionKey: aliceConfidential, - }); - - const aliceTxResp = await sendAndWaitTx(aliceRegisterVBTxBody, alice); - - console.log("gas used:", aliceTxResp.gas_used); - }); - - it("should deposit money to Alice's account", async () => { - const depositTx = await confidentialAsset.deposit({ - sender: alice.accountAddress, - tokenAddress, - amount: depositAmount, - }); - - const resp = await sendAndWaitTx(depositTx, alice); - - console.log("gas used:", resp.gas_used); - }); - - it("Should rollover the balance", async () => { - const rolloverTx = await confidentialAsset.rolloverPendingBalance({ - sender: alice.accountAddress, - tokenAddress, - }); - - const resp = await sendAndWaitTx(rolloverTx, alice); - - console.log("gas used:", resp.gas_used); - }); - - it("should transfer money from Alice actual to pending balance", async () => { - const balances = await getBalances(aliceConfidential, alice.accountAddress, tokenAddress); - - console.log({ balances }); - - const normalizeTx = await confidentialAsset.normalizeBalance({ - tokenAddress, - senderDecryptionKey: aliceConfidential, - sender: alice.accountAddress, - }); - - const txResp = await sendAndWaitTx(normalizeTx, alice); - - console.log("gas used:", txResp.gas_used); - - const balancesAfterTransfer = await getBalances(aliceConfidential, alice.accountAddress, tokenAddress); - - console.log(balancesAfterTransfer.availableBalance()); - - expect(txResp.success).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/api/negativeTransfer.test.ts b/confidential-assets/tests/units/api/negativeTransfer.test.ts deleted file mode 100644 index fe4f1b01d..000000000 --- a/confidential-assets/tests/units/api/negativeTransfer.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Account } from "@aptos-labs/ts-sdk"; -import { - aptos, - confidentialAsset, - getBalances, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, -} from "../../helpers"; -import { numberToBytesLE, bytesToNumberLE } from "@noble/curves/abstract/utils"; -import { bytesToHex } from "@noble/hashes/utils"; -import { ed25519modN } from "../../../src"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Transfer", () => { - const alice = Account.generate(); - const aliceConfidential = getTestConfidentialAccount(alice); - - const coinType = "0x1::aptos_coin::AptosCoin"; - const tokenAddress = "0x000000000000000000000000000000000000000000000000000000000000000a"; - const fundAmount = 1 * 10 ** 8; - const depositAmount = 0.5 * 10 ** 8; - const recipientAccAddr = "0x82094619a5e8621f2bf9e6479a62ed694dca9b8fd69b0383fce359a3070aa0d4"; - const transferAmount = BigInt(0.1 * 10 ** 8); - - console.log("pk", alice.privateKey.toString()); - console.log("dk", aliceConfidential.toString()); - console.log("ek", aliceConfidential.publicKey().toString()); - console.log( - "dk(move)", - bytesToHex(numberToBytesLE(ed25519modN(bytesToNumberLE(aliceConfidential.toUint8Array())), 32)), - ); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - it("should fund Alice's account", async () => { - await aptos.fundAccount({ - accountAddress: alice.accountAddress, - amount: fundAmount, - }); - }); - - it("should register Alice's balance", async () => { - const aliceRegisterVBTxBody = await confidentialAsset.registerBalance({ - sender: alice.accountAddress, - tokenAddress: tokenAddress, - decryptionKey: aliceConfidential, - }); - - const aliceTxResp = await sendAndWaitTx(aliceRegisterVBTxBody, alice); - - console.log("gas used:", aliceTxResp.gas_used); - }); - - it.skip("should deposit money to Alice's account", async () => { - const depositTx = await confidentialAsset.deposit({ - sender: alice.accountAddress, - tokenAddress, - amount: depositAmount, - }); - - const resp = await sendAndWaitTx(depositTx, alice); - - console.log("gas used:", resp.gas_used); - }); - - it("should transfer money from Alice actual to pending balance", async () => { - const transferTx = await confidentialAsset.transfer({ - senderDecryptionKey: aliceConfidential, - recipient: recipientAccAddr, - amount: transferAmount, - sender: alice.accountAddress, - tokenAddress, - }); - const txResp = await sendAndWaitTx(transferTx, alice); - - console.log("gas used:", txResp.gas_used); - - const balancesAfterTransfer = await getBalances(aliceConfidential, alice.accountAddress, tokenAddress); - - console.log(balancesAfterTransfer.availableBalance()); - - expect(txResp.success).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/api/negativeWithdraw.test.ts b/confidential-assets/tests/units/api/negativeWithdraw.test.ts deleted file mode 100644 index 4064e2109..000000000 --- a/confidential-assets/tests/units/api/negativeWithdraw.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - aptos, - confidentialAsset, - getTestAccount, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, -} from "../../helpers"; -import { numberToBytesLE, bytesToNumberLE } from "@noble/curves/abstract/utils"; -import { bytesToHex } from "@noble/hashes/utils"; -import { ed25519modN } from "../../../src"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Negative withdraw", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - const coinType = "0x1::aptos_coin::AptosCoin"; - const tokenAddress = "0x000000000000000000000000000000000000000000000000000000000000000a"; - const fundAmount = 1 * 10 ** 8; - const depositAmount = 0.5 * 10 ** 8; - const withdrawAmount = BigInt(0.1 * 10 ** 8); - - console.log("pk", alice.privateKey.toString()); - console.log("dk", aliceConfidential.toString()); - console.log("ek", aliceConfidential.publicKey().toString()); - console.log( - "dk(move)", - bytesToHex(numberToBytesLE(ed25519modN(bytesToNumberLE(aliceConfidential.toUint8Array())), 32)), - ); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - it("should fund Alice's account", async () => { - await aptos.fundAccount({ - accountAddress: alice.accountAddress, - amount: fundAmount, - }); - }); - - it("should register Alice's balance", async () => { - const aliceRegisterVBTxBody = await confidentialAsset.registerBalance({ - sender: alice.accountAddress, - tokenAddress: tokenAddress, - decryptionKey: aliceConfidential, - }); - - const aliceTxResp = await sendAndWaitTx(aliceRegisterVBTxBody, alice); - - console.log("gas used:", aliceTxResp.gas_used); - }); - - it("should deposit money to Alice's account", async () => { - const depositTx = await confidentialAsset.deposit({ - sender: alice.accountAddress, - tokenAddress, - amount: depositAmount, - }); - - const resp = await sendAndWaitTx(depositTx, alice); - - console.log("gas used:", resp.gas_used); - }); - - it("should throw error withdrawing money from Alice actual balance", async () => { - const withdrawTx = await confidentialAsset.withdraw({ - sender: alice.accountAddress, - tokenAddress, - senderDecryptionKey: aliceConfidential, - amount: withdrawAmount, - }); - - const txRespPromise = sendAndWaitTx(withdrawTx, alice); - - expect(txRespPromise).rejects.toThrow(); - }); -}); diff --git a/confidential-assets/tests/units/api/normalize.test.ts b/confidential-assets/tests/units/api/normalize.test.ts deleted file mode 100644 index e79dfce03..000000000 --- a/confidential-assets/tests/units/api/normalize.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - confidentialAsset, - getBalances, - getTestAccount, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, - TOKEN_ADDRESS, -} from "../../helpers"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Normalize", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - it( - "it should normalize Alice's confidential balance", - async () => { - const balances = await getBalances(aliceConfidential, alice.accountAddress); - - const normalizeTx = await confidentialAsset.normalizeUserBalance({ - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - unnormalizedEncryptedBalance: balances.actual.getAmountEncrypted(aliceConfidential.publicKey()), - balanceAmount: balances.actual.amount, - - sender: alice.accountAddress, - }); - - const txResp = await sendAndWaitTx(normalizeTx, alice); - - console.log("gas used:", txResp.gas_used); - - expect(txResp.success).toBeTruthy(); - }, - longTestTimeout, - ); -}); diff --git a/confidential-assets/tests/units/api/register.test.ts b/confidential-assets/tests/units/api/register.test.ts deleted file mode 100644 index 706db01d3..000000000 --- a/confidential-assets/tests/units/api/register.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - confidentialAsset, - getTestAccount, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, - TOKEN_ADDRESS, -} from "../../helpers"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Register", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - it( - "it should register Alice confidential balance", - async () => { - const aliceRegisterVBTxBody = await confidentialAsset.registerBalance({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - publicKey: aliceConfidential.publicKey(), - }); - - const aliceTxResp = await sendAndWaitTx(aliceRegisterVBTxBody, alice); - - console.log("gas used:", aliceTxResp.gas_used); - - expect(aliceTxResp.success).toBeTruthy(); - }, - longTestTimeout, - ); -}); diff --git a/confidential-assets/tests/units/api/safelyRollover.test.ts b/confidential-assets/tests/units/api/safelyRollover.test.ts deleted file mode 100644 index 144f55392..000000000 --- a/confidential-assets/tests/units/api/safelyRollover.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - confidentialAsset, - getTestAccount, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitBatchTxs, - TOKEN_ADDRESS, -} from "../../helpers"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Safely Rollover", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - it("Should safely rollover Alice confidential balance", async () => { - const rolloverTxPayloads = await confidentialAsset.safeRolloverPendingCB({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - withFreezeBalance: false, - decryptionKey: aliceConfidential, - }); - - const txResponses = await sendAndWaitBatchTxs(rolloverTxPayloads, alice); - - console.log("gas used:", txResponses.map((el) => `${el.hash} - ${el.gas_used}`).join("\n")); - - expect(txResponses.every((el) => el.success)).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/api/transfer.test.ts b/confidential-assets/tests/units/api/transfer.test.ts deleted file mode 100644 index 138b9588b..000000000 --- a/confidential-assets/tests/units/api/transfer.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - confidentialAsset, - getTestAccount, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, - TOKEN_ADDRESS, -} from "../../helpers"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Transfer", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - const TRANSFER_AMOUNT = 2n; - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - it("should transfer money from Alice actual to pending balance", async () => { - const recipientAccAddr = "0xbae983154b659e5d0e9cb7f84001fdedb06482125a8e2945f47c2bc6ccd00690"; - - const transferTx = await confidentialAsset.transfer({ - senderDecryptionKey: aliceConfidential, - amount: TRANSFER_AMOUNT, - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - recipient: recipientAccAddr, - }); - const txResp = await sendAndWaitTx(transferTx, alice); - - console.log("gas used:", txResp.gas_used); - - expect(txResp.success).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/api/transferWithAuditor.test.ts b/confidential-assets/tests/units/api/transferWithAuditor.test.ts deleted file mode 100644 index 4c417c97d..000000000 --- a/confidential-assets/tests/units/api/transferWithAuditor.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { TwistedEd25519PrivateKey } from "../../../src"; -import { - confidentialAsset, - getTestAccount, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, - TOKEN_ADDRESS, -} from "../../helpers"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Transfer with auditor", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - const AUDITOR = TwistedEd25519PrivateKey.generate(); - const TRANSFER_AMOUNT = 2n; - test("it should transfer Alice's tokens to Alice's confidential balance with auditor", async () => { - const transferTx = await confidentialAsset.transfer({ - senderDecryptionKey: aliceConfidential, - recipient: alice.accountAddress, - amount: TRANSFER_AMOUNT, - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - additionalAuditorEncryptionKeys: [AUDITOR.publicKey()], - }); - const txResp = await sendAndWaitTx(transferTx, alice); - - console.log("gas used:", txResp.gas_used); - - expect(txResp.success).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/api/withdraw.test.ts b/confidential-assets/tests/units/api/withdraw.test.ts deleted file mode 100644 index c34d8590b..000000000 --- a/confidential-assets/tests/units/api/withdraw.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - confidentialAsset, - getBalances, - getTestAccount, - getTestConfidentialAccount, - longTestTimeout, - sendAndWaitTx, - TOKEN_ADDRESS, -} from "../../helpers"; -import { preloadTables } from "../../helpers/wasmPollardKangaroo"; - -describe("Withdraw", () => { - const alice = getTestAccount(); - const aliceConfidential = getTestConfidentialAccount(alice); - - it( - "Pre load wasm table map", - async () => { - await preloadTables(); - }, - longTestTimeout, - ); - - const WITHDRAW_AMOUNT = 1n; - it("should withdraw confidential amount", async () => { - const balances = await getBalances(aliceConfidential, alice.accountAddress); - - const withdrawTx = await confidentialAsset.withdraw({ - sender: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - encryptedActualBalance: balances.actual.getAmountEncrypted(aliceConfidential.publicKey()), - amountToWithdraw: WITHDRAW_AMOUNT, - }); - const txResp = await sendAndWaitTx(withdrawTx, alice); - - console.log("gas used:", txResp.gas_used); - - expect(txResp.success).toBeTruthy(); - }); -}); diff --git a/confidential-assets/tests/units/confidentialProofs.test.ts b/confidential-assets/tests/units/confidentialProofs.test.ts index 148024064..359d61749 100644 --- a/confidential-assets/tests/units/confidentialProofs.test.ts +++ b/confidential-assets/tests/units/confidentialProofs.test.ts @@ -3,17 +3,17 @@ import { CHUNK_BITS_BIG_INT, ChunkedAmount, ConfidentialKeyRotation, - ConfidentialKeyRotationSigmaProof, ConfidentialNormalization, - ConfidentialNormalizationSigmaProof, ConfidentialTransfer, ConfidentialTransferRangeProof, - ConfidentialTransferSigmaProof, ConfidentialWithdraw, - ConfidentialWithdrawSigmaProof, EncryptedAmount, TwistedEd25519PrivateKey, } from "../../src"; +import type { SigmaProtocolProof } from "../../src/crypto/sigmaProtocol"; +import { verifyWithdrawal } from "../../src/crypto/sigmaProtocolWithdraw"; +import { verifyNormalization } from "../../src/crypto/sigmaProtocolWithdraw"; +import { verifyTransfer } from "../../src/crypto/sigmaProtocolTransfer"; import { longTestTimeout } from "../helpers"; describe("Generate 'confidential coin' proofs", () => { @@ -29,9 +29,14 @@ describe("Generate 'confidential coin' proofs", () => { }); const aliceEncryptedBalanceCipherText = aliceEncryptedBalance.getCipherText(); + // Use dummy addresses for the unit tests (32 bytes each) + const dummySenderAddress = new Uint8Array(32); + const dummyRecipientAddress = new Uint8Array(32).fill(0x0b); + const dummyTokenAddress = new Uint8Array(32).fill(0x0a); // 0xa = APT metadata address + const WITHDRAW_AMOUNT = 2n ** 16n; let confidentialWithdraw: ConfidentialWithdraw; - let confidentialWithdrawSigmaProof: ConfidentialWithdrawSigmaProof; + let confidentialWithdrawSigmaProof: SigmaProtocolProof; test( "Generate withdraw sigma proof", async () => { @@ -39,24 +44,34 @@ describe("Generate 'confidential coin' proofs", () => { decryptionKey: aliceConfidentialDecryptionKey, senderAvailableBalanceCipherText: aliceEncryptedBalanceCipherText, amount: WITHDRAW_AMOUNT, + senderAddress: dummySenderAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, }); - confidentialWithdrawSigmaProof = await confidentialWithdraw.genSigmaProof(); + confidentialWithdrawSigmaProof = confidentialWithdraw.genSigmaProof(); expect(confidentialWithdrawSigmaProof).toBeDefined(); + expect(confidentialWithdrawSigmaProof.commitment.length).toBeGreaterThan(0); + expect(confidentialWithdrawSigmaProof.response.length).toBeGreaterThan(0); }, longTestTimeout, ); test( "Verify withdraw sigma proof", - async () => { - const isValid = ConfidentialWithdraw.verifySigmaProof({ - senderEncryptedAvailableBalance: confidentialWithdraw.senderEncryptedAvailableBalance, - senderEncryptedAvailableBalanceAfterWithdrawal: - confidentialWithdraw.senderEncryptedAvailableBalanceAfterWithdrawal, - amountToWithdraw: WITHDRAW_AMOUNT, - sigmaProof: confidentialWithdrawSigmaProof, + () => { + const isValid = verifyWithdrawal({ + senderAddress: dummySenderAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, + amount: WITHDRAW_AMOUNT, + ekBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), + oldBalanceC: confidentialWithdraw.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), + oldBalanceD: confidentialWithdraw.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.D), + newBalanceC: confidentialWithdraw.senderEncryptedAvailableBalanceAfterWithdrawal.getCipherText().map((ct) => ct.C), + newBalanceD: confidentialWithdraw.senderEncryptedAvailableBalanceAfterWithdrawal.getCipherText().map((ct) => ct.D), + proof: confidentialWithdrawSigmaProof, }); expect(isValid).toBeTruthy(); @@ -88,7 +103,7 @@ describe("Generate 'confidential coin' proofs", () => { const TRANSFER_AMOUNT = 10n; let confidentialTransfer: ConfidentialTransfer; - let confidentialTransferSigmaProof: ConfidentialTransferSigmaProof; + let confidentialTransferSigmaProof: SigmaProtocolProof; test( "Generate transfer sigma proof", async () => { @@ -97,11 +112,17 @@ describe("Generate 'confidential coin' proofs", () => { senderAvailableBalanceCipherText: aliceEncryptedBalanceCipherText, amount: TRANSFER_AMOUNT, recipientEncryptionKey: bobConfidentialDecryptionKey.publicKey(), + senderAddress: dummySenderAddress, + recipientAddress: dummyRecipientAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, }); - confidentialTransferSigmaProof = await confidentialTransfer.genSigmaProof(); + confidentialTransferSigmaProof = confidentialTransfer.genSigmaProof(); expect(confidentialTransferSigmaProof).toBeDefined(); + expect(confidentialTransferSigmaProof.commitment.length).toBeGreaterThan(0); + expect(confidentialTransferSigmaProof.response.length).toBeGreaterThan(0); }, longTestTimeout, ); @@ -109,14 +130,22 @@ describe("Generate 'confidential coin' proofs", () => { test( "Verify transfer sigma proof", () => { - const isValid = ConfidentialTransfer.verifySigmaProof({ - senderPrivateKey: aliceConfidentialDecryptionKey, - recipientPublicKey: bobConfidentialDecryptionKey.publicKey(), - encryptedActualBalance: aliceEncryptedBalanceCipherText, - encryptedTransferAmountBySender: confidentialTransfer.transferAmountEncryptedBySender, - encryptedActualBalanceAfterTransfer: confidentialTransfer.senderEncryptedAvailableBalanceAfterTransfer, - encryptedTransferAmountByRecipient: confidentialTransfer.transferAmountEncryptedByRecipient, - sigmaProof: confidentialTransferSigmaProof, + const isValid = verifyTransfer({ + senderAddress: dummySenderAddress, + recipientAddress: dummyRecipientAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, + ekSidBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), + ekRidBytes: bobConfidentialDecryptionKey.publicKey().toUint8Array(), + oldBalanceC: confidentialTransfer.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), + oldBalanceD: confidentialTransfer.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.D), + newBalanceC: confidentialTransfer.senderEncryptedAvailableBalanceAfterTransfer.getCipherText().map((ct) => ct.C), + newBalanceD: confidentialTransfer.senderEncryptedAvailableBalanceAfterTransfer.getCipherText().map((ct) => ct.D), + transferAmountC: confidentialTransfer.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.C), + transferAmountDSender: confidentialTransfer.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.D), + transferAmountDRecipient: confidentialTransfer.transferAmountEncryptedByRecipient.getCipherText().map((ct) => ct.D), + hasEffectiveAuditor: false, + proof: confidentialTransferSigmaProof, }); expect(isValid).toBeTruthy(); @@ -149,7 +178,7 @@ describe("Generate 'confidential coin' proofs", () => { const auditor = TwistedEd25519PrivateKey.generate(); let confidentialTransferWithAuditors: ConfidentialTransfer; - let confidentialTransferWithAuditorsSigmaProof: ConfidentialTransferSigmaProof; + let confidentialTransferWithAuditorsSigmaProof: SigmaProtocolProof; test( "Generate transfer with auditors sigma proof", async () => { @@ -159,64 +188,53 @@ describe("Generate 'confidential coin' proofs", () => { amount: TRANSFER_AMOUNT, recipientEncryptionKey: bobConfidentialDecryptionKey.publicKey(), auditorEncryptionKeys: [auditor.publicKey()], + senderAddress: dummySenderAddress, + recipientAddress: dummyRecipientAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, }); - confidentialTransferWithAuditorsSigmaProof = await confidentialTransferWithAuditors.genSigmaProof(); + confidentialTransferWithAuditorsSigmaProof = confidentialTransferWithAuditors.genSigmaProof(); expect(confidentialTransferWithAuditorsSigmaProof).toBeDefined(); + expect(confidentialTransferWithAuditorsSigmaProof.commitment.length).toBeGreaterThan(0); + expect(confidentialTransferWithAuditorsSigmaProof.response.length).toBeGreaterThan(0); }, longTestTimeout, ); test( "Verify transfer with auditors sigma proof", () => { - const isValid = ConfidentialTransfer.verifySigmaProof({ - senderPrivateKey: aliceConfidentialDecryptionKey, - recipientPublicKey: bobConfidentialDecryptionKey.publicKey(), - encryptedActualBalance: aliceEncryptedBalanceCipherText, - encryptedActualBalanceAfterTransfer: - confidentialTransferWithAuditors.senderEncryptedAvailableBalanceAfterTransfer, - encryptedTransferAmountByRecipient: confidentialTransferWithAuditors.transferAmountEncryptedByRecipient, - encryptedTransferAmountBySender: confidentialTransferWithAuditors.transferAmountEncryptedBySender, - sigmaProof: confidentialTransferWithAuditorsSigmaProof, - auditors: { - publicKeys: [auditor.publicKey()], - auditorsCBList: confidentialTransferWithAuditors.transferAmountEncryptedByAuditors!.map((el) => - el.getCipherText(), - ), - }, + // This test uses a voluntary auditor (not an on-chain effective auditor) + const isValid = verifyTransfer({ + senderAddress: dummySenderAddress, + recipientAddress: dummyRecipientAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, + ekSidBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), + ekRidBytes: bobConfidentialDecryptionKey.publicKey().toUint8Array(), + oldBalanceC: confidentialTransferWithAuditors.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), + oldBalanceD: confidentialTransferWithAuditors.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.D), + newBalanceC: confidentialTransferWithAuditors.senderEncryptedAvailableBalanceAfterTransfer.getCipherText().map((ct) => ct.C), + newBalanceD: confidentialTransferWithAuditors.senderEncryptedAvailableBalanceAfterTransfer.getCipherText().map((ct) => ct.D), + transferAmountC: confidentialTransferWithAuditors.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.C), + transferAmountDSender: confidentialTransferWithAuditors.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.D), + transferAmountDRecipient: confidentialTransferWithAuditors.transferAmountEncryptedByRecipient.getCipherText().map((ct) => ct.D), + hasEffectiveAuditor: false, + auditorEkBytes: [auditor.publicKey().toUint8Array()], + newBalanceDAud: confidentialTransferWithAuditors.auditorEncryptedBalancesAfterTransfer.map( + (ea) => ea.getCipherText().map((ct) => ct.D), + ), + transferAmountDAud: confidentialTransferWithAuditors.transferAmountEncryptedByAuditors.map( + (ea) => ea.getCipherText().map((ct) => ct.D), + ), + proof: confidentialTransferWithAuditorsSigmaProof, }); expect(isValid).toBeTruthy(); }, longTestTimeout, ); - test( - "Should fail transfer sigma proof verification with wrong auditors", - () => { - const invalidAuditor = TwistedEd25519PrivateKey.generate(); - - const isValid = ConfidentialTransfer.verifySigmaProof({ - senderPrivateKey: aliceConfidentialDecryptionKey, - recipientPublicKey: bobConfidentialDecryptionKey.publicKey(), - encryptedActualBalance: aliceEncryptedBalanceCipherText, - encryptedActualBalanceAfterTransfer: - confidentialTransferWithAuditors.senderEncryptedAvailableBalanceAfterTransfer, - encryptedTransferAmountByRecipient: confidentialTransferWithAuditors.transferAmountEncryptedByRecipient, - encryptedTransferAmountBySender: confidentialTransferWithAuditors.transferAmountEncryptedBySender, - sigmaProof: confidentialTransferWithAuditorsSigmaProof, - auditors: { - publicKeys: [invalidAuditor.publicKey()], - auditorsCBList: confidentialTransferWithAuditors.transferAmountEncryptedByAuditors!.map((el) => - el.getCipherText(), - ), - }, - }); - - expect(isValid).toBeFalsy(); - }, - longTestTimeout, - ); let confidentialTransferWithAuditorsRangeProofs: ConfidentialTransferRangeProof; test( "Generate transfer with auditors range proofs", @@ -244,55 +262,55 @@ describe("Generate 'confidential coin' proofs", () => { ); const newAliceConfidentialPrivateKey = TwistedEd25519PrivateKey.generate(); - let confidentialKeyRotation: ConfidentialKeyRotation; - let confidentialKeyRotationSigmaProof: ConfidentialKeyRotationSigmaProof; + + let keyRotationProofResult: ReturnType; + test( "Generate key rotation sigma proof", - async () => { - confidentialKeyRotation = await ConfidentialKeyRotation.create({ + () => { + const confidentialKeyRotation = ConfidentialKeyRotation.create({ senderDecryptionKey: aliceConfidentialDecryptionKey, currentEncryptedAvailableBalance: aliceEncryptedBalance, newSenderDecryptionKey: newAliceConfidentialPrivateKey, + senderAddress: dummySenderAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, }); - confidentialKeyRotationSigmaProof = await confidentialKeyRotation.genSigmaProof(); - - expect(confidentialKeyRotationSigmaProof).toBeDefined(); + keyRotationProofResult = confidentialKeyRotation.authorizeKeyRotation(); + + const { newEkBytes, newDBytes, proof } = keyRotationProofResult; + + // Verify the proof structure + expect(newEkBytes).toBeDefined(); + expect(newEkBytes.length).toBe(32); + expect(newDBytes).toBeDefined(); + expect(newDBytes.length).toBe(AVAILABLE_BALANCE_CHUNK_COUNT); + newDBytes.forEach((d: Uint8Array) => expect(d.length).toBe(32)); + // Commitment has 3 + numChunks points (psi output size) + expect(proof.commitment.length).toBe(3 + AVAILABLE_BALANCE_CHUNK_COUNT); + proof.commitment.forEach((c: Uint8Array) => expect(c.length).toBe(32)); + // Response has 3 scalars (dk, delta, delta_inv) + expect(proof.response.length).toBe(3); + proof.response.forEach((r: Uint8Array) => expect(r.length).toBe(32)); }, longTestTimeout, ); + test( "Verify key rotation sigma proof", () => { - const isValid = ConfidentialKeyRotation.verifySigmaProof({ - sigmaProof: confidentialKeyRotationSigmaProof, - currPublicKey: aliceConfidentialDecryptionKey.publicKey(), - newPublicKey: newAliceConfidentialPrivateKey.publicKey(), - currEncryptedBalance: aliceEncryptedBalanceCipherText, - newEncryptedBalance: confidentialKeyRotation.newEncryptedAvailableBalance.getCipherText(), - }); - - expect(isValid).toBeTruthy(); - }, - longTestTimeout, - ); - - let confidentialKeyRotationRangeProof: Uint8Array; - test( - "Generate key rotation range proof", - async () => { - confidentialKeyRotationRangeProof = await confidentialKeyRotation.genRangeProof(); - - expect(confidentialKeyRotationRangeProof).toBeDefined(); - }, - longTestTimeout, - ); - test( - "Verify key rotation range proof", - async () => { - const isValid = ConfidentialKeyRotation.verifyRangeProof({ - rangeProof: confidentialKeyRotationRangeProof, - newEncryptedBalance: confidentialKeyRotation.newEncryptedAvailableBalance.getCipherText(), + const { newEkBytes, newDBytes, proof } = keyRotationProofResult; + + const isValid = ConfidentialKeyRotation.verify({ + oldEk: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), + newEk: newEkBytes, + oldD: aliceEncryptedBalanceCipherText.map((ct) => ct.D.toRawBytes()), + newD: newDBytes, + senderAddress: dummySenderAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, + proof, }); expect(isValid).toBeTruthy(); @@ -300,18 +318,6 @@ describe("Generate 'confidential coin' proofs", () => { longTestTimeout, ); - test( - "Authorize Key Rotation", - async () => { - const [{ sigmaProof, rangeProof }, newVB] = await confidentialKeyRotation.authorizeKeyRotation(); - - expect(sigmaProof).toBeDefined(); - expect(rangeProof).toBeDefined(); - expect(newVB).toBeDefined(); - }, - longTestTimeout, - ); - const unnormalizedAliceConfidentialAmount = ChunkedAmount.fromChunks([ ...Array.from({ length: AVAILABLE_BALANCE_CHUNK_COUNT - 1 }, () => 2n ** CHUNK_BITS_BIG_INT + 100n), 0n, @@ -322,29 +328,40 @@ describe("Generate 'confidential coin' proofs", () => { }); let confidentialNormalization: ConfidentialNormalization; - let confidentialNormalizationSigmaProof: ConfidentialNormalizationSigmaProof; + let confidentialNormalizationSigmaProof: SigmaProtocolProof; test( "Generate normalization sigma proof", async () => { confidentialNormalization = await ConfidentialNormalization.create({ decryptionKey: aliceConfidentialDecryptionKey, unnormalizedAvailableBalance: unnormalizedEncryptedBalance, + senderAddress: dummySenderAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, }); - confidentialNormalizationSigmaProof = await confidentialNormalization.genSigmaProof(); + confidentialNormalizationSigmaProof = confidentialNormalization.genSigmaProof(); expect(confidentialNormalizationSigmaProof).toBeDefined(); + expect(confidentialNormalizationSigmaProof.commitment.length).toBeGreaterThan(0); + expect(confidentialNormalizationSigmaProof.response.length).toBeGreaterThan(0); }, longTestTimeout, ); test( "Verify normalization sigma proof", () => { - const isValid = ConfidentialNormalization.verifySigmaProof({ - publicKey: aliceConfidentialDecryptionKey.publicKey(), - sigmaProof: confidentialNormalizationSigmaProof, - unnormalizedEncryptedBalance: confidentialNormalization.unnormalizedEncryptedAvailableBalance, - normalizedEncryptedBalance: confidentialNormalization.normalizedEncryptedAvailableBalance, + const isValid = verifyNormalization({ + senderAddress: dummySenderAddress, + tokenAddress: dummyTokenAddress, + chainId: 4, + amount: 0n, + ekBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), + oldBalanceC: confidentialNormalization.unnormalizedEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), + oldBalanceD: confidentialNormalization.unnormalizedEncryptedAvailableBalance.getCipherText().map((ct) => ct.D), + newBalanceC: confidentialNormalization.normalizedEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), + newBalanceD: confidentialNormalization.normalizedEncryptedAvailableBalance.getCipherText().map((ct) => ct.D), + proof: confidentialNormalizationSigmaProof, }); expect(isValid).toBeTruthy(); diff --git a/confidential-assets/tests/units/discrete-log.test.ts b/confidential-assets/tests/units/discrete-log.test.ts new file mode 100644 index 000000000..9100f2623 --- /dev/null +++ b/confidential-assets/tests/units/discrete-log.test.ts @@ -0,0 +1,357 @@ +/** + * Discrete Logarithm (DLP) tests and benchmarks. + * + * Covers two implementations: + * - WASM-based solver (algorithm determined at compile time) + * - Baby-Step Giant-Step (pure TypeScript) + */ + +import { RistrettoPoint } from "@noble/curves/ed25519"; +import { TwistedElGamal, TwistedEd25519PrivateKey } from "../../src"; +import { createBsgsTable, BsgsSolver } from "../../src/crypto/bsgs"; +import crypto from "crypto"; + +// ============================================================================ +// Helpers +// ============================================================================ + +const BENCHMARK_ITERATIONS = 10; + +function generateRandomInteger(bits: number): bigint { + if (bits <= 0) return 0n; + + const bytes = Math.ceil(bits / 8); + const randomBytes = crypto.getRandomValues(new Uint8Array(bytes)); + + let result = 0n; + for (let i = 0; i < bytes; i++) { + result = (result << 8n) | BigInt(randomBytes[i]); + } + + return result & ((1n << BigInt(bits)) - 1n); +} + +/** + * Split a nonnegative integer v into base-(2^radix_decomp_bits) digits, then greedily + * "borrow" from higher digits to maximize each lower chunk while keeping each + * chunk < 2^bits_per_chunk. + */ +function maximalRadixChunks( + v: bigint, + radix_decomp_bits: number, + v_max_bits: number, + bits_per_chunk: number, +): bigint[] { + if (radix_decomp_bits <= 0) throw new Error("radix_decomp_bits must be > 0"); + if (v_max_bits <= 0) throw new Error("v_max_bits must be > 0"); + if (bits_per_chunk <= 0) throw new Error("bits_per_chunk must be > 0"); + if (v_max_bits % radix_decomp_bits !== 0) { + throw new Error("v_max_bits must be a multiple of radix_decomp_bits"); + } + if (bits_per_chunk < radix_decomp_bits) { + throw new Error("bits_per_chunk must be >= radix_decomp_bits"); + } + if (v < 0n) throw new Error("v must be nonnegative"); + + const ell = v_max_bits / radix_decomp_bits; + + const RADIX = 1n << BigInt(radix_decomp_bits); + const DIGIT_MASK = RADIX - 1n; + const CHUNK_LIM = (1n << BigInt(bits_per_chunk)) - 1n; + + const V_MAX = 1n << BigInt(v_max_bits); + if (v >= V_MAX) throw new Error("v does not fit in v_max_bits"); + + // 1) canonical base-B digits + const w: bigint[] = new Array(ell); + for (let i = 0; i < ell; i++) { + const shift = BigInt(i * radix_decomp_bits); + w[i] = (v >> shift) & DIGIT_MASK; + } + + // 2) greedy borrowing to maximize each w[i] + let changed = true; + while (changed) { + changed = false; + for (let i = 0; i < ell - 1; i++) { + const room = CHUNK_LIM - w[i]; + if (room < RADIX) continue; + + const tCap = room / RADIX; + const t = w[i + 1] < tCap ? w[i + 1] : tCap; + + if (t > 0n) { + w[i] += t * RADIX; + w[i + 1] -= t; + changed = true; + } + } + } + + return w; +} + +/** Recompose value from chunks: Σ (2^radix_decomp_bits)^i * chunks[i] */ +function recompose(chunks: bigint[], radix_decomp_bits: number): bigint { + const RADIX = 1n << BigInt(radix_decomp_bits); + let acc = 0n; + let pow = 1n; + for (let i = 0; i < chunks.length; i++) { + acc += chunks[i] * pow; + pow *= RADIX; + } + return acc; +} + +interface BenchmarkResult { + algorithm: string; + bitWidth: number; + iterations: number; + avgMs: number; + minMs: number; + maxMs: number; + tableCreationMs?: number; +} + +function formatResult(r: BenchmarkResult): string { + const tableInfo = r.tableCreationMs !== undefined ? `, table=${r.tableCreationMs.toFixed(0)}ms` : ""; + return `${r.algorithm} ${r.bitWidth}-bit: avg=${r.avgMs.toFixed(2)}ms, min=${r.minMs.toFixed(2)}ms, max=${r.maxMs.toFixed(2)}ms${tableInfo}`; +} + +// ============================================================================ +// WASM Discrete Log Solver Tests +// ============================================================================ + +describe("Discrete Log Solver (WASM)", () => { + beforeAll(async () => { + // WASM auto-loads from node_modules in Node.js environment + await TwistedElGamal.initializeSolver(); + console.log(`WASM algorithm: ${TwistedElGamal.getAlgorithmName()}`); + }, 30000); + + describe("correctness", () => { + it("decrypts 16-bit values correctly", async () => { + for (let i = 0; i < BENCHMARK_ITERATIONS; i++) { + const x = generateRandomInteger(16); + const alice = TwistedEd25519PrivateKey.generate(); + const encrypted = TwistedElGamal.encryptWithPK(x, alice.publicKey()); + const decrypted = await TwistedElGamal.decryptWithPK(encrypted, alice); + expect(decrypted).toBe(x); + } + }); + + it("decrypts 32-bit values correctly", async () => { + for (let i = 0; i < BENCHMARK_ITERATIONS; i++) { + const x = generateRandomInteger(32); + const alice = TwistedEd25519PrivateKey.generate(); + const encrypted = TwistedElGamal.encryptWithPK(x, alice.publicKey()); + const decrypted = await TwistedElGamal.decryptWithPK(encrypted, alice); + expect(decrypted).toBe(x); + } + }); + }); + + describe("benchmark", () => { + const benchmarkWasm = async (bitWidth: number): Promise => { + expect(TwistedElGamal.isInitialized()).toBe(true); + + const times: number[] = []; + const alice = TwistedEd25519PrivateKey.generate(); + + for (let i = 0; i < BENCHMARK_ITERATIONS; i++) { + const x = generateRandomInteger(bitWidth); + const encrypted = TwistedElGamal.encryptWithPK(x, alice.publicKey()); + + const start = performance.now(); + const result = await TwistedElGamal.decryptWithPK(encrypted, alice); + const elapsed = performance.now() - start; + + times.push(elapsed); + expect(result).toBe(x); + } + + const avg = times.reduce((a, b) => a + b, 0) / times.length; + return { + algorithm: `WASM`, + bitWidth, + iterations: BENCHMARK_ITERATIONS, + avgMs: avg, + minMs: Math.min(...times), + maxMs: Math.max(...times), + }; + }; + + it("benchmarks 16-bit DLP", async () => { + const result = await benchmarkWasm(16); + console.log(formatResult(result)); + }); + + it("benchmarks 32-bit DLP", async () => { + const result = await benchmarkWasm(32); + console.log(formatResult(result)); + }); + }); +}); + +// ============================================================================ +// Baby-Step Giant-Step Tests (TypeScript) +// ============================================================================ + +describe("Baby-Step Giant-Step (TypeScript)", () => { + let solver: BsgsSolver; + + beforeAll(async () => { + solver = new BsgsSolver(); + await solver.initialize([16, 32]); + }); + + describe.skip("createBsgsTable", () => { + it("creates correct table for 16-bit DLPs", () => { + const table16 = createBsgsTable(16); + expect(table16.bitWidth).toBe(16); + expect(table16.m).toBe(256n); + expect(table16.babySteps.size).toBe(256); + }); + + it("throws for odd bit widths", () => { + expect(() => createBsgsTable(15)).toThrow("bitWidth must be even"); + }); + + it("throws for non-positive bit widths", () => { + expect(() => createBsgsTable(0)).toThrow("bitWidth must be positive"); + expect(() => createBsgsTable(-2)).toThrow("bitWidth must be positive"); + }); + }); + + describe("BsgsSolver", () => { + it("solves known 16-bit values correctly", () => { + for (const x of [0n, 1n, 255n, 256n, 1000n, 65535n]) { + const P = x === 0n ? RistrettoPoint.ZERO : RistrettoPoint.BASE.multiply(x); + const result = solver.solve(P.toRawBytes()); + expect(result).toBe(x); + } + }); + + it("solves random 16-bit values correctly", () => { + for (let i = 0; i < BENCHMARK_ITERATIONS; i++) { + const x = generateRandomInteger(16); + const P = x === 0n ? RistrettoPoint.ZERO : RistrettoPoint.BASE.multiply(x); + const result = solver.solve(P.toRawBytes()); + expect(result).toBe(x); + } + }); + + it("throws for values outside search space", () => { + const x = 1n << 32n; + const P = RistrettoPoint.BASE.multiply(x); + expect(() => solver.solve(P.toRawBytes())).toThrow(); + }); + + it("solves using smallest sufficient table", () => { + expect(solver.hasTable(16)).toBe(true); + expect(solver.hasTable(32)).toBe(true); + + const x1 = 100n; + const P1 = RistrettoPoint.BASE.multiply(x1); + expect(solver.solve(P1.toRawBytes())).toBe(x1); + + const x2 = 100000n; + const P2 = RistrettoPoint.BASE.multiply(x2); + expect(solver.solve(P2.toRawBytes())).toBe(x2); + }); + }); + + describe("benchmarks", () => { + it("benchmarks 16-bit DLP", () => { + const times: number[] = []; + + for (let i = 0; i < BENCHMARK_ITERATIONS; i++) { + const x = generateRandomInteger(16); + const P = x === 0n ? RistrettoPoint.ZERO : RistrettoPoint.BASE.multiply(x); + + const start = performance.now(); + const result = solver.solve(P.toRawBytes()); + const elapsed = performance.now() - start; + + times.push(elapsed); + expect(result).toBe(x); + } + + const avg = times.reduce((a, b) => a + b, 0) / times.length; + console.log( + `TS BSGS 16-bit: avg=${avg.toFixed(2)}ms, min=${Math.min(...times).toFixed(2)}ms, max=${Math.max(...times).toFixed(2)}ms`, + ); + }); + + it("benchmarks 32-bit DLP", () => { + const times: number[] = []; + + for (let i = 0; i < BENCHMARK_ITERATIONS; i++) { + const x = generateRandomInteger(32); + const P = x === 0n ? RistrettoPoint.ZERO : RistrettoPoint.BASE.multiply(x); + + const start = performance.now(); + const result = solver.solve(P.toRawBytes()); + const elapsed = performance.now() - start; + + times.push(elapsed); + expect(result).toBe(x); + } + + const avg = times.reduce((a, b) => a + b, 0) / times.length; + console.log( + `TS BSGS 32-bit: avg=${avg.toFixed(2)}ms, min=${Math.min(...times).toFixed(2)}ms, max=${Math.max(...times).toFixed(2)}ms`, + ); + }); + }); +}); + +// ============================================================================ +// Chunking Utilities (for balance decomposition) +// ============================================================================ + +describe.skip("maximalRadixChunks", () => { + it("correctly decomposes and recomposes random 128-bit values", () => { + const v = generateRandomInteger(128); + const chunks = maximalRadixChunks(v, 16, 128, 32); + + const vBitWidth = v === 0n ? 0 : v.toString(2).length; + const bitWidths = chunks.map((c) => (c === 0n ? 0 : c.toString(2).length)); + console.log(`v=${v} (${vBitWidth} bits), num chunks=${chunks.length}, chunk bit widths=[${bitWidths.join(", ")}]`); + + expect(chunks.length).toBe(8); + + for (const c of chunks) { + expect(c >= 0n).toBe(true); + expect(c < 1n << 32n).toBe(true); + } + + expect(recompose(chunks, 16)).toBe(v); + + // local maximality check + for (let i = 0; i < 7; i++) { + const saturated = chunks[i] + (1n << 16n) > (1n << 32n) - 1n; + const noBorrow = chunks[i + 1] === 0n; + expect(saturated || noBorrow).toBe(true); + } + }); + + const testDecomposition = (v: bigint, vMaxBits: number) => { + const chunks = maximalRadixChunks(v, 16, vMaxBits, 32); + const vBitWidth = v === 0n ? 0 : v.toString(2).length; + const bitWidths = chunks.map((c) => (c === 0n ? 0 : c.toString(2).length)); + console.log(`v=${v} (${vBitWidth} bits), num chunks=${chunks.length}, chunk bit widths=[${bitWidths.join(", ")}]`); + expect(recompose(chunks, 16)).toBe(v); + }; + + it("handles zero correctly", () => testDecomposition(0n, 128)); + it("handles 32-bit values correctly", () => testDecomposition(generateRandomInteger(32), 32)); + it("handles 48-bit values correctly", () => testDecomposition(generateRandomInteger(48), 48)); + it("handles 64-bit values correctly", () => testDecomposition(generateRandomInteger(64), 64)); + it("handles 96-bit values correctly", () => testDecomposition(generateRandomInteger(96), 96)); + it("handles the maximum 128-bit value correctly", () => testDecomposition((1n << 128n) - 1n, 128)); + + it("throws if v does not fit in v_max_bits", () => { + expect(() => maximalRadixChunks(1n << 128n, 16, 128, 32)).toThrow(); + }); +}); diff --git a/confidential-assets/tests/units/kangaroo-decryption.test.ts b/confidential-assets/tests/units/kangaroo-decryption.test.ts deleted file mode 100644 index 5d6a90b15..000000000 --- a/confidential-assets/tests/units/kangaroo-decryption.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { EncryptedAmount, TwistedEd25519PrivateKey, TwistedElGamal } from "../../src"; - -function generateRandomInteger(bits: number): bigint { - // eslint-disable-next-line no-bitwise - const max = (1n << BigInt(bits)) - 1n; - const randomValue = BigInt(Math.floor(Math.random() * (Number(max) + 1))); - - return randomValue; -} - -const executionSimple = async ( - bitsAmount: number, - length = 50, -): Promise<{ randBalances: bigint[]; results: { result: bigint; elapsedTime: number }[] }> => { - const randBalances = Array.from({ length }, () => generateRandomInteger(bitsAmount)); - - const decryptedAmounts: { result: bigint; elapsedTime: number }[] = []; - - for (const balance of randBalances) { - const newAlice = TwistedEd25519PrivateKey.generate(); - - const encryptedBalance = TwistedElGamal.encryptWithPK(balance, newAlice.publicKey()); - - const startMainTime = performance.now(); - const decryptedBalance = await TwistedElGamal.decryptWithPK(encryptedBalance, newAlice); - const endMainTime = performance.now(); - - const elapsedMainTime = endMainTime - startMainTime; - - decryptedAmounts.push({ result: decryptedBalance, elapsedTime: elapsedMainTime }); - } - - const averageTime = decryptedAmounts.reduce((acc, { elapsedTime }) => acc + elapsedTime, 0) / decryptedAmounts.length; - - const lowestTime = decryptedAmounts.reduce((acc, { elapsedTime }) => Math.min(acc, elapsedTime), Infinity); - const highestTime = decryptedAmounts.reduce((acc, { elapsedTime }) => Math.max(acc, elapsedTime), 0); - - console.log( - `Pollard kangaroo(table ${bitsAmount}):\n`, - `Average time: ${averageTime} ms\n`, - `Lowest time: ${lowestTime} ms\n`, - `Highest time: ${highestTime} ms`, - ); - - return { - randBalances, - results: decryptedAmounts, - }; -}; - -const executionBalance = async ( - bitsAmount: number, - length = 50, -): Promise<{ randBalances: bigint[]; results: { result: bigint; elapsedTime: number }[] }> => { - const randBalances = Array.from({ length }, () => generateRandomInteger(bitsAmount)); - - const decryptedAmounts: { result: bigint; elapsedTime: number }[] = []; - - for (const balance of randBalances) { - const newAlice = TwistedEd25519PrivateKey.generate(); - - const startMainTime = performance.now(); - const decryptedBalance = ( - await EncryptedAmount.fromAmountAndPublicKey({ - amount: balance, - publicKey: newAlice.publicKey(), - }) - ).getAmount(); - - const endMainTime = performance.now(); - - const elapsedMainTime = endMainTime - startMainTime; - - decryptedAmounts.push({ result: decryptedBalance, elapsedTime: elapsedMainTime }); - } - - const averageTime = decryptedAmounts.reduce((acc, { elapsedTime }) => acc + elapsedTime, 0) / decryptedAmounts.length; - - const lowestTime = decryptedAmounts.reduce((acc, { elapsedTime }) => Math.min(acc, elapsedTime), Infinity); - const highestTime = decryptedAmounts.reduce((acc, { elapsedTime }) => Math.max(acc, elapsedTime), 0); - - console.log( - `Pollard kangaroo(balance: ${bitsAmount}):\n`, - `Average time: ${averageTime} ms\n`, - `Lowest time: ${lowestTime} ms\n`, - `Highest time: ${highestTime} ms`, - // decryptedAmounts, - ); - - return { - randBalances, - results: decryptedAmounts, - }; -}; - -describe("decrypt amount", () => { - it.skip("kangarooWasmAll(16): Should decrypt 50 rand numbers", async () => { - console.log("WASM:"); - - const { randBalances, results } = await executionSimple(16); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it.skip("kangarooWasmAll(32): Should decrypt 50 rand numbers", async () => { - console.log("WASM:"); - - const { randBalances, results } = await executionSimple(32); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it.skip("kangarooWasmAll(48): Should decrypt 50 rand numbers", async () => { - console.log("WASM:"); - - const { randBalances, results } = await executionSimple(48); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it("kangarooWasmAll(16): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(16); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it("kangarooWasmAll(32): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(32); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it("kangarooWasmAll(48): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(48); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it("kangarooWasmAll(64): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(64); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it("kangarooWasmAll(96): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(96); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it("kangarooWasmAll(128): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(128); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); -}); diff --git a/confidential-assets/vitest.config.ts b/confidential-assets/vitest.config.ts index 5542381aa..733282c7d 100644 --- a/confidential-assets/vitest.config.ts +++ b/confidential-assets/vitest.config.ts @@ -6,7 +6,10 @@ export default defineConfig({ globals: true, environment: "node", setupFiles: [path.resolve(__dirname, "../tests/setupDotenv.ts")], - globalSetup: [path.resolve(__dirname, "../tests/preTest.ts")], + // NOTE: We typically test confidential assets after making changes to the + // Aptos framework, which require a manual localnet re-deployment. So this + // automatic deployment before every test is disabled, as a result. + //globalSetup: [path.resolve(__dirname, "../tests/preTest.ts")], include: ["tests/**/*.test.ts"], exclude: ["tests/units/api/**"], coverage: {