From cbf04a2cb749b69e4dce4432e84fb169e9805c62 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Fri, 23 Jan 2026 13:24:45 -0600 Subject: [PATCH 01/22] add README and TODO --- confidential-assets/README.md | 19 +++++++++++++++++++ .../src/crypto/twistedElGamal.ts | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 confidential-assets/README.md diff --git a/confidential-assets/README.md b/confidential-assets/README.md new file mode 100644 index 000000000..2763a4468 --- /dev/null +++ b/confidential-assets/README.md @@ -0,0 +1,19 @@ +# Test + +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 +``` + +Or, run all tests: +``` +pnpm test +``` diff --git a/confidential-assets/src/crypto/twistedElGamal.ts b/confidential-assets/src/crypto/twistedElGamal.ts index 22bd5a486..b5d236753 100644 --- a/confidential-assets/src/crypto/twistedElGamal.ts +++ b/confidential-assets/src/crypto/twistedElGamal.ts @@ -221,6 +221,8 @@ export class TwistedElGamal { * @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, From 80a92ecb9a03fe57c87a03e6ba63dfa4e8905653 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 28 Jan 2026 13:50:05 -0600 Subject: [PATCH 02/22] fix decryption time benchmarks --- confidential-assets/README.md | 8 + .../tests/units/kangaroo-decryption.test.ts | 270 +++++++++++------- 2 files changed, 178 insertions(+), 100 deletions(-) diff --git a/confidential-assets/README.md b/confidential-assets/README.md index 2763a4468..5b69fd8fc 100644 --- a/confidential-assets/README.md +++ b/confidential-assets/README.md @@ -17,3 +17,11 @@ Or, run all tests: ``` pnpm test ``` + +## Useful tests to know about + +### Decryption times + +``` +pnpm jest tests/units/kangaroo-decryption.test.ts +``` diff --git a/confidential-assets/tests/units/kangaroo-decryption.test.ts b/confidential-assets/tests/units/kangaroo-decryption.test.ts index 5d6a90b15..76ebe7d92 100644 --- a/confidential-assets/tests/units/kangaroo-decryption.test.ts +++ b/confidential-assets/tests/units/kangaroo-decryption.test.ts @@ -1,56 +1,103 @@ import { EncryptedAmount, TwistedEd25519PrivateKey, TwistedElGamal } from "../../src"; +import crypto from "crypto"; + +const BENCHMARK_ITERATIONS = 10; 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))); + if (bits <= 0) return 0n; - return randomValue; -} + const bytes = Math.ceil(bits / 8); + const randomBytes = crypto.getRandomValues(new Uint8Array(bytes)); -const executionSimple = async ( - bitsAmount: number, - length = 50, -): Promise<{ randBalances: bigint[]; results: { result: bigint; elapsedTime: number }[] }> => { - const randBalances = Array.from({ length }, () => generateRandomInteger(bitsAmount)); + let result = 0n; + for (let i = 0; i < bytes; i++) { + result = (result << 8n) | BigInt(randomBytes[i]); + } - const decryptedAmounts: { result: bigint; elapsedTime: number }[] = []; + // Mask to the requested bit size + return result & ((1n << BigInt(bits)) - 1n); +} - for (const balance of randBalances) { - const newAlice = TwistedEd25519PrivateKey.generate(); +/** + * 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 encryptedBalance = TwistedElGamal.encryptWithPK(balance, newAlice.publicKey()); + const ell = v_max_bits / radix_decomp_bits; - const startMainTime = performance.now(); - const decryptedBalance = await TwistedElGamal.decryptWithPK(encryptedBalance, newAlice); - const endMainTime = performance.now(); + const RADIX = 1n << BigInt(radix_decomp_bits); // B + const DIGIT_MASK = RADIX - 1n; + const CHUNK_LIM = (1n << BigInt(bits_per_chunk)) - 1n; - const elapsedMainTime = endMainTime - startMainTime; + const V_MAX = 1n << BigInt(v_max_bits); + if (v >= V_MAX) throw new Error("v does not fit in v_max_bits"); - decryptedAmounts.push({ result: decryptedBalance, elapsedTime: elapsedMainTime }); + // 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; } - 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); + // 2) greedy borrowing to maximize each w[i] + // Keep iterating until no more borrowing is possible. + 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; // Can't fit another full RADIX unit + + const tCap = room / RADIX; // floor(room / B) + const t = w[i + 1] < tCap ? w[i + 1] : tCap; + + if (t > 0n) { + w[i] += t * RADIX; + w[i + 1] -= t; + changed = true; + } + } + } - console.log( - `Pollard kangaroo(table ${bitsAmount}):\n`, - `Average time: ${averageTime} ms\n`, - `Lowest time: ${lowestTime} ms\n`, - `Highest time: ${highestTime} ms`, - ); + return w; +} - return { - randBalances, - results: decryptedAmounts, - }; -}; +/** 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; +} -const executionBalance = async ( +/** + * Benchmarks decryption of a SINGLE twisted ElGamal ciphertext element. + * This tests the raw Pollard Kangaroo DLP performance for the given bit size. + */ +const benchmarkSingleElementDecryption = async ( bitsAmount: number, - length = 50, + length = BENCHMARK_ITERATIONS, ): Promise<{ randBalances: bigint[]; results: { result: bigint; elapsedTime: number }[] }> => { const randBalances = Array.from({ length }, () => generateRandomInteger(bitsAmount)); @@ -59,14 +106,10 @@ const executionBalance = async ( 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 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; @@ -80,11 +123,10 @@ const executionBalance = async ( 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, + `Single element decryption (${bitsAmount}-bit):\n`, + `Average time: ${averageTime.toFixed(2)} ms\n`, + `Lowest time: ${lowestTime.toFixed(2)} ms\n`, + `Highest time: ${highestTime.toFixed(2)} ms`, ); return { @@ -93,82 +135,110 @@ const executionBalance = async ( }; }; -describe("decrypt amount", () => { - it.skip("kangarooWasmAll(16): Should decrypt 50 rand numbers", async () => { - console.log("WASM:"); +describe("Pollard Kangaroo decryption benchmarks", () => { + // Initialize kangaroo tables before running benchmarks to avoid + // counting table computation time in the first iteration + beforeAll(async () => { + await TwistedElGamal.initializeKangaroos(); + }, 30000); - const { randBalances, results } = await executionSimple(16); + describe("Single element decryption (one DLP per value)", () => { + it(`16-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values`, async () => { + const { randBalances, results } = await benchmarkSingleElementDecryption(16); - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); + 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); + it(`32-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values`, async () => { + const { randBalances, results } = await benchmarkSingleElementDecryption(32); - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); + 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); + it.skip(`48-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values (slow)`, async () => { + const { randBalances, results } = await benchmarkSingleElementDecryption(48); - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); + 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]); + describe("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(", ")}]`, + ); + + // length: 128 / 16 = 8 chunks + expect(chunks.length).toBe(8); + + // each chunk fits in 32 bits + for (const c of chunks) { + expect(c >= 0n).toBe(true); + expect(c < 1n << 32n).toBe(true); + } + + // recomposition correctness + expect(recompose(chunks, 16)).toBe(v); + + // local maximality: + // for each i < ell-1, either saturated or no borrow left + 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); + } }); - }); - it("kangarooWasmAll(32): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(32); + const testDecomposition = (v: bigint, vMaxBits: number) => { + const chunks = maximalRadixChunks(v, 16, vMaxBits, 32); - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); + 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(", ")}]`, + ); - it("kangarooWasmAll(48): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(48); + expect(recompose(chunks, 16)).toBe(v); + }; - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); + it("handles zero correctly", () => { + testDecomposition(0n, 128); }); - }); - it("kangarooWasmAll(64): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(64); + it("handles 32-bit values correctly", () => { + testDecomposition(generateRandomInteger(32), 32); + }); - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); + it("handles 48-bit values correctly", () => { + testDecomposition(generateRandomInteger(48), 48); }); - }); - it("kangarooWasmAll(96): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(96); + it("handles 64-bit values correctly", () => { + testDecomposition(generateRandomInteger(64), 64); + }); - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); + it("handles 96-bit values correctly", () => { + testDecomposition(generateRandomInteger(96), 96); }); - }); - it("kangarooWasmAll(128): Should decrypt 50 rand numbers", async () => { - const { randBalances, results } = await executionBalance(128); + it("handles the maximum 128-bit value correctly", () => { + testDecomposition((1n << 128n) - 1n, 128); + }); - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); + it("throws if v does not fit in v_max_bits", () => { + expect(() => maximalRadixChunks(1n << 128n, 16, 128, 32)).toThrow(); }); }); -}); +}); \ No newline at end of file From 238fc53ad336c159641dd299615e132ad80be7e8 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 28 Jan 2026 18:54:39 -0600 Subject: [PATCH 03/22] add BSGS TS benches --- confidential-assets/src/crypto/bsgs.ts | 219 +++++++++++ confidential-assets/src/crypto/index.ts | 1 + .../tests/units/discrete-log.test.ts | 370 ++++++++++++++++++ .../tests/units/kangaroo-decryption.test.ts | 244 ------------ 4 files changed, 590 insertions(+), 244 deletions(-) create mode 100644 confidential-assets/src/crypto/bsgs.ts create mode 100644 confidential-assets/tests/units/discrete-log.test.ts delete mode 100644 confidential-assets/tests/units/kangaroo-decryption.test.ts diff --git a/confidential-assets/src/crypto/bsgs.ts b/confidential-assets/src/crypto/bsgs.ts new file mode 100644 index 000000000..9650d72b2 --- /dev/null +++ b/confidential-assets/src/crypto/bsgs.ts @@ -0,0 +1,219 @@ +// 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; + } +} + +/** + * Creates a decryption function using BSGS that can be passed to + * TwistedElGamal.setDecryptionFn(). + * + * @param bitWidths - Bit widths to support (e.g., [16, 32]) + * @returns A decryption function compatible with TwistedElGamal + */ +export async function createBsgsDecryptionFn( + bitWidths: number[], +): Promise<(point: Uint8Array) => Promise> { + const solver = new BsgsSolver(); + await solver.initialize(bitWidths); + + return async (point: Uint8Array): Promise => { + // Check for zero point + const isZero = point.every((b) => b === 0); + if (isZero) return 0n; + + return solver.solve(point); + }; +} diff --git a/confidential-assets/src/crypto/index.ts b/confidential-assets/src/crypto/index.ts index dc6a1b1b6..80e715bb0 100644 --- a/confidential-assets/src/crypto/index.ts +++ b/confidential-assets/src/crypto/index.ts @@ -1,5 +1,6 @@ export * from "./twistedEd25519"; export * from "./twistedElGamal"; +export * from "./bsgs"; export * from "./rangeProof"; export * from "./chunkedAmount"; export * from "./encryptedAmount"; 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..209731294 --- /dev/null +++ b/confidential-assets/tests/units/discrete-log.test.ts @@ -0,0 +1,370 @@ +/** + * Discrete Logarithm (DLP) tests and benchmarks. + * + * Covers two algorithms: + * - Pollard Kangaroo (WASM-based, faster) + * - Baby-Step Giant-Step (pure TypeScript) + */ + +import { RistrettoPoint } from "@noble/curves/ed25519"; +import { TwistedElGamal, TwistedEd25519PrivateKey } from "../../src"; +import { createBsgsTable, BsgsSolver, createBsgsDecryptionFn } from "../../src/crypto/bsgs"; +import crypto from "crypto"; + +// ============================================================================ +// Helpers +// ============================================================================ + +const BENCHMARK_ITERATIONS = 5; + +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}`; +} + +// ============================================================================ +// Pollard Kangaroo Tests (WASM) +// ============================================================================ + +describe("Pollard Kangaroo (WASM)", () => { + beforeAll(async () => { + await TwistedElGamal.initializeKangaroos(); + }, 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 benchmarkKangaroo = async (bitWidth: number): Promise => { + const times: number[] = []; + + for (let i = 0; i < BENCHMARK_ITERATIONS; i++) { + const x = generateRandomInteger(bitWidth); + const P = x === 0n ? RistrettoPoint.ZERO : RistrettoPoint.BASE.multiply(x); + + const start = performance.now(); + const result = await TwistedElGamal.decryptionFn!(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; + return { + algorithm: "Kangaroo", + bitWidth, + iterations: BENCHMARK_ITERATIONS, + avgMs: avg, + minMs: Math.min(...times), + maxMs: Math.max(...times), + }; + }; + + it("benchmarks 16-bit DLP", async () => { + const result = await benchmarkKangaroo(16); + console.log(formatResult(result)); + }); + + it("benchmarks 32-bit DLP", async () => { + const result = await benchmarkKangaroo(32); + console.log(formatResult(result)); + }); + + it.skip("benchmarks 48-bit DLP (slow)", async () => { + const result = await benchmarkKangaroo(48); + 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.skip("createBsgsDecryptionFn", () => { + it("creates a decryption function compatible with TwistedElGamal", async () => { + const decryptFn = await createBsgsDecryptionFn([16]); + + const zeroPoint = RistrettoPoint.ZERO.toRawBytes(); + expect(await decryptFn(zeroPoint)).toBe(0n); + + const x = 42n; + const P = RistrettoPoint.BASE.multiply(x); + expect(await decryptFn(P.toRawBytes())).toBe(x); + }); + }); + + 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( + `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( + `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 76ebe7d92..000000000 --- a/confidential-assets/tests/units/kangaroo-decryption.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { EncryptedAmount, TwistedEd25519PrivateKey, TwistedElGamal } from "../../src"; -import crypto from "crypto"; - -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]); - } - - // Mask to the requested bit size - 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); // B - 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] - // Keep iterating until no more borrowing is possible. - 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; // Can't fit another full RADIX unit - - const tCap = room / RADIX; // floor(room / B) - 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; -} - -/** - * Benchmarks decryption of a SINGLE twisted ElGamal ciphertext element. - * This tests the raw Pollard Kangaroo DLP performance for the given bit size. - */ -const benchmarkSingleElementDecryption = async ( - bitsAmount: number, - length = BENCHMARK_ITERATIONS, -): 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( - `Single element decryption (${bitsAmount}-bit):\n`, - `Average time: ${averageTime.toFixed(2)} ms\n`, - `Lowest time: ${lowestTime.toFixed(2)} ms\n`, - `Highest time: ${highestTime.toFixed(2)} ms`, - ); - - return { - randBalances, - results: decryptedAmounts, - }; -}; - -describe("Pollard Kangaroo decryption benchmarks", () => { - // Initialize kangaroo tables before running benchmarks to avoid - // counting table computation time in the first iteration - beforeAll(async () => { - await TwistedElGamal.initializeKangaroos(); - }, 30000); - - describe("Single element decryption (one DLP per value)", () => { - it(`16-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values`, async () => { - const { randBalances, results } = await benchmarkSingleElementDecryption(16); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it(`32-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values`, async () => { - const { randBalances, results } = await benchmarkSingleElementDecryption(32); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - - it.skip(`48-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values (slow)`, async () => { - const { randBalances, results } = await benchmarkSingleElementDecryption(48); - - results.forEach(({ result }, i) => { - expect(result).toEqual(randBalances[i]); - }); - }); - }); - - describe("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(", ")}]`, - ); - - // length: 128 / 16 = 8 chunks - expect(chunks.length).toBe(8); - - // each chunk fits in 32 bits - for (const c of chunks) { - expect(c >= 0n).toBe(true); - expect(c < 1n << 32n).toBe(true); - } - - // recomposition correctness - expect(recompose(chunks, 16)).toBe(v); - - // local maximality: - // for each i < ell-1, either saturated or no borrow left - 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(); - }); - }); -}); \ No newline at end of file From a6f89bb7e2a0ea4dc70aea8afc116ab9ef6071b8 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Mon, 2 Feb 2026 18:24:03 -0600 Subject: [PATCH 04/22] add unified WASM --- confidential-assets/README.md | 90 ++++++++++- confidential-assets/package.json | 2 +- confidential-assets/pnpm-lock.yaml | 10 +- confidential-assets/src/crypto/bsgs.ts | 22 --- confidential-assets/src/crypto/index.ts | 1 + confidential-assets/src/crypto/rangeProof.ts | 48 ++++-- .../src/crypto/twistedElGamal.ts | 149 +++++++----------- confidential-assets/src/crypto/wasmLoader.ts | 109 +++++++++++++ .../internal/confidentialAssetTxnBuilder.ts | 2 +- .../tests/units/discrete-log.test.ts | 53 +++---- 10 files changed, 315 insertions(+), 171 deletions(-) create mode 100644 confidential-assets/src/crypto/wasmLoader.ts diff --git a/confidential-assets/README.md b/confidential-assets/README.md index 5b69fd8fc..c944f2de1 100644 --- a/confidential-assets/README.md +++ b/confidential-assets/README.md @@ -1,4 +1,82 @@ -# Test +# 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: @@ -20,8 +98,14 @@ pnpm test ## Useful tests to know about -### Decryption times +### Discrete log / decryption benchmarks +```bash +pnpm jest tests/units/discrete-log.test.ts ``` -pnpm jest tests/units/kangaroo-decryption.test.ts + +### Range proof tests + +```bash +pnpm jest 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/crypto/bsgs.ts b/confidential-assets/src/crypto/bsgs.ts index 9650d72b2..f774282e3 100644 --- a/confidential-assets/src/crypto/bsgs.ts +++ b/confidential-assets/src/crypto/bsgs.ts @@ -195,25 +195,3 @@ export class BsgsSolver { this.initPromise = undefined; } } - -/** - * Creates a decryption function using BSGS that can be passed to - * TwistedElGamal.setDecryptionFn(). - * - * @param bitWidths - Bit widths to support (e.g., [16, 32]) - * @returns A decryption function compatible with TwistedElGamal - */ -export async function createBsgsDecryptionFn( - bitWidths: number[], -): Promise<(point: Uint8Array) => Promise> { - const solver = new BsgsSolver(); - await solver.initialize(bitWidths); - - return async (point: Uint8Array): Promise => { - // Check for zero point - const isZero = point.every((b) => b === 0); - if (isZero) return 0n; - - return solver.solve(point); - }; -} diff --git a/confidential-assets/src/crypto/index.ts b/confidential-assets/src/crypto/index.ts index 80e715bb0..3739470fb 100644 --- a/confidential-assets/src/crypto/index.ts +++ b/confidential-assets/src/crypto/index.ts @@ -2,6 +2,7 @@ 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 "./confidentialKeyRotation"; 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/twistedElGamal.ts b/confidential-assets/src/crypto/twistedElGamal.ts index b5d236753..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,12 +169,32 @@ 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( @@ -231,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()); } /** @@ -277,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/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index f5311a99e..9c4651c6c 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -33,7 +33,7 @@ export class ConfidentialAssetTransactionBuilder { constructor(config: AptosConfig, confidentialAssetModuleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS) { this.client = new Aptos(config); this.confidentialAssetModuleAddress = confidentialAssetModuleAddress; - TwistedElGamal.initializeKangaroos(); + TwistedElGamal.initializeSolver(); } /** diff --git a/confidential-assets/tests/units/discrete-log.test.ts b/confidential-assets/tests/units/discrete-log.test.ts index 209731294..9100f2623 100644 --- a/confidential-assets/tests/units/discrete-log.test.ts +++ b/confidential-assets/tests/units/discrete-log.test.ts @@ -1,21 +1,21 @@ /** * Discrete Logarithm (DLP) tests and benchmarks. * - * Covers two algorithms: - * - Pollard Kangaroo (WASM-based, faster) + * 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, createBsgsDecryptionFn } from "../../src/crypto/bsgs"; +import { createBsgsTable, BsgsSolver } from "../../src/crypto/bsgs"; import crypto from "crypto"; // ============================================================================ // Helpers // ============================================================================ -const BENCHMARK_ITERATIONS = 5; +const BENCHMARK_ITERATIONS = 10; function generateRandomInteger(bits: number): bigint { if (bits <= 0) return 0n; @@ -119,12 +119,14 @@ function formatResult(r: BenchmarkResult): string { } // ============================================================================ -// Pollard Kangaroo Tests (WASM) +// WASM Discrete Log Solver Tests // ============================================================================ -describe("Pollard Kangaroo (WASM)", () => { +describe("Discrete Log Solver (WASM)", () => { beforeAll(async () => { - await TwistedElGamal.initializeKangaroos(); + // WASM auto-loads from node_modules in Node.js environment + await TwistedElGamal.initializeSolver(); + console.log(`WASM algorithm: ${TwistedElGamal.getAlgorithmName()}`); }, 30000); describe("correctness", () => { @@ -150,15 +152,18 @@ describe("Pollard Kangaroo (WASM)", () => { }); describe("benchmark", () => { - const benchmarkKangaroo = async (bitWidth: number): Promise => { + 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 P = x === 0n ? RistrettoPoint.ZERO : RistrettoPoint.BASE.multiply(x); + const encrypted = TwistedElGamal.encryptWithPK(x, alice.publicKey()); const start = performance.now(); - const result = await TwistedElGamal.decryptionFn!(P.toRawBytes()); + const result = await TwistedElGamal.decryptWithPK(encrypted, alice); const elapsed = performance.now() - start; times.push(elapsed); @@ -167,7 +172,7 @@ describe("Pollard Kangaroo (WASM)", () => { const avg = times.reduce((a, b) => a + b, 0) / times.length; return { - algorithm: "Kangaroo", + algorithm: `WASM`, bitWidth, iterations: BENCHMARK_ITERATIONS, avgMs: avg, @@ -177,17 +182,12 @@ describe("Pollard Kangaroo (WASM)", () => { }; it("benchmarks 16-bit DLP", async () => { - const result = await benchmarkKangaroo(16); + const result = await benchmarkWasm(16); console.log(formatResult(result)); }); it("benchmarks 32-bit DLP", async () => { - const result = await benchmarkKangaroo(32); - console.log(formatResult(result)); - }); - - it.skip("benchmarks 48-bit DLP (slow)", async () => { - const result = await benchmarkKangaroo(48); + const result = await benchmarkWasm(32); console.log(formatResult(result)); }); }); @@ -261,19 +261,6 @@ describe("Baby-Step Giant-Step (TypeScript)", () => { }); }); - describe.skip("createBsgsDecryptionFn", () => { - it("creates a decryption function compatible with TwistedElGamal", async () => { - const decryptFn = await createBsgsDecryptionFn([16]); - - const zeroPoint = RistrettoPoint.ZERO.toRawBytes(); - expect(await decryptFn(zeroPoint)).toBe(0n); - - const x = 42n; - const P = RistrettoPoint.BASE.multiply(x); - expect(await decryptFn(P.toRawBytes())).toBe(x); - }); - }); - describe("benchmarks", () => { it("benchmarks 16-bit DLP", () => { const times: number[] = []; @@ -292,7 +279,7 @@ describe("Baby-Step Giant-Step (TypeScript)", () => { const avg = times.reduce((a, b) => a + b, 0) / times.length; console.log( - `BSGS 16-bit: avg=${avg.toFixed(2)}ms, min=${Math.min(...times).toFixed(2)}ms, max=${Math.max(...times).toFixed(2)}ms`, + `TS BSGS 16-bit: avg=${avg.toFixed(2)}ms, min=${Math.min(...times).toFixed(2)}ms, max=${Math.max(...times).toFixed(2)}ms`, ); }); @@ -313,7 +300,7 @@ describe("Baby-Step Giant-Step (TypeScript)", () => { const avg = times.reduce((a, b) => a + b, 0) / times.length; console.log( - `BSGS 32-bit: avg=${avg.toFixed(2)}ms, min=${Math.min(...times).toFixed(2)}ms, max=${Math.max(...times).toFixed(2)}ms`, + `TS BSGS 32-bit: avg=${avg.toFixed(2)}ms, min=${Math.min(...times).toFixed(2)}ms, max=${Math.max(...times).toFixed(2)}ms`, ); }); }); From b5ce91081a06c131bad274e00dec23f68cda4e42 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Sat, 14 Feb 2026 10:28:02 +0800 Subject: [PATCH 05/22] breaking changes to on-chain structs, rewrite key rotation proof, generic sigma framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Key rotation — migrate to generic sigma protocol framework: - The old ConfidentialKeyRotation had a bespoke Fiat-Shamir construction that did not match the Move verifier in sigma_protocol_key_rotation.move, and proved the wrong statement (re-encrypting the full balance under the new key, requiring a range proof). - The new implementation uses sigmaProtocol.ts (sigmaProtocolProve/Verify) whose Fiat-Shamir matches the Move verifier byte-for-byte via BCS. - create() and authorizeKeyRotation() are now synchronous. - Rename ConfidentialKeyRotationResult -> KeyRotationProof: the flat sigmaProtoComm / sigmaProtoResp fields are now a nested proof: SigmaProtocolProof sub-field. - Eliminate duplicate view-function implementations in ConfidentialAsset: getAssetAuditorEncryptionKey and hasUserRegistered were re-implementing logic already in ConfidentialAssetTransactionBuilder and viewFunctions.ts, with stale Move function names (get_auditor, has_confidential_asset_store). Both now delegate to the canonical implementations. - rotateEncryptionKey: fix missing freeze when pending balance is already zero. The on-chain entry function requires incoming transfers to be paused; the condition is now pendingBalance() > 0 || !isFrozen. - remove SIGMA_PROOF_KEY_ROTATION_SIZE, unused numberToBytesLE and MODULE_NAME imports. - replace freeze with pause --- confidential-assets/README.md | 6 + .../src/api/confidentialAsset.ts | 61 +- confidential-assets/src/consts.ts | 2 - .../src/crypto/confidentialKeyRotation.ts | 545 ++++++++---------- confidential-assets/src/crypto/index.ts | 1 + .../src/crypto/sigmaProtocol.ts | 308 ++++++++++ .../internal/confidentialAssetTxnBuilder.ts | 74 ++- .../src/internal/viewFunctions.ts | 34 +- .../tests/e2e/confidentialAsset.test.ts | 31 +- .../e2e/confidentialAssetTxnBuilder.test.ts | 34 +- .../tests/units/api/checkBalances.test.ts | 34 -- .../tests/units/api/checkIsFrozen.test.ts | 16 - .../tests/units/api/checkIsNormalized.test.ts | 14 - .../tests/units/api/checkIsRegistered.test.ts | 14 - .../tests/units/api/deposit.test.ts | 23 - .../tests/units/api/getAssetAuditor.test.ts | 13 - .../tests/units/api/negativeNormalize.test.ts | 103 ---- .../tests/units/api/negativeTransfer.test.ts | 91 --- .../tests/units/api/negativeWithdraw.test.ts | 83 --- .../tests/units/api/normalize.test.ts | 46 -- .../tests/units/api/register.test.ts | 40 -- .../tests/units/api/safelyRollover.test.ts | 37 -- .../tests/units/api/transfer.test.ts | 40 -- .../units/api/transferWithAuditor.test.ts | 41 -- .../tests/units/api/withdraw.test.ts | 41 -- .../tests/units/confidentialProofs.test.ts | 86 ++- 26 files changed, 707 insertions(+), 1111 deletions(-) create mode 100644 confidential-assets/src/crypto/sigmaProtocol.ts delete mode 100644 confidential-assets/tests/units/api/checkBalances.test.ts delete mode 100644 confidential-assets/tests/units/api/checkIsFrozen.test.ts delete mode 100644 confidential-assets/tests/units/api/checkIsNormalized.test.ts delete mode 100644 confidential-assets/tests/units/api/checkIsRegistered.test.ts delete mode 100644 confidential-assets/tests/units/api/deposit.test.ts delete mode 100644 confidential-assets/tests/units/api/getAssetAuditor.test.ts delete mode 100644 confidential-assets/tests/units/api/negativeNormalize.test.ts delete mode 100644 confidential-assets/tests/units/api/negativeTransfer.test.ts delete mode 100644 confidential-assets/tests/units/api/negativeWithdraw.test.ts delete mode 100644 confidential-assets/tests/units/api/normalize.test.ts delete mode 100644 confidential-assets/tests/units/api/register.test.ts delete mode 100644 confidential-assets/tests/units/api/safelyRollover.test.ts delete mode 100644 confidential-assets/tests/units/api/transfer.test.ts delete mode 100644 confidential-assets/tests/units/api/transferWithAuditor.test.ts delete mode 100644 confidential-assets/tests/units/api/withdraw.test.ts diff --git a/confidential-assets/README.md b/confidential-assets/README.md index c944f2de1..f22f4f48d 100644 --- a/confidential-assets/README.md +++ b/confidential-assets/README.md @@ -89,6 +89,12 @@ cargo run -p aptos -- node run-localnet --with-indexer-api --assume-yes --force- 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 jest tests/e2e/confidentialAsset.test.ts -t "rotate Alice" --runInBand ``` Or, run all tests: diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index 519e8c538..fa12439e8 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -18,12 +18,13 @@ import { ConfidentialBalance, getBalance, 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 +55,7 @@ type TransferParams = WithdrawParams & { type RolloverParams = ConfidentialAssetSubmissionParams & { senderDecryptionKey?: TwistedEd25519PrivateKey; - withFreezeBalance?: boolean; + withPauseIncoming?: boolean; }; type RotateKeyParams = ConfidentialAssetSubmissionParams & { @@ -206,7 +207,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 +257,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 +325,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 +352,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 +380,17 @@ export class ConfidentialAsset { tokenAddress, decryptionKey: senderDecryptionKey, }); - if (balance.pendingBalance() > 0n) { + + // The on-chain rotate_encryption_key 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 +426,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; } /** diff --git a/confidential-assets/src/consts.ts b/confidential-assets/src/consts.ts index 0076fb33e..cfda41c8d 100644 --- a/confidential-assets/src/consts.ts +++ b/confidential-assets/src/consts.ts @@ -4,8 +4,6 @@ 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. */ diff --git a/confidential-assets/src/crypto/confidentialKeyRotation.ts b/confidential-assets/src/crypto/confidentialKeyRotation.ts index cd4671886..03193030a 100644 --- a/confidential-assets/src/crypto/confidentialKeyRotation.ts +++ b/confidential-assets/src/crypto/confidentialKeyRotation.ts @@ -1,343 +1,292 @@ -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, + type DomainSeparator, + type SigmaProtocolStatement, + type SigmaProtocolProof, + type PsiFunction, + type TransformationFunction, +} from "./sigmaProtocol"; + +/** Protocol ID matching the Move constant */ +const PROTOCOL_ID = "AptosConfidentialAsset/KeyRotationV1"; + +/** 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; +}; + +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; - newEncryptedAvailableBalance: EncryptedAmount; + private tokenAddress: Uint8Array; constructor(args: { - randomness: bigint[]; currentDecryptionKey: TwistedEd25519PrivateKey; newDecryptionKey: TwistedEd25519PrivateKey; currentEncryptedAvailableBalance: EncryptedAmount; - newEncryptedAvailableBalance: EncryptedAmount; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; }) { - 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; } - 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, }); } - 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` 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 = { + 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, 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; + proof: SigmaProtocolProof; + }): boolean { + const { oldEk, newEk, oldD, newD, senderAddress, tokenAddress, 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 = { + 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, makeKeyRotationPsi(numChunks), makeKeyRotationF(numChunks), stmt, proof); } } diff --git a/confidential-assets/src/crypto/index.ts b/confidential-assets/src/crypto/index.ts index 3739470fb..e0fc824ed 100644 --- a/confidential-assets/src/crypto/index.ts +++ b/confidential-assets/src/crypto/index.ts @@ -5,6 +5,7 @@ export * from "./rangeProof"; export { initializeWasm, isWasmInitialized, ensureWasmInitialized } from "./wasmLoader"; export * from "./chunkedAmount"; export * from "./encryptedAmount"; +export * from "./sigmaProtocol"; export * from "./confidentialKeyRotation"; export * from "./confidentialNormalization"; export * from "./confidentialTransfer"; diff --git a/confidential-assets/src/crypto/sigmaProtocol.ts b/confidential-assets/src/crypto/sigmaProtocol.ts new file mode 100644 index 000000000..d58235375 --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocol.ts @@ -0,0 +1,308 @@ +// 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 { RistrettoPoint } from "."; +import type { RistPoint } from "."; + +// ============================================================================= +// Domain Separator & Session +// ============================================================================= + +/** + * Matches the Move `DomainSeparator` struct: + * ```move + * struct DomainSeparator { protocol_id: vector, session_id: vector } + * ``` + */ +export interface DomainSeparator { + protocolId: Uint8Array; + sessionId: Uint8Array; +} + +/** + * BCS-serializable DomainSeparator. + */ +class BcsDomainSeparator extends Serializable { + constructor(public readonly dst: DomainSeparator) { + super(); + } + + serialize(serializer: Serializer): void { + 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, + * k: u64, + * stmt_X: vector, + * stmt_x: vector, + * proof_A: vector, + * } + * ``` + */ +class BcsFiatShamirInputs extends Serializable { + constructor( + public readonly dst: DomainSeparator, + 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)); + 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`. + * + * Returns `{ e, betas }` where `e` is the challenge scalar and `betas = [1, beta, beta^2, ...]`. + */ +export function sigmaProtocolFiatShamir( + dst: DomainSeparator, + 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, k, stmt.compressedPoints, stmt.scalars, compressedA); + const bytes = fiatShamirInputs.bcsToBytes(); + + const eHash = sha512(bytes); + const betaHash = sha512(eHash); + + 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, + 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, 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, + 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, 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/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 9c4651c6c..c8112446d 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { + AccountAddress, AccountAddressInput, AnyNumber, Aptos, @@ -21,7 +22,7 @@ import { TwistedEd25519PrivateKey, } from "../crypto"; 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 @@ -165,7 +166,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 +175,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 +193,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 +218,17 @@ export class ConfidentialAssetTransactionBuilder { tokenAddress: AccountAddressInput; options?: LedgerVersionArg; }): Promise { - const [{ vec: globalAuditorPubKey }] = await this.client.view<[{ vec: Uint8Array }]>({ + const [{ vec: globalAuditorPubKey }] = await this.client.view<[{ vec: { data: string }[] }]>({ options: args.options, payload: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_auditor`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_auditor_for_asset_type`, functionArguments: [args.tokenAddress], }, }); if (globalAuditorPubKey.length === 0) { return undefined; } - return new TwistedEd25519PublicKey(globalAuditorPubKey); + return new TwistedEd25519PublicKey(globalAuditorPubKey[0].data); } /** @@ -277,14 +278,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({ @@ -344,18 +345,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 +364,7 @@ export class ConfidentialAssetTransactionBuilder { newSenderDecryptionKey: TwistedEd25519PrivateKey; tokenAddress: AccountAddressInput; checkPendingBalanceEmpty?: boolean; - withUnfreezePendingBalance?: boolean; + unpause?: boolean; withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { @@ -376,12 +374,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 +392,34 @@ 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); + + // Create the confidential key rotation object and generate the proof + const confidentialKeyRotation = ConfidentialKeyRotation.create({ senderDecryptionKey, newSenderDecryptionKey, currentEncryptedAvailableBalance, + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), }); - // 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`, functionArguments: [ args.tokenAddress, - newPublicKeyBytes, - newEncryptedAvailableBalance.getCipherTextBytes(), - rangeProof, - ConfidentialKeyRotation.serializeSigmaProof(sigmaProof), + newEkBytes, + unpause, + newDBytes, + proof.commitment, + proof.response, ], }, options: args.options, diff --git a/confidential-assets/src/internal/viewFunctions.ts b/confidential-assets/src/internal/viewFunctions.ts index 737621174..c878b2381 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 }; - }[]; + C: { data: string }[]; + D: { 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.C.map( + (c, i) => new TwistedElGamalCiphertext(c.data.slice(2), chunkedPendingBalance.D[i].data.slice(2)), ), - available: chunkedActualBalances.chunks.map( - (el) => new TwistedElGamalCiphertext(el.left.data.slice(2), el.right.data.slice(2)), + available: chunkedActualBalances.C.map( + (c, i) => new TwistedElGamalCiphertext(c.data.slice(2), chunkedActualBalances.D[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], }, @@ -268,14 +266,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 diff --git a/confidential-assets/tests/e2e/confidentialAsset.test.ts b/confidential-assets/tests/e2e/confidentialAsset.test.ts index 0bc9bbc44..eb690309c 100644 --- a/confidential-assets/tests/e2e/confidentialAsset.test.ts +++ b/confidential-assets/tests/e2e/confidentialAsset.test.ts @@ -61,12 +61,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({ @@ -405,27 +405,27 @@ describe("Confidential Asset Sender API", () => { ); 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 Bob's incoming transfers are paused and he has no registered balance", async () => { await expect( - confidentialAsset.isPendingBalanceFrozen({ + confidentialAsset.isIncomingTransfersPaused({ accountAddress: bob.accountAddress, tokenAddress: TOKEN_ADDRESS, }), - ).rejects.toThrow("ECA_STORE_NOT_PUBLISHED"); + ).rejects.toThrow("E_CONFIDENTIAL_STORE_NOT_REGISTERED"); }, longTestTimeout, ); @@ -501,7 +501,7 @@ describe("Confidential Asset Sender API", () => { tokenAddress: TOKEN_ADDRESS, accountAddress: bob.accountAddress, }), - ).rejects.toThrow("ECA_STORE_NOT_PUBLISHED"); + ).rejects.toThrow("E_CONFIDENTIAL_STORE_NOT_REGISTERED"); }, longTestTimeout, ); @@ -593,20 +593,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 index 0ac32dabd..0e74046e3 100644 --- a/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts +++ b/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts @@ -51,12 +51,12 @@ describe.skip("Confidential balance 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({ @@ -353,23 +353,23 @@ describe.skip("Confidential balance api", () => { ); 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 Bob's incoming transfers are paused and he has no registered balance", async () => { await expect( - confidentialAsset.isPendingBalanceFrozen({ + confidentialAsset.isIncomingTransfersPaused({ accountAddress: bob.accountAddress, tokenAddress: TOKEN_ADDRESS, }), @@ -506,10 +506,9 @@ describe.skip("Confidential balance api", () => { await expect( transactionBuilder.rotateEncryptionKey({ sender: alice.accountAddress, - senderDecryptionKey: aliceConfidential, newSenderDecryptionKey: ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, - withUnfreezePendingBalance: true, + unpause: true, tokenAddress: TOKEN_ADDRESS, }), ).rejects.toThrow("Pending balance must be 0 before rotating encryption key"); @@ -517,12 +516,12 @@ describe.skip("Confidential balance api", () => { const rolloverTx = await transactionBuilder.rolloverPendingBalance({ sender: alice.accountAddress, tokenAddress: TOKEN_ADDRESS, - withFreezeBalance: true, + withPauseIncoming: true, }); txResp = await sendAndWaitTx(rolloverTx, alice); expect(txResp.success).toBeTruthy(); - await checkAliceBalanceFrozenStatus(true); + await checkAliceIncomingTransfersPausedStatus(true); // Get the current balance before rotation const confidentialBalance = await confidentialAsset.getBalance({ @@ -531,19 +530,18 @@ describe.skip("Confidential balance 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 transactionBuilder.rotateEncryptionKey({ + // This will unpause incoming transfers after rotation (unpause defaults to true). + const keyRotationAndUnpauseTx = await transactionBuilder.rotateEncryptionKey({ sender: alice.accountAddress, senderDecryptionKey: aliceConfidential, newSenderDecryptionKey: ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, tokenAddress: TOKEN_ADDRESS, }); - txResp = await sendAndWaitTx(keyRotationAndUnfreezeTx, alice); + txResp = await sendAndWaitTx(keyRotationAndUnpauseTx, alice); expect(txResp.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/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..448762749 100644 --- a/confidential-assets/tests/units/confidentialProofs.test.ts +++ b/confidential-assets/tests/units/confidentialProofs.test.ts @@ -3,7 +3,6 @@ import { CHUNK_BITS_BIG_INT, ChunkedAmount, ConfidentialKeyRotation, - ConfidentialKeyRotationSigmaProof, ConfidentialNormalization, ConfidentialNormalizationSigmaProof, ConfidentialTransfer, @@ -244,55 +243,56 @@ describe("Generate 'confidential coin' proofs", () => { ); const newAliceConfidentialPrivateKey = TwistedEd25519PrivateKey.generate(); - let confidentialKeyRotation: ConfidentialKeyRotation; - let confidentialKeyRotationSigmaProof: ConfidentialKeyRotationSigmaProof; + // Use dummy addresses for the unit test (32 bytes each) + const dummySenderAddress = new Uint8Array(32); + const dummyTokenAddress = new Uint8Array(32).fill(0x0a); // 0xa = APT metadata address + + 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, }); - 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, + proof, }); expect(isValid).toBeTruthy(); @@ -300,18 +300,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, From 464e153f2c6662e694e13b8c02638d76a5692293 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 25 Feb 2026 22:14:41 -0600 Subject: [PATCH 06/22] remove legacy sigma protocol code --- confidential-assets/src/consts.ts | 6 - .../src/crypto/confidentialNormalization.ts | 233 +-------- .../src/crypto/confidentialTransfer.ts | 468 +----------------- .../src/crypto/confidentialWithdraw.ts | 240 +-------- .../internal/confidentialAssetTxnBuilder.ts | 4 +- .../tests/units/confidentialProofs.test.ts | 26 - 6 files changed, 53 insertions(+), 924 deletions(-) diff --git a/confidential-assets/src/consts.ts b/confidential-assets/src/consts.ts index cfda41c8d..b0a010839 100644 --- a/confidential-assets/src/consts.ts +++ b/confidential-assets/src/consts.ts @@ -1,11 +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_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/confidentialNormalization.ts b/confidential-assets/src/crypto/confidentialNormalization.ts index 1123e3ebc..a915c90bb 100644 --- a/confidential-assets/src/crypto/confidentialNormalization.ts +++ b/confidential-assets/src/crypto/confidentialNormalization.ts @@ -1,25 +1,16 @@ 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[]; -}; +/** Stub type — sigma proof is not yet implemented. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type ConfidentialNormalizationSigmaProof = {}; export type CreateConfidentialNormalizationOpArgs = { decryptionKey: TwistedEd25519PrivateKey; @@ -68,210 +59,24 @@ export class ConfidentialNormalization { }); } - 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, - }; + /** Returns an empty sigma proof (stub — sigma proof is not yet implemented). */ + static serializeSigmaProof(): Uint8Array { + return new Uint8Array(0); } + /** Stub — always returns an empty sigma proof. */ 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 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 { - 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()), - }; + return {} as ConfidentialNormalizationSigmaProof; } - static verifySigmaProof(opts: { + /** Stub — always returns true. */ + 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]))) - ); + return true; } async genRangeProof(): Promise { @@ -299,10 +104,8 @@ export class ConfidentialNormalization { }); } - async authorizeNormalization(): Promise< - [{ sigmaProof: ConfidentialNormalizationSigmaProof; rangeProof: Uint8Array }, EncryptedAmount] - > { - const sigmaProof = await this.genSigmaProof(); + async authorizeNormalization(): Promise<[{ sigmaProof: Uint8Array; rangeProof: Uint8Array }, EncryptedAmount]> { + const sigmaProof = ConfidentialNormalization.serializeSigmaProof(); const rangeProof = await this.genRangeProof(); return [{ sigmaProof, rangeProof }, this.normalizedEncryptedAvailableBalance]; @@ -326,7 +129,7 @@ export class ConfidentialNormalization { args.tokenAddress, normalizedCB.getCipherTextBytes(), rangeProof, - ConfidentialNormalization.serializeSigmaProof(sigmaProof), + sigmaProof, ], }, options: args.options, diff --git a/confidential-assets/src/crypto/confidentialTransfer.ts b/confidential-assets/src/crypto/confidentialTransfer.ts index 4ce86a278..28dbaf664 100644 --- a/confidential-assets/src/crypto/confidentialTransfer.ts +++ b/confidential-assets/src/crypto/confidentialTransfer.ts @@ -1,38 +1,21 @@ -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[]; -}; +/** Stub type — sigma proof is not yet implemented. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type ConfidentialTransferSigmaProof = {}; export type ConfidentialTransferRangeProof = { rangeProofAmount: Uint8Array; @@ -201,275 +184,18 @@ export class ConfidentialTransfer { }); } - 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, - }; + /** Returns an empty sigma proof (stub — sigma proof is not yet implemented). */ + static serializeSigmaProof(): Uint8Array { + return new Uint8Array(0); } + /** Stub — always returns an empty sigma proof. */ 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, - }; + return {} as ConfidentialTransferSigmaProof; } - static verifySigmaProof(opts: { + /** Stub — always returns true. */ + static verifySigmaProof(_opts: { senderPrivateKey: TwistedEd25519PrivateKey; recipientPublicKey: TwistedEd25519PublicKey; encryptedActualBalance: TwistedElGamalCiphertext[]; @@ -482,168 +208,7 @@ export class ConfidentialTransfer { 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); - }); - - 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]))) - ); + return true; } async genRangeProof(): Promise { @@ -671,14 +236,13 @@ export class ConfidentialTransfer { async authorizeTransfer(): Promise< [ - { sigmaProof: ConfidentialTransferSigmaProof; rangeProof: ConfidentialTransferRangeProof }, + { sigmaProof: Uint8Array; rangeProof: ConfidentialTransferRangeProof }, EncryptedAmount, EncryptedAmount, EncryptedAmount[], ] > { - const sigmaProof = await this.genSigmaProof(); - + const sigmaProof = ConfidentialTransfer.serializeSigmaProof(); const rangeProof = await this.genRangeProof(); return [ diff --git a/confidential-assets/src/crypto/confidentialWithdraw.ts b/confidential-assets/src/crypto/confidentialWithdraw.ts index e38f4bb6f..1b06edcb0 100644 --- a/confidential-assets/src/crypto/confidentialWithdraw.ts +++ b/confidential-assets/src/crypto/confidentialWithdraw.ts @@ -1,15 +1,9 @@ -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, H_RISTRETTO, @@ -17,16 +11,9 @@ import { EncryptedAmount, } from "."; -export type ConfidentialWithdrawSigmaProof = { - alpha1List: Uint8Array[]; - alpha2: Uint8Array; - alpha3: Uint8Array; - alpha4List: Uint8Array[]; - X1: Uint8Array; - X2: Uint8Array; - X3List: Uint8Array[]; - X4List: Uint8Array[]; -}; +/** Stub type — sigma proof is not yet implemented. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type ConfidentialWithdrawSigmaProof = {}; export type CreateConfidentialWithdrawOpArgs = { decryptionKey: TwistedEd25519PrivateKey; @@ -40,7 +27,7 @@ export class ConfidentialWithdraw { senderEncryptedAvailableBalance: EncryptedAmount; - amount: ChunkedAmount; + amount: bigint; senderEncryptedAvailableBalanceAfterWithdrawal: EncryptedAmount; @@ -77,7 +64,7 @@ export class ConfidentialWithdraw { ); } - this.amount = ChunkedAmount.createTransferAmount(amount); + this.amount = amount; this.decryptionKey = decryptionKey; this.senderEncryptedAvailableBalance = senderEncryptedAvailableBalance; this.randomness = randomness; @@ -106,217 +93,24 @@ export class ConfidentialWithdraw { }); } - 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)); - } - - 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, - }; + /** Returns an empty sigma proof (stub — sigma proof is not yet implemented). */ + static serializeSigmaProof(): Uint8Array { + return new Uint8Array(0); } + /** Stub — always returns an empty sigma proof. */ 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()), - }; + return {} as ConfidentialWithdrawSigmaProof; } - static verifySigmaProof(opts: { + /** Stub — always returns true. */ + 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, - }); - - 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]))) - ); + return true; } async genRangeProof() { @@ -334,13 +128,13 @@ export class ConfidentialWithdraw { async authorizeWithdrawal(): Promise< [ { - sigmaProof: ConfidentialWithdrawSigmaProof; + sigmaProof: Uint8Array; rangeProof: Uint8Array; }, EncryptedAmount, ] > { - const sigmaProof = await this.genSigmaProof(); + const sigmaProof = ConfidentialWithdraw.serializeSigmaProof(); const rangeProof = await this.genRangeProof(); return [{ sigmaProof, rangeProof }, this.senderEncryptedAvailableBalanceAfterWithdrawal]; diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index c8112446d..67dc364f7 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -154,7 +154,7 @@ export class ConfidentialAssetTransactionBuilder { String(amount), encryptedAmountAfterWithdraw.getCipherTextBytes(), rangeProof, - ConfidentialWithdraw.serializeSigmaProof(sigmaProof), + sigmaProof, ], }, options, @@ -336,7 +336,7 @@ export class ConfidentialAssetTransactionBuilder { concatBytes(...auditorBalances), rangeProofNewBalance, rangeProofAmount, - ConfidentialTransfer.serializeSigmaProof(sigmaProof), + sigmaProof, ], }, }); diff --git a/confidential-assets/tests/units/confidentialProofs.test.ts b/confidential-assets/tests/units/confidentialProofs.test.ts index 448762749..4bef072c6 100644 --- a/confidential-assets/tests/units/confidentialProofs.test.ts +++ b/confidential-assets/tests/units/confidentialProofs.test.ts @@ -190,32 +190,6 @@ describe("Generate 'confidential coin' proofs", () => { }, 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", From 1c5b537426fb89c16835f12d9686aa8b2518201c Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Thu, 26 Feb 2026 08:49:51 -0600 Subject: [PATCH 07/22] rename rotate_encryption_key* funcs --- confidential-assets/src/api/confidentialAsset.ts | 2 +- confidential-assets/src/crypto/confidentialKeyRotation.ts | 2 +- confidential-assets/src/internal/confidentialAssetTxnBuilder.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index fa12439e8..e6083b9be 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -381,7 +381,7 @@ export class ConfidentialAsset { decryptionKey: senderDecryptionKey, }); - // The on-chain rotate_encryption_key requires incoming transfers to be paused. + // 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, diff --git a/confidential-assets/src/crypto/confidentialKeyRotation.ts b/confidential-assets/src/crypto/confidentialKeyRotation.ts index 03193030a..9f2001960 100644 --- a/confidential-assets/src/crypto/confidentialKeyRotation.ts +++ b/confidential-assets/src/crypto/confidentialKeyRotation.ts @@ -167,7 +167,7 @@ export class ConfidentialKeyRotation { /** * Generate the key rotation proof and re-encrypted balance components. * - * Returns everything needed to call the `rotate_encryption_key` entry function. + * Returns everything needed to call the `rotate_encryption_key_raw` entry function. */ authorizeKeyRotation(): KeyRotationProof { const numChunks = AVAILABLE_BALANCE_CHUNK_COUNT; diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 67dc364f7..071223088 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -412,7 +412,7 @@ export class ConfidentialAssetTransactionBuilder { withFeePayer: args.withFeePayer, sender: args.sender, data: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::rotate_encryption_key`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::rotate_encryption_key_raw`, functionArguments: [ args.tokenAddress, newEkBytes, From 21b2565a555539c4e6442cd002850917c9d0d5c7 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Thu, 26 Feb 2026 21:53:21 -0600 Subject: [PATCH 08/22] keep up with Move refactoring of entry function interface and proof structs --- .../src/crypto/confidentialNormalization.ts | 7 ++- .../internal/confidentialAssetTxnBuilder.ts | 55 ++++++++++--------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/confidential-assets/src/crypto/confidentialNormalization.ts b/confidential-assets/src/crypto/confidentialNormalization.ts index a915c90bb..7884f06ca 100644 --- a/confidential-assets/src/crypto/confidentialNormalization.ts +++ b/confidential-assets/src/crypto/confidentialNormalization.ts @@ -119,17 +119,18 @@ export class ConfidentialNormalization { withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { - const [{ sigmaProof, rangeProof }, normalizedCB] = await this.authorizeNormalization(); + const [{ rangeProof }, normalizedCB] = await this.authorizeNormalization(); 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(), rangeProof, - sigmaProof, + [] as Uint8Array[], // sigma_proto_comm (stub) + [] as Uint8Array[], // sigma_proto_resp (stub) ], }, options: args.options, diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 071223088..1f81236dc 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -11,7 +11,6 @@ import { LedgerVersionArg, SimpleTransaction, } from "@aptos-labs/ts-sdk"; -import { concatBytes } from "@noble/hashes/utils"; import { TwistedElGamal, ConfidentialNormalization, @@ -58,21 +57,20 @@ export class ConfidentialAssetTransactionBuilder { 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(), [] as Uint8Array[], [] as Uint8Array[]], }, }); } /** - * 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 @@ -81,12 +79,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); @@ -94,8 +90,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], }, }); } @@ -142,19 +138,20 @@ export class ConfidentialAssetTransactionBuilder { amount: BigInt(amount), }); - const [{ sigmaProof, rangeProof }, encryptedAmountAfterWithdraw] = await confidentialWithdraw.authorizeWithdrawal(); + const [{ rangeProof }, encryptedAmountAfterWithdraw] = await confidentialWithdraw.authorizeWithdrawal(); 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(), rangeProof, - sigmaProof, + [] as Uint8Array[], // sigma_proto_comm (stub) + [] as Uint8Array[], // sigma_proto_resp (stub) ], }, options, @@ -296,21 +293,24 @@ export class ConfidentialAssetTransactionBuilder { decryptionKey: senderDecryptionKey, }); + // Build the full auditor list for proof generation: [...extra, global (if set)] + // The contract will append the global auditor itself, so we only send extra auditor EKs on-chain. + const allAuditorEncryptionKeys = [ + ...additionalAuditorEncryptionKeys, + ...(globalAuditorPubKey ? [globalAuditorPubKey] : []), + ]; + // Create the confidential transfer object const confidentialTransfer = await ConfidentialTransfer.create({ senderDecryptionKey, senderAvailableBalanceCipherText: senderEncryptedAvailableBalance.getCipherText(), amount, recipientEncryptionKey, - auditorEncryptionKeys: [ - ...(globalAuditorPubKey ? [globalAuditorPubKey] : []), - ...additionalAuditorEncryptionKeys, - ], + auditorEncryptionKeys: allAuditorEncryptionKeys, }); const [ { - sigmaProof, rangeProof: { rangeProofAmount, rangeProofNewBalance }, }, encryptedAmountAfterTransfer, @@ -318,25 +318,30 @@ export class ConfidentialAssetTransactionBuilder { auditorsCBList, ] = await confidentialTransfer.authorizeTransfer(); - const auditorEncryptionKeys = confidentialTransfer.auditorEncryptionKeys.map((pk) => pk.toUint8Array()); - const auditorBalances = auditorsCBList.map((el) => el.getCipherTextBytes()); + // Only send extra auditor EKs on-chain (not the global auditor, which the contract fetches itself) + const extraAuditorEncryptionKeys = 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()); + const auditorDPoints = auditorsCBList.map((cb) => cb.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), + recipientDPoints, + extraAuditorEncryptionKeys, + auditorDPoints, rangeProofNewBalance, rangeProofAmount, - sigmaProof, + [] as Uint8Array[], // sigma_proto_comm (stub) + [] as Uint8Array[], // sigma_proto_resp (stub) ], }, }); From c9cb762ca7a81bd217fba255483c00515b8b31bb Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Fri, 27 Feb 2026 11:49:41 -0600 Subject: [PATCH 09/22] add global auditor --- .../src/api/confidentialAsset.ts | 11 + .../src/crypto/confidentialNormalization.ts | 133 ++- .../src/crypto/confidentialTransfer.ts | 130 ++- .../src/crypto/confidentialWithdraw.ts | 116 ++- confidential-assets/src/crypto/index.ts | 3 + .../src/crypto/sigmaProtocolRegistration.ts | 140 ++++ .../src/crypto/sigmaProtocolTransfer.ts | 770 ++++++++++++++++++ .../src/crypto/sigmaProtocolWithdraw.ts | 483 +++++++++++ .../internal/confidentialAssetTxnBuilder.ts | 91 ++- .../tests/units/confidentialProofs.test.ts | 140 ++-- 10 files changed, 1870 insertions(+), 147 deletions(-) create mode 100644 confidential-assets/src/crypto/sigmaProtocolRegistration.ts create mode 100644 confidential-assets/src/crypto/sigmaProtocolTransfer.ts create mode 100644 confidential-assets/src/crypto/sigmaProtocolWithdraw.ts diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index e6083b9be..fc3ef8303 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, @@ -499,9 +500,19 @@ export class ConfidentialAsset { useCachedValue: true, }); + // Resolve addresses to 32-byte arrays + const senderAddr = AccountAddress.from(signer.accountAddress); + const tokenAddr = AccountAddress.from(tokenAddress); + + // Get the auditor public key for the token + const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + const confidentialNormalization = await ConfidentialNormalization.create({ decryptionKey: senderDecryptionKey, unnormalizedAvailableBalance: available, + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + auditorEncryptionKey: globalAuditorPubKey, }); const transaction = await confidentialNormalization.createTransaction({ diff --git a/confidential-assets/src/crypto/confidentialNormalization.ts b/confidential-assets/src/crypto/confidentialNormalization.ts index 7884f06ca..15c0da730 100644 --- a/confidential-assets/src/crypto/confidentialNormalization.ts +++ b/confidential-assets/src/crypto/confidentialNormalization.ts @@ -7,14 +7,18 @@ import { ed25519GenListOfRandom } from "../utils"; import { EncryptedAmount } from "./encryptedAmount"; import { AVAILABLE_BALANCE_CHUNK_COUNT, CHUNK_BITS } from "./chunkedAmount"; import { Aptos, SimpleTransaction, AccountAddressInput, InputGenerateTransactionOptions } from "@aptos-labs/ts-sdk"; - -/** Stub type — sigma proof is not yet implemented. */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export type ConfidentialNormalizationSigmaProof = {}; +import type { SigmaProtocolProof } from "./sigmaProtocol"; +import { proveNormalization } from "./sigmaProtocolWithdraw"; export type CreateConfidentialNormalizationOpArgs = { decryptionKey: TwistedEd25519PrivateKey; unnormalizedAvailableBalance: EncryptedAmount; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte token address */ + tokenAddress: Uint8Array; + /** Optional auditor encryption key */ + auditorEncryptionKey?: TwistedEd25519PublicKey; randomness?: bigint[]; }; @@ -25,16 +29,33 @@ export class ConfidentialNormalization { normalizedEncryptedAvailableBalance: EncryptedAmount; + /** Optional: normalized balance encrypted under auditor key */ + auditorEncryptedNormalizedBalance?: EncryptedAmount; + randomness: bigint[]; + senderAddress: Uint8Array; + + tokenAddress: Uint8Array; + + auditorEncryptionKey?: TwistedEd25519PublicKey; + constructor(args: { decryptionKey: TwistedEd25519PrivateKey; unnormalizedEncryptedAvailableBalance: EncryptedAmount; normalizedEncryptedAvailableBalance: EncryptedAmount; + auditorEncryptedNormalizedBalance?: EncryptedAmount; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + 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.auditorEncryptionKey = args.auditorEncryptionKey; const randomness = this.normalizedEncryptedAvailableBalance.getRandomness(); if (!randomness) { throw new Error("Randomness is not set"); @@ -43,7 +64,13 @@ 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, + auditorEncryptionKey, + } = args; const unnormalizedEncryptedAvailableBalance = args.unnormalizedAvailableBalance; @@ -52,31 +79,62 @@ 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, + auditorEncryptionKey, }); } - /** Returns an empty sigma proof (stub — sigma proof is not yet implemented). */ - static serializeSigmaProof(): Uint8Array { - return new Uint8Array(0); - } - - /** Stub — always returns an empty sigma proof. */ - async genSigmaProof(): Promise { - return {} as ConfidentialNormalizationSigmaProof; - } + /** + * 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); + } - /** Stub — always returns true. */ - static verifySigmaProof(_opts: { - publicKey: TwistedEd25519PublicKey; - sigmaProof: ConfidentialNormalizationSigmaProof; - unnormalizedEncryptedBalance: EncryptedAmount; - normalizedEncryptedBalance: EncryptedAmount; - }): boolean { - return true; + return proveNormalization({ + dk: this.decryptionKey, + senderAddress: this.senderAddress, + tokenAddress: this.tokenAddress, + amount: 0n, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks: this.normalizedEncryptedAvailableBalance.getAmountChunks(), + newRandomness: this.randomness, + auditorEncryptionKey, + newBalanceDAud, + }); } async genRangeProof(): Promise { @@ -104,11 +162,21 @@ export class ConfidentialNormalization { }); } - async authorizeNormalization(): Promise<[{ sigmaProof: Uint8Array; rangeProof: Uint8Array }, EncryptedAmount]> { - const sigmaProof = ConfidentialNormalization.serializeSigmaProof(); + async authorizeNormalization(): Promise< + [ + { sigmaProof: SigmaProtocolProof; rangeProof: Uint8Array }, + EncryptedAmount, + EncryptedAmount | undefined, + ] + > { + const sigmaProof = this.genSigmaProof(); const rangeProof = await this.genRangeProof(); - return [{ sigmaProof, rangeProof }, this.normalizedEncryptedAvailableBalance]; + return [ + { sigmaProof, rangeProof }, + this.normalizedEncryptedAvailableBalance, + this.auditorEncryptedNormalizedBalance, + ]; } async createTransaction(args: { @@ -119,7 +187,12 @@ export class ConfidentialNormalization { withFeePayer?: boolean; options?: InputGenerateTransactionOptions; }): Promise { - const [{ 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, @@ -127,10 +200,12 @@ export class ConfidentialNormalization { 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, - [] as Uint8Array[], // sigma_proto_comm (stub) - [] as Uint8Array[], // sigma_proto_resp (stub) + sigmaProof.commitment, + sigmaProof.response, ], }, options: args.options, diff --git a/confidential-assets/src/crypto/confidentialTransfer.ts b/confidential-assets/src/crypto/confidentialTransfer.ts index 28dbaf664..600292db1 100644 --- a/confidential-assets/src/crypto/confidentialTransfer.ts +++ b/confidential-assets/src/crypto/confidentialTransfer.ts @@ -12,10 +12,8 @@ import { TwistedEd25519PrivateKey, TwistedEd25519PublicKey, H_RISTRETTO } from " import { TwistedElGamalCiphertext } from "./twistedElGamal"; import { ed25519GenListOfRandom } from "../utils"; import { EncryptedAmount } from "./encryptedAmount"; - -/** Stub type — sigma proof is not yet implemented. */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export type ConfidentialTransferSigmaProof = {}; +import type { SigmaProtocolProof } from "./sigmaProtocol"; +import { proveTransfer } from "./sigmaProtocolTransfer"; export type ConfidentialTransferRangeProof = { rangeProofAmount: Uint8Array; @@ -29,6 +27,12 @@ export type CreateConfidentialTransferOpArgs = { recipientEncryptionKey: TwistedEd25519PublicKey; auditorEncryptionKeys?: TwistedEd25519PublicKey[]; transferAmountRandomness?: bigint[]; + /** 32-byte sender address */ + senderAddress: Uint8Array; + /** 32-byte recipient address */ + recipientAddress: Uint8Array; + /** 32-byte token address */ + tokenAddress: Uint8Array; }; export class ConfidentialTransfer { @@ -74,6 +78,15 @@ export class ConfidentialTransfer { */ newBalanceRandomness: bigint[]; + /** Optional: new balance encrypted under each auditor key */ + auditorEncryptedBalancesAfterTransfer: EncryptedAmount[]; + + senderAddress: Uint8Array; + + recipientAddress: Uint8Array; + + tokenAddress: Uint8Array; + private constructor(args: { senderDecryptionKey: TwistedEd25519PrivateKey; recipientEncryptionKey: TwistedEd25519PublicKey; @@ -84,6 +97,10 @@ export class ConfidentialTransfer { transferAmountEncryptedByRecipient: EncryptedAmount; transferAmountEncryptedByAuditors: EncryptedAmount[]; senderEncryptedAvailableBalanceAfterTransfer: EncryptedAmount; + auditorEncryptedBalancesAfterTransfer: EncryptedAmount[]; + senderAddress: Uint8Array; + recipientAddress: Uint8Array; + tokenAddress: Uint8Array; }) { const { senderDecryptionKey, @@ -95,6 +112,10 @@ export class ConfidentialTransfer { transferAmountEncryptedByRecipient, transferAmountEncryptedByAuditors, senderEncryptedAvailableBalanceAfterTransfer, + auditorEncryptedBalancesAfterTransfer, + senderAddress, + recipientAddress, + tokenAddress, } = args; this.senderDecryptionKey = senderDecryptionKey; this.recipientEncryptionKey = recipientEncryptionKey; @@ -113,6 +134,7 @@ export class ConfidentialTransfer { this.transferAmountEncryptedByRecipient = transferAmountEncryptedByRecipient; this.transferAmountEncryptedByAuditors = transferAmountEncryptedByAuditors; this.senderEncryptedAvailableBalanceAfterTransfer = senderEncryptedAvailableBalanceAfterTransfer; + this.auditorEncryptedBalancesAfterTransfer = auditorEncryptedBalancesAfterTransfer; const transferAmountRandomness = transferAmountEncryptedBySender.getRandomness(); if (!transferAmountRandomness) { @@ -125,6 +147,10 @@ export class ConfidentialTransfer { throw new Error("New balance randomness is not set"); } this.newBalanceRandomness = newBalanceRandomness; + + this.senderAddress = senderAddress; + this.recipientAddress = recipientAddress; + this.tokenAddress = tokenAddress; } static async create(args: CreateConfidentialTransferOpArgs) { @@ -134,6 +160,9 @@ export class ConfidentialTransfer { recipientEncryptionKey, auditorEncryptionKeys = [], transferAmountRandomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), + senderAddress, + recipientAddress, + tokenAddress, } = args; const amount = BigInt(args.amount); const newBalanceRandomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); @@ -171,6 +200,16 @@ 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, @@ -181,34 +220,61 @@ export class ConfidentialTransfer { transferAmountEncryptedByRecipient, transferAmountEncryptedByAuditors, senderEncryptedAvailableBalanceAfterTransfer, + auditorEncryptedBalancesAfterTransfer, + senderAddress, + recipientAddress, + tokenAddress, }); } - /** Returns an empty sigma proof (stub — sigma proof is not yet implemented). */ - static serializeSigmaProof(): Uint8Array { - return new Uint8Array(0); - } - - /** Stub — always returns an empty sigma proof. */ - async genSigmaProof(): Promise { - return {} as ConfidentialTransferSigmaProof; - } - - /** Stub — always returns true. */ - static verifySigmaProof(_opts: { - senderPrivateKey: TwistedEd25519PrivateKey; - recipientPublicKey: TwistedEd25519PublicKey; - encryptedActualBalance: TwistedElGamalCiphertext[]; - encryptedActualBalanceAfterTransfer: EncryptedAmount; - encryptedTransferAmountByRecipient: EncryptedAmount; - encryptedTransferAmountBySender: EncryptedAmount; - sigmaProof: ConfidentialTransferSigmaProof; - auditors?: { - publicKeys: TwistedEd25519PublicKey[]; - auditorsCBList: TwistedElGamalCiphertext[][]; - }; - }): boolean { - return true; + /** + * 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, + 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), + auditorEncryptionKeys, + newBalanceDAud, + transferAmountDAud, + }); } async genRangeProof(): Promise { @@ -236,13 +302,14 @@ export class ConfidentialTransfer { async authorizeTransfer(): Promise< [ - { sigmaProof: Uint8Array; rangeProof: ConfidentialTransferRangeProof }, + { sigmaProof: SigmaProtocolProof; rangeProof: ConfidentialTransferRangeProof }, EncryptedAmount, EncryptedAmount, EncryptedAmount[], + EncryptedAmount[], ] > { - const sigmaProof = ConfidentialTransfer.serializeSigmaProof(); + const sigmaProof = this.genSigmaProof(); const rangeProof = await this.genRangeProof(); return [ @@ -253,6 +320,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 1b06edcb0..79d7c718c 100644 --- a/confidential-assets/src/crypto/confidentialWithdraw.ts +++ b/confidential-assets/src/crypto/confidentialWithdraw.ts @@ -6,19 +6,24 @@ import { CHUNK_BITS, RangeProofExecutor, TwistedEd25519PrivateKey, + TwistedEd25519PublicKey, H_RISTRETTO, TwistedElGamalCiphertext, EncryptedAmount, } from "."; - -/** Stub type — sigma proof is not yet implemented. */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export type ConfidentialWithdrawSigmaProof = {}; +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; + /** Optional auditor encryption key */ + auditorEncryptionKey?: TwistedEd25519PublicKey; randomness?: bigint[]; }; @@ -31,14 +36,27 @@ export class ConfidentialWithdraw { senderEncryptedAvailableBalanceAfterWithdrawal: EncryptedAmount; + /** Optional: new balance encrypted under auditor key */ + auditorEncryptedBalanceAfterWithdrawal?: EncryptedAmount; + randomness: bigint[]; + senderAddress: Uint8Array; + + tokenAddress: Uint8Array; + + auditorEncryptionKey?: TwistedEd25519PublicKey; + constructor(args: { decryptionKey: TwistedEd25519PrivateKey; senderEncryptedAvailableBalance: EncryptedAmount; amount: bigint; senderEncryptedAvailableBalanceAfterWithdrawal: EncryptedAmount; + auditorEncryptedBalanceAfterWithdrawal?: EncryptedAmount; randomness: bigint[]; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + auditorEncryptionKey?: TwistedEd25519PublicKey; }) { const { decryptionKey, @@ -46,6 +64,10 @@ export class ConfidentialWithdraw { amount, randomness, senderEncryptedAvailableBalanceAfterWithdrawal, + auditorEncryptedBalanceAfterWithdrawal, + senderAddress, + tokenAddress, + auditorEncryptionKey, } = args; if (amount < 0n) { throw new Error("Amount to withdraw must not be negative"); @@ -69,10 +91,20 @@ export class ConfidentialWithdraw { this.senderEncryptedAvailableBalance = senderEncryptedAvailableBalance; this.randomness = randomness; this.senderEncryptedAvailableBalanceAfterWithdrawal = senderEncryptedAvailableBalanceAfterWithdrawal; + this.auditorEncryptedBalanceAfterWithdrawal = auditorEncryptedBalanceAfterWithdrawal; + this.senderAddress = senderAddress; + this.tokenAddress = tokenAddress; + 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, + auditorEncryptionKey, + } = args; const senderEncryptedAvailableBalance = await EncryptedAmount.fromCipherTextAndPrivateKey( args.senderAvailableBalanceCipherText, @@ -84,33 +116,62 @@ 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, + auditorEncryptionKey, }); } - /** Returns an empty sigma proof (stub — sigma proof is not yet implemented). */ - static serializeSigmaProof(): Uint8Array { - return new Uint8Array(0); - } - - /** Stub — always returns an empty sigma proof. */ - async genSigmaProof(): Promise { - return {} as ConfidentialWithdrawSigmaProof; - } + /** + * 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); + } - /** Stub — always returns true. */ - static verifySigmaProof(_opts: { - sigmaProof: ConfidentialWithdrawSigmaProof; - senderEncryptedAvailableBalance: EncryptedAmount; - senderEncryptedAvailableBalanceAfterWithdrawal: EncryptedAmount; - amountToWithdraw: bigint; - }): boolean { - return true; + return proveWithdrawal({ + dk: this.decryptionKey, + senderAddress: this.senderAddress, + tokenAddress: this.tokenAddress, + amount: this.amount, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks: this.senderEncryptedAvailableBalanceAfterWithdrawal.getAmountChunks(), + newRandomness: this.randomness, + auditorEncryptionKey, + newBalanceDAud, + }); } async genRangeProof() { @@ -128,16 +189,21 @@ export class ConfidentialWithdraw { async authorizeWithdrawal(): Promise< [ { - sigmaProof: Uint8Array; + sigmaProof: SigmaProtocolProof; rangeProof: Uint8Array; }, EncryptedAmount, + EncryptedAmount | undefined, ] > { - const sigmaProof = ConfidentialWithdraw.serializeSigmaProof(); + 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 e0fc824ed..116f4caf5 100644 --- a/confidential-assets/src/crypto/index.ts +++ b/confidential-assets/src/crypto/index.ts @@ -6,6 +6,9 @@ export { initializeWasm, isWasmInitialized, ensureWasmInitialized } from "./wasm 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/sigmaProtocolRegistration.ts b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts new file mode 100644 index 000000000..4a8ce206a --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts @@ -0,0 +1,140 @@ +// 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, + 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"; + +/** 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; +}): SigmaProtocolProof { + const { dk, senderAddress, tokenAddress } = 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 = { + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolProve(dst, makeRegistrationPsi(), stmt, witness); +} + +/** + * Verify a registration sigma protocol proof. + */ +export function verifyRegistration(args: { + ek: Uint8Array; + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + proof: SigmaProtocolProof; +}): boolean { + const { ek: ekBytes, senderAddress, tokenAddress, 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 = { + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolVerify(dst, 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..4fa1cd805 --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts @@ -0,0 +1,770 @@ +// 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 (auditorless): + * [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]] + * + * With auditor: + * append [ek_aud, new_R_aud[ell], R_aud[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, + 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"; + +/** + * 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, + * } + * ``` + */ +export function bcsSerializeTransferSession( + senderAddress: Uint8Array, + recipientAddress: Uint8Array, + tokenTypeAddress: Uint8Array, + numAvailChunks: number, + numTransferChunks: 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)); + 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): + * 0: G + * 1: H + * 2: ek_sid + * 3: ek_rid + * 4 .. 4+ell-1: old_P[ell] + * 4+ell .. 4+2ell-1: old_R[ell] + * 4+2ell .. 4+3ell-1: new_P[ell] + * 4+3ell .. 4+4ell-1: new_R[ell] + * 4+4ell .. 4+4ell+n-1: P[n] (transfer amount commitments) + * 4+4ell+n .. 4+4ell+2n-1: R_sid[n] (transfer amount D for sender) + * 4+4ell+2n .. 4+4ell+3n-1: R_rid[n] (transfer amount D for recipient) + * + * With auditor, append: + * 4+4ell+3n: ek_aud + * 4+4ell+3n+1 .. 4+5ell+3n: new_R_aud[ell] + * 4+5ell+3n+1 .. 4+5ell+4n: R_aud[n] + */ +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; +} +function getIdxEkAud(ell: number, n: number): number { + return START_IDX_OLD_P + 4 * ell + 3 * n; +} +function getStartIdxNewRAud(ell: number, n: number): number { + return START_IDX_OLD_P + 4 * ell + 3 * n + 1; +} +function getStartIdxRAud(ell: number, n: number): number { + return START_IDX_OLD_P + 5 * ell + 3 * n + 1; +} + +/** + * Build the homomorphism psi for transfer. + * + * Witness layout: [dk, new_a[0..ell-1], new_r[0..ell-1], v[0..n-1], r[0..n-1]] + * + * psi outputs (auditorless, m = 2 + 2ell + 3n): + * 0: dk * ek_sid + * 1..ell: new_a[i]*G + new_r[i]*H + * ell+1..2ell: new_r[i]*ek_sid + * 2ell+1: dk* + ( + )*G (balance equation) + * 2ell+2..2ell+1+n: v[j]*G + r[j]*H + * 2ell+2+n..2ell+1+2n: r[j]*ek_sid + * 2ell+2+2n..2ell+1+3n: r[j]*ek_rid + * + * With auditor (m = 2 + 3ell + 4n): + * After new_r[i]*ek_sid, insert new_r[i]*ek_aud + * After r[j]*ek_rid, insert r[j]*ek_aud + */ +function makeTransferPsi(ell: number, n: 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 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[] = []; + + // 0: dk * ek_sid + result.push(ekSid.multiply(dk)); + + // 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]))); + } + + // ell+1..2ell: new_r[i]*ek_sid + for (let i = 0; i < ell; i++) { + result.push(ekSid.multiply(newR[i])); + } + + // Auditor: new_r[i]*ek_aud + if (hasAuditor) { + const ekAud = s.points[getIdxEkAud(ell, n)]; + for (let i = 0; i < ell; i++) { + result.push(ekAud.multiply(newR[i])); + } + } + + // Balance equation: dk* + ( + )*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); + + // Transfer amount commitments: v[j]*G + r[j]*H + for (let j = 0; j < n; j++) { + result.push(G.multiply(vChunks[j]).add(H.multiply(rTransfer[j]))); + } + + // r[j]*ek_sid + for (let j = 0; j < n; j++) { + result.push(ekSid.multiply(rTransfer[j])); + } + + // r[j]*ek_rid + for (let j = 0; j < n; j++) { + result.push(ekRid.multiply(rTransfer[j])); + } + + // Auditor: r[j]*ek_aud + if (hasAuditor) { + const ekAud = s.points[getIdxEkAud(ell, n)]; + for (let j = 0; j < n; j++) { + result.push(ekAud.multiply(rTransfer[j])); + } + } + + return result; + }; +} + +/** + * Build the transformation function f for transfer. + * + * f outputs mirror psi but with statement points. + */ +function makeTransferF(ell: number, n: number, hasAuditor: boolean): TransformationFunction { + return (s: SigmaProtocolStatement): RistPoint[] => { + const G = s.points[IDX_G]; + const result: RistPoint[] = []; + + // 0: H + result.push(s.points[IDX_H]); + + // 1..ell: new_P[i] + const startNewP = getStartIdxNewP(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewP + i]); + } + + // ell+1..2ell: new_R[i] + const startNewR = getStartIdxNewR(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewR + i]); + } + + // Auditor: new_R_aud[i] + if (hasAuditor) { + const startNewRAud = getStartIdxNewRAud(ell, n); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewRAud + i]); + } + } + + // Balance equation target: - ()*G + // But v is secret in transfer, so the target is just + // Actually, the balance equation in transfer is: + // dk* + ( + )*G = dk* + *G + *G + // which simplifies to: dk* + ... = (since the secret terms cancel) + // Wait, let's re-derive. The relation is: + // = dk* + *G + *G + // So f's balance output = + 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); + + // Transfer amount: P[j] + const startP = getStartIdxP(ell); + for (let j = 0; j < n; j++) { + result.push(s.points[startP + j]); + } + + // R_sid[j] + const startRSid = getStartIdxRSid(ell, n); + for (let j = 0; j < n; j++) { + result.push(s.points[startRSid + j]); + } + + // R_rid[j] + const startRRid = getStartIdxRRid(ell, n); + for (let j = 0; j < n; j++) { + result.push(s.points[startRRid + j]); + } + + // Auditor: R_aud[j] + if (hasAuditor) { + const startRAud = getStartIdxRAud(ell, n); + for (let j = 0; j < n; j++) { + result.push(s.points[startRAud + j]); + } + } + + return result; + }; +} + +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; + /** 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[]; + /** Optional auditor encryption keys */ + auditorEncryptionKeys?: TwistedEd25519PublicKey[]; + /** Optional new balance D points encrypted under each auditor key */ + newBalanceDAud?: RistPoint[][]; + /** Optional transfer amount D points encrypted under each auditor key */ + transferAmountDAud?: RistPoint[][]; +}; + +/** + * Prove a confidential transfer. + * + * When multiple auditors are present, we produce one sigma proof per auditor + * (each with a single ek_aud), plus one proof for the auditorless base case. + * The Move verifier expects exactly this structure. + * + * However, looking at the Move code more carefully, the verifier handles + * multiple auditors in a single proof by concatenating all auditor points. + * Let me re-examine... + * + * Actually, the Move contract does: for each auditor, it appends [ek_aud, new_R_aud[], R_aud[]] + * and the sigma proof covers ALL auditors in a single proof. So we need to support + * multiple auditors in one proof. + * + * Wait -- let me re-read the spec. The spec says "With auditor" singular. Looking at + * the Move code, it handles a vector of auditors. But the sigma protocol proof structure + * has a single ek_aud. The Move contract likely creates separate proofs or handles + * auditors differently. + * + * For now, we support 0 or 1 auditor in the sigma proof (matching the current Move contract). + * Multiple auditors: the contract creates one proof that covers all auditors by extending + * the statement with [ek_aud_0, ..., ek_aud_k] etc. But for simplicity and to match + * what the transaction builder currently does, we handle the single-auditor case. + */ +export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { + const { + dk, + senderAddress, + recipientAddress, + tokenAddress, + senderEncryptionKey, + recipientEncryptionKey, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + newAmountChunks, + newRandomness, + transferAmountC, + transferAmountDSender, + transferAmountDRecipient, + transferAmountChunks, + transferRandomness, + auditorEncryptionKeys = [], + newBalanceDAud = [], + transferAmountDAud = [], + } = args; + + const ell = oldBalanceC.length; + const n = transferAmountC.length; + // We support at most one auditor in the sigma proof + const hasAuditor = auditorEncryptionKeys.length > 0; + 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 + const stmtPoints: RistPoint[] = [G, H, ekSid, ekRid]; + const stmtCompressed: Uint8Array[] = [G.toRawBytes(), H.toRawBytes(), ekSidBytes, ekRidBytes]; + + // old_P + for (let i = 0; i < ell; i++) { + stmtPoints.push(oldBalanceC[i]); + stmtCompressed.push(oldBalanceC[i].toRawBytes()); + } + // old_R + for (let i = 0; i < ell; i++) { + stmtPoints.push(oldBalanceD[i]); + stmtCompressed.push(oldBalanceD[i].toRawBytes()); + } + // new_P + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceC[i]); + stmtCompressed.push(newBalanceC[i].toRawBytes()); + } + // new_R + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceD[i]); + stmtCompressed.push(newBalanceD[i].toRawBytes()); + } + // P (transfer amount commitments, shared between sender and recipient) + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountC[j]); + stmtCompressed.push(transferAmountC[j].toRawBytes()); + } + // R_sid (transfer amount D for sender) + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountDSender[j]); + stmtCompressed.push(transferAmountDSender[j].toRawBytes()); + } + // R_rid (transfer amount D for recipient) + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountDRecipient[j]); + stmtCompressed.push(transferAmountDRecipient[j].toRawBytes()); + } + + // Auditor points + if (hasAuditor) { + // For each auditor: ek_aud, then new_R_aud[ell], then R_aud[n] + for (let a = 0; a < auditorEncryptionKeys.length; a++) { + const ekAudBytes = auditorEncryptionKeys[a].toUint8Array(); + stmtPoints.push(RistrettoPoint.fromHex(ekAudBytes)); + stmtCompressed.push(ekAudBytes); + + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceDAud[a][i]); + stmtCompressed.push(newBalanceDAud[a][i].toRawBytes()); + } + + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountDAud[a][j]); + stmtCompressed.push(transferAmountDAud[a][j].toRawBytes()); + } + } + } + + // Transfer has no public scalars (v is secret) + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressed, + scalars: [], + }; + + // Build witness: [dk, new_a[ell], new_r[ell], v[n], r[n]] + const witness: bigint[] = [dkBigint, ...newAmountChunks, ...newRandomness, ...transferAmountChunks, ...transferRandomness]; + + // Build domain separator + const sessionId = bcsSerializeTransferSession(senderAddress, recipientAddress, tokenAddress, ell, n); + const dst: DomainSeparator = { + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolProve(dst, makeTransferPsiMultiAuditor(ell, n, auditorEncryptionKeys.length), stmt, witness); +} + +/** + * Build psi for transfer with support for multiple auditors. + * + * Statement layout with k auditors: + * [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], + * ek_aud_0, new_R_aud_0[ell], R_aud_0[n], + * ek_aud_1, new_R_aud_1[ell], R_aud_1[n], ...] + */ +function makeTransferPsiMultiAuditor(ell: number, n: number, numAuditors: 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[] = []; + + // 0: dk * ek_sid + result.push(ekSid.multiply(dk)); + + // 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]))); + } + + // ell+1..2ell: new_r[i]*ek_sid + for (let i = 0; i < ell; i++) { + result.push(ekSid.multiply(newR[i])); + } + + // For each auditor: new_r[i]*ek_aud_a + const auditorBaseIdx = START_IDX_OLD_P + 4 * ell + 3 * n; + for (let a = 0; a < numAuditors; a++) { + const ekAudIdx = auditorBaseIdx + a * (1 + ell + n); + const ekAud = s.points[ekAudIdx]; + for (let i = 0; i < ell; i++) { + result.push(ekAud.multiply(newR[i])); + } + } + + // Balance equation: dk* + ( + )*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); + + // Transfer amount commitments: v[j]*G + r[j]*H + for (let j = 0; j < n; j++) { + result.push(G.multiply(vChunks[j]).add(H.multiply(rTransfer[j]))); + } + + // r[j]*ek_sid + for (let j = 0; j < n; j++) { + result.push(ekSid.multiply(rTransfer[j])); + } + + // r[j]*ek_rid + for (let j = 0; j < n; j++) { + result.push(ekRid.multiply(rTransfer[j])); + } + + // For each auditor: r[j]*ek_aud_a + for (let a = 0; a < numAuditors; a++) { + const ekAudIdx = auditorBaseIdx + a * (1 + ell + n); + const ekAud = s.points[ekAudIdx]; + for (let j = 0; j < n; j++) { + result.push(ekAud.multiply(rTransfer[j])); + } + } + + return result; + }; +} + +/** + * Build f for transfer with multiple auditors. + */ +function makeTransferFMultiAuditor(ell: number, n: number, numAuditors: number): TransformationFunction { + return (s: SigmaProtocolStatement): RistPoint[] => { + const result: RistPoint[] = []; + + // 0: H + result.push(s.points[IDX_H]); + + // 1..ell: new_P[i] + const startNewP = getStartIdxNewP(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewP + i]); + } + + // ell+1..2ell: new_R[i] + const startNewR = getStartIdxNewR(ell); + for (let i = 0; i < ell; i++) { + result.push(s.points[startNewR + i]); + } + + // For each auditor: new_R_aud_a[i] + const auditorBaseIdx = START_IDX_OLD_P + 4 * ell + 3 * n; + for (let a = 0; a < numAuditors; a++) { + const newRAudStart = auditorBaseIdx + a * (1 + ell + n) + 1; + for (let i = 0; i < ell; i++) { + result.push(s.points[newRAudStart + i]); + } + } + + // Balance equation target: + 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); + + // Transfer amount: P[j] + const startP = getStartIdxP(ell); + for (let j = 0; j < n; j++) { + result.push(s.points[startP + j]); + } + + // R_sid[j] + const startRSid = getStartIdxRSid(ell, n); + for (let j = 0; j < n; j++) { + result.push(s.points[startRSid + j]); + } + + // R_rid[j] + const startRRid = getStartIdxRRid(ell, n); + for (let j = 0; j < n; j++) { + result.push(s.points[startRRid + j]); + } + + // For each auditor: R_aud_a[j] + for (let a = 0; a < numAuditors; a++) { + const rAudStart = auditorBaseIdx + a * (1 + ell + n) + 1 + ell; + for (let j = 0; j < n; j++) { + result.push(s.points[rAudStart + j]); + } + } + + return result; + }; +} + +/** + * Verify a confidential transfer proof. + */ +export function verifyTransfer(args: { + senderAddress: Uint8Array; + recipientAddress: Uint8Array; + tokenAddress: Uint8Array; + ekSidBytes: Uint8Array; + ekRidBytes: Uint8Array; + oldBalanceC: RistPoint[]; + oldBalanceD: RistPoint[]; + newBalanceC: RistPoint[]; + newBalanceD: RistPoint[]; + transferAmountC: RistPoint[]; + transferAmountDSender: RistPoint[]; + transferAmountDRecipient: RistPoint[]; + auditorEkBytes?: Uint8Array[]; + newBalanceDAud?: RistPoint[][]; + transferAmountDAud?: RistPoint[][]; + proof: SigmaProtocolProof; +}): boolean { + const { + senderAddress, + recipientAddress, + tokenAddress, + ekSidBytes, + ekRidBytes, + oldBalanceC, + oldBalanceD, + newBalanceC, + newBalanceD, + transferAmountC, + transferAmountDSender, + transferAmountDRecipient, + auditorEkBytes = [], + newBalanceDAud = [], + transferAmountDAud = [], + proof, + } = args; + + const ell = oldBalanceC.length; + const n = transferAmountC.length; + const numAuditors = 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]; + + 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()); + } + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountC[j]); + stmtCompressed.push(transferAmountC[j].toRawBytes()); + } + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountDSender[j]); + stmtCompressed.push(transferAmountDSender[j].toRawBytes()); + } + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountDRecipient[j]); + stmtCompressed.push(transferAmountDRecipient[j].toRawBytes()); + } + + for (let a = 0; a < numAuditors; a++) { + stmtPoints.push(RistrettoPoint.fromHex(auditorEkBytes[a])); + stmtCompressed.push(auditorEkBytes[a]); + + for (let i = 0; i < ell; i++) { + stmtPoints.push(newBalanceDAud[a][i]); + stmtCompressed.push(newBalanceDAud[a][i].toRawBytes()); + } + + for (let j = 0; j < n; j++) { + stmtPoints.push(transferAmountDAud[a][j]); + stmtCompressed.push(transferAmountDAud[a][j].toRawBytes()); + } + } + + const stmt: SigmaProtocolStatement = { + points: stmtPoints, + compressedPoints: stmtCompressed, + scalars: [], + }; + + const sessionId = bcsSerializeTransferSession(senderAddress, recipientAddress, tokenAddress, ell, n); + const dst: DomainSeparator = { + protocolId: utf8ToBytes(PROTOCOL_ID), + sessionId, + }; + + return sigmaProtocolVerify( + dst, + makeTransferPsiMultiAuditor(ell, n, numAuditors), + makeTransferFMultiAuditor(ell, n, numAuditors), + stmt, + proof, + ); +} diff --git a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts new file mode 100644 index 000000000..2df91ebfb --- /dev/null +++ b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts @@ -0,0 +1,483 @@ +// 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 and the protocol ID differs. + * + * 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, + 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"; +const PROTOCOL_ID_NORMALIZATION = "AptosConfidentialAsset/NormalizationV1"; + +/** + * BCS-serialize a WithdrawSession matching the Move struct: + * ```move + * struct WithdrawSession { sender: address, asset_type: Object, num_chunks: u64 } + * ``` + */ +export function bcsSerializeWithdrawSession( + senderAddress: Uint8Array, + tokenTypeAddress: Uint8Array, + numChunks: number, +): Uint8Array { + const serializer = new Serializer(); + serializer.serialize(new FixedBytes(senderAddress)); + serializer.serialize(new FixedBytes(tokenTypeAddress)); + serializer.serialize(new U64(numChunks)); + 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; + /** 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, + 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); + const dst: DomainSeparator = { + protocolId: utf8ToBytes(protocolId), + sessionId, + }; + + return sigmaProtocolProve(dst, 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). + */ +export function proveNormalization(args: WithdrawProofArgs): SigmaProtocolProof { + return proveWithdrawInternal(PROTOCOL_ID_NORMALIZATION, args); +} + +/** + * Verify a confidential withdrawal proof. + */ +export function verifyWithdrawal(args: { + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + 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. + */ +export function verifyNormalization(args: { + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + amount: bigint; + ekBytes: Uint8Array; + oldBalanceC: RistPoint[]; + oldBalanceD: RistPoint[]; + newBalanceC: RistPoint[]; + newBalanceD: RistPoint[]; + auditorEkBytes?: Uint8Array; + newBalanceDAud?: RistPoint[]; + proof: SigmaProtocolProof; +}): boolean { + return verifyWithdrawInternal(PROTOCOL_ID_NORMALIZATION, args); +} + +function verifyWithdrawInternal( + protocolId: string, + args: { + senderAddress: Uint8Array; + tokenAddress: Uint8Array; + amount: bigint; + ekBytes: Uint8Array; + oldBalanceC: RistPoint[]; + oldBalanceD: RistPoint[]; + newBalanceC: RistPoint[]; + newBalanceD: RistPoint[]; + auditorEkBytes?: Uint8Array; + newBalanceDAud?: RistPoint[]; + proof: SigmaProtocolProof; + }, +): boolean { + const { + senderAddress, + tokenAddress, + 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); + const dst: DomainSeparator = { + protocolId: utf8ToBytes(protocolId), + sessionId, + }; + + return sigmaProtocolVerify( + dst, + makeWithdrawPsi(ell, hasAuditor), + makeWithdrawF(ell, hasAuditor, amount), + stmt, + proof, + ); +} diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 1f81236dc..892951fd4 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -1,4 +1,4 @@ -// Copyright © Aptos Foundation +// Copyright (c) Aptos Foundation // SPDX-License-Identifier: Apache-2.0 import { @@ -20,6 +20,7 @@ import { TwistedEd25519PublicKey, TwistedEd25519PrivateKey, } from "../crypto"; +import { proveRegistration } from "../crypto/sigmaProtocolRegistration"; import { DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS, MODULE_NAME } from "../consts"; import { getBalance, getEncryptionKey, isBalanceNormalized, isIncomingTransfersPaused } from "./viewFunctions"; @@ -53,12 +54,29 @@ 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); + + // Generate the registration sigma proof + const sigmaProof = proveRegistration({ + dk: decryptionKey, + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + }); + return this.client.transaction.build.simple({ ...args, data: { function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::register_raw`, - functionArguments: [tokenAddress, decryptionKey.publicKey().toUint8Array(), [] as Uint8Array[], [] as Uint8Array[]], + functionArguments: [ + tokenAddress, + decryptionKey.publicKey().toUint8Array(), + sigmaProof.commitment, + sigmaProof.response, + ], }, }); } @@ -123,6 +141,13 @@ 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 the auditor public key for the token + const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + // Get the sender's available balance from the chain const { available: senderEncryptedAvailableBalance } = await getBalance({ client: this.client, @@ -136,9 +161,18 @@ export class ConfidentialAssetTransactionBuilder { decryptionKey: senderDecryptionKey, senderAvailableBalanceCipherText: senderEncryptedAvailableBalance.getCipherText(), amount: BigInt(amount), + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + auditorEncryptionKey: globalAuditorPubKey, }); - const [{ 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, @@ -148,10 +182,12 @@ export class ConfidentialAssetTransactionBuilder { 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, - [] as Uint8Array[], // sigma_proto_comm (stub) - [] as Uint8Array[], // sigma_proto_resp (stub) + sigmaProof.commitment, + sigmaProof.response, ], }, options, @@ -218,7 +254,7 @@ export class ConfidentialAssetTransactionBuilder { const [{ vec: globalAuditorPubKey }] = await this.client.view<[{ vec: { data: string }[] }]>({ options: args.options, payload: { - function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_auditor_for_asset_type`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_effective_auditor`, functionArguments: [args.tokenAddress], }, }); @@ -256,9 +292,14 @@ 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 the auditor public key for the token const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress, @@ -307,15 +348,20 @@ export class ConfidentialAssetTransactionBuilder { amount, recipientEncryptionKey, auditorEncryptionKeys: allAuditorEncryptionKeys, + senderAddress: senderAddr.toUint8Array(), + recipientAddress: recipientAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), }); const [ { + sigmaProof, rangeProof: { rangeProofAmount, rangeProofNewBalance }, }, encryptedAmountAfterTransfer, encryptedAmountByRecipient, auditorsCBList, + auditorNewBalanceList, ] = await confidentialTransfer.authorizeTransfer(); // Only send extra auditor EKs on-chain (not the global auditor, which the contract fetches itself) @@ -325,6 +371,11 @@ export class ConfidentialAssetTransactionBuilder { const recipientDPoints = encryptedAmountByRecipient.getCipherText().map((ct) => ct.D.toRawBytes()); const auditorDPoints = auditorsCBList.map((cb) => cb.getCipherText().map((ct) => ct.D.toRawBytes())); + // Build A components for new balance (D points encrypted under the effective auditor key, i.e., the last one) + const newBalanceA = auditorNewBalanceList.length > 0 + ? auditorNewBalanceList[auditorNewBalanceList.length - 1].getCipherText().map((ct) => ct.D.toRawBytes()) + : []; + return this.client.transaction.build.simple({ ...args, withFeePayer: args.withFeePayer, @@ -333,15 +384,18 @@ export class ConfidentialAssetTransactionBuilder { functionArguments: [ tokenAddress, recipient, - encryptedAmountAfterTransfer.getCipherTextBytes(), - confidentialTransfer.transferAmountEncryptedBySender.getCipherTextBytes(), + 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, extraAuditorEncryptionKeys, auditorDPoints, rangeProofNewBalance, rangeProofAmount, - [] as Uint8Array[], // sigma_proto_comm (stub) - [] as Uint8Array[], // sigma_proto_resp (stub) + sigmaProof.commitment, + sigmaProof.response, ], }, }); @@ -450,6 +504,14 @@ 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 the auditor public key for the token + const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + const { available } = await getBalance({ client: this.client, moduleAddress: this.confidentialAssetModuleAddress, @@ -461,6 +523,9 @@ export class ConfidentialAssetTransactionBuilder { const confidentialNormalization = await ConfidentialNormalization.create({ decryptionKey: senderDecryptionKey, unnormalizedAvailableBalance: available, + senderAddress: senderAddr.toUint8Array(), + tokenAddress: tokenAddr.toUint8Array(), + auditorEncryptionKey: globalAuditorPubKey, }); return confidentialNormalization.createTransaction({ diff --git a/confidential-assets/tests/units/confidentialProofs.test.ts b/confidential-assets/tests/units/confidentialProofs.test.ts index 4bef072c6..8e33b9606 100644 --- a/confidential-assets/tests/units/confidentialProofs.test.ts +++ b/confidential-assets/tests/units/confidentialProofs.test.ts @@ -4,15 +4,16 @@ import { ChunkedAmount, ConfidentialKeyRotation, 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", () => { @@ -28,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 () => { @@ -38,24 +44,32 @@ describe("Generate 'confidential coin' proofs", () => { decryptionKey: aliceConfidentialDecryptionKey, senderAvailableBalanceCipherText: aliceEncryptedBalanceCipherText, amount: WITHDRAW_AMOUNT, + senderAddress: dummySenderAddress, + tokenAddress: dummyTokenAddress, }); - 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, + 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(); @@ -87,7 +101,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 () => { @@ -96,11 +110,16 @@ describe("Generate 'confidential coin' proofs", () => { senderAvailableBalanceCipherText: aliceEncryptedBalanceCipherText, amount: TRANSFER_AMOUNT, recipientEncryptionKey: bobConfidentialDecryptionKey.publicKey(), + senderAddress: dummySenderAddress, + recipientAddress: dummyRecipientAddress, + tokenAddress: dummyTokenAddress, }); - confidentialTransferSigmaProof = await confidentialTransfer.genSigmaProof(); + confidentialTransferSigmaProof = confidentialTransfer.genSigmaProof(); expect(confidentialTransferSigmaProof).toBeDefined(); + expect(confidentialTransferSigmaProof.commitment.length).toBeGreaterThan(0); + expect(confidentialTransferSigmaProof.response.length).toBeGreaterThan(0); }, longTestTimeout, ); @@ -108,14 +127,20 @@ 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, + 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), + proof: confidentialTransferSigmaProof, }); expect(isValid).toBeTruthy(); @@ -148,7 +173,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 () => { @@ -158,32 +183,43 @@ describe("Generate 'confidential coin' proofs", () => { amount: TRANSFER_AMOUNT, recipientEncryptionKey: bobConfidentialDecryptionKey.publicKey(), auditorEncryptionKeys: [auditor.publicKey()], + senderAddress: dummySenderAddress, + recipientAddress: dummyRecipientAddress, + tokenAddress: dummyTokenAddress, }); - 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(), - ), - }, + const isValid = verifyTransfer({ + senderAddress: dummySenderAddress, + recipientAddress: dummyRecipientAddress, + tokenAddress: dummyTokenAddress, + 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), + 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(); @@ -217,9 +253,6 @@ describe("Generate 'confidential coin' proofs", () => { ); const newAliceConfidentialPrivateKey = TwistedEd25519PrivateKey.generate(); - // Use dummy addresses for the unit test (32 bytes each) - const dummySenderAddress = new Uint8Array(32); - const dummyTokenAddress = new Uint8Array(32).fill(0x0a); // 0xa = APT metadata address let keyRotationProofResult: ReturnType; @@ -284,29 +317,38 @@ 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, }); - 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, + 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(); From f99f8f062dd6dff3aadf092dea29f6d4a37e397e Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Mon, 2 Mar 2026 12:17:30 -0600 Subject: [PATCH 10/22] do not start localnet automatically for confidential assets (TODO: for now; remove later) --- confidential-assets/vitest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confidential-assets/vitest.config.ts b/confidential-assets/vitest.config.ts index 5542381aa..8e820aab8 100644 --- a/confidential-assets/vitest.config.ts +++ b/confidential-assets/vitest.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ globals: true, environment: "node", setupFiles: [path.resolve(__dirname, "../tests/setupDotenv.ts")], - globalSetup: [path.resolve(__dirname, "../tests/preTest.ts")], + //globalSetup: [path.resolve(__dirname, "../tests/preTest.ts")], include: ["tests/**/*.test.ts"], exclude: ["tests/units/api/**"], coverage: { From 7bb8f6be4d3da95387c8d45d52ed49ec7d91b37d Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Mon, 2 Mar 2026 13:47:15 -0600 Subject: [PATCH 11/22] add functions to fetch the auditor epoch --- .../src/api/confidentialAsset.ts | 54 +++++++++++++ .../src/internal/viewFunctions.ts | 77 +++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index fc3ef8303..b0ed6c9cb 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -22,6 +22,9 @@ import { hasUserRegistered, isBalanceNormalized, isIncomingTransfersPaused, + getGlobalAuditorEpoch, + getAuditorEpochForAssetType, + getEffectiveAuditorEpoch, } from "../internal"; // Constants @@ -478,6 +481,57 @@ export class ConfidentialAsset { }); } + /** + * Get the global auditor epoch counter. + * + * @param args.options - Optional ledger version for the view call + * @returns The global auditor epoch + */ + async getGlobalAuditorEpoch(args?: { options?: LedgerVersionArg }): Promise { + return getGlobalAuditorEpoch({ + client: this.client(), + moduleAddress: this.moduleAddress(), + ...args, + }); + } + + /** + * Get the auditor epoch counter for a specific asset type. + * + * @param args.tokenAddress - The token address of the asset + * @param args.options - Optional ledger version for the view call + * @returns The asset-specific auditor epoch + */ + async getAuditorEpochForAssetType(args: { + tokenAddress: AccountAddressInput; + options?: LedgerVersionArg; + }): Promise { + return getAuditorEpochForAssetType({ + client: this.client(), + moduleAddress: this.moduleAddress(), + ...args, + }); + } + + /** + * Get the effective auditor epoch: asset-specific epoch if the asset has an auditor, + * otherwise global auditor epoch. + * + * @param args.tokenAddress - The token address of the asset + * @param args.options - Optional ledger version for the view call + * @returns The effective auditor epoch + */ + async getEffectiveAuditorEpoch(args: { + tokenAddress: AccountAddressInput; + options?: LedgerVersionArg; + }): Promise { + return getEffectiveAuditorEpoch({ + client: this.client(), + moduleAddress: this.moduleAddress(), + ...args, + }); + } + /** * Normalize a user's balance. * diff --git a/confidential-assets/src/internal/viewFunctions.ts b/confidential-assets/src/internal/viewFunctions.ts index c878b2381..ef49481cb 100644 --- a/confidential-assets/src/internal/viewFunctions.ts +++ b/confidential-assets/src/internal/viewFunctions.ts @@ -283,3 +283,80 @@ export async function getEncryptionKey( throw error; } } + +type AuditorEpochViewFunctionParams = { + client: Aptos; + tokenAddress: AccountAddressInput; + options?: LedgerVersionArg; + moduleAddress?: string; +}; + +/** + * Get the global auditor epoch counter. + * + * @param args.client - The Aptos client instance + * @param args.options - Optional ledger version for the view call + * @param args.moduleAddress - Optional module address + * @returns The global auditor epoch as a number + */ +export async function getGlobalAuditorEpoch(args: { + client: Aptos; + options?: LedgerVersionArg; + moduleAddress?: string; +}): Promise { + const { client, options, moduleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS } = args; + const [epoch] = await client.view<[string]>({ + payload: { + function: `${moduleAddress}::${MODULE_NAME}::get_global_auditor_epoch`, + typeArguments: [], + functionArguments: [], + }, + options, + }); + return Number(epoch); +} + +/** + * Get the auditor epoch counter for a specific asset type. + * + * @param args.client - The Aptos client instance + * @param args.tokenAddress - The token address of the asset + * @param args.options - Optional ledger version for the view call + * @param args.moduleAddress - Optional module address + * @returns The asset-specific auditor epoch as a number + */ +export async function getAuditorEpochForAssetType(args: AuditorEpochViewFunctionParams): Promise { + const { client, tokenAddress, options, moduleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS } = args; + const [epoch] = await client.view<[string]>({ + payload: { + function: `${moduleAddress}::${MODULE_NAME}::get_auditor_epoch_for_asset_type`, + typeArguments: [], + functionArguments: [tokenAddress], + }, + options, + }); + return Number(epoch); +} + +/** + * Get the effective auditor epoch: asset-specific epoch if the asset has an auditor, + * otherwise global auditor epoch. + * + * @param args.client - The Aptos client instance + * @param args.tokenAddress - The token address of the asset + * @param args.options - Optional ledger version for the view call + * @param args.moduleAddress - Optional module address + * @returns The effective auditor epoch as a number + */ +export async function getEffectiveAuditorEpoch(args: AuditorEpochViewFunctionParams): Promise { + const { client, tokenAddress, options, moduleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS } = args; + const [epoch] = await client.view<[string]>({ + payload: { + function: `${moduleAddress}::${MODULE_NAME}::get_effective_auditor_epoch`, + typeArguments: [], + functionArguments: [tokenAddress], + }, + options, + }); + return Number(epoch); +} From 76de85cc8d56483d51b36d3b3078407cf2df1d4e Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Tue, 3 Mar 2026 13:07:39 -0600 Subject: [PATCH 12/22] fix transfer e2e tests --- .../src/api/confidentialAsset.ts | 4 +- .../src/crypto/confidentialTransfer.ts | 14 + .../src/crypto/sigmaProtocolTransfer.ts | 547 ++++++------------ .../internal/confidentialAssetTxnBuilder.ts | 19 +- .../src/internal/viewFunctions.ts | 12 +- .../tests/e2e/confidentialAsset.test.ts | 18 +- .../tests/units/confidentialProofs.test.ts | 3 + 7 files changed, 224 insertions(+), 393 deletions(-) diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index b0ed6c9cb..007c7aa1c 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -559,14 +559,14 @@ export class ConfidentialAsset { const tokenAddr = AccountAddress.from(tokenAddress); // Get the auditor public key for the token - const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); const confidentialNormalization = await ConfidentialNormalization.create({ decryptionKey: senderDecryptionKey, unnormalizedAvailableBalance: available, senderAddress: senderAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), - auditorEncryptionKey: globalAuditorPubKey, + auditorEncryptionKey: effectiveAuditorPubKey, }); const transaction = await confidentialNormalization.createTransaction({ diff --git a/confidential-assets/src/crypto/confidentialTransfer.ts b/confidential-assets/src/crypto/confidentialTransfer.ts index 600292db1..090b0b3eb 100644 --- a/confidential-assets/src/crypto/confidentialTransfer.ts +++ b/confidential-assets/src/crypto/confidentialTransfer.ts @@ -25,6 +25,12 @@ 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 */ @@ -40,6 +46,8 @@ export class ConfidentialTransfer { recipientEncryptionKey: TwistedEd25519PublicKey; + hasEffectiveAuditor: boolean; + auditorEncryptionKeys: TwistedEd25519PublicKey[]; transferAmountEncryptedByAuditors: EncryptedAmount[]; @@ -91,6 +99,7 @@ export class ConfidentialTransfer { senderDecryptionKey: TwistedEd25519PrivateKey; recipientEncryptionKey: TwistedEd25519PublicKey; amount: bigint; + hasEffectiveAuditor: boolean; auditorEncryptionKeys: TwistedEd25519PublicKey[]; senderEncryptedAvailableBalance: EncryptedAmount; transferAmountEncryptedBySender: EncryptedAmount; @@ -105,6 +114,7 @@ export class ConfidentialTransfer { const { senderDecryptionKey, recipientEncryptionKey, + hasEffectiveAuditor, auditorEncryptionKeys, senderEncryptedAvailableBalance, amount, @@ -119,6 +129,7 @@ export class ConfidentialTransfer { } = args; this.senderDecryptionKey = senderDecryptionKey; this.recipientEncryptionKey = recipientEncryptionKey; + this.hasEffectiveAuditor = hasEffectiveAuditor; this.auditorEncryptionKeys = auditorEncryptionKeys; this.senderEncryptedAvailableBalance = senderEncryptedAvailableBalance; if (amount < 0n) { @@ -158,6 +169,7 @@ export class ConfidentialTransfer { senderAvailableBalanceCipherText, senderDecryptionKey, recipientEncryptionKey, + hasEffectiveAuditor = false, auditorEncryptionKeys = [], transferAmountRandomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), senderAddress, @@ -213,6 +225,7 @@ export class ConfidentialTransfer { return new ConfidentialTransfer({ senderDecryptionKey, recipientEncryptionKey, + hasEffectiveAuditor, auditorEncryptionKeys, senderEncryptedAvailableBalance, amount, @@ -271,6 +284,7 @@ export class ConfidentialTransfer { transferAmountDRecipient, transferAmountChunks: this.transferAmountEncryptedBySender.getAmountChunks(), transferRandomness: this.transferAmountRandomness.slice(0, TRANSFER_AMOUNT_CHUNK_COUNT), + hasEffectiveAuditor: this.hasEffectiveAuditor, auditorEncryptionKeys, newBalanceDAud, transferAmountDAud, diff --git a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts index 4fa1cd805..d858b53b5 100644 --- a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts +++ b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts @@ -9,14 +9,10 @@ * 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 (auditorless): - * [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]] - * - * With auditor: - * append [ek_aud, new_R_aud[ell], R_aud[n]] + * 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 extra auditor: + [ek_extra, R_extra[n]] * * Witness: [dk, new_a[ell], new_r[ell], v[n], r[n]] */ @@ -49,6 +45,8 @@ const PROTOCOL_ID = "AptosConfidentialAsset/TransferV1"; * asset_type: Object, * num_avail_chunks: u64, * num_transfer_chunks: u64, + * has_effective_auditor: bool, + * num_extra_auditors: u64, * } * ``` */ @@ -58,6 +56,8 @@ export function bcsSerializeTransferSession( tokenTypeAddress: Uint8Array, numAvailChunks: number, numTransferChunks: number, + hasEffectiveAuditor: boolean, + numExtraAuditors: number, ): Uint8Array { const serializer = new Serializer(); serializer.serialize(new FixedBytes(senderAddress)); @@ -65,6 +65,8 @@ export function bcsSerializeTransferSession( serializer.serialize(new FixedBytes(tokenTypeAddress)); serializer.serialize(new U64(numAvailChunks)); serializer.serialize(new U64(numTransferChunks)); + serializer.serializeBool(hasEffectiveAuditor); + serializer.serialize(new U64(numExtraAuditors)); return serializer.toUint8Array(); } @@ -81,23 +83,19 @@ function computeBPowers(count: number): bigint[] { } /** - * Statement point layout (auditorless): - * 0: G - * 1: H - * 2: ek_sid - * 3: ek_rid - * 4 .. 4+ell-1: old_P[ell] - * 4+ell .. 4+2ell-1: old_R[ell] - * 4+2ell .. 4+3ell-1: new_P[ell] - * 4+3ell .. 4+4ell-1: new_R[ell] - * 4+4ell .. 4+4ell+n-1: P[n] (transfer amount commitments) - * 4+4ell+n .. 4+4ell+2n-1: R_sid[n] (transfer amount D for sender) - * 4+4ell+2n .. 4+4ell+3n-1: R_rid[n] (transfer amount D for recipient) + * 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 * - * With auditor, append: - * 4+4ell+3n: ek_aud - * 4+4ell+3n+1 .. 4+5ell+3n: new_R_aud[ell] - * 4+5ell+3n+1 .. 4+5ell+4n: R_aud[n] + * For each extra auditor i ∈ [num_extra]: + * + [ek_extra_i, R_extra_i[n]] + * → +(1 + n) points per extra */ const IDX_G = 0; const IDX_H = 1; @@ -123,190 +121,17 @@ function getStartIdxRSid(ell: number, n: number): number { function getStartIdxRRid(ell: number, n: number): number { return START_IDX_OLD_P + 4 * ell + 2 * n; } -function getIdxEkAud(ell: number, n: number): number { +/** Start of the effective auditor section (if present). */ +function getIdxEkAudEff(ell: number, n: number): number { return START_IDX_OLD_P + 4 * ell + 3 * n; } -function getStartIdxNewRAud(ell: number, n: number): number { - return START_IDX_OLD_P + 4 * ell + 3 * n + 1; -} -function getStartIdxRAud(ell: number, n: number): number { - return START_IDX_OLD_P + 5 * ell + 3 * n + 1; -} - -/** - * Build the homomorphism psi for transfer. - * - * Witness layout: [dk, new_a[0..ell-1], new_r[0..ell-1], v[0..n-1], r[0..n-1]] - * - * psi outputs (auditorless, m = 2 + 2ell + 3n): - * 0: dk * ek_sid - * 1..ell: new_a[i]*G + new_r[i]*H - * ell+1..2ell: new_r[i]*ek_sid - * 2ell+1: dk* + ( + )*G (balance equation) - * 2ell+2..2ell+1+n: v[j]*G + r[j]*H - * 2ell+2+n..2ell+1+2n: r[j]*ek_sid - * 2ell+2+2n..2ell+1+3n: r[j]*ek_rid - * - * With auditor (m = 2 + 3ell + 4n): - * After new_r[i]*ek_sid, insert new_r[i]*ek_aud - * After r[j]*ek_rid, insert r[j]*ek_aud - */ -function makeTransferPsi(ell: number, n: 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 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[] = []; - - // 0: dk * ek_sid - result.push(ekSid.multiply(dk)); - - // 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]))); - } - - // ell+1..2ell: new_r[i]*ek_sid - for (let i = 0; i < ell; i++) { - result.push(ekSid.multiply(newR[i])); - } - - // Auditor: new_r[i]*ek_aud - if (hasAuditor) { - const ekAud = s.points[getIdxEkAud(ell, n)]; - for (let i = 0; i < ell; i++) { - result.push(ekAud.multiply(newR[i])); - } - } - - // Balance equation: dk* + ( + )*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); - - // Transfer amount commitments: v[j]*G + r[j]*H - for (let j = 0; j < n; j++) { - result.push(G.multiply(vChunks[j]).add(H.multiply(rTransfer[j]))); - } - - // r[j]*ek_sid - for (let j = 0; j < n; j++) { - result.push(ekSid.multiply(rTransfer[j])); - } - - // r[j]*ek_rid - for (let j = 0; j < n; j++) { - result.push(ekRid.multiply(rTransfer[j])); - } - - // Auditor: r[j]*ek_aud - if (hasAuditor) { - const ekAud = s.points[getIdxEkAud(ell, n)]; - for (let j = 0; j < n; j++) { - result.push(ekAud.multiply(rTransfer[j])); - } - } - - return result; - }; +/** Start of the extra auditors section. */ +function getStartIdxExtras(ell: number, n: number, hasEffective: boolean): number { + return START_IDX_OLD_P + 4 * ell + 3 * n + (hasEffective ? 1 + ell + n : 0); } -/** - * Build the transformation function f for transfer. - * - * f outputs mirror psi but with statement points. - */ -function makeTransferF(ell: number, n: number, hasAuditor: boolean): TransformationFunction { - return (s: SigmaProtocolStatement): RistPoint[] => { - const G = s.points[IDX_G]; - const result: RistPoint[] = []; - - // 0: H - result.push(s.points[IDX_H]); - - // 1..ell: new_P[i] - const startNewP = getStartIdxNewP(ell); - for (let i = 0; i < ell; i++) { - result.push(s.points[startNewP + i]); - } - - // ell+1..2ell: new_R[i] - const startNewR = getStartIdxNewR(ell); - for (let i = 0; i < ell; i++) { - result.push(s.points[startNewR + i]); - } - - // Auditor: new_R_aud[i] - if (hasAuditor) { - const startNewRAud = getStartIdxNewRAud(ell, n); - for (let i = 0; i < ell; i++) { - result.push(s.points[startNewRAud + i]); - } - } - - // Balance equation target: - ()*G - // But v is secret in transfer, so the target is just - // Actually, the balance equation in transfer is: - // dk* + ( + )*G = dk* + *G + *G - // which simplifies to: dk* + ... = (since the secret terms cancel) - // Wait, let's re-derive. The relation is: - // = dk* + *G + *G - // So f's balance output = - 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); - - // Transfer amount: P[j] - const startP = getStartIdxP(ell); - for (let j = 0; j < n; j++) { - result.push(s.points[startP + j]); - } - - // R_sid[j] - const startRSid = getStartIdxRSid(ell, n); - for (let j = 0; j < n; j++) { - result.push(s.points[startRSid + j]); - } - - // R_rid[j] - const startRRid = getStartIdxRRid(ell, n); - for (let j = 0; j < n; j++) { - result.push(s.points[startRRid + j]); - } - - // Auditor: R_aud[j] - if (hasAuditor) { - const startRAud = getStartIdxRAud(ell, n); - for (let j = 0; j < n; j++) { - result.push(s.points[startRAud + j]); - } - } - - return result; - }; -} +// 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 */ @@ -343,38 +168,30 @@ export type TransferProofArgs = { transferAmountChunks: bigint[]; /** Transfer amount randomness (n values) */ transferRandomness: bigint[]; - /** Optional auditor encryption keys */ + /** + * 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 extra auditors. + */ + hasEffectiveAuditor: boolean; + /** Auditor encryption keys: extras first, then effective (if hasEffectiveAuditor) */ auditorEncryptionKeys?: TwistedEd25519PublicKey[]; - /** Optional new balance D points encrypted under each auditor key */ + /** New balance D points encrypted under each auditor key (only effective auditor's is used in sigma proof) */ newBalanceDAud?: RistPoint[][]; - /** Optional transfer amount D points encrypted under each auditor key */ + /** Transfer amount D points encrypted under each auditor key (all used in sigma proof) */ transferAmountDAud?: RistPoint[][]; }; /** * Prove a confidential transfer. * - * When multiple auditors are present, we produce one sigma proof per auditor - * (each with a single ek_aud), plus one proof for the auditorless base case. - * The Move verifier expects exactly this structure. - * - * However, looking at the Move code more carefully, the verifier handles - * multiple auditors in a single proof by concatenating all auditor points. - * Let me re-examine... - * - * Actually, the Move contract does: for each auditor, it appends [ek_aud, new_R_aud[], R_aud[]] - * and the sigma proof covers ALL auditors in a single proof. So we need to support - * multiple auditors in one proof. + * The sigma proof covers all auditors in a single proof. The statement layout + * distinguishes between the effective auditor (sees balance + transfer amount) + * and extra auditors (see only transfer amount). * - * Wait -- let me re-read the spec. The spec says "With auditor" singular. Looking at - * the Move code, it handles a vector of auditors. But the sigma protocol proof structure - * has a single ek_aud. The Move contract likely creates separate proofs or handles - * auditors differently. - * - * For now, we support 0 or 1 auditor in the sigma proof (matching the current Move contract). - * Multiple auditors: the contract creates one proof that covers all auditors by extending - * the statement with [ek_aud_0, ..., ek_aud_k] etc. But for simplicity and to match - * what the transaction builder currently does, we handle the single-auditor case. + * Convention: if hasEffectiveAuditor is true, the LAST element in auditorEncryptionKeys + * (and newBalanceDAud / transferAmountDAud) is the effective auditor. All preceding + * elements are extras. */ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { const { @@ -395,6 +212,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { transferAmountDRecipient, transferAmountChunks, transferRandomness, + hasEffectiveAuditor, auditorEncryptionKeys = [], newBalanceDAud = [], transferAmountDAud = [], @@ -402,8 +220,9 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { const ell = oldBalanceC.length; const n = transferAmountC.length; - // We support at most one auditor in the sigma proof - const hasAuditor = auditorEncryptionKeys.length > 0; + const numExtra = hasEffectiveAuditor + ? auditorEncryptionKeys.length - 1 + : auditorEncryptionKeys.length; const dkBigint = bytesToNumberLE(dk.toUint8Array()); const G = RistrettoPoint.BASE; @@ -413,95 +232,74 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { const ekRidBytes = recipientEncryptionKey.toUint8Array(); const ekRid = RistrettoPoint.fromHex(ekRidBytes); - // Build statement points + // Build statement points — base const stmtPoints: RistPoint[] = [G, H, ekSid, ekRid]; const stmtCompressed: Uint8Array[] = [G.toRawBytes(), H.toRawBytes(), ekSidBytes, ekRidBytes]; - // old_P - for (let i = 0; i < ell; i++) { - stmtPoints.push(oldBalanceC[i]); - stmtCompressed.push(oldBalanceC[i].toRawBytes()); - } - // old_R - for (let i = 0; i < ell; i++) { - stmtPoints.push(oldBalanceD[i]); - stmtCompressed.push(oldBalanceD[i].toRawBytes()); - } - // new_P - for (let i = 0; i < ell; i++) { - stmtPoints.push(newBalanceC[i]); - stmtCompressed.push(newBalanceC[i].toRawBytes()); + 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]); } - // new_R - for (let i = 0; i < ell; i++) { - stmtPoints.push(newBalanceD[i]); - stmtCompressed.push(newBalanceD[i].toRawBytes()); - } - // P (transfer amount commitments, shared between sender and recipient) - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountC[j]); - stmtCompressed.push(transferAmountC[j].toRawBytes()); - } - // R_sid (transfer amount D for sender) - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountDSender[j]); - stmtCompressed.push(transferAmountDSender[j].toRawBytes()); - } - // R_rid (transfer amount D for recipient) - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountDRecipient[j]); - stmtCompressed.push(transferAmountDRecipient[j].toRawBytes()); - } - - // Auditor points - if (hasAuditor) { - // For each auditor: ek_aud, then new_R_aud[ell], then R_aud[n] - for (let a = 0; a < auditorEncryptionKeys.length; a++) { - const ekAudBytes = auditorEncryptionKeys[a].toUint8Array(); - stmtPoints.push(RistrettoPoint.fromHex(ekAudBytes)); - stmtCompressed.push(ekAudBytes); - for (let i = 0; i < ell; i++) { - stmtPoints.push(newBalanceDAud[a][i]); - stmtCompressed.push(newBalanceDAud[a][i].toRawBytes()); - } - - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountDAud[a][j]); - stmtCompressed.push(transferAmountDAud[a][j].toRawBytes()); - } - } + // Extra auditors: for each, [ek_extra, R_extra[n]] + for (let a = 0; a < numExtra; a++) { + const ekExtraBytes = auditorEncryptionKeys[a].toUint8Array(); + pushPointBytes(RistrettoPoint.fromHex(ekExtraBytes), ekExtraBytes); + for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[a][j]); } - // Transfer has no public scalars (v is secret) const stmt: SigmaProtocolStatement = { points: stmtPoints, compressedPoints: stmtCompressed, scalars: [], }; - // Build witness: [dk, new_a[ell], new_r[ell], v[n], r[n]] + // Witness: [dk, new_a[ell], new_r[ell], v[n], r[n]] const witness: bigint[] = [dkBigint, ...newAmountChunks, ...newRandomness, ...transferAmountChunks, ...transferRandomness]; - // Build domain separator - const sessionId = bcsSerializeTransferSession(senderAddress, recipientAddress, tokenAddress, ell, n); + // Domain separator + const sessionId = bcsSerializeTransferSession( + senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numExtra, + ); const dst: DomainSeparator = { protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; - return sigmaProtocolProve(dst, makeTransferPsiMultiAuditor(ell, n, auditorEncryptionKeys.length), stmt, witness); + return sigmaProtocolProve(dst, makeTransferPsi(ell, n, hasEffectiveAuditor, numExtra), stmt, witness); } /** - * Build psi for transfer with support for multiple auditors. + * Build the homomorphism psi for the transfer relation. * - * Statement layout with k auditors: - * [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], - * ek_aud_0, new_R_aud_0[ell], R_aud_0[n], - * ek_aud_1, new_R_aud_1[ell], R_aud_1[n], ...] + * 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_extra_t, ∀j ∈ [n], ∀t ∈ [T] (extra auditors) */ -function makeTransferPsiMultiAuditor(ell: number, n: number, numAuditors: number): PsiFunction { +function makeTransferPsi(ell: number, n: number, hasEffective: boolean, numExtra: number): PsiFunction { return (s: SigmaProtocolStatement, w: bigint[]): RistPoint[] => { const dk = w[0]; const newA = w.slice(1, 1 + ell); @@ -516,30 +314,28 @@ function makeTransferPsiMultiAuditor(ell: number, n: number, numAuditors: number const result: RistPoint[] = []; - // 0: dk * ek_sid + // 1. dk * ek_sid result.push(ekSid.multiply(dk)); - // 1..ell: new_a[i]*G + new_r[i]*H + // 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]))); } - // ell+1..2ell: new_r[i]*ek_sid + // 3. new_r[i]*ek_sid for (let i = 0; i < ell; i++) { result.push(ekSid.multiply(newR[i])); } - // For each auditor: new_r[i]*ek_aud_a - const auditorBaseIdx = START_IDX_OLD_P + 4 * ell + 3 * n; - for (let a = 0; a < numAuditors; a++) { - const ekAudIdx = auditorBaseIdx + a * (1 + ell + n); - const ekAud = s.points[ekAudIdx]; + // 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(ekAud.multiply(newR[i])); + result.push(ekAudEff.multiply(newR[i])); } } - // Balance equation: dk* + ( + )*G + // 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; @@ -555,27 +351,36 @@ function makeTransferPsiMultiAuditor(ell: number, n: number, numAuditors: number } result.push(balanceResult); - // Transfer amount commitments: v[j]*G + r[j]*H + // 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]))); } - // r[j]*ek_sid + // 6. r[j]*ek_sid for (let j = 0; j < n; j++) { result.push(ekSid.multiply(rTransfer[j])); } - // r[j]*ek_rid + // 7. r[j]*ek_rid for (let j = 0; j < n; j++) { result.push(ekRid.multiply(rTransfer[j])); } - // For each auditor: r[j]*ek_aud_a - for (let a = 0; a < numAuditors; a++) { - const ekAudIdx = auditorBaseIdx + a * (1 + ell + n); - const ekAud = s.points[ekAudIdx]; + // 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(ekAud.multiply(rTransfer[j])); + result.push(ekAudEff.multiply(rTransfer[j])); + } + } + + // 7c. (extra auditors) r[j]*ek_extra_t + const extrasStart = getStartIdxExtras(ell, n, hasEffective); + for (let t = 0; t < numExtra; t++) { + const ekExtraIdx = extrasStart + t * (1 + n); + const ekExtra = s.points[ekExtraIdx]; + for (let j = 0; j < n; j++) { + result.push(ekExtra.multiply(rTransfer[j])); } } @@ -584,37 +389,38 @@ function makeTransferPsiMultiAuditor(ell: number, n: number, numAuditors: number } /** - * Build f for transfer with multiple auditors. + * Build the transformation function f for the transfer relation. + * + * Matches the Move implementation ordering (mirrors psi with statement points). */ -function makeTransferFMultiAuditor(ell: number, n: number, numAuditors: number): TransformationFunction { +function makeTransferF(ell: number, n: number, hasEffective: boolean, numExtra: number): TransformationFunction { return (s: SigmaProtocolStatement): RistPoint[] => { const result: RistPoint[] = []; - // 0: H + // 1. H result.push(s.points[IDX_H]); - // 1..ell: new_P[i] + // 2. new_P[i] const startNewP = getStartIdxNewP(ell); for (let i = 0; i < ell; i++) { result.push(s.points[startNewP + i]); } - // ell+1..2ell: new_R[i] + // 3. new_R[i] const startNewR = getStartIdxNewR(ell); for (let i = 0; i < ell; i++) { result.push(s.points[startNewR + i]); } - // For each auditor: new_R_aud_a[i] - const auditorBaseIdx = START_IDX_OLD_P + 4 * ell + 3 * n; - for (let a = 0; a < numAuditors; a++) { - const newRAudStart = auditorBaseIdx + a * (1 + ell + n) + 1; + // 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]); } } - // Balance equation target: + // 4. Balance equation target: ⟨B,old_P⟩ const bPowersEll = computeBPowers(ell); let balanceTarget = RistrettoPoint.ZERO; for (let i = 0; i < ell; i++) { @@ -622,29 +428,38 @@ function makeTransferFMultiAuditor(ell: number, n: number, numAuditors: number): } result.push(balanceTarget); - // Transfer amount: P[j] + // 5. P[j] const startP = getStartIdxP(ell); for (let j = 0; j < n; j++) { result.push(s.points[startP + j]); } - // R_sid[j] + // 6. R_sid[j] const startRSid = getStartIdxRSid(ell, n); for (let j = 0; j < n; j++) { result.push(s.points[startRSid + j]); } - // R_rid[j] + // 7. R_rid[j] const startRRid = getStartIdxRRid(ell, n); for (let j = 0; j < n; j++) { result.push(s.points[startRRid + j]); } - // For each auditor: R_aud_a[j] - for (let a = 0; a < numAuditors; a++) { - const rAudStart = auditorBaseIdx + a * (1 + ell + n) + 1 + ell; + // 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. (extra auditors) R_extra_t[j] + const extrasStart = getStartIdxExtras(ell, n, hasEffective); + for (let t = 0; t < numExtra; t++) { + const rExtraStart = extrasStart + t * (1 + n) + 1; for (let j = 0; j < n; j++) { - result.push(s.points[rAudStart + j]); + result.push(s.points[rExtraStart + j]); } } @@ -654,6 +469,9 @@ function makeTransferFMultiAuditor(ell: number, n: number, numAuditors: number): /** * Verify a confidential transfer proof. + * + * Convention: if hasEffectiveAuditor, the last element in auditorEkBytes / newBalanceDAud / + * transferAmountDAud is the effective auditor; preceding elements are extras. */ export function verifyTransfer(args: { senderAddress: Uint8Array; @@ -668,6 +486,7 @@ export function verifyTransfer(args: { transferAmountC: RistPoint[]; transferAmountDSender: RistPoint[]; transferAmountDRecipient: RistPoint[]; + hasEffectiveAuditor: boolean; auditorEkBytes?: Uint8Array[]; newBalanceDAud?: RistPoint[][]; transferAmountDAud?: RistPoint[][]; @@ -686,6 +505,7 @@ export function verifyTransfer(args: { transferAmountC, transferAmountDSender, transferAmountDRecipient, + hasEffectiveAuditor, auditorEkBytes = [], newBalanceDAud = [], transferAmountDAud = [], @@ -694,7 +514,7 @@ export function verifyTransfer(args: { const ell = oldBalanceC.length; const n = transferAmountC.length; - const numAuditors = auditorEkBytes.length; + const numExtra = hasEffectiveAuditor ? auditorEkBytes.length - 1 : auditorEkBytes.length; const G = RistrettoPoint.BASE; const H = H_RISTRETTO; @@ -704,48 +524,29 @@ export function verifyTransfer(args: { const stmtPoints: RistPoint[] = [G, H, ekSid, ekRid]; const stmtCompressed: Uint8Array[] = [G.toRawBytes(), H.toRawBytes(), ekSidBytes, ekRidBytes]; - 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()); + 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]); } - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountC[j]); - stmtCompressed.push(transferAmountC[j].toRawBytes()); - } - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountDSender[j]); - stmtCompressed.push(transferAmountDSender[j].toRawBytes()); - } - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountDRecipient[j]); - stmtCompressed.push(transferAmountDRecipient[j].toRawBytes()); - } - - for (let a = 0; a < numAuditors; a++) { - stmtPoints.push(RistrettoPoint.fromHex(auditorEkBytes[a])); - stmtCompressed.push(auditorEkBytes[a]); - - for (let i = 0; i < ell; i++) { - stmtPoints.push(newBalanceDAud[a][i]); - stmtCompressed.push(newBalanceDAud[a][i].toRawBytes()); - } - for (let j = 0; j < n; j++) { - stmtPoints.push(transferAmountDAud[a][j]); - stmtCompressed.push(transferAmountDAud[a][j].toRawBytes()); - } + // Extra auditors: [ek_extra, R_extra[n]] + for (let a = 0; a < numExtra; a++) { + pushPointBytes(RistrettoPoint.fromHex(auditorEkBytes[a]), auditorEkBytes[a]); + for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[a][j]); } const stmt: SigmaProtocolStatement = { @@ -754,7 +555,9 @@ export function verifyTransfer(args: { scalars: [], }; - const sessionId = bcsSerializeTransferSession(senderAddress, recipientAddress, tokenAddress, ell, n); + const sessionId = bcsSerializeTransferSession( + senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numExtra, + ); const dst: DomainSeparator = { protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, @@ -762,8 +565,8 @@ export function verifyTransfer(args: { return sigmaProtocolVerify( dst, - makeTransferPsiMultiAuditor(ell, n, numAuditors), - makeTransferFMultiAuditor(ell, n, numAuditors), + makeTransferPsi(ell, n, hasEffectiveAuditor, numExtra), + makeTransferF(ell, n, hasEffectiveAuditor, numExtra), stmt, proof, ); diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 892951fd4..d9e416493 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -146,7 +146,7 @@ export class ConfidentialAssetTransactionBuilder { const tokenAddr = AccountAddress.from(tokenAddress); // Get the auditor public key for the token - const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); // Get the sender's available balance from the chain const { available: senderEncryptedAvailableBalance } = await getBalance({ @@ -163,7 +163,7 @@ export class ConfidentialAssetTransactionBuilder { amount: BigInt(amount), senderAddress: senderAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), - auditorEncryptionKey: globalAuditorPubKey, + auditorEncryptionKey: effectiveAuditorPubKey, }); const [{ sigmaProof, rangeProof }, encryptedAmountAfterWithdraw, auditorEncryptedBalance] = @@ -251,17 +251,17 @@ export class ConfidentialAssetTransactionBuilder { tokenAddress: AccountAddressInput; options?: LedgerVersionArg; }): Promise { - const [{ vec: globalAuditorPubKey }] = await this.client.view<[{ vec: { data: string }[] }]>({ + const [{ vec: effectiveAuditorPubKey }] = await this.client.view<[{ vec: { data: string }[] }]>({ options: args.options, payload: { function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_effective_auditor`, functionArguments: [args.tokenAddress], }, }); - if (globalAuditorPubKey.length === 0) { + if (effectiveAuditorPubKey.length === 0) { return undefined; } - return new TwistedEd25519PublicKey(globalAuditorPubKey[0].data); + return new TwistedEd25519PublicKey(effectiveAuditorPubKey[0].data); } /** @@ -301,7 +301,7 @@ export class ConfidentialAssetTransactionBuilder { const tokenAddr = AccountAddress.from(tokenAddress); // Get the auditor public key for the token - const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ + const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress, }); @@ -338,7 +338,7 @@ export class ConfidentialAssetTransactionBuilder { // The contract will append the global auditor itself, so we only send extra auditor EKs on-chain. const allAuditorEncryptionKeys = [ ...additionalAuditorEncryptionKeys, - ...(globalAuditorPubKey ? [globalAuditorPubKey] : []), + ...(effectiveAuditorPubKey ? [effectiveAuditorPubKey] : []), ]; // Create the confidential transfer object @@ -347,6 +347,7 @@ export class ConfidentialAssetTransactionBuilder { senderAvailableBalanceCipherText: senderEncryptedAvailableBalance.getCipherText(), amount, recipientEncryptionKey, + hasEffectiveAuditor: !!effectiveAuditorPubKey, auditorEncryptionKeys: allAuditorEncryptionKeys, senderAddress: senderAddr.toUint8Array(), recipientAddress: recipientAddr.toUint8Array(), @@ -510,7 +511,7 @@ export class ConfidentialAssetTransactionBuilder { const tokenAddr = AccountAddress.from(tokenAddress); // Get the auditor public key for the token - const globalAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); + const effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress }); const { available } = await getBalance({ client: this.client, @@ -525,7 +526,7 @@ export class ConfidentialAssetTransactionBuilder { unnormalizedAvailableBalance: available, senderAddress: senderAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), - auditorEncryptionKey: globalAuditorPubKey, + auditorEncryptionKey: effectiveAuditorPubKey, }); return confidentialNormalization.createTransaction({ diff --git a/confidential-assets/src/internal/viewFunctions.ts b/confidential-assets/src/internal/viewFunctions.ts index ef49481cb..e556c559a 100644 --- a/confidential-assets/src/internal/viewFunctions.ts +++ b/confidential-assets/src/internal/viewFunctions.ts @@ -26,8 +26,8 @@ type ViewFunctionParams = { }; export type ConfidentialBalanceResponse = { - C: { data: string }[]; - D: { data: string }[]; + P: { data: string }[]; + R: { data: string }[]; }[]; /** @@ -187,11 +187,11 @@ async function getBalanceCipherText(args: ViewFunctionParams): Promise<{ ]); return { - pending: chunkedPendingBalance.C.map( - (c, i) => new TwistedElGamalCiphertext(c.data.slice(2), chunkedPendingBalance.D[i].data.slice(2)), + pending: chunkedPendingBalance.P.map( + (p, i) => new TwistedElGamalCiphertext(p.data.slice(2), chunkedPendingBalance.R[i].data.slice(2)), ), - available: chunkedActualBalances.C.map( - (c, i) => new TwistedElGamalCiphertext(c.data.slice(2), chunkedActualBalances.D[i].data.slice(2)), + available: chunkedActualBalances.P.map( + (p, i) => new TwistedElGamalCiphertext(p.data.slice(2), chunkedActualBalances.R[i].data.slice(2)), ), }; } diff --git a/confidential-assets/tests/e2e/confidentialAsset.test.ts b/confidential-assets/tests/e2e/confidentialAsset.test.ts index eb690309c..d0e96a280 100644 --- a/confidential-assets/tests/e2e/confidentialAsset.test.ts +++ b/confidential-assets/tests/e2e/confidentialAsset.test.ts @@ -374,9 +374,13 @@ describe("Confidential Asset Sender API", () => { longTestTimeout, ); - const AUDITOR = TwistedEd25519PrivateKey.generate(); + // --- Auditor configuration tests --- + + const EXTRA_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 extra auditor only (no effective auditor on-chain)", async () => { const confidentialBalance = await confidentialAsset.getBalance({ accountAddress: alice.accountAddress, @@ -390,12 +394,11 @@ describe("Confidential Asset Sender API", () => { signer: alice, tokenAddress: TOKEN_ADDRESS, recipient: alice.accountAddress, - additionalAuditorEncryptionKeys: [AUDITOR.publicKey()], + additionalAuditorEncryptionKeys: [EXTRA_AUDITOR.publicKey()], }); expect(transferTx.success).toBeTruthy(); - // Verify the confidential balance has been updated correctly await checkAliceDecryptedBalance( confidentialBalance.availableBalance() - TRANSFER_AMOUNT, confidentialBalance.pendingBalance() + TRANSFER_AMOUNT, @@ -404,6 +407,13 @@ describe("Confidential Asset Sender API", () => { longTestTimeout, ); + // Effective-auditor e2e tests require calling set_auditor_for_asset_type 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 extras) + // - "effective + extra auditors" (set global auditor, transfer with extra auditors) + test( "it should check that Alice's incoming transfers are not paused", async () => { diff --git a/confidential-assets/tests/units/confidentialProofs.test.ts b/confidential-assets/tests/units/confidentialProofs.test.ts index 8e33b9606..0add7484e 100644 --- a/confidential-assets/tests/units/confidentialProofs.test.ts +++ b/confidential-assets/tests/units/confidentialProofs.test.ts @@ -140,6 +140,7 @@ describe("Generate 'confidential coin' proofs", () => { 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, }); @@ -199,6 +200,7 @@ describe("Generate 'confidential coin' proofs", () => { test( "Verify transfer with auditors sigma proof", () => { + // This test uses an extra auditor (not an on-chain effective auditor) const isValid = verifyTransfer({ senderAddress: dummySenderAddress, recipientAddress: dummyRecipientAddress, @@ -212,6 +214,7 @@ describe("Generate 'confidential coin' proofs", () => { 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), From a0c29b781436f1dddc35ae191f17143a2ae51324 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Tue, 3 Mar 2026 19:37:54 -0600 Subject: [PATCH 13/22] simplify normalization --- .../src/crypto/confidentialNormalization.ts | 4 ++-- .../src/crypto/sigmaProtocolWithdraw.ts | 9 +++++---- .../src/internal/confidentialAssetTxnBuilder.ts | 12 ++++++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/confidential-assets/src/crypto/confidentialNormalization.ts b/confidential-assets/src/crypto/confidentialNormalization.ts index 15c0da730..920a3a143 100644 --- a/confidential-assets/src/crypto/confidentialNormalization.ts +++ b/confidential-assets/src/crypto/confidentialNormalization.ts @@ -8,7 +8,7 @@ import { EncryptedAmount } from "./encryptedAmount"; import { AVAILABLE_BALANCE_CHUNK_COUNT, CHUNK_BITS } from "./chunkedAmount"; import { Aptos, SimpleTransaction, AccountAddressInput, InputGenerateTransactionOptions } from "@aptos-labs/ts-sdk"; import type { SigmaProtocolProof } from "./sigmaProtocol"; -import { proveNormalization } from "./sigmaProtocolWithdraw"; +import { proveWithdrawal } from "./sigmaProtocolWithdraw"; export type CreateConfidentialNormalizationOpArgs = { decryptionKey: TwistedEd25519PrivateKey; @@ -121,7 +121,7 @@ export class ConfidentialNormalization { newBalanceDAud = this.auditorEncryptedNormalizedBalance.getCipherText().map((ct) => ct.D); } - return proveNormalization({ + return proveWithdrawal({ dk: this.decryptionKey, senderAddress: this.senderAddress, tokenAddress: this.tokenAddress, diff --git a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts index 2df91ebfb..ac001038a 100644 --- a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts +++ b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts @@ -16,7 +16,7 @@ * * where B = (1, 2^16, 2^32, ...) are the chunk base powers. * - * For normalization, v = 0 and the protocol ID differs. + * For normalization, v = 0 (same protocol ID). * * When an auditor is present, additional outputs prove new_R_aud[i] = new_r[i] * ek_aud. */ @@ -39,7 +39,6 @@ import { import { Serializer, FixedBytes, U64 } from "@aptos-labs/ts-sdk"; const PROTOCOL_ID_WITHDRAWAL = "AptosConfidentialAsset/WithdrawalV1"; -const PROTOCOL_ID_NORMALIZATION = "AptosConfidentialAsset/NormalizationV1"; /** * BCS-serialize a WithdrawSession matching the Move struct: @@ -348,9 +347,10 @@ export function proveWithdrawal(args: WithdrawProofArgs): SigmaProtocolProof { /** * 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_NORMALIZATION, args); + return proveWithdrawInternal(PROTOCOL_ID_WITHDRAWAL, args); } /** @@ -374,6 +374,7 @@ export function verifyWithdrawal(args: { /** * Verify a confidential normalization proof. + * @deprecated Use `verifyWithdrawal` instead — normalization is just withdrawal with amount = 0. */ export function verifyNormalization(args: { senderAddress: Uint8Array; @@ -388,7 +389,7 @@ export function verifyNormalization(args: { newBalanceDAud?: RistPoint[]; proof: SigmaProtocolProof; }): boolean { - return verifyWithdrawInternal(PROTOCOL_ID_NORMALIZATION, args); + return verifyWithdrawInternal(PROTOCOL_ID_WITHDRAWAL, args); } function verifyWithdrawInternal( diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index d9e416493..250d2d018 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -370,7 +370,14 @@ export class ConfidentialAssetTransactionBuilder { // Only send D components for recipient and auditors (C components are shared with sender_amount) const recipientDPoints = encryptedAmountByRecipient.getCipherText().map((ct) => ct.D.toRawBytes()); - const auditorDPoints = auditorsCBList.map((cb) => cb.getCipherText().map((ct) => ct.D.toRawBytes())); + // Split auditor D points into effective (last, if present) and extra (remaining) + const effectiveAuditorDPoints = effectiveAuditorPubKey + ? auditorsCBList[auditorsCBList.length - 1].getCipherText().map((ct) => ct.D.toRawBytes()) + : []; + const extraAuditorDPoints = (effectiveAuditorPubKey + ? auditorsCBList.slice(0, -1) + : auditorsCBList + ).map((cb) => cb.getCipherText().map((ct) => ct.D.toRawBytes())); // Build A components for new balance (D points encrypted under the effective auditor key, i.e., the last one) const newBalanceA = auditorNewBalanceList.length > 0 @@ -391,8 +398,9 @@ export class ConfidentialAssetTransactionBuilder { confidentialTransfer.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.C.toRawBytes()), // sender_amount_C confidentialTransfer.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.D.toRawBytes()), // sender_amount_D recipientDPoints, + effectiveAuditorDPoints, extraAuditorEncryptionKeys, - auditorDPoints, + extraAuditorDPoints, rangeProofNewBalance, rangeProofAmount, sigmaProof.commitment, From d1266104afa52df9593efb9031a1cffd308d079b Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 4 Mar 2026 12:34:56 -0600 Subject: [PATCH 14/22] rename variable for all auditor amount ciphertexts --- .../src/internal/confidentialAssetTxnBuilder.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 250d2d018..038a22cd9 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -361,7 +361,7 @@ export class ConfidentialAssetTransactionBuilder { }, encryptedAmountAfterTransfer, encryptedAmountByRecipient, - auditorsCBList, + allAuditorAmountCiphertexts, auditorNewBalanceList, ] = await confidentialTransfer.authorizeTransfer(); @@ -372,11 +372,11 @@ export class ConfidentialAssetTransactionBuilder { const recipientDPoints = encryptedAmountByRecipient.getCipherText().map((ct) => ct.D.toRawBytes()); // Split auditor D points into effective (last, if present) and extra (remaining) const effectiveAuditorDPoints = effectiveAuditorPubKey - ? auditorsCBList[auditorsCBList.length - 1].getCipherText().map((ct) => ct.D.toRawBytes()) + ? allAuditorAmountCiphertexts[allAuditorAmountCiphertexts.length - 1].getCipherText().map((ct) => ct.D.toRawBytes()) : []; const extraAuditorDPoints = (effectiveAuditorPubKey - ? auditorsCBList.slice(0, -1) - : auditorsCBList + ? allAuditorAmountCiphertexts.slice(0, -1) + : allAuditorAmountCiphertexts ).map((cb) => cb.getCipherText().map((ct) => ct.D.toRawBytes())); // Build A components for new balance (D points encrypted under the effective auditor key, i.e., the last one) From 245204dd37975ea72597abc89c81685cb3f706da Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 4 Mar 2026 15:52:57 -0600 Subject: [PATCH 15/22] add contract address to Fiat-Shamir DST and remove unnecessary tests that are ignored anyway --- .../src/crypto/confidentialKeyRotation.ts | 3 + .../src/crypto/sigmaProtocol.ts | 14 +- .../src/crypto/sigmaProtocolRegistration.ts | 3 + .../src/crypto/sigmaProtocolTransfer.ts | 3 + .../src/crypto/sigmaProtocolWithdraw.ts | 3 + .../internal/confidentialAssetTxnBuilder.ts | 5 +- .../e2e/confidentialAssetTxnBuilder.test.ts | 555 ------------------ 7 files changed, 28 insertions(+), 558 deletions(-) delete mode 100644 confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts diff --git a/confidential-assets/src/crypto/confidentialKeyRotation.ts b/confidential-assets/src/crypto/confidentialKeyRotation.ts index 9f2001960..6400994a6 100644 --- a/confidential-assets/src/crypto/confidentialKeyRotation.ts +++ b/confidential-assets/src/crypto/confidentialKeyRotation.ts @@ -38,6 +38,7 @@ import { sigmaProtocolProve, sigmaProtocolVerify, bcsSerializeKeyRotationSession, + APTOS_EXPERIMENTAL_ADDRESS, type DomainSeparator, type SigmaProtocolStatement, type SigmaProtocolProof, @@ -216,6 +217,7 @@ export class ConfidentialKeyRotation { // Build domain separator const sessionId = bcsSerializeKeyRotationSession(this.senderAddress, this.tokenAddress, numChunks); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; @@ -283,6 +285,7 @@ export class ConfidentialKeyRotation { // Build domain separator const sessionId = bcsSerializeKeyRotationSession(senderAddress, tokenAddress, numChunks); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; diff --git a/confidential-assets/src/crypto/sigmaProtocol.ts b/confidential-assets/src/crypto/sigmaProtocol.ts index d58235375..d39c82404 100644 --- a/confidential-assets/src/crypto/sigmaProtocol.ts +++ b/confidential-assets/src/crypto/sigmaProtocol.ts @@ -19,13 +19,24 @@ 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 - * struct DomainSeparator { protocol_id: vector, session_id: vector } + * struct DomainSeparator { contract_address: address, protocol_id: vector, session_id: vector } * ``` */ export interface DomainSeparator { + contractAddress: Uint8Array; protocolId: Uint8Array; sessionId: Uint8Array; } @@ -39,6 +50,7 @@ class BcsDomainSeparator extends Serializable { } serialize(serializer: Serializer): void { + serializer.serialize(new FixedBytes(this.dst.contractAddress)); serializer.serializeBytes(this.dst.protocolId); serializer.serializeBytes(this.dst.sessionId); } diff --git a/confidential-assets/src/crypto/sigmaProtocolRegistration.ts b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts index 4a8ce206a..d2c0c7d8e 100644 --- a/confidential-assets/src/crypto/sigmaProtocolRegistration.ts +++ b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts @@ -24,6 +24,7 @@ import type { RistPoint } from "."; import { sigmaProtocolProve, sigmaProtocolVerify, + APTOS_EXPERIMENTAL_ADDRESS, type DomainSeparator, type SigmaProtocolStatement, type SigmaProtocolProof, @@ -104,6 +105,7 @@ export function proveRegistration(args: { const sessionId = bcsSerializeRegistrationSession(senderAddress, tokenAddress); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; @@ -132,6 +134,7 @@ export function verifyRegistration(args: { const sessionId = bcsSerializeRegistrationSession(senderAddress, tokenAddress); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; diff --git a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts index d858b53b5..f8cf519c8 100644 --- a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts +++ b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts @@ -26,6 +26,7 @@ import { ed25519modN } from "../utils"; import { sigmaProtocolProve, sigmaProtocolVerify, + APTOS_EXPERIMENTAL_ADDRESS, type DomainSeparator, type SigmaProtocolStatement, type SigmaProtocolProof, @@ -277,6 +278,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numExtra, ); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; @@ -559,6 +561,7 @@ export function verifyTransfer(args: { senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numExtra, ); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; diff --git a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts index ac001038a..8bb0682a2 100644 --- a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts +++ b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts @@ -30,6 +30,7 @@ import { ed25519modN } from "../utils"; import { sigmaProtocolProve, sigmaProtocolVerify, + APTOS_EXPERIMENTAL_ADDRESS, type DomainSeparator, type SigmaProtocolStatement, type SigmaProtocolProof, @@ -331,6 +332,7 @@ function proveWithdrawInternal( // Build domain separator const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(protocolId), sessionId, }; @@ -470,6 +472,7 @@ function verifyWithdrawInternal( const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell); const dst: DomainSeparator = { + contractAddress: APTOS_EXPERIMENTAL_ADDRESS, protocolId: utf8ToBytes(protocolId), sessionId, }; diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 038a22cd9..1e3099376 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -379,8 +379,9 @@ export class ConfidentialAssetTransactionBuilder { : allAuditorAmountCiphertexts ).map((cb) => cb.getCipherText().map((ct) => ct.D.toRawBytes())); - // Build A components for new balance (D points encrypted under the effective auditor key, i.e., the last one) - const newBalanceA = auditorNewBalanceList.length > 0 + // 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 — extra auditors don't get new balance R components. + const newBalanceA = effectiveAuditorPubKey ? auditorNewBalanceList[auditorNewBalanceList.length - 1].getCipherText().map((ct) => ct.D.toRawBytes()) : []; diff --git a/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts b/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts deleted file mode 100644 index 0e74046e3..000000000 --- a/confidential-assets/tests/e2e/confidentialAssetTxnBuilder.test.ts +++ /dev/null @@ -1,555 +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 checkAliceIncomingTransfersPausedStatus(expectedStatus: boolean) { - const isPaused = await confidentialAsset.isIncomingTransfersPaused({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - expect(isPaused).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 that Alice's incoming transfers are not paused", - async () => { - const isPaused = await confidentialAsset.isIncomingTransfersPaused({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - }); - - expect(isPaused).toBeFalsy(); - }, - longTestTimeout, - ); - - test( - "it should throw if checking whether Bob's incoming transfers are paused and he has no registered balance", - async () => { - await expect( - confidentialAsset.isIncomingTransfersPaused({ - 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, - unpause: 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, - withPauseIncoming: true, - }); - txResp = await sendAndWaitTx(rolloverTx, alice); - expect(txResp.success).toBeTruthy(); - - await checkAliceIncomingTransfersPausedStatus(true); - - // Get the current balance before rotation - const confidentialBalance = await confidentialAsset.getBalance({ - accountAddress: alice.accountAddress, - tokenAddress: TOKEN_ADDRESS, - decryptionKey: aliceConfidential, - }); - - // This will unpause incoming transfers after rotation (unpause defaults to true). - const keyRotationAndUnpauseTx = await transactionBuilder.rotateEncryptionKey({ - sender: alice.accountAddress, - senderDecryptionKey: aliceConfidential, - newSenderDecryptionKey: ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, - tokenAddress: TOKEN_ADDRESS, - }); - txResp = await sendAndWaitTx(keyRotationAndUnpauseTx, alice); - expect(txResp.success).toBeTruthy(); - - // Check that incoming transfers are unpaused - await checkAliceIncomingTransfersPausedStatus(false); - - // If this decrypts correctly, then the key rotation worked. - await checkAliceDecryptedBalance( - confidentialBalance.availableBalance(), - confidentialBalance.pendingBalance(), - ALICE_NEW_CONFIDENTIAL_PRIVATE_KEY, - ); - }, - longTestTimeout, - ); -}); From 39a542dab2ac00e377dec5f5d59eb714088ef045 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 4 Mar 2026 16:37:58 -0600 Subject: [PATCH 16/22] add chain ID as domain separator --- .../src/api/confidentialAsset.ts | 4 +++ .../src/crypto/confidentialKeyRotation.ts | 12 +++++++- .../src/crypto/confidentialNormalization.ts | 9 ++++++ .../src/crypto/confidentialTransfer.ts | 10 +++++++ .../src/crypto/confidentialWithdraw.ts | 10 +++++++ .../src/crypto/sigmaProtocol.ts | 4 ++- .../src/crypto/sigmaProtocolRegistration.ts | 8 +++-- .../src/crypto/sigmaProtocolTransfer.ts | 7 +++++ .../src/crypto/sigmaProtocolWithdraw.ts | 9 ++++++ .../internal/confidentialAssetTxnBuilder.ts | 29 +++++++++++++++++++ .../tests/units/confidentialProofs.test.ts | 10 +++++++ 11 files changed, 108 insertions(+), 4 deletions(-) diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index 007c7aa1c..822b9cea0 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -558,6 +558,9 @@ export class ConfidentialAsset { 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 }); @@ -566,6 +569,7 @@ export class ConfidentialAsset { unnormalizedAvailableBalance: available, senderAddress: senderAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), + chainId, auditorEncryptionKey: effectiveAuditorPubKey, }); diff --git a/confidential-assets/src/crypto/confidentialKeyRotation.ts b/confidential-assets/src/crypto/confidentialKeyRotation.ts index 6400994a6..69199970c 100644 --- a/confidential-assets/src/crypto/confidentialKeyRotation.ts +++ b/confidential-assets/src/crypto/confidentialKeyRotation.ts @@ -119,6 +119,8 @@ export type CreateConfidentialKeyRotationOpArgs = { senderAddress: Uint8Array; /** 32-byte token/metadata object address */ tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; }; export type KeyRotationProof = { @@ -141,18 +143,22 @@ export class ConfidentialKeyRotation { private tokenAddress: Uint8Array; + private chainId: number; + constructor(args: { currentDecryptionKey: TwistedEd25519PrivateKey; newDecryptionKey: TwistedEd25519PrivateKey; currentEncryptedAvailableBalance: EncryptedAmount; senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; }) { this.currentDecryptionKey = args.currentDecryptionKey; this.newDecryptionKey = args.newDecryptionKey; this.currentEncryptedAvailableBalance = args.currentEncryptedAvailableBalance; this.senderAddress = args.senderAddress; this.tokenAddress = args.tokenAddress; + this.chainId = args.chainId; } static create(args: CreateConfidentialKeyRotationOpArgs): ConfidentialKeyRotation { @@ -162,6 +168,7 @@ export class ConfidentialKeyRotation { currentEncryptedAvailableBalance: args.currentEncryptedAvailableBalance, senderAddress: args.senderAddress, tokenAddress: args.tokenAddress, + chainId: args.chainId, }); } @@ -218,6 +225,7 @@ export class ConfidentialKeyRotation { const sessionId = bcsSerializeKeyRotationSession(this.senderAddress, this.tokenAddress, numChunks); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId: this.chainId, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; @@ -251,9 +259,10 @@ export class ConfidentialKeyRotation { newD: Uint8Array[]; senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; proof: SigmaProtocolProof; }): boolean { - const { oldEk, newEk, oldD, newD, senderAddress, tokenAddress, proof } = args; + const { oldEk, newEk, oldD, newD, senderAddress, tokenAddress, chainId, proof } = args; const numChunks = oldD.length; if (newD.length !== numChunks) { @@ -286,6 +295,7 @@ export class ConfidentialKeyRotation { const sessionId = bcsSerializeKeyRotationSession(senderAddress, tokenAddress, numChunks); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; diff --git a/confidential-assets/src/crypto/confidentialNormalization.ts b/confidential-assets/src/crypto/confidentialNormalization.ts index 920a3a143..ae75218ff 100644 --- a/confidential-assets/src/crypto/confidentialNormalization.ts +++ b/confidential-assets/src/crypto/confidentialNormalization.ts @@ -17,6 +17,8 @@ export type CreateConfidentialNormalizationOpArgs = { senderAddress: Uint8Array; /** 32-byte token address */ tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; /** Optional auditor encryption key */ auditorEncryptionKey?: TwistedEd25519PublicKey; randomness?: bigint[]; @@ -40,6 +42,8 @@ export class ConfidentialNormalization { auditorEncryptionKey?: TwistedEd25519PublicKey; + chainId: number; + constructor(args: { decryptionKey: TwistedEd25519PrivateKey; unnormalizedEncryptedAvailableBalance: EncryptedAmount; @@ -47,6 +51,7 @@ export class ConfidentialNormalization { auditorEncryptedNormalizedBalance?: EncryptedAmount; senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; auditorEncryptionKey?: TwistedEd25519PublicKey; }) { this.decryptionKey = args.decryptionKey; @@ -55,6 +60,7 @@ export class ConfidentialNormalization { 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) { @@ -69,6 +75,7 @@ export class ConfidentialNormalization { randomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), senderAddress, tokenAddress, + chainId, auditorEncryptionKey, } = args; @@ -97,6 +104,7 @@ export class ConfidentialNormalization { auditorEncryptedNormalizedBalance, senderAddress, tokenAddress, + chainId, auditorEncryptionKey, }); } @@ -125,6 +133,7 @@ export class ConfidentialNormalization { dk: this.decryptionKey, senderAddress: this.senderAddress, tokenAddress: this.tokenAddress, + chainId: this.chainId, amount: 0n, oldBalanceC, oldBalanceD, diff --git a/confidential-assets/src/crypto/confidentialTransfer.ts b/confidential-assets/src/crypto/confidentialTransfer.ts index 090b0b3eb..2909fe51b 100644 --- a/confidential-assets/src/crypto/confidentialTransfer.ts +++ b/confidential-assets/src/crypto/confidentialTransfer.ts @@ -39,6 +39,8 @@ export type CreateConfidentialTransferOpArgs = { recipientAddress: Uint8Array; /** 32-byte token address */ tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; }; export class ConfidentialTransfer { @@ -95,6 +97,8 @@ export class ConfidentialTransfer { tokenAddress: Uint8Array; + chainId: number; + private constructor(args: { senderDecryptionKey: TwistedEd25519PrivateKey; recipientEncryptionKey: TwistedEd25519PublicKey; @@ -110,6 +114,7 @@ export class ConfidentialTransfer { senderAddress: Uint8Array; recipientAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; }) { const { senderDecryptionKey, @@ -126,6 +131,7 @@ export class ConfidentialTransfer { senderAddress, recipientAddress, tokenAddress, + chainId, } = args; this.senderDecryptionKey = senderDecryptionKey; this.recipientEncryptionKey = recipientEncryptionKey; @@ -162,6 +168,7 @@ export class ConfidentialTransfer { this.senderAddress = senderAddress; this.recipientAddress = recipientAddress; this.tokenAddress = tokenAddress; + this.chainId = chainId; } static async create(args: CreateConfidentialTransferOpArgs) { @@ -175,6 +182,7 @@ export class ConfidentialTransfer { senderAddress, recipientAddress, tokenAddress, + chainId, } = args; const amount = BigInt(args.amount); const newBalanceRandomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT); @@ -237,6 +245,7 @@ export class ConfidentialTransfer { senderAddress, recipientAddress, tokenAddress, + chainId, }); } @@ -271,6 +280,7 @@ export class ConfidentialTransfer { senderAddress: this.senderAddress, recipientAddress: this.recipientAddress, tokenAddress: this.tokenAddress, + chainId: this.chainId, senderEncryptionKey: this.senderDecryptionKey.publicKey(), recipientEncryptionKey: this.recipientEncryptionKey, oldBalanceC, diff --git a/confidential-assets/src/crypto/confidentialWithdraw.ts b/confidential-assets/src/crypto/confidentialWithdraw.ts index 79d7c718c..e5acbb5b0 100644 --- a/confidential-assets/src/crypto/confidentialWithdraw.ts +++ b/confidential-assets/src/crypto/confidentialWithdraw.ts @@ -22,6 +22,8 @@ export type CreateConfidentialWithdrawOpArgs = { senderAddress: Uint8Array; /** 32-byte token address */ tokenAddress: Uint8Array; + /** Chain ID for domain separation */ + chainId: number; /** Optional auditor encryption key */ auditorEncryptionKey?: TwistedEd25519PublicKey; randomness?: bigint[]; @@ -47,6 +49,8 @@ export class ConfidentialWithdraw { auditorEncryptionKey?: TwistedEd25519PublicKey; + chainId: number; + constructor(args: { decryptionKey: TwistedEd25519PrivateKey; senderEncryptedAvailableBalance: EncryptedAmount; @@ -56,6 +60,7 @@ export class ConfidentialWithdraw { randomness: bigint[]; senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; auditorEncryptionKey?: TwistedEd25519PublicKey; }) { const { @@ -67,6 +72,7 @@ export class ConfidentialWithdraw { auditorEncryptedBalanceAfterWithdrawal, senderAddress, tokenAddress, + chainId, auditorEncryptionKey, } = args; if (amount < 0n) { @@ -94,6 +100,7 @@ export class ConfidentialWithdraw { this.auditorEncryptedBalanceAfterWithdrawal = auditorEncryptedBalanceAfterWithdrawal; this.senderAddress = senderAddress; this.tokenAddress = tokenAddress; + this.chainId = chainId; this.auditorEncryptionKey = auditorEncryptionKey; } @@ -103,6 +110,7 @@ export class ConfidentialWithdraw { randomness = ed25519GenListOfRandom(AVAILABLE_BALANCE_CHUNK_COUNT), senderAddress, tokenAddress, + chainId, auditorEncryptionKey, } = args; @@ -135,6 +143,7 @@ export class ConfidentialWithdraw { randomness, senderAddress, tokenAddress, + chainId, auditorEncryptionKey, }); } @@ -162,6 +171,7 @@ export class ConfidentialWithdraw { dk: this.decryptionKey, senderAddress: this.senderAddress, tokenAddress: this.tokenAddress, + chainId: this.chainId, amount: this.amount, oldBalanceC, oldBalanceD, diff --git a/confidential-assets/src/crypto/sigmaProtocol.ts b/confidential-assets/src/crypto/sigmaProtocol.ts index d39c82404..2c0617fcc 100644 --- a/confidential-assets/src/crypto/sigmaProtocol.ts +++ b/confidential-assets/src/crypto/sigmaProtocol.ts @@ -32,11 +32,12 @@ export const APTOS_EXPERIMENTAL_ADDRESS = (() => { /** * Matches the Move `DomainSeparator` struct: * ```move - * struct DomainSeparator { contract_address: address, protocol_id: vector, session_id: vector } + * struct DomainSeparator { contract_address: address, chain_id: u8, protocol_id: vector, session_id: vector } * ``` */ export interface DomainSeparator { contractAddress: Uint8Array; + chainId: number; protocolId: Uint8Array; sessionId: Uint8Array; } @@ -51,6 +52,7 @@ class BcsDomainSeparator extends Serializable { serialize(serializer: Serializer): void { serializer.serialize(new FixedBytes(this.dst.contractAddress)); + serializer.serializeU8(this.dst.chainId); serializer.serializeBytes(this.dst.protocolId); serializer.serializeBytes(this.dst.sessionId); } diff --git a/confidential-assets/src/crypto/sigmaProtocolRegistration.ts b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts index d2c0c7d8e..77d8ccf1a 100644 --- a/confidential-assets/src/crypto/sigmaProtocolRegistration.ts +++ b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts @@ -87,8 +87,9 @@ export function proveRegistration(args: { dk: TwistedEd25519PrivateKey; senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; }): SigmaProtocolProof { - const { dk, senderAddress, tokenAddress } = args; + const { dk, senderAddress, tokenAddress, chainId } = args; const dkBigint = bytesToNumberLE(dk.toUint8Array()); const ekBytes = dk.publicKey().toUint8Array(); @@ -106,6 +107,7 @@ export function proveRegistration(args: { const sessionId = bcsSerializeRegistrationSession(senderAddress, tokenAddress); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; @@ -120,9 +122,10 @@ export function verifyRegistration(args: { ek: Uint8Array; senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; proof: SigmaProtocolProof; }): boolean { - const { ek: ekBytes, senderAddress, tokenAddress, proof } = args; + const { ek: ekBytes, senderAddress, tokenAddress, chainId, proof } = args; const ek = RistrettoPoint.fromHex(ekBytes); const H = H_RISTRETTO; @@ -135,6 +138,7 @@ export function verifyRegistration(args: { const sessionId = bcsSerializeRegistrationSession(senderAddress, tokenAddress); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; diff --git a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts index f8cf519c8..ac8f3a784 100644 --- a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts +++ b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts @@ -143,6 +143,8 @@ export type TransferProofArgs = { 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 */ @@ -200,6 +202,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { senderAddress, recipientAddress, tokenAddress, + chainId, senderEncryptionKey, recipientEncryptionKey, oldBalanceC, @@ -279,6 +282,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { ); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; @@ -479,6 +483,7 @@ export function verifyTransfer(args: { senderAddress: Uint8Array; recipientAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; ekSidBytes: Uint8Array; ekRidBytes: Uint8Array; oldBalanceC: RistPoint[]; @@ -498,6 +503,7 @@ export function verifyTransfer(args: { senderAddress, recipientAddress, tokenAddress, + chainId, ekSidBytes, ekRidBytes, oldBalanceC, @@ -562,6 +568,7 @@ export function verifyTransfer(args: { ); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, protocolId: utf8ToBytes(PROTOCOL_ID), sessionId, }; diff --git a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts index 8bb0682a2..d33b31beb 100644 --- a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts +++ b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts @@ -228,6 +228,8 @@ export type WithdrawProofArgs = { 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 */ @@ -259,6 +261,7 @@ function proveWithdrawInternal( dk, senderAddress, tokenAddress, + chainId, amount, oldBalanceC, oldBalanceD, @@ -333,6 +336,7 @@ function proveWithdrawInternal( const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, protocolId: utf8ToBytes(protocolId), sessionId, }; @@ -361,6 +365,7 @@ export function proveNormalization(args: WithdrawProofArgs): SigmaProtocolProof export function verifyWithdrawal(args: { senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; amount: bigint; ekBytes: Uint8Array; oldBalanceC: RistPoint[]; @@ -381,6 +386,7 @@ export function verifyWithdrawal(args: { export function verifyNormalization(args: { senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; amount: bigint; ekBytes: Uint8Array; oldBalanceC: RistPoint[]; @@ -399,6 +405,7 @@ function verifyWithdrawInternal( args: { senderAddress: Uint8Array; tokenAddress: Uint8Array; + chainId: number; amount: bigint; ekBytes: Uint8Array; oldBalanceC: RistPoint[]; @@ -413,6 +420,7 @@ function verifyWithdrawInternal( const { senderAddress, tokenAddress, + chainId, amount, ekBytes, oldBalanceC, @@ -473,6 +481,7 @@ function verifyWithdrawInternal( const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, + chainId, protocolId: utf8ToBytes(protocolId), sessionId, }; diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 1e3099376..4feb4fcce 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -31,6 +31,15 @@ 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; @@ -60,11 +69,15 @@ export class ConfidentialAssetTransactionBuilder { 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({ @@ -145,6 +158,9 @@ export class ConfidentialAssetTransactionBuilder { 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 }); @@ -163,6 +179,7 @@ export class ConfidentialAssetTransactionBuilder { amount: BigInt(amount), senderAddress: senderAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), + chainId, auditorEncryptionKey: effectiveAuditorPubKey, }); @@ -300,6 +317,9 @@ export class ConfidentialAssetTransactionBuilder { 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 effectiveAuditorPubKey = await this.getAssetAuditorEncryptionKey({ tokenAddress, @@ -352,6 +372,7 @@ export class ConfidentialAssetTransactionBuilder { senderAddress: senderAddr.toUint8Array(), recipientAddress: recipientAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), + chainId, }); const [ @@ -465,6 +486,9 @@ export class ConfidentialAssetTransactionBuilder { 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, @@ -472,6 +496,7 @@ export class ConfidentialAssetTransactionBuilder { currentEncryptedAvailableBalance, senderAddress: senderAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), + chainId, }); const { newEkBytes, newDBytes, proof } = confidentialKeyRotation.authorizeKeyRotation(); @@ -519,6 +544,9 @@ export class ConfidentialAssetTransactionBuilder { 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 }); @@ -535,6 +563,7 @@ export class ConfidentialAssetTransactionBuilder { unnormalizedAvailableBalance: available, senderAddress: senderAddr.toUint8Array(), tokenAddress: tokenAddr.toUint8Array(), + chainId, auditorEncryptionKey: effectiveAuditorPubKey, }); diff --git a/confidential-assets/tests/units/confidentialProofs.test.ts b/confidential-assets/tests/units/confidentialProofs.test.ts index 0add7484e..928efba59 100644 --- a/confidential-assets/tests/units/confidentialProofs.test.ts +++ b/confidential-assets/tests/units/confidentialProofs.test.ts @@ -46,6 +46,7 @@ describe("Generate 'confidential coin' proofs", () => { amount: WITHDRAW_AMOUNT, senderAddress: dummySenderAddress, tokenAddress: dummyTokenAddress, + chainId: 4, }); confidentialWithdrawSigmaProof = confidentialWithdraw.genSigmaProof(); @@ -63,6 +64,7 @@ describe("Generate 'confidential coin' proofs", () => { const isValid = verifyWithdrawal({ senderAddress: dummySenderAddress, tokenAddress: dummyTokenAddress, + chainId: 4, amount: WITHDRAW_AMOUNT, ekBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), oldBalanceC: confidentialWithdraw.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), @@ -113,6 +115,7 @@ describe("Generate 'confidential coin' proofs", () => { senderAddress: dummySenderAddress, recipientAddress: dummyRecipientAddress, tokenAddress: dummyTokenAddress, + chainId: 4, }); confidentialTransferSigmaProof = confidentialTransfer.genSigmaProof(); @@ -131,6 +134,7 @@ describe("Generate 'confidential coin' proofs", () => { senderAddress: dummySenderAddress, recipientAddress: dummyRecipientAddress, tokenAddress: dummyTokenAddress, + chainId: 4, ekSidBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), ekRidBytes: bobConfidentialDecryptionKey.publicKey().toUint8Array(), oldBalanceC: confidentialTransfer.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), @@ -187,6 +191,7 @@ describe("Generate 'confidential coin' proofs", () => { senderAddress: dummySenderAddress, recipientAddress: dummyRecipientAddress, tokenAddress: dummyTokenAddress, + chainId: 4, }); confidentialTransferWithAuditorsSigmaProof = confidentialTransferWithAuditors.genSigmaProof(); @@ -205,6 +210,7 @@ describe("Generate 'confidential coin' proofs", () => { senderAddress: dummySenderAddress, recipientAddress: dummyRecipientAddress, tokenAddress: dummyTokenAddress, + chainId: 4, ekSidBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), ekRidBytes: bobConfidentialDecryptionKey.publicKey().toUint8Array(), oldBalanceC: confidentialTransferWithAuditors.senderEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), @@ -268,6 +274,7 @@ describe("Generate 'confidential coin' proofs", () => { newSenderDecryptionKey: newAliceConfidentialPrivateKey, senderAddress: dummySenderAddress, tokenAddress: dummyTokenAddress, + chainId: 4, }); keyRotationProofResult = confidentialKeyRotation.authorizeKeyRotation(); @@ -302,6 +309,7 @@ describe("Generate 'confidential coin' proofs", () => { newD: newDBytes, senderAddress: dummySenderAddress, tokenAddress: dummyTokenAddress, + chainId: 4, proof, }); @@ -329,6 +337,7 @@ describe("Generate 'confidential coin' proofs", () => { unnormalizedAvailableBalance: unnormalizedEncryptedBalance, senderAddress: dummySenderAddress, tokenAddress: dummyTokenAddress, + chainId: 4, }); confidentialNormalizationSigmaProof = confidentialNormalization.genSigmaProof(); @@ -345,6 +354,7 @@ describe("Generate 'confidential coin' proofs", () => { const isValid = verifyNormalization({ senderAddress: dummySenderAddress, tokenAddress: dummyTokenAddress, + chainId: 4, amount: 0n, ekBytes: aliceConfidentialDecryptionKey.publicKey().toUint8Array(), oldBalanceC: confidentialNormalization.unnormalizedEncryptedAvailableBalance.getCipherText().map((ct) => ct.C), From 7ab21ccae81f364e9ffc1bb603f41b7fadb8d2c3 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 4 Mar 2026 23:02:19 -0600 Subject: [PATCH 17/22] update SHA2-512 hashing in FS transform --- confidential-assets/src/crypto/sigmaProtocol.ts | 14 ++++++++++++-- confidential-assets/src/helpers.ts | 12 ------------ confidential-assets/src/index.ts | 1 - 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/confidential-assets/src/crypto/sigmaProtocol.ts b/confidential-assets/src/crypto/sigmaProtocol.ts index 2c0617fcc..e28b689f4 100644 --- a/confidential-assets/src/crypto/sigmaProtocol.ts +++ b/confidential-assets/src/crypto/sigmaProtocol.ts @@ -171,8 +171,18 @@ export function sigmaProtocolFiatShamir( const fiatShamirInputs = new BcsFiatShamirInputs(dst, k, stmt.compressedPoints, stmt.scalars, compressedA); const bytes = fiatShamirInputs.bcsToBytes(); - const eHash = sha512(bytes); - const betaHash = sha512(eHash); + // 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); 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"; From b2ca636c3c91357cbdabcafa709c20061238c62a Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 4 Mar 2026 23:17:03 -0600 Subject: [PATCH 18/22] include proof type name in FS transcript --- .../src/crypto/confidentialKeyRotation.ts | 7 ++++-- .../src/crypto/sigmaProtocol.ts | 24 ++++++++++++++++--- .../src/crypto/sigmaProtocolRegistration.ts | 7 ++++-- .../src/crypto/sigmaProtocolTransfer.ts | 6 ++++- .../src/crypto/sigmaProtocolWithdraw.ts | 6 ++++- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/confidential-assets/src/crypto/confidentialKeyRotation.ts b/confidential-assets/src/crypto/confidentialKeyRotation.ts index 69199970c..1bfbe847a 100644 --- a/confidential-assets/src/crypto/confidentialKeyRotation.ts +++ b/confidential-assets/src/crypto/confidentialKeyRotation.ts @@ -49,6 +49,9 @@ import { /** 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; @@ -231,7 +234,7 @@ export class ConfidentialKeyRotation { }; // Generate the proof - const proof: SigmaProtocolProof = sigmaProtocolProve(dst, makeKeyRotationPsi(numChunks), stmt, witness); + const proof: SigmaProtocolProof = sigmaProtocolProve(dst, TYPE_NAME, makeKeyRotationPsi(numChunks), stmt, witness); return { newEkBytes: compressedNewEk, @@ -300,6 +303,6 @@ export class ConfidentialKeyRotation { sessionId, }; - return sigmaProtocolVerify(dst, makeKeyRotationPsi(numChunks), makeKeyRotationF(numChunks), stmt, proof); + return sigmaProtocolVerify(dst, TYPE_NAME, makeKeyRotationPsi(numChunks), makeKeyRotationF(numChunks), stmt, proof); } } diff --git a/confidential-assets/src/crypto/sigmaProtocol.ts b/confidential-assets/src/crypto/sigmaProtocol.ts index e28b689f4..08f8ca71d 100644 --- a/confidential-assets/src/crypto/sigmaProtocol.ts +++ b/confidential-assets/src/crypto/sigmaProtocol.ts @@ -12,6 +12,7 @@ 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 "."; @@ -107,6 +108,7 @@ export interface SigmaProtocolStatement { * ```move * struct FiatShamirInputs { * dst: DomainSeparator, + * type_name: String, * k: u64, * stmt_X: vector, * stmt_x: vector, @@ -117,6 +119,7 @@ export interface SigmaProtocolStatement { 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[], @@ -127,6 +130,8 @@ class BcsFiatShamirInputs extends Serializable { 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); @@ -157,10 +162,14 @@ function scalarFromUniform64Bytes(hash: Uint8Array): bigint { /** * 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, @@ -168,7 +177,14 @@ export function sigmaProtocolFiatShamir( const m = compressedA.length; if (m === 0) throw new Error("Proof commitment must not be empty"); - const fiatShamirInputs = new BcsFiatShamirInputs(dst, k, stmt.compressedPoints, stmt.scalars, compressedA); + const fiatShamirInputs = new BcsFiatShamirInputs( + dst, + typeName, + k, + stmt.compressedPoints, + stmt.scalars, + compressedA, + ); const bytes = fiatShamirInputs.bcsToBytes(); // seed = SHA2-512(BCS(inputs)) @@ -239,6 +255,7 @@ export interface SigmaProtocolProof { */ export function sigmaProtocolProve( dst: DomainSeparator, + typeName: string, psi: PsiFunction, stmt: SigmaProtocolStatement, witness: bigint[], @@ -255,7 +272,7 @@ export function sigmaProtocolProve( const compressedA = _A.map((p) => p.toRawBytes()); // Step 4: Derive challenge e via Fiat-Shamir - const { e } = sigmaProtocolFiatShamir(dst, stmt, compressedA, k); + 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)); @@ -288,6 +305,7 @@ export function sigmaProtocolProve( */ export function sigmaProtocolVerify( dst: DomainSeparator, + typeName: string, psi: PsiFunction, f: TransformationFunction, stmt: SigmaProtocolStatement, @@ -303,7 +321,7 @@ export function sigmaProtocolVerify( const sigma = response.map((r) => bytesToNumberLE(r)); // Recompute the challenge e - const { e } = sigmaProtocolFiatShamir(dst, stmt, commitment, k); + const { e } = sigmaProtocolFiatShamir(dst, typeName, stmt, commitment, k); // Compute psi(sigma) - evaluating the homomorphism on the response const psiSigma = psi(stmt, sigma); diff --git a/confidential-assets/src/crypto/sigmaProtocolRegistration.ts b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts index 77d8ccf1a..51209abea 100644 --- a/confidential-assets/src/crypto/sigmaProtocolRegistration.ts +++ b/confidential-assets/src/crypto/sigmaProtocolRegistration.ts @@ -36,6 +36,9 @@ 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; @@ -112,7 +115,7 @@ export function proveRegistration(args: { sessionId, }; - return sigmaProtocolProve(dst, makeRegistrationPsi(), stmt, witness); + return sigmaProtocolProve(dst, TYPE_NAME, makeRegistrationPsi(), stmt, witness); } /** @@ -143,5 +146,5 @@ export function verifyRegistration(args: { sessionId, }; - return sigmaProtocolVerify(dst, makeRegistrationPsi(), makeRegistrationF(), stmt, proof); + 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 index ac8f3a784..ea3b1a5ee 100644 --- a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts +++ b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts @@ -37,6 +37,9 @@ 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 @@ -287,7 +290,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { sessionId, }; - return sigmaProtocolProve(dst, makeTransferPsi(ell, n, hasEffectiveAuditor, numExtra), stmt, witness); + return sigmaProtocolProve(dst, TYPE_NAME, makeTransferPsi(ell, n, hasEffectiveAuditor, numExtra), stmt, witness); } /** @@ -575,6 +578,7 @@ export function verifyTransfer(args: { return sigmaProtocolVerify( dst, + TYPE_NAME, makeTransferPsi(ell, n, hasEffectiveAuditor, numExtra), makeTransferF(ell, n, hasEffectiveAuditor, numExtra), stmt, diff --git a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts index d33b31beb..79fd896b5 100644 --- a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts +++ b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts @@ -41,6 +41,9 @@ 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 @@ -341,7 +344,7 @@ function proveWithdrawInternal( sessionId, }; - return sigmaProtocolProve(dst, makeWithdrawPsi(ell, hasAuditor), stmt, witness); + return sigmaProtocolProve(dst, TYPE_NAME, makeWithdrawPsi(ell, hasAuditor), stmt, witness); } /** @@ -488,6 +491,7 @@ function verifyWithdrawInternal( return sigmaProtocolVerify( dst, + TYPE_NAME, makeWithdrawPsi(ell, hasAuditor), makeWithdrawF(ell, hasAuditor, amount), stmt, From d2f28c625800b06b7ed4fb367229428f9fd12a61 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Thu, 5 Mar 2026 16:36:04 -0600 Subject: [PATCH 19/22] rename 'extra auditors' to 'voluntary auditors' --- .../src/crypto/sigmaProtocolTransfer.ts | 82 +++++++++---------- .../internal/confidentialAssetTxnBuilder.ts | 18 ++-- .../tests/e2e/confidentialAsset.test.ts | 65 +++++++++++---- .../tests/units/confidentialProofs.test.ts | 2 +- 4 files changed, 99 insertions(+), 68 deletions(-) diff --git a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts index ea3b1a5ee..2a71971a6 100644 --- a/confidential-assets/src/crypto/sigmaProtocolTransfer.ts +++ b/confidential-assets/src/crypto/sigmaProtocolTransfer.ts @@ -12,7 +12,7 @@ * 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 extra auditor: + [ek_extra, R_extra[n]] + * For each voluntary auditor: + [ek_volun, R_volun[n]] * * Witness: [dk, new_a[ell], new_r[ell], v[n], r[n]] */ @@ -50,7 +50,7 @@ const TYPE_NAME = "0x7::sigma_protocol_transfer::Transfer"; * num_avail_chunks: u64, * num_transfer_chunks: u64, * has_effective_auditor: bool, - * num_extra_auditors: u64, + * num_volun_auditors: u64, * } * ``` */ @@ -61,7 +61,7 @@ export function bcsSerializeTransferSession( numAvailChunks: number, numTransferChunks: number, hasEffectiveAuditor: boolean, - numExtraAuditors: number, + numVolunAuditors: number, ): Uint8Array { const serializer = new Serializer(); serializer.serialize(new FixedBytes(senderAddress)); @@ -70,7 +70,7 @@ export function bcsSerializeTransferSession( serializer.serialize(new U64(numAvailChunks)); serializer.serialize(new U64(numTransferChunks)); serializer.serializeBool(hasEffectiveAuditor); - serializer.serialize(new U64(numExtraAuditors)); + serializer.serialize(new U64(numVolunAuditors)); return serializer.toUint8Array(); } @@ -97,9 +97,9 @@ function computeBPowers(count: number): bigint[] { * + [ek_aud_eff, new_R_aud_eff[ell], R_aud_eff[n]] * → +1 + ell + n points * - * For each extra auditor i ∈ [num_extra]: - * + [ek_extra_i, R_extra_i[n]] - * → +(1 + n) points per extra + * 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; @@ -129,8 +129,8 @@ function getStartIdxRRid(ell: number, n: number): number { function getIdxEkAudEff(ell: number, n: number): number { return START_IDX_OLD_P + 4 * ell + 3 * n; } -/** Start of the extra auditors section. */ -function getStartIdxExtras(ell: number, n: number, hasEffective: boolean): number { +/** 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); } @@ -177,10 +177,10 @@ export type TransferProofArgs = { /** * 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 extra auditors. + * is the effective auditor; all preceding elements are voluntary auditors. */ hasEffectiveAuditor: boolean; - /** Auditor encryption keys: extras first, then effective (if hasEffectiveAuditor) */ + /** 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[][]; @@ -193,11 +193,11 @@ export type TransferProofArgs = { * * The sigma proof covers all auditors in a single proof. The statement layout * distinguishes between the effective auditor (sees balance + transfer amount) - * and extra auditors (see only 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 extras. + * elements are voluntary. */ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { const { @@ -227,7 +227,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { const ell = oldBalanceC.length; const n = transferAmountC.length; - const numExtra = hasEffectiveAuditor + const numVolun = hasEffectiveAuditor ? auditorEncryptionKeys.length - 1 : auditorEncryptionKeys.length; const dkBigint = bytesToNumberLE(dk.toUint8Array()); @@ -263,10 +263,10 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[effIdx][j]); } - // Extra auditors: for each, [ek_extra, R_extra[n]] - for (let a = 0; a < numExtra; a++) { - const ekExtraBytes = auditorEncryptionKeys[a].toUint8Array(); - pushPointBytes(RistrettoPoint.fromHex(ekExtraBytes), ekExtraBytes); + // 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]); } @@ -281,7 +281,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { // Domain separator const sessionId = bcsSerializeTransferSession( - senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numExtra, + senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numVolun, ); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, @@ -290,7 +290,7 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { sessionId, }; - return sigmaProtocolProve(dst, TYPE_NAME, makeTransferPsi(ell, n, hasEffectiveAuditor, numExtra), stmt, witness); + return sigmaProtocolProve(dst, TYPE_NAME, makeTransferPsi(ell, n, hasEffectiveAuditor, numVolun), stmt, witness); } /** @@ -306,9 +306,9 @@ export function proveTransfer(args: TransferProofArgs): SigmaProtocolProof { * 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_extra_t, ∀j ∈ [n], ∀t ∈ [T] (extra auditors) + * 7c. r[j]*ek_volun_t, ∀j ∈ [n], ∀t ∈ [T] (voluntary auditors) */ -function makeTransferPsi(ell: number, n: number, hasEffective: boolean, numExtra: number): PsiFunction { +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); @@ -383,13 +383,13 @@ function makeTransferPsi(ell: number, n: number, hasEffective: boolean, numExtra } } - // 7c. (extra auditors) r[j]*ek_extra_t - const extrasStart = getStartIdxExtras(ell, n, hasEffective); - for (let t = 0; t < numExtra; t++) { - const ekExtraIdx = extrasStart + t * (1 + n); - const ekExtra = s.points[ekExtraIdx]; + // 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(ekExtra.multiply(rTransfer[j])); + result.push(ekVolun.multiply(rTransfer[j])); } } @@ -402,7 +402,7 @@ function makeTransferPsi(ell: number, n: number, hasEffective: boolean, numExtra * * Matches the Move implementation ordering (mirrors psi with statement points). */ -function makeTransferF(ell: number, n: number, hasEffective: boolean, numExtra: number): TransformationFunction { +function makeTransferF(ell: number, n: number, hasEffective: boolean, numVolun: number): TransformationFunction { return (s: SigmaProtocolStatement): RistPoint[] => { const result: RistPoint[] = []; @@ -463,12 +463,12 @@ function makeTransferF(ell: number, n: number, hasEffective: boolean, numExtra: } } - // 7c. (extra auditors) R_extra_t[j] - const extrasStart = getStartIdxExtras(ell, n, hasEffective); - for (let t = 0; t < numExtra; t++) { - const rExtraStart = extrasStart + t * (1 + n) + 1; + // 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[rExtraStart + j]); + result.push(s.points[rVolunStart + j]); } } @@ -480,7 +480,7 @@ function makeTransferF(ell: number, n: number, hasEffective: boolean, numExtra: * Verify a confidential transfer proof. * * Convention: if hasEffectiveAuditor, the last element in auditorEkBytes / newBalanceDAud / - * transferAmountDAud is the effective auditor; preceding elements are extras. + * transferAmountDAud is the effective auditor; preceding elements are voluntary. */ export function verifyTransfer(args: { senderAddress: Uint8Array; @@ -525,7 +525,7 @@ export function verifyTransfer(args: { const ell = oldBalanceC.length; const n = transferAmountC.length; - const numExtra = hasEffectiveAuditor ? auditorEkBytes.length - 1 : auditorEkBytes.length; + const numVolun = hasEffectiveAuditor ? auditorEkBytes.length - 1 : auditorEkBytes.length; const G = RistrettoPoint.BASE; const H = H_RISTRETTO; @@ -554,8 +554,8 @@ export function verifyTransfer(args: { for (let j = 0; j < n; j++) pushPoint(transferAmountDAud[effIdx][j]); } - // Extra auditors: [ek_extra, R_extra[n]] - for (let a = 0; a < numExtra; a++) { + // 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]); } @@ -567,7 +567,7 @@ export function verifyTransfer(args: { }; const sessionId = bcsSerializeTransferSession( - senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numExtra, + senderAddress, recipientAddress, tokenAddress, ell, n, hasEffectiveAuditor, numVolun, ); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, @@ -579,8 +579,8 @@ export function verifyTransfer(args: { return sigmaProtocolVerify( dst, TYPE_NAME, - makeTransferPsi(ell, n, hasEffectiveAuditor, numExtra), - makeTransferF(ell, n, hasEffectiveAuditor, numExtra), + makeTransferPsi(ell, n, hasEffectiveAuditor, numVolun), + makeTransferF(ell, n, hasEffectiveAuditor, numVolun), stmt, proof, ); diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 4feb4fcce..65f7b5ef8 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -354,8 +354,8 @@ export class ConfidentialAssetTransactionBuilder { decryptionKey: senderDecryptionKey, }); - // Build the full auditor list for proof generation: [...extra, global (if set)] - // The contract will append the global auditor itself, so we only send extra auditor EKs on-chain. + // 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] : []), @@ -386,22 +386,22 @@ export class ConfidentialAssetTransactionBuilder { auditorNewBalanceList, ] = await confidentialTransfer.authorizeTransfer(); - // Only send extra auditor EKs on-chain (not the global auditor, which the contract fetches itself) - const extraAuditorEncryptionKeys = additionalAuditorEncryptionKeys.map((pk) => pk.toUint8Array()); + // 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 extra (remaining) + // 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 extraAuditorDPoints = (effectiveAuditorPubKey + 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 — extra auditors don't get new balance R components. + // 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()) : []; @@ -421,8 +421,8 @@ export class ConfidentialAssetTransactionBuilder { confidentialTransfer.transferAmountEncryptedBySender.getCipherText().map((ct) => ct.D.toRawBytes()), // sender_amount_D recipientDPoints, effectiveAuditorDPoints, - extraAuditorEncryptionKeys, - extraAuditorDPoints, + volunAuditorEncryptionKeys, + volunAuditorDPoints, rangeProofNewBalance, rangeProofAmount, sigmaProof.commitment, diff --git a/confidential-assets/tests/e2e/confidentialAsset.test.ts b/confidential-assets/tests/e2e/confidentialAsset.test.ts index d0e96a280..1a7262ecd 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, @@ -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,14 +390,14 @@ 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, @@ -376,11 +405,11 @@ describe("Confidential Asset Sender API", () => { // --- Auditor configuration tests --- - const EXTRA_AUDITOR = TwistedEd25519PrivateKey.generate(); + const VOLUNTARY_AUDITOR = TwistedEd25519PrivateKey.generate(); const EFFECTIVE_AUDITOR = TwistedEd25519PrivateKey.generate(); test( - "it should transfer with extra auditor only (no effective auditor on-chain)", + "it should transfer with voluntary auditor only (no effective auditor on-chain)", async () => { const confidentialBalance = await confidentialAsset.getBalance({ accountAddress: alice.accountAddress, @@ -393,15 +422,15 @@ describe("Confidential Asset Sender API", () => { amount: TRANSFER_AMOUNT, signer: alice, tokenAddress: TOKEN_ADDRESS, - recipient: alice.accountAddress, - additionalAuditorEncryptionKeys: [EXTRA_AUDITOR.publicKey()], + recipient: bob.accountAddress, + additionalAuditorEncryptionKeys: [VOLUNTARY_AUDITOR.publicKey()], }); expect(transferTx.success).toBeTruthy(); await checkAliceDecryptedBalance( confidentialBalance.availableBalance() - TRANSFER_AMOUNT, - confidentialBalance.pendingBalance() + TRANSFER_AMOUNT, + confidentialBalance.pendingBalance(), ); }, longTestTimeout, @@ -411,8 +440,8 @@ describe("Confidential Asset Sender API", () => { // 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 extras) - // - "effective + extra auditors" (set global auditor, transfer with extra auditors) + // - "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 that Alice's incoming transfers are not paused", @@ -428,11 +457,12 @@ describe("Confidential Asset Sender API", () => { ); test( - "it should throw if checking whether Bob's incoming transfers are paused and he has no registered balance", + "it should throw if checking whether an unregistered account's incoming transfers are paused", async () => { + const unregistered = Account.generate(); await expect( confidentialAsset.isIncomingTransfersPaused({ - accountAddress: bob.accountAddress, + accountAddress: unregistered.accountAddress, tokenAddress: TOKEN_ADDRESS, }), ).rejects.toThrow("E_CONFIDENTIAL_STORE_NOT_REGISTERED"); @@ -506,10 +536,11 @@ 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("E_CONFIDENTIAL_STORE_NOT_REGISTERED"); }, @@ -575,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(); diff --git a/confidential-assets/tests/units/confidentialProofs.test.ts b/confidential-assets/tests/units/confidentialProofs.test.ts index 928efba59..359d61749 100644 --- a/confidential-assets/tests/units/confidentialProofs.test.ts +++ b/confidential-assets/tests/units/confidentialProofs.test.ts @@ -205,7 +205,7 @@ describe("Generate 'confidential coin' proofs", () => { test( "Verify transfer with auditors sigma proof", () => { - // This test uses an extra auditor (not an on-chain effective auditor) + // This test uses a voluntary auditor (not an on-chain effective auditor) const isValid = verifyTransfer({ senderAddress: dummySenderAddress, recipientAddress: dummyRecipientAddress, From a1ee60dc9a95c74f025d8496d0a51d44170cf674 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Thu, 5 Mar 2026 18:04:21 -0600 Subject: [PATCH 20/22] oops, fixed SDK DST issue and update README --- confidential-assets/README.md | 6 +++--- confidential-assets/src/crypto/sigmaProtocol.ts | 5 +++-- confidential-assets/src/crypto/sigmaProtocolWithdraw.ts | 8 +++++--- confidential-assets/vitest.config.ts | 3 +++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/confidential-assets/README.md b/confidential-assets/README.md index f22f4f48d..42646df92 100644 --- a/confidential-assets/README.md +++ b/confidential-assets/README.md @@ -94,7 +94,7 @@ pnpm test tests/e2e/ pnpm test decryption -pnpm jest tests/e2e/confidentialAsset.test.ts -t "rotate Alice" --runInBand +pnpm test tests/e2e/confidentialAsset.test.ts -t "rotate Alice" --runInBand ``` Or, run all tests: @@ -107,11 +107,11 @@ pnpm test ### Discrete log / decryption benchmarks ```bash -pnpm jest tests/units/discrete-log.test.ts +pnpm test tests/units/discrete-log.test.ts ``` ### Range proof tests ```bash -pnpm jest tests/units/confidentialProofs.test.ts +pnpm test tests/units/confidentialProofs.test.ts ``` diff --git a/confidential-assets/src/crypto/sigmaProtocol.ts b/confidential-assets/src/crypto/sigmaProtocol.ts index 08f8ca71d..8961d482d 100644 --- a/confidential-assets/src/crypto/sigmaProtocol.ts +++ b/confidential-assets/src/crypto/sigmaProtocol.ts @@ -33,7 +33,7 @@ export const APTOS_EXPERIMENTAL_ADDRESS = (() => { /** * Matches the Move `DomainSeparator` struct: * ```move - * struct DomainSeparator { contract_address: address, chain_id: u8, protocol_id: vector, session_id: vector } + * enum DomainSeparator { V1 { contract_address: address, chain_id: u8, protocol_id: vector, session_id: vector } } * ``` */ export interface DomainSeparator { @@ -44,7 +44,7 @@ export interface DomainSeparator { } /** - * BCS-serializable DomainSeparator. + * BCS-serializable DomainSeparator (enum with V1 variant). */ class BcsDomainSeparator extends Serializable { constructor(public readonly dst: DomainSeparator) { @@ -52,6 +52,7 @@ class BcsDomainSeparator extends Serializable { } 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); diff --git a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts index 79fd896b5..4eb3db6f8 100644 --- a/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts +++ b/confidential-assets/src/crypto/sigmaProtocolWithdraw.ts @@ -47,18 +47,20 @@ 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 } + * 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(); } @@ -336,7 +338,7 @@ function proveWithdrawInternal( const witness: bigint[] = [dkBigint, ...newAmountChunks, ...newRandomness]; // Build domain separator - const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell); + const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell, hasAuditor); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, chainId, @@ -481,7 +483,7 @@ function verifyWithdrawInternal( scalars: [vScalar], }; - const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell); + const sessionId = bcsSerializeWithdrawSession(senderAddress, tokenAddress, ell, hasAuditor); const dst: DomainSeparator = { contractAddress: APTOS_EXPERIMENTAL_ADDRESS, chainId, diff --git a/confidential-assets/vitest.config.ts b/confidential-assets/vitest.config.ts index 8e820aab8..733282c7d 100644 --- a/confidential-assets/vitest.config.ts +++ b/confidential-assets/vitest.config.ts @@ -6,6 +6,9 @@ export default defineConfig({ globals: true, environment: "node", setupFiles: [path.resolve(__dirname, "../tests/setupDotenv.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/**"], From 15e01f4d22bd43b811981152c947544b8292f9f8 Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Wed, 18 Mar 2026 18:40:02 -0500 Subject: [PATCH 21/22] update SDK to handle new EffectiveAuditorConfig (untested though) --- .../src/api/confidentialAsset.ts | 54 ------------- .../internal/confidentialAssetTxnBuilder.ts | 16 +++- .../src/internal/viewFunctions.ts | 76 ------------------- 3 files changed, 12 insertions(+), 134 deletions(-) diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index 822b9cea0..0c70e61ff 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -22,9 +22,6 @@ import { hasUserRegistered, isBalanceNormalized, isIncomingTransfersPaused, - getGlobalAuditorEpoch, - getAuditorEpochForAssetType, - getEffectiveAuditorEpoch, } from "../internal"; // Constants @@ -481,57 +478,6 @@ export class ConfidentialAsset { }); } - /** - * Get the global auditor epoch counter. - * - * @param args.options - Optional ledger version for the view call - * @returns The global auditor epoch - */ - async getGlobalAuditorEpoch(args?: { options?: LedgerVersionArg }): Promise { - return getGlobalAuditorEpoch({ - client: this.client(), - moduleAddress: this.moduleAddress(), - ...args, - }); - } - - /** - * Get the auditor epoch counter for a specific asset type. - * - * @param args.tokenAddress - The token address of the asset - * @param args.options - Optional ledger version for the view call - * @returns The asset-specific auditor epoch - */ - async getAuditorEpochForAssetType(args: { - tokenAddress: AccountAddressInput; - options?: LedgerVersionArg; - }): Promise { - return getAuditorEpochForAssetType({ - client: this.client(), - moduleAddress: this.moduleAddress(), - ...args, - }); - } - - /** - * Get the effective auditor epoch: asset-specific epoch if the asset has an auditor, - * otherwise global auditor epoch. - * - * @param args.tokenAddress - The token address of the asset - * @param args.options - Optional ledger version for the view call - * @returns The effective auditor epoch - */ - async getEffectiveAuditorEpoch(args: { - tokenAddress: AccountAddressInput; - options?: LedgerVersionArg; - }): Promise { - return getEffectiveAuditorEpoch({ - client: this.client(), - moduleAddress: this.moduleAddress(), - ...args, - }); - } - /** * Normalize a user's balance. * diff --git a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts index 65f7b5ef8..1600dff47 100644 --- a/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts +++ b/confidential-assets/src/internal/confidentialAssetTxnBuilder.ts @@ -268,17 +268,25 @@ export class ConfidentialAssetTransactionBuilder { tokenAddress: AccountAddressInput; options?: LedgerVersionArg; }): Promise { - const [{ vec: effectiveAuditorPubKey }] = await this.client.view<[{ vec: { data: string }[] }]>({ + // 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_effective_auditor`, + function: `${this.confidentialAssetModuleAddress}::${MODULE_NAME}::get_effective_auditor_config`, functionArguments: [args.tokenAddress], }, }); - if (effectiveAuditorPubKey.length === 0) { + if (config.ek.vec.length === 0) { return undefined; } - return new TwistedEd25519PublicKey(effectiveAuditorPubKey[0].data); + return new TwistedEd25519PublicKey(config.ek.vec[0].data); } /** diff --git a/confidential-assets/src/internal/viewFunctions.ts b/confidential-assets/src/internal/viewFunctions.ts index e556c559a..5e3b0d891 100644 --- a/confidential-assets/src/internal/viewFunctions.ts +++ b/confidential-assets/src/internal/viewFunctions.ts @@ -284,79 +284,3 @@ export async function getEncryptionKey( } } -type AuditorEpochViewFunctionParams = { - client: Aptos; - tokenAddress: AccountAddressInput; - options?: LedgerVersionArg; - moduleAddress?: string; -}; - -/** - * Get the global auditor epoch counter. - * - * @param args.client - The Aptos client instance - * @param args.options - Optional ledger version for the view call - * @param args.moduleAddress - Optional module address - * @returns The global auditor epoch as a number - */ -export async function getGlobalAuditorEpoch(args: { - client: Aptos; - options?: LedgerVersionArg; - moduleAddress?: string; -}): Promise { - const { client, options, moduleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS } = args; - const [epoch] = await client.view<[string]>({ - payload: { - function: `${moduleAddress}::${MODULE_NAME}::get_global_auditor_epoch`, - typeArguments: [], - functionArguments: [], - }, - options, - }); - return Number(epoch); -} - -/** - * Get the auditor epoch counter for a specific asset type. - * - * @param args.client - The Aptos client instance - * @param args.tokenAddress - The token address of the asset - * @param args.options - Optional ledger version for the view call - * @param args.moduleAddress - Optional module address - * @returns The asset-specific auditor epoch as a number - */ -export async function getAuditorEpochForAssetType(args: AuditorEpochViewFunctionParams): Promise { - const { client, tokenAddress, options, moduleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS } = args; - const [epoch] = await client.view<[string]>({ - payload: { - function: `${moduleAddress}::${MODULE_NAME}::get_auditor_epoch_for_asset_type`, - typeArguments: [], - functionArguments: [tokenAddress], - }, - options, - }); - return Number(epoch); -} - -/** - * Get the effective auditor epoch: asset-specific epoch if the asset has an auditor, - * otherwise global auditor epoch. - * - * @param args.client - The Aptos client instance - * @param args.tokenAddress - The token address of the asset - * @param args.options - Optional ledger version for the view call - * @param args.moduleAddress - Optional module address - * @returns The effective auditor epoch as a number - */ -export async function getEffectiveAuditorEpoch(args: AuditorEpochViewFunctionParams): Promise { - const { client, tokenAddress, options, moduleAddress = DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS } = args; - const [epoch] = await client.view<[string]>({ - payload: { - function: `${moduleAddress}::${MODULE_NAME}::get_effective_auditor_epoch`, - typeArguments: [], - functionArguments: [tokenAddress], - }, - options, - }); - return Number(epoch); -} From 04e45a4610eb408ce55870f065f74bdfb9db7c9e Mon Sep 17 00:00:00 2001 From: Alin Tomescu Date: Fri, 20 Mar 2026 15:28:05 -0500 Subject: [PATCH 22/22] add corresponding getEffectiveAuditorHint function --- .../src/api/confidentialAsset.ts | 22 +++++++++++ .../src/internal/viewFunctions.ts | 38 +++++++++++++++++++ .../tests/e2e/confidentialAsset.test.ts | 2 +- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/confidential-assets/src/api/confidentialAsset.ts b/confidential-assets/src/api/confidentialAsset.ts index 0c70e61ff..e308159e1 100644 --- a/confidential-assets/src/api/confidentialAsset.ts +++ b/confidential-assets/src/api/confidentialAsset.ts @@ -18,6 +18,7 @@ import { ConfidentialAssetTransactionBuilder, ConfidentialBalance, getBalance, + getEffectiveAuditorHint, getEncryptionKey, hasUserRegistered, isBalanceNormalized, @@ -478,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. * diff --git a/confidential-assets/src/internal/viewFunctions.ts b/confidential-assets/src/internal/viewFunctions.ts index 5e3b0d891..034293fad 100644 --- a/confidential-assets/src/internal/viewFunctions.ts +++ b/confidential-assets/src/internal/viewFunctions.ts @@ -257,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; diff --git a/confidential-assets/tests/e2e/confidentialAsset.test.ts b/confidential-assets/tests/e2e/confidentialAsset.test.ts index 1a7262ecd..975400a5a 100644 --- a/confidential-assets/tests/e2e/confidentialAsset.test.ts +++ b/confidential-assets/tests/e2e/confidentialAsset.test.ts @@ -436,7 +436,7 @@ describe("Confidential Asset Sender API", () => { longTestTimeout, ); - // Effective-auditor e2e tests require calling set_auditor_for_asset_type as the @aptos_framework (0x1) + // 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: