@@ -95,114 +95,104 @@ class LibsignalStore implements SignalStorage {
9595}
9696
9797// ============================================================================
98- // Setup for Rust WASM benchmarks
98+ // Setup helpers — each bench group gets its own fresh session pair so the
99+ // ratchet doesn't drift past libsignal's MAX_FORWARD_JUMPS (25_000) when one
100+ // bench advances the chain solo (e.g. an Encrypt-only bench) before Decrypt
101+ // has a chance to catch up.
99102// ============================================================================
100- const aliceStorage = new FakeStorage ( ) ;
101- const bobStorage = new FakeStorage ( ) ;
102103
103- const wasmBobAddress = new ProtocolAddress ( "bob" , 1 ) ;
104- const wasmAliceAddress = new ProtocolAddress ( "alice" , 1 ) ;
105-
106- // Trust each other's identities
107- aliceStorage . trustIdentity ( "bob" , bobStorage . ourIdentityKeyPair . pubKey ) ;
108- bobStorage . trustIdentity ( "alice" , aliceStorage . ourIdentityKeyPair . pubKey ) ;
109-
110- // Generate Bob's pre-keys for WASM
111- const bobSignedPreKeyId = 1 ;
112- const bobSignedPreKey = generateSignedPreKey (
113- bobStorage . ourIdentityKeyPair ,
114- bobSignedPreKeyId ,
115- ) ;
116- const bobOneTimePreKey = generatePreKey ( 100 ) ;
117-
118- bobStorage . storeSignedPreKey ( bobSignedPreKey . keyId , bobSignedPreKey ) ;
119- bobStorage . storePreKey ( bobOneTimePreKey . keyId , bobOneTimePreKey . keyPair ) ;
120-
121- const bobBundle = {
122- registrationId : bobStorage . ourRegistrationId ,
123- identityKey : bobStorage . ourIdentityKeyPair . pubKey ,
124- signedPreKey : {
125- keyId : bobSignedPreKey . keyId ,
126- publicKey : bobSignedPreKey . keyPair . pubKey ,
127- signature : bobSignedPreKey . signature ,
128- } ,
129- preKey : {
130- keyId : bobOneTimePreKey . keyId ,
131- publicKey : bobOneTimePreKey . keyPair . pubKey ,
132- } ,
104+ type WasmPair = {
105+ alice : SessionCipher ;
106+ bob : SessionCipher ;
133107} ;
134108
135- // Alice processes Bob's bundle and establishes session
136- const aliceSessionBuilder = new SessionBuilder ( aliceStorage , wasmBobAddress ) ;
137- await aliceSessionBuilder . processPreKeyBundle ( bobBundle ) ;
138-
139- const aliceCipher = new SessionCipher ( aliceStorage , wasmBobAddress ) ;
140- const bobCipher = new SessionCipher ( bobStorage , wasmAliceAddress ) ;
109+ async function makeWasmPair ( ) : Promise < WasmPair > {
110+ const aliceStorage = new FakeStorage ( ) ;
111+ const bobStorage = new FakeStorage ( ) ;
112+ const bobAddr = new ProtocolAddress ( "bob" , 1 ) ;
113+ const aliceAddr = new ProtocolAddress ( "alice" , 1 ) ;
114+
115+ aliceStorage . trustIdentity ( "bob" , bobStorage . ourIdentityKeyPair . pubKey ) ;
116+ bobStorage . trustIdentity ( "alice" , aliceStorage . ourIdentityKeyPair . pubKey ) ;
117+
118+ const sk = generateSignedPreKey ( bobStorage . ourIdentityKeyPair , 1 ) ;
119+ const pk = generatePreKey ( 100 ) ;
120+ bobStorage . storeSignedPreKey ( sk . keyId , sk ) ;
121+ bobStorage . storePreKey ( pk . keyId , pk . keyPair ) ;
122+
123+ await new SessionBuilder ( aliceStorage , bobAddr ) . processPreKeyBundle ( {
124+ registrationId : bobStorage . ourRegistrationId ,
125+ identityKey : bobStorage . ourIdentityKeyPair . pubKey ,
126+ signedPreKey : {
127+ keyId : sk . keyId ,
128+ publicKey : sk . keyPair . pubKey ,
129+ signature : sk . signature ,
130+ } ,
131+ preKey : { keyId : pk . keyId , publicKey : pk . keyPair . pubKey } ,
132+ } ) ;
141133
142- // ============================================================================
143- // Setup for libsignal-node benchmarks
144- // ============================================================================
145- const aliceLibsignalStorage = new LibsignalStore ( ) ;
146- const bobLibsignalStorage = new LibsignalStore ( ) ;
134+ const alice = new SessionCipher ( aliceStorage , bobAddr ) ;
135+ const bob = new SessionCipher ( bobStorage , aliceAddr ) ;
147136
148- const libsignalBobAddress = new libsignalNode . ProtocolAddress ( "bob" , 1 ) ;
149- const libsignalAliceAddress = new libsignalNode . ProtocolAddress ( "alice" , 1 ) ;
137+ // PreKey handshake + reply so both sides have established sessions
138+ const first = await alice . encrypt ( Buffer . from ( "hi" ) ) ;
139+ await bob . decryptPreKeyWhisperMessage ( first . body ) ;
140+ const reply = await bob . encrypt ( Buffer . from ( "ack" ) ) ;
141+ await alice . decryptWhisperMessage ( reply . body ) ;
150142
151- // Trust each other's identities
152- aliceLibsignalStorage . trustIdentity (
153- "bob" ,
154- Buffer . from ( bobLibsignalStorage . ourIdentityKeyPair . pubKey ) ,
155- ) ;
156- bobLibsignalStorage . trustIdentity (
157- "alice" ,
158- Buffer . from ( aliceLibsignalStorage . ourIdentityKeyPair . pubKey ) ,
159- ) ;
143+ return { alice, bob } ;
144+ }
160145
161- // Generate Bob's pre-keys for libsignal-node
162- const bobLibSignedPreKey = libsignalKeyHelper . generateSignedPreKey (
163- bobLibsignalStorage . ourIdentityKeyPair ,
164- bobSignedPreKeyId ,
165- ) ;
166- const bobLibOneTimePreKey = libsignalKeyHelper . generatePreKey ( 100 ) ;
146+ type LibPair = {
147+ alice : InstanceType < typeof libsignalNode . SessionCipher > ;
148+ bob : InstanceType < typeof libsignalNode . SessionCipher > ;
149+ } ;
167150
168- bobLibsignalStorage . storeSignedPreKey (
169- bobLibSignedPreKey . keyId ,
170- bobLibSignedPreKey ,
171- ) ;
172- bobLibsignalStorage . storePreKey (
173- bobLibOneTimePreKey . keyId ,
174- bobLibOneTimePreKey . keyPair ,
175- ) ;
151+ async function makeLibPair ( ) : Promise < LibPair > {
152+ const aliceStorage = new LibsignalStore ( ) ;
153+ const bobStorage = new LibsignalStore ( ) ;
154+ const bobAddr = new libsignalNode . ProtocolAddress ( "bob" , 1 ) ;
155+ const aliceAddr = new libsignalNode . ProtocolAddress ( "alice" , 1 ) ;
156+
157+ aliceStorage . trustIdentity (
158+ "bob" ,
159+ Buffer . from ( bobStorage . ourIdentityKeyPair . pubKey ) ,
160+ ) ;
161+ bobStorage . trustIdentity (
162+ "alice" ,
163+ Buffer . from ( aliceStorage . ourIdentityKeyPair . pubKey ) ,
164+ ) ;
165+
166+ const sk = libsignalKeyHelper . generateSignedPreKey (
167+ bobStorage . ourIdentityKeyPair ,
168+ 1 ,
169+ ) ;
170+ const pk = libsignalKeyHelper . generatePreKey ( 100 ) ;
171+ bobStorage . storeSignedPreKey ( sk . keyId , sk ) ;
172+ bobStorage . storePreKey ( pk . keyId , pk . keyPair ) ;
173+
174+ const builder = new libsignalNode . SessionBuilder ( aliceStorage , bobAddr ) ;
175+ await builder . initOutgoing ( {
176+ registrationId : bobStorage . ourRegistrationId ,
177+ identityKey : bobStorage . ourIdentityKeyPair . pubKey ,
178+ signedPreKey : {
179+ keyId : sk . keyId ,
180+ publicKey : sk . keyPair . pubKey ,
181+ signature : sk . signature ,
182+ } ,
183+ preKey : { keyId : pk . keyId , publicKey : pk . keyPair . pubKey } ,
184+ } ) ;
176185
177- const bobLibsignalBundle = {
178- registrationId : bobLibsignalStorage . ourRegistrationId ,
179- identityKey : bobLibsignalStorage . ourIdentityKeyPair . pubKey ,
180- signedPreKey : {
181- keyId : bobLibSignedPreKey . keyId ,
182- publicKey : bobLibSignedPreKey . keyPair . pubKey ,
183- signature : bobLibSignedPreKey . signature ,
184- } ,
185- preKey : {
186- keyId : bobLibOneTimePreKey . keyId ,
187- publicKey : bobLibOneTimePreKey . keyPair . pubKey ,
188- } ,
189- } ;
186+ const alice = new libsignalNode . SessionCipher ( aliceStorage , bobAddr ) ;
187+ const bob = new libsignalNode . SessionCipher ( bobStorage , aliceAddr ) ;
190188
191- // Alice processes Bob's bundle and establishes session
192- const aliceLibsignalBuilder = new libsignalNode . SessionBuilder (
193- aliceLibsignalStorage ,
194- libsignalBobAddress ,
195- ) ;
196- await aliceLibsignalBuilder . initOutgoing ( bobLibsignalBundle ) ;
189+ const first = await alice . encrypt ( Buffer . from ( "hi" ) ) ;
190+ await bob . decryptPreKeyWhisperMessage ( first . body as unknown as Uint8Array ) ;
191+ const reply = await bob . encrypt ( Buffer . from ( "ack" ) ) ;
192+ await alice . decryptWhisperMessage ( reply . body as unknown as Uint8Array ) ;
197193
198- const aliceLibsignalCipher = new libsignalNode . SessionCipher (
199- aliceLibsignalStorage ,
200- libsignalBobAddress ,
201- ) ;
202- const bobLibsignalCipher = new libsignalNode . SessionCipher (
203- bobLibsignalStorage ,
204- libsignalAliceAddress ,
205- ) ;
194+ return { alice, bob } ;
195+ }
206196
207197// ============================================================================
208198// Test Messages
@@ -215,95 +205,92 @@ const typicalMessage = Buffer.from(
215205 ) ,
216206) ;
217207
218- // ============================================================================
219- // Establish bidirectional sessions (required for decrypt benchmarks)
220- // ============================================================================
221-
222- // WASM: Alice sends first message (PreKeyWhisperMessage)
223- const wasmFirstEncrypted = await aliceCipher . encrypt ( typicalMessage ) ;
224- // Bob decrypts first message - this establishes Bob's session with Alice
225- await bobCipher . decryptPreKeyWhisperMessage ( wasmFirstEncrypted . body ) ;
226- // Bob replies to Alice - this completes the ratchet setup
227- const wasmBobReply = await bobCipher . encrypt ( typicalMessage ) ;
228- // Alice decrypts Bob's reply - now both sides have established sessions
229- await aliceCipher . decryptWhisperMessage ( wasmBobReply . body ) ;
230-
231- // libsignal-node: Same pattern
232- const libsignalFirstEncrypted =
233- await aliceLibsignalCipher . encrypt ( typicalMessage ) ;
234- await bobLibsignalCipher . decryptPreKeyWhisperMessage (
235- libsignalFirstEncrypted . body as unknown as Uint8Array ,
236- ) ;
237- const libsignalBobReply = await bobLibsignalCipher . encrypt ( typicalMessage ) ;
238- await aliceLibsignalCipher . decryptWhisperMessage (
239- libsignalBobReply . body as unknown as Uint8Array ,
240- ) ;
241-
242- // Now both Alice and Bob have fully established sessions in both libraries
208+ // Each bench group below gets its own fresh session pair. This prevents one
209+ // bench (e.g. Encrypt) from advancing the ratchet past the
210+ // MAX_FORWARD_JUMPS=25_000 threshold and breaking subsequent Decrypt benches.
243211
244212// ============================================================================
245- // Benchmarks
213+ // Encrypt benchmarks
246214// ============================================================================
215+ const encWasm = await makeWasmPair ( ) ;
216+ const encLib = await makeLibPair ( ) ;
247217
248218boxplot ( ( ) => {
249219 summary ( ( ) => {
250220 bench ( "Encrypt typical message (Rust WASM)" , async ( ) => {
251- const result = await aliceCipher . encrypt ( typicalMessage ) ;
221+ const result = await encWasm . alice . encrypt ( typicalMessage ) ;
252222 do_not_optimize ( result ) ;
253223 } ) . gc ( "inner" ) ;
254224
255225 bench ( "Encrypt typical message (libsignal-node)" , async ( ) => {
256- const result = await aliceLibsignalCipher . encrypt ( typicalMessage ) ;
226+ const result = await encLib . alice . encrypt ( typicalMessage ) ;
257227 do_not_optimize ( result ) ;
258228 } ) . gc ( "inner" ) ;
259229 } ) ;
230+ } ) ;
231+
232+ // ============================================================================
233+ // Decrypt benchmarks (fresh sessions — independent of Encrypt above)
234+ // ============================================================================
235+ const decWasm = await makeWasmPair ( ) ;
236+ const decLib = await makeLibPair ( ) ;
260237
238+ // Use mitata's generator form so the encrypt setup runs outside the timed
239+ // section (the [0] callback is invoked per-iteration before t0). The bench
240+ // body only measures Bob's decrypt. Each iteration consumes a fresh
241+ // ciphertext because both ratchets advance on every encrypt/decrypt.
242+ boxplot ( ( ) => {
261243 summary ( ( ) => {
262- // Decrypt benchmarks - Alice encrypts, Bob decrypts
263- // Both sides now have established sessions, so these are WhisperMessages (type 1)
264- bench ( "Decrypt WhisperMessage (Rust WASM)" , async ( ) => {
265- // Alice encrypts a fresh message to Bob
266- const encrypted = await aliceCipher . encrypt ( typicalMessage ) ;
267- // Bob decrypts it - this advances the ratchet
268- const result = await bobCipher . decryptWhisperMessage ( encrypted . body ) ;
269- do_not_optimize ( result ) ;
244+ bench ( " Decrypt WhisperMessage (Rust WASM)" , function * ( ) {
245+ yield {
246+ [ 0 ] : async ( ) => await decWasm . alice . encrypt ( typicalMessage ) ,
247+ bench : async ( encrypted : { body : Uint8Array } ) => {
248+ const result = await decWasm . bob . decryptWhisperMessage ( encrypted . body ) ;
249+ do_not_optimize ( result ) ;
250+ } ,
251+ } ;
270252 } ) . gc ( "inner" ) ;
271253
272- bench ( "Decrypt WhisperMessage (libsignal-node)" , async ( ) => {
273- // Alice encrypts a fresh message to Bob
274- const encrypted = await aliceLibsignalCipher . encrypt ( typicalMessage ) ;
275- // Bob decrypts it - this advances the ratchet
276- const result = await bobLibsignalCipher . decryptWhisperMessage (
277- encrypted . body as unknown as Uint8Array ,
278- ) ;
279- do_not_optimize ( result ) ;
254+ bench ( "Decrypt WhisperMessage (libsignal-node)" , function * ( ) {
255+ yield {
256+ [ 0 ] : async ( ) => await decLib . alice . encrypt ( typicalMessage ) ,
257+ bench : async ( encrypted : { body : unknown } ) => {
258+ const result = await decLib . bob . decryptWhisperMessage (
259+ encrypted . body as unknown as Uint8Array ,
260+ ) ;
261+ do_not_optimize ( result ) ;
262+ } ,
263+ } ;
280264 } ) . gc ( "inner" ) ;
281265 } ) ;
266+ } ) ;
282267
268+ // ============================================================================
269+ // Full round-trip benchmarks (fresh sessions)
270+ // ============================================================================
271+ const rtWasm = await makeWasmPair ( ) ;
272+ const rtLib = await makeLibPair ( ) ;
273+
274+ boxplot ( ( ) => {
283275 summary ( ( ) => {
284- // Full round-trip: Alice -> Bob -> Alice
285276 bench ( "Full round-trip encrypt+decrypt (Rust WASM)" , async ( ) => {
286- // Alice sends to Bob
287- const toBob = await aliceCipher . encrypt ( typicalMessage ) ;
288- const decryptedByBob = await bobCipher . decryptWhisperMessage ( toBob . body ) ;
289- // Bob replies to Alice
290- const toAlice = await bobCipher . encrypt ( typicalMessage ) ;
291- const decryptedByAlice = await aliceCipher . decryptWhisperMessage (
277+ const toBob = await rtWasm . alice . encrypt ( typicalMessage ) ;
278+ const decryptedByBob = await rtWasm . bob . decryptWhisperMessage ( toBob . body ) ;
279+ const toAlice = await rtWasm . bob . encrypt ( typicalMessage ) ;
280+ const decryptedByAlice = await rtWasm . alice . decryptWhisperMessage (
292281 toAlice . body ,
293282 ) ;
294283 do_not_optimize ( decryptedByBob ) ;
295284 do_not_optimize ( decryptedByAlice ) ;
296285 } ) . gc ( "inner" ) ;
297286
298287 bench ( "Full round-trip encrypt+decrypt (libsignal-node)" , async ( ) => {
299- // Alice sends to Bob
300- const toBob = await aliceLibsignalCipher . encrypt ( typicalMessage ) ;
301- const decryptedByBob = await bobLibsignalCipher . decryptWhisperMessage (
288+ const toBob = await rtLib . alice . encrypt ( typicalMessage ) ;
289+ const decryptedByBob = await rtLib . bob . decryptWhisperMessage (
302290 toBob . body as unknown as Uint8Array ,
303291 ) ;
304- // Bob replies to Alice
305- const toAlice = await bobLibsignalCipher . encrypt ( typicalMessage ) ;
306- const decryptedByAlice = await aliceLibsignalCipher . decryptWhisperMessage (
292+ const toAlice = await rtLib . bob . encrypt ( typicalMessage ) ;
293+ const decryptedByAlice = await rtLib . alice . decryptWhisperMessage (
307294 toAlice . body as unknown as Uint8Array ,
308295 ) ;
309296 do_not_optimize ( decryptedByBob ) ;
0 commit comments