11import { EncryptedAmount , TwistedEd25519PrivateKey , TwistedElGamal } from "../../src" ;
2+ import crypto from "crypto" ;
3+
4+ const BENCHMARK_ITERATIONS = 10 ;
25
36function 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