Skip to content

Commit a460af6

Browse files
committed
fix decryption time benchmarks
1 parent a61f56c commit a460af6

File tree

2 files changed

+178
-100
lines changed

2 files changed

+178
-100
lines changed

confidential-assets/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,11 @@ Or, run all tests:
1717
```
1818
pnpm test
1919
```
20+
21+
## Useful tests to know about
22+
23+
### Decryption times
24+
25+
```
26+
pnpm jest tests/units/kangaroo-decryption.test.ts
27+
```
Lines changed: 170 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,103 @@
11
import { EncryptedAmount, TwistedEd25519PrivateKey, TwistedElGamal } from "../../src";
2+
import crypto from "crypto";
3+
4+
const BENCHMARK_ITERATIONS = 10;
25

36
function generateRandomInteger(bits: number): bigint {
4-
// eslint-disable-next-line no-bitwise
5-
const max = (1n << BigInt(bits)) - 1n;
6-
const randomValue = BigInt(Math.floor(Math.random() * (Number(max) + 1)));
7+
if (bits <= 0) return 0n;
78

8-
return randomValue;
9-
}
9+
const bytes = Math.ceil(bits / 8);
10+
const randomBytes = crypto.getRandomValues(new Uint8Array(bytes));
1011

11-
const executionSimple = async (
12-
bitsAmount: number,
13-
length = 50,
14-
): Promise<{ randBalances: bigint[]; results: { result: bigint; elapsedTime: number }[] }> => {
15-
const randBalances = Array.from({ length }, () => generateRandomInteger(bitsAmount));
12+
let result = 0n;
13+
for (let i = 0; i < bytes; i++) {
14+
result = (result << 8n) | BigInt(randomBytes[i]);
15+
}
1616

17-
const decryptedAmounts: { result: bigint; elapsedTime: number }[] = [];
17+
// Mask to the requested bit size
18+
return result & ((1n << BigInt(bits)) - 1n);
19+
}
1820

19-
for (const balance of randBalances) {
20-
const newAlice = TwistedEd25519PrivateKey.generate();
21+
/**
22+
* Split a nonnegative integer v into base-(2^radix_decomp_bits) digits, then greedily
23+
* "borrow" from higher digits to maximize each lower chunk while keeping each
24+
* chunk < 2^bits_per_chunk.
25+
*/
26+
function maximalRadixChunks(
27+
v: bigint,
28+
radix_decomp_bits: number,
29+
v_max_bits: number,
30+
bits_per_chunk: number
31+
): bigint[] {
32+
if (radix_decomp_bits <= 0) throw new Error("radix_decomp_bits must be > 0");
33+
if (v_max_bits <= 0) throw new Error("v_max_bits must be > 0");
34+
if (bits_per_chunk <= 0) throw new Error("bits_per_chunk must be > 0");
35+
if (v_max_bits % radix_decomp_bits !== 0) {
36+
throw new Error("v_max_bits must be a multiple of radix_decomp_bits");
37+
}
38+
if (bits_per_chunk < radix_decomp_bits) {
39+
throw new Error("bits_per_chunk must be >= radix_decomp_bits");
40+
}
41+
if (v < 0n) throw new Error("v must be nonnegative");
2142

22-
const encryptedBalance = TwistedElGamal.encryptWithPK(balance, newAlice.publicKey());
43+
const ell = v_max_bits / radix_decomp_bits;
2344

24-
const startMainTime = performance.now();
25-
const decryptedBalance = await TwistedElGamal.decryptWithPK(encryptedBalance, newAlice);
26-
const endMainTime = performance.now();
45+
const RADIX = 1n << BigInt(radix_decomp_bits); // B
46+
const DIGIT_MASK = RADIX - 1n;
47+
const CHUNK_LIM = (1n << BigInt(bits_per_chunk)) - 1n;
2748

28-
const elapsedMainTime = endMainTime - startMainTime;
49+
const V_MAX = 1n << BigInt(v_max_bits);
50+
if (v >= V_MAX) throw new Error("v does not fit in v_max_bits");
2951

30-
decryptedAmounts.push({ result: decryptedBalance, elapsedTime: elapsedMainTime });
52+
// 1) canonical base-B digits
53+
const w: bigint[] = new Array(ell);
54+
for (let i = 0; i < ell; i++) {
55+
const shift = BigInt(i * radix_decomp_bits);
56+
w[i] = (v >> shift) & DIGIT_MASK;
3157
}
3258

33-
const averageTime = decryptedAmounts.reduce((acc, { elapsedTime }) => acc + elapsedTime, 0) / decryptedAmounts.length;
34-
35-
const lowestTime = decryptedAmounts.reduce((acc, { elapsedTime }) => Math.min(acc, elapsedTime), Infinity);
36-
const highestTime = decryptedAmounts.reduce((acc, { elapsedTime }) => Math.max(acc, elapsedTime), 0);
59+
// 2) greedy borrowing to maximize each w[i]
60+
// Keep iterating until no more borrowing is possible.
61+
let changed = true;
62+
while (changed) {
63+
changed = false;
64+
for (let i = 0; i < ell - 1; i++) {
65+
const room = CHUNK_LIM - w[i];
66+
if (room < RADIX) continue; // Can't fit another full RADIX unit
67+
68+
const tCap = room / RADIX; // floor(room / B)
69+
const t = w[i + 1] < tCap ? w[i + 1] : tCap;
70+
71+
if (t > 0n) {
72+
w[i] += t * RADIX;
73+
w[i + 1] -= t;
74+
changed = true;
75+
}
76+
}
77+
}
3778

38-
console.log(
39-
`Pollard kangaroo(table ${bitsAmount}):\n`,
40-
`Average time: ${averageTime} ms\n`,
41-
`Lowest time: ${lowestTime} ms\n`,
42-
`Highest time: ${highestTime} ms`,
43-
);
79+
return w;
80+
}
4481

45-
return {
46-
randBalances,
47-
results: decryptedAmounts,
48-
};
49-
};
82+
/** Recompose value from chunks: Σ (2^radix_decomp_bits)^i * chunks[i] */
83+
function recompose(chunks: bigint[], radix_decomp_bits: number): bigint {
84+
const RADIX = 1n << BigInt(radix_decomp_bits);
85+
let acc = 0n;
86+
let pow = 1n;
87+
for (let i = 0; i < chunks.length; i++) {
88+
acc += chunks[i] * pow;
89+
pow *= RADIX;
90+
}
91+
return acc;
92+
}
5093

51-
const executionBalance = async (
94+
/**
95+
* Benchmarks decryption of a SINGLE twisted ElGamal ciphertext element.
96+
* This tests the raw Pollard Kangaroo DLP performance for the given bit size.
97+
*/
98+
const benchmarkSingleElementDecryption = async (
5299
bitsAmount: number,
53-
length = 50,
100+
length = BENCHMARK_ITERATIONS,
54101
): Promise<{ randBalances: bigint[]; results: { result: bigint; elapsedTime: number }[] }> => {
55102
const randBalances = Array.from({ length }, () => generateRandomInteger(bitsAmount));
56103

@@ -59,14 +106,10 @@ const executionBalance = async (
59106
for (const balance of randBalances) {
60107
const newAlice = TwistedEd25519PrivateKey.generate();
61108

62-
const startMainTime = performance.now();
63-
const decryptedBalance = (
64-
await EncryptedAmount.fromAmountAndPublicKey({
65-
amount: balance,
66-
publicKey: newAlice.publicKey(),
67-
})
68-
).getAmount();
109+
const encryptedBalance = TwistedElGamal.encryptWithPK(balance, newAlice.publicKey());
69110

111+
const startMainTime = performance.now();
112+
const decryptedBalance = await TwistedElGamal.decryptWithPK(encryptedBalance, newAlice);
70113
const endMainTime = performance.now();
71114

72115
const elapsedMainTime = endMainTime - startMainTime;
@@ -80,11 +123,10 @@ const executionBalance = async (
80123
const highestTime = decryptedAmounts.reduce((acc, { elapsedTime }) => Math.max(acc, elapsedTime), 0);
81124

82125
console.log(
83-
`Pollard kangaroo(balance: ${bitsAmount}):\n`,
84-
`Average time: ${averageTime} ms\n`,
85-
`Lowest time: ${lowestTime} ms\n`,
86-
`Highest time: ${highestTime} ms`,
87-
// decryptedAmounts,
126+
`Single element decryption (${bitsAmount}-bit):\n`,
127+
`Average time: ${averageTime.toFixed(2)} ms\n`,
128+
`Lowest time: ${lowestTime.toFixed(2)} ms\n`,
129+
`Highest time: ${highestTime.toFixed(2)} ms`,
88130
);
89131

90132
return {
@@ -93,82 +135,110 @@ const executionBalance = async (
93135
};
94136
};
95137

96-
describe("decrypt amount", () => {
97-
it.skip("kangarooWasmAll(16): Should decrypt 50 rand numbers", async () => {
98-
console.log("WASM:");
138+
describe("Pollard Kangaroo decryption benchmarks", () => {
139+
// Initialize kangaroo tables before running benchmarks to avoid
140+
// counting table computation time in the first iteration
141+
beforeAll(async () => {
142+
await TwistedElGamal.initializeKangaroos();
143+
}, 30000);
99144

100-
const { randBalances, results } = await executionSimple(16);
145+
describe("Single element decryption (one DLP per value)", () => {
146+
it(`16-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values`, async () => {
147+
const { randBalances, results } = await benchmarkSingleElementDecryption(16);
101148

102-
results.forEach(({ result }, i) => {
103-
expect(result).toEqual(randBalances[i]);
149+
results.forEach(({ result }, i) => {
150+
expect(result).toEqual(randBalances[i]);
151+
});
104152
});
105-
});
106-
107-
it.skip("kangarooWasmAll(32): Should decrypt 50 rand numbers", async () => {
108-
console.log("WASM:");
109153

110-
const { randBalances, results } = await executionSimple(32);
154+
it(`32-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values`, async () => {
155+
const { randBalances, results } = await benchmarkSingleElementDecryption(32);
111156

112-
results.forEach(({ result }, i) => {
113-
expect(result).toEqual(randBalances[i]);
157+
results.forEach(({ result }, i) => {
158+
expect(result).toEqual(randBalances[i]);
159+
});
114160
});
115-
});
116-
117-
it.skip("kangarooWasmAll(48): Should decrypt 50 rand numbers", async () => {
118-
console.log("WASM:");
119161

120-
const { randBalances, results } = await executionSimple(48);
162+
it.skip(`48-bit: Should decrypt ${BENCHMARK_ITERATIONS} random values (slow)`, async () => {
163+
const { randBalances, results } = await benchmarkSingleElementDecryption(48);
121164

122-
results.forEach(({ result }, i) => {
123-
expect(result).toEqual(randBalances[i]);
165+
results.forEach(({ result }, i) => {
166+
expect(result).toEqual(randBalances[i]);
167+
});
124168
});
125169
});
126170

127-
it("kangarooWasmAll(16): Should decrypt 50 rand numbers", async () => {
128-
const { randBalances, results } = await executionBalance(16);
129-
130-
results.forEach(({ result }, i) => {
131-
expect(result).toEqual(randBalances[i]);
171+
describe("maximalRadixChunks", () => {
172+
it("correctly decomposes and recomposes random 128-bit values", () => {
173+
const v = generateRandomInteger(128);
174+
175+
const chunks = maximalRadixChunks(v, 16, 128, 32);
176+
177+
const vBitWidth = v === 0n ? 0 : v.toString(2).length;
178+
const bitWidths = chunks.map((c) => (c === 0n ? 0 : c.toString(2).length));
179+
console.log(
180+
`v=${v} (${vBitWidth} bits), num chunks=${chunks.length}, chunk bit widths=[${bitWidths.join(", ")}]`,
181+
);
182+
183+
// length: 128 / 16 = 8 chunks
184+
expect(chunks.length).toBe(8);
185+
186+
// each chunk fits in 32 bits
187+
for (const c of chunks) {
188+
expect(c >= 0n).toBe(true);
189+
expect(c < 1n << 32n).toBe(true);
190+
}
191+
192+
// recomposition correctness
193+
expect(recompose(chunks, 16)).toBe(v);
194+
195+
// local maximality:
196+
// for each i < ell-1, either saturated or no borrow left
197+
for (let i = 0; i < 7; i++) {
198+
const saturated = chunks[i] + (1n << 16n) > (1n << 32n) - 1n;
199+
const noBorrow = chunks[i + 1] === 0n;
200+
expect(saturated || noBorrow).toBe(true);
201+
}
132202
});
133-
});
134203

135-
it("kangarooWasmAll(32): Should decrypt 50 rand numbers", async () => {
136-
const { randBalances, results } = await executionBalance(32);
204+
const testDecomposition = (v: bigint, vMaxBits: number) => {
205+
const chunks = maximalRadixChunks(v, 16, vMaxBits, 32);
137206

138-
results.forEach(({ result }, i) => {
139-
expect(result).toEqual(randBalances[i]);
140-
});
141-
});
207+
const vBitWidth = v === 0n ? 0 : v.toString(2).length;
208+
const bitWidths = chunks.map((c) => (c === 0n ? 0 : c.toString(2).length));
209+
console.log(
210+
`v=${v} (${vBitWidth} bits), num chunks=${chunks.length}, chunk bit widths=[${bitWidths.join(", ")}]`,
211+
);
142212

143-
it("kangarooWasmAll(48): Should decrypt 50 rand numbers", async () => {
144-
const { randBalances, results } = await executionBalance(48);
213+
expect(recompose(chunks, 16)).toBe(v);
214+
};
145215

146-
results.forEach(({ result }, i) => {
147-
expect(result).toEqual(randBalances[i]);
216+
it("handles zero correctly", () => {
217+
testDecomposition(0n, 128);
148218
});
149-
});
150219

151-
it("kangarooWasmAll(64): Should decrypt 50 rand numbers", async () => {
152-
const { randBalances, results } = await executionBalance(64);
220+
it("handles 32-bit values correctly", () => {
221+
testDecomposition(generateRandomInteger(32), 32);
222+
});
153223

154-
results.forEach(({ result }, i) => {
155-
expect(result).toEqual(randBalances[i]);
224+
it("handles 48-bit values correctly", () => {
225+
testDecomposition(generateRandomInteger(48), 48);
156226
});
157-
});
158227

159-
it("kangarooWasmAll(96): Should decrypt 50 rand numbers", async () => {
160-
const { randBalances, results } = await executionBalance(96);
228+
it("handles 64-bit values correctly", () => {
229+
testDecomposition(generateRandomInteger(64), 64);
230+
});
161231

162-
results.forEach(({ result }, i) => {
163-
expect(result).toEqual(randBalances[i]);
232+
it("handles 96-bit values correctly", () => {
233+
testDecomposition(generateRandomInteger(96), 96);
164234
});
165-
});
166235

167-
it("kangarooWasmAll(128): Should decrypt 50 rand numbers", async () => {
168-
const { randBalances, results } = await executionBalance(128);
236+
it("handles the maximum 128-bit value correctly", () => {
237+
testDecomposition((1n << 128n) - 1n, 128);
238+
});
169239

170-
results.forEach(({ result }, i) => {
171-
expect(result).toEqual(randBalances[i]);
240+
it("throws if v does not fit in v_max_bits", () => {
241+
expect(() => maximalRadixChunks(1n << 128n, 16, 128, 32)).toThrow();
172242
});
173243
});
174-
});
244+
});

0 commit comments

Comments
 (0)