Skip to content

Commit e3987bf

Browse files
committed
add BSGS TS benches
1 parent a460af6 commit e3987bf

File tree

5 files changed

+591
-244
lines changed

5 files changed

+591
-244
lines changed

confidential-assets/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
statements: 50, // 95,
1111
},
1212
},
13+
collectCoverage: false,
1314
globalSetup: "../tests/preTest.cjs",
1415
globalTeardown: "../tests/postTest.cjs",
1516
};
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Copyright © Aptos Foundation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/**
5+
* Baby-Step Giant-Step (BSGS) algorithm for solving discrete logarithms.
6+
*
7+
* Given a point P = x * G, finds x in O(sqrt(n)) time using O(sqrt(n)) space,
8+
* where n = 2^bitWidth is the search space size.
9+
*/
10+
11+
import { RistrettoPoint } from "@noble/curves/ed25519";
12+
import { RistPoint } from "./twistedEd25519";
13+
14+
/**
15+
* BSGS table for a specific bit width.
16+
*/
17+
export interface BsgsTable {
18+
/** The bit width this table was created for */
19+
bitWidth: number;
20+
/** m = 2^(bitWidth/2), the number of baby steps */
21+
m: bigint;
22+
/** The baby-step table: maps point (as bigint) to index j */
23+
babySteps: Map<bigint, bigint>;
24+
/** Precomputed -m * G for giant steps */
25+
giantStep: RistPoint;
26+
}
27+
28+
/**
29+
* Convert bytes to a BigInt key for fast Map lookups.
30+
* Uses little-endian interpretation of the full 32 bytes.
31+
*/
32+
function bytesToBigInt(bytes: Uint8Array): bigint {
33+
let result = 0n;
34+
for (let i = bytes.length - 1; i >= 0; i--) {
35+
result = (result << 8n) | BigInt(bytes[i]);
36+
}
37+
return result;
38+
}
39+
40+
/**
41+
* Creates a BSGS table for solving DLPs up to the given bit width.
42+
*
43+
* Time complexity: O(2^(bitWidth/2))
44+
* Space complexity: O(2^(bitWidth/2))
45+
*
46+
* @param bitWidth - Maximum bit width of discrete logs to solve (must be even)
47+
* @returns The BSGS table
48+
*/
49+
export function createBsgsTable(bitWidth: number): BsgsTable {
50+
if (bitWidth <= 0) {
51+
throw new Error("bitWidth must be positive");
52+
}
53+
if (bitWidth % 2 !== 0) {
54+
throw new Error("bitWidth must be even for BSGS");
55+
}
56+
57+
const m = 1n << BigInt(bitWidth / 2);
58+
const babySteps = new Map<bigint, bigint>();
59+
60+
// Baby steps: compute j * G for j = 0, 1, ..., m-1 using only additions
61+
let current = RistrettoPoint.ZERO;
62+
const G = RistrettoPoint.BASE;
63+
64+
for (let j = 0n; j < m; j++) {
65+
const key = bytesToBigInt(current.toRawBytes());
66+
babySteps.set(key, j);
67+
current = current.add(G);
68+
}
69+
70+
// After the loop, current = m * G (no need to recompute with multiply)
71+
const giantStep = current.negate();
72+
73+
return {
74+
bitWidth,
75+
m,
76+
babySteps,
77+
giantStep,
78+
};
79+
}
80+
81+
/**
82+
* Solves the discrete log problem using BSGS.
83+
*
84+
* Given P = x * G, finds x where 0 <= x < 2^bitWidth.
85+
*
86+
* Time complexity: O(2^(bitWidth/2))
87+
*
88+
* @param point - The point P = x * G (as Uint8Array serialization)
89+
* @param table - The precomputed BSGS table
90+
* @returns The discrete log x, or null if not found
91+
*/
92+
export function solveDlpBsgs(point: Uint8Array, table: BsgsTable): bigint | null {
93+
// Handle zero point (identity) - check if in baby steps first
94+
const pointKey = bytesToBigInt(point);
95+
if (table.babySteps.has(pointKey)) {
96+
return table.babySteps.get(pointKey)!;
97+
}
98+
99+
const P = RistrettoPoint.fromHex(point);
100+
const { m, babySteps, giantStep } = table;
101+
102+
// Giant steps: for i = 1, 2, ..., m-1
103+
// Compute gamma = P + i * giantStep = P - i * m * G
104+
// If gamma is in babySteps at index j, then x = i * m + j
105+
let gamma = P.add(giantStep); // Start with i = 1
106+
107+
for (let i = 1n; i < m; i++) {
108+
const gammaKey = bytesToBigInt(gamma.toBytes());
109+
110+
if (babySteps.has(gammaKey)) {
111+
const j = babySteps.get(gammaKey)!;
112+
return i * m + j;
113+
}
114+
115+
gamma = gamma.add(giantStep);
116+
}
117+
118+
// Not found in search space
119+
return null;
120+
}
121+
122+
/**
123+
* BSGS solver class that manages multiple tables for different bit widths.
124+
*/
125+
export class BsgsSolver {
126+
private tables: Map<number, BsgsTable> = new Map();
127+
private initPromise: Promise<void> | undefined;
128+
129+
/**
130+
* Initialize tables for the specified bit widths.
131+
* @param bitWidths - Array of bit widths to precompute tables for (must be even)
132+
*/
133+
async initialize(bitWidths: number[]): Promise<void> {
134+
if (this.initPromise) {
135+
return this.initPromise;
136+
}
137+
138+
this.initPromise = (async () => {
139+
for (const bitWidth of bitWidths) {
140+
if (!this.tables.has(bitWidth)) {
141+
//console.log(`BSGS: Creating table for ${bitWidth}-bit DLPs...`);
142+
const startTime = performance.now();
143+
const table = createBsgsTable(bitWidth);
144+
const elapsed = performance.now() - startTime;
145+
//console.log(`BSGS: ${bitWidth}-bit table created in ${elapsed.toFixed(2)}ms (${table.babySteps.size} entries)`);
146+
this.tables.set(bitWidth, table);
147+
}
148+
}
149+
})();
150+
151+
return this.initPromise;
152+
}
153+
154+
/**
155+
* Solve DLP using the appropriate table.
156+
* Tries tables from smallest to largest bit width.
157+
*
158+
* @param point - The point P = x * G (as Uint8Array)
159+
* @returns The discrete log x
160+
* @throws If no solution found in any table
161+
*/
162+
solve(point: Uint8Array): bigint {
163+
// Sort tables by bit width (smallest first for efficiency)
164+
const sortedTables = [...this.tables.entries()].sort((a, b) => a[0] - b[0]);
165+
166+
for (const [bitWidth, table] of sortedTables) {
167+
const result = solveDlpBsgs(point, table);
168+
if (result !== null) {
169+
return result;
170+
}
171+
}
172+
173+
throw new Error("BSGS: No solution found in search space");
174+
}
175+
176+
/**
177+
* Check if a table exists for the given bit width.
178+
*/
179+
hasTable(bitWidth: number): boolean {
180+
return this.tables.has(bitWidth);
181+
}
182+
183+
/**
184+
* Get the table for a specific bit width.
185+
*/
186+
getTable(bitWidth: number): BsgsTable | undefined {
187+
return this.tables.get(bitWidth);
188+
}
189+
190+
/**
191+
* Clear all tables to free memory.
192+
*/
193+
clear(): void {
194+
this.tables.clear();
195+
this.initPromise = undefined;
196+
}
197+
}
198+
199+
/**
200+
* Creates a decryption function using BSGS that can be passed to
201+
* TwistedElGamal.setDecryptionFn().
202+
*
203+
* @param bitWidths - Bit widths to support (e.g., [16, 32])
204+
* @returns A decryption function compatible with TwistedElGamal
205+
*/
206+
export async function createBsgsDecryptionFn(
207+
bitWidths: number[],
208+
): Promise<(point: Uint8Array) => Promise<bigint>> {
209+
const solver = new BsgsSolver();
210+
await solver.initialize(bitWidths);
211+
212+
return async (point: Uint8Array): Promise<bigint> => {
213+
// Check for zero point
214+
const isZero = point.every((b) => b === 0);
215+
if (isZero) return 0n;
216+
217+
return solver.solve(point);
218+
};
219+
}

confidential-assets/src/crypto/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./twistedEd25519";
22
export * from "./twistedElGamal";
3+
export * from "./bsgs";
34
export * from "./rangeProof";
45
export * from "./chunkedAmount";
56
export * from "./encryptedAmount";

0 commit comments

Comments
 (0)