From 6dcecadf72055a75ac553a0b26efca511617ced8 Mon Sep 17 00:00:00 2001 From: sasikumar-bitgo Date: Wed, 15 Oct 2025 12:43:39 +0530 Subject: [PATCH] feat(sdk-lib-mpc): implement EdDSA DKG MPS functionality TICKET: WP-6122 - Add EdDSA DKG DKLS implementation with core types and utilities - Implement distributed key generation for EdDSA signatures --- modules/sdk-lib-mpc/package.json | 9 +- modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts | 441 +++++++++++++++ .../sdk-lib-mpc/src/tss/eddsa-mps/index.ts | 3 + .../sdk-lib-mpc/src/tss/eddsa-mps/types.ts | 52 ++ modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts | 34 ++ modules/sdk-lib-mpc/src/tss/index.ts | 1 + .../sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts | 526 ++++++++++++++++++ .../test/unit/tss/eddsa/eddsa-utils.ts | 17 + .../sdk-lib-mpc/test/unit/tss/eddsa/util.ts | 37 ++ yarn.lock | 15 + 10 files changed, 1134 insertions(+), 1 deletion(-) create mode 100644 modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts create mode 100644 modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts create mode 100644 modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts create mode 100644 modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts create mode 100644 modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts create mode 100644 modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts create mode 100644 modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts diff --git a/modules/sdk-lib-mpc/package.json b/modules/sdk-lib-mpc/package.json index 5e21f463dd..ae0b3cf7b8 100644 --- a/modules/sdk-lib-mpc/package.json +++ b/modules/sdk-lib-mpc/package.json @@ -39,6 +39,8 @@ "@noble/curves": "1.8.1", "@silencelaboratories/dkls-wasm-ll-node": "1.2.0-pre.4", "@silencelaboratories/dkls-wasm-ll-web": "1.2.0-pre.4", + "@silencelaboratories/eddsa-wasm-ll-node": "1.0.0-pre.3", + "@silencelaboratories/eddsa-wasm-ll-web": "1.0.0-pre.3", "@types/superagent": "4.1.15", "@wasmer/wasi": "^1.2.2", "bigint-crypto-utils": "3.1.4", @@ -54,6 +56,7 @@ "devDependencies": { "@bitgo/sdk-opensslbytes": "^2.1.0", "@silencelaboratories/dkls-wasm-ll-bundler": "1.2.0-pre.4", + "@silencelaboratories/eddsa-wasm-ll-bundler": "1.0.0-pre.3", "@types/lodash": "^4.14.151", "@types/node": "^22.15.29", "@types/sjcl": "1.0.34", @@ -61,11 +64,15 @@ "sjcl": "1.0.8" }, "peerDependencies": { - "@silencelaboratories/dkls-wasm-ll-bundler": "1.2.0-pre.4" + "@silencelaboratories/dkls-wasm-ll-bundler": "1.2.0-pre.4", + "@silencelaboratories/eddsa-wasm-ll-bundler": "1.0.0-pre.3" }, "peerDependenciesMeta": { "@silencelaboratories/dkls-wasm-ll-bundler": { "optional": true + }, + "@silencelaboratories/eddsa-wasm-ll-bundler": { + "optional": true } }, "files": [ diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts new file mode 100644 index 0000000000..666afe4203 --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts @@ -0,0 +1,441 @@ +import type { EncryptionKeyPair, KeygenSession } from '@silencelaboratories/eddsa-wasm-ll-node'; +import { decode, encode } from 'cbor-x'; +import { BundlerWasmer, EddsaMPSWasm, EddsaReducedKeyShare, DkgState, DeserializedMessage } from './types'; + +/** + * EdDSA Distributed Key Generation (DKG) implementation using multi-Party Schnorr protocol + * + * This class handles the complete DKG protocol for EdDSA key generation across multiple parties. + * It supports both seeded (deterministic) and random key generation. + * + * @example + * ```typescript + * // Basic usage + * const dkg = new DKG(3, 2, 0); // 3 parties, threshold 2, party index 0 + * const publicKey = await dkg.getPublicKey(); + * await dkg.initDkg(concatenatedPublicKeys); + * const firstMessage = dkg.getFirstMessage(); + * const nextMessages = dkg.handleIncomingMessages(incomingMessages); + * const keyShare = dkg.getKeyShare(); + * + * // Session persistence + * const session = dkg.getSession(); + * const restoredDkg = new DKG(3, 2, 0); + * await restoredDkg.restoreSession(session); + * ``` + */ +export class DKG { + /** Total number of parties participating in the DKG */ + protected n: number; + /** Threshold value - minimum number of parties needed to reconstruct the key */ + protected t: number; + /** Index of this party (0-based) */ + protected partyIdx: number; + /** WASM module instance for EdDSA operations */ + protected wasm: EddsaMPSWasm | null; + /** Optional seed for deterministic key generation */ + protected seed: Buffer | undefined; + /** Encryption key pair for secure communication */ + protected keyPair: EncryptionKeyPair | null; + /** Public key for the DKG session */ + protected sessionPublicKey: Uint8Array; + /** Internal DKG session instance */ + protected dkgSession: KeygenSession | null = null; + /** Cached key share after DKG completion */ + private keyShare: Buffer | null = null; + /** Current state of the DKG session */ + protected dkgState: DkgState = DkgState.Uninitialized; + + /** + * Creates a new DKG instance for EdDSA distributed key generation + * + * @param n - Total number of parties participating in the DKG (must be > 0) + * @param t - Threshold value - minimum number of parties needed to reconstruct the key (must be > 0 and <= n) + * @param partyIdx - Index of this party in the DKG protocol (0-based, must be < n) + * @param seed - Optional seed for deterministic key generation (32 bytes recommended) + * @param wasm - Optional WASM module instance + */ + constructor(n: number, t: number, partyIdx: number, seed?: Buffer, wasm?: BundlerWasmer) { + this.n = n; + this.t = t; + this.partyIdx = partyIdx; + this.seed = seed; + this.wasm = wasm ?? null; + this.keyPair = null; + } + + /** + * Gets the public key for this party + * + * @returns The public key as a Uint8Array + */ + async getPublicKey() { + if (!this.keyPair) { + if (!this.wasm) { + await this.loadWasm(); + } + await this.initBrowserWasm(); + const { generateEncryptionKeypair } = this.getWasm(); + this.keyPair = generateEncryptionKeypair(); + } + return this.keyPair.publicKey; + } + + /** + * Gets the current state of the DKG session + * @returns The current DKG state as a DkgState enum value + */ + getState(): DkgState { + return this.dkgState; + } + + /** + * Initializes the WASM module for browser environments + * This is only needed for browsers/web because it uses fetch to resolve the wasm asset + * @private + */ + private async initBrowserWasm(): Promise { + /* checks for electron processes */ + if (typeof window !== 'undefined' && !window.process?.['type']) { + /* This is only needed for browsers/web because it uses fetch to resolve the wasm asset for the web */ + const initMPS = await import('@silencelaboratories/eddsa-wasm-ll-web'); + await initMPS.default(); + } + } + + /** + * Loads the EdDSA MPS WASM module if not already loaded + * + * @private + */ + private async loadWasm(): Promise { + if (!this.wasm) { + this.wasm = await import('@silencelaboratories/eddsa-wasm-ll-node'); + } + } + + /** + * Gets the loaded WASM module instance + * + * @returns The WASM module instance + * @throws {Error} If WASM module is not loaded + * + * @private + */ + private getWasm(): EddsaMPSWasm { + if (!this.wasm) { + throw Error('EDDSA MPS wasm not loaded'); + } + + return this.wasm; + } + + /** + * Ensures that the DKG session is initialized + * + * @throws {Error} If DKG session is not initialized + * + * @private + */ + private ensureDkgSessionInitialized(): void { + if (!this.dkgSession) { + throw Error('DKG session not initialized'); + } + } + + /** + * Initializes the DKG session with all parties' public keys + * + * This method must be called before any message exchange can begin. + * It sets up the internal DKG session and prepares for the key generation protocol. + * + * @param publicKey - Concatenated public keys of all participating parties + * + * @throws {Error} If publicKey is missing or invalid + * @throws {Error} If DKG parameters are invalid (t > n or partyIdx >= n) + * @throws {Error} If WASM module fails to load + */ + async initDkg(publicKey: Uint8Array) { + if (!publicKey) { + throw Error('Missing all parties public key'); + } + if (!this.wasm) { + await this.loadWasm(); + } + if (this.t > this.n || this.partyIdx >= this.n) { + throw Error('Invalid parameters for DKG'); + } + if (!this.keyPair) { + throw Error('keyPair not initialized for DKG. Use getPublicKey() first.'); + } + + await this.initBrowserWasm(); + + const { KeygenSession } = this.getWasm(); + + this.dkgSession = new KeygenSession( + this.n, + this.t, + this.partyIdx, + this.keyPair.secretKey, + publicKey, + this.seed ? new Uint8Array(this.seed) : undefined + ); + this.dkgState = DkgState.Init; + } + + /** + * Creates the first message for the DKG protocol + * + * This method generates the initial broadcast message that this party will send + * to all other parties to start the DKG protocol. + * + * @returns The first message containing payload and sender information + * + * @throws {Error} If DKG session is not initialized + */ + getFirstMessage(): DeserializedMessage { + this.ensureDkgSessionInitialized(); + const message = this.dkgSession!.createFirstMessage(); + const returnMessage = { + payload: message.payload, + from: message.from_id, + }; + message.free(); + // Transition from Init to WaitMsg1 after creating first message + if (this.dkgState === DkgState.Init) { + this.dkgState = DkgState.WaitMsg1; + } + return returnMessage; + } + + /** + * Handles incoming messages and generates the next round of messages + * + * This method processes messages from other parties and generates the next round + * of messages to be sent. The DKG protocol typically consists of 2-3 rounds. + * The method also manages state transitions automatically based on the current state. + * + * Valid states for calling this method: + * - WaitMsg1: After getFirstMessage() has been called + * - WaitMsg2: After first round of message handling + * - Share: After second round of message handling + * + * State transitions: + * - WaitMsg1 -> WaitMsg2 (after first message handling) + * - WaitMsg2 -> Share (after second message handling) + * - Share -> Complete (when no more messages to send) + * + * @param messagesForIthRound - Array of messages received from other parties + * @returns Array of messages to be sent to other parties in the next round + * + * @throws {Error} If DKG session is already completed + * @throws {Error} If DKG session is not initialized + * @throws {Error} If DKG session is in Uninitialized state + * @throws {Error} If DKG session is in Init state (must call getFirstMessage first) + * @throws {Error} If number of messages doesn't match expected count (n) + */ + handleIncomingMessages(messagesForIthRound: DeserializedMessage[]): DeserializedMessage[] { + if (this.dkgState === DkgState.Complete) { + throw Error('DKG session already completed'); + } + this.ensureDkgSessionInitialized(); + + // Check that we're in a valid state to handle messages + if (this.dkgState === DkgState.Uninitialized) { + throw Error('DKG session must be initialized before handling messages. Call initDkg() first.'); + } + if (this.dkgState === DkgState.Init) { + throw Error( + 'DKG session must call getFirstMessage() before handling incoming messages. Call getFirstMessage() first.' + ); + } + + if (messagesForIthRound.length !== this.n) { + throw Error('Invalid number of messages for the round. Number of messages should be equal to N'); + } + + const { Message } = this.getWasm(); + const nextRoundMessage = this.dkgSession!.handleMessages( + messagesForIthRound.map((m) => new Message(m.payload, m.from)) + ); + + const result = nextRoundMessage.map((m) => ({ + payload: m.payload, + from: m.from_id, + })); + + // Clean up WASM objects + nextRoundMessage.forEach((m) => m.free()); + + // Update state based on the current state and message handling + if (this.dkgState === DkgState.WaitMsg1) { + this.dkgState = DkgState.WaitMsg2; + } else if (this.dkgState === DkgState.WaitMsg2) { + this.dkgState = DkgState.Share; + } + + // Check if this is the final round (round 2 in EdDSA DKG) + if (nextRoundMessage.length === 0) { + this.dkgState = DkgState.Complete; + } + + return result; + } + + /** + * Gets the key share for this party + * + * This method returns the EdDSA key share that was generated during the DKG protocol. + * The key share can be used for signing operations and must be kept secure. + * + * @returns The key share as a Buffer containing the serialized key data + * + * @throws {Error} If DKG session is not initialized + * @throws {Error} If DKG session is not complete + */ + getKeyShare(): Buffer { + this.ensureDkgSessionInitialized(); + if (this.dkgState !== DkgState.Complete) { + throw Error('DKG session is not complete'); + } + if (this.keyShare) { + return this.keyShare; + } + const keyShare = this.dkgSession!.keyshare(); + this.keyShare = Buffer.from(keyShare.toBytes()); + return this.keyShare; + } + + /** + * Gets the reduced key share for this party + * + * This method returns a simplified version of the key share containing only + * the essential information needed for signing operations. The reduced key share + * is more compact and contains only the private key material, public key, and root chain code. + * + * @returns The reduced key share as a Buffer containing the serialized reduced key data + * + * @throws {Error} If DKG session is not initialized or complete + */ + getReducedKeyShare(): Buffer { + const shareBuffer = this.getKeyShare(); + const decodedKeyshare = decode(shareBuffer); + const reducedKeyShare: EddsaReducedKeyShare = { + rootChainCode: decodedKeyshare.root_chain_code, + prv: decodedKeyshare.d_i, + pub: decodedKeyshare.public_key, + }; + return encode(reducedKeyShare); + } + + /** + * Syncs the internal DKG state with the WASM library state + * + * This method examines the current WASM session state and updates the internal + * DKG state to match. This ensures that the internal state accurately reflects + * the actual state of the underlying WASM library, which is important for + * debugging and maintaining state consistency. + * + * The method maps WASM states to internal DkgState enum values: + * - Init -> DkgState.Init + * - WaitMsg1 -> DkgState.WaitMsg1 + * - WaitMsg2 -> DkgState.WaitMsg2 + * - Share -> DkgState.Share + * + * @throws {Error} If DKG session is not initialized + */ + private syncStateWithWasm(): void { + this.ensureDkgSessionInitialized(); + + if (!this.dkgSession) { + this.dkgState = DkgState.Uninitialized; + throw Error('DKG session not initialized'); + } + + // Get the current WASM session state + const sessionBytes = this.dkgSession.toBytes(); + const wasmState = decode(sessionBytes); + // Map WASM states to our DkgState enum + if (wasmState.round?.Init) { + this.dkgState = DkgState.Init; + } else if (wasmState.round?.WaitMsg1) { + this.dkgState = DkgState.WaitMsg1; + } else if (wasmState.round?.WaitMsg2) { + this.dkgState = DkgState.WaitMsg2; + } else if (wasmState.round?.Share) { + this.dkgState = DkgState.Share; + } + } + + /** + * Exports the current DKG session state as a base64-encoded string + * + * This method allows you to save the current DKG session state and restore it later + * using the restoreSession method. This is useful for implementing session persistence + * or resuming interrupted DKG protocols. + * + * @returns Base64-encoded string representing the current session state + * + * @throws {Error} If DKG session is not initialized + * @throws {Error} If DKG session is complete (sessions cannot be exported after completion) + */ + getSession(): string { + this.ensureDkgSessionInitialized(); + if (this.dkgState === DkgState.Complete) { + throw Error('DKG session is complete. Exporting the session is not allowed.'); + } + return Buffer.from(this.dkgSession!.toBytes()).toString('base64'); + } + + /** + * Restores a DKG session from a previously exported session string + * + * This method allows you to restore a DKG session that was previously exported + * using the getSession method. The restored session will be in the same state + * as when it was exported, allowing you to continue the DKG protocol from that point. + * + * @param session - Base64-encoded session string from getSession() + * + * @throws {Error} If session string is invalid or malformed + * @throws {Error} If session cannot be restored due to WASM errors + * @throws {Error} If WASM module fails to load + */ + async restoreSession(session: string): Promise { + if (!this.wasm) { + await this.loadWasm(); + } + const { KeygenSession } = this.getWasm(); + this.dkgSession = KeygenSession.fromBytes(new Uint8Array(Buffer.from(session, 'base64'))); + + const error = this.dkgSession.error(); + if (error) { + throw error; + } + + this.syncStateWithWasm(); + } + + /** + * Ends the DKG session by freeing any heap allocations from WASM + * + * This method should be called when the DKG session is no longer needed + * to properly clean up WASM resources. After calling this method, the session + * returns to Uninitialized state. + */ + endSession(): void { + try { + this.dkgSession?.free(); + } catch (error) { + // Resources may already be freed, ignore errors + } + try { + this.keyPair?.free(); + } catch (error) { + // Resources may already be freed, ignore errors + } + this.dkgSession = null; + this.keyPair = null; + this.dkgState = DkgState.Uninitialized; + } +} diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts new file mode 100644 index 0000000000..eb9fa2a6d3 --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/index.ts @@ -0,0 +1,3 @@ +export * as MPSDkg from './dkg'; +export * as MPSUtil from './util'; +export * as MPSTypes from './types'; diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts new file mode 100644 index 0000000000..b9e10e0d3e --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts @@ -0,0 +1,52 @@ +import * as t from 'io-ts'; + +export const ReducedKeyShareType = t.type({ + rootChainCode: t.array(t.number), + prv: t.array(t.number), + pub: t.array(t.number), +}); + +export type EddsaReducedKeyShare = t.TypeOf; + +export interface KeyShareReadable { + threshold: number; + total_parties: number; + party_id: number; + d_i: string; + public_key: string; + key_id: string; + root_chain_code: string; + final_session_id: string; +} + +type NodeWasmer = typeof import('@silencelaboratories/eddsa-wasm-ll-node'); +type WebWasmer = typeof import('@silencelaboratories/eddsa-wasm-ll-web'); +export type BundlerWasmer = typeof import('@silencelaboratories/eddsa-wasm-ll-bundler'); + +export type EddsaMPSWasm = NodeWasmer | WebWasmer | BundlerWasmer; + +/** + * Represents the state of a DKG (Distributed Key Generation) session + * These states correspond to the internal WASM library states + */ +export enum DkgState { + /** DKG session has not been initialized */ + Uninitialized = 'Uninitialized', + /** DKG session has been initialized (Init state in WASM) */ + Init = 'Init', + /** DKG session is waiting for first message (WaitMsg1 state in WASM) */ + WaitMsg1 = 'WaitMsg1', + /** DKG session is waiting for second message (WaitMsg2 state in WASM) */ + WaitMsg2 = 'WaitMsg2', + /** DKG session has generated key shares (Share state in WASM) */ + Share = 'Share', + /** DKG session has completed successfully and key shares are available */ + Complete = 'Complete', +} + +interface Message { + payload: T; + from: number; +} + +export type DeserializedMessage = Message; diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts new file mode 100644 index 0000000000..f4f71a83a3 --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/util.ts @@ -0,0 +1,34 @@ +import { decode } from 'cbor-x'; +import { KeyShareReadable } from './types'; + +/** + * Concatenates multiple Uint8Array instances into a single Uint8Array + * @param chunks - Array of Uint8Array instances to concatenate + * @returns Concatenated Uint8Array + */ +export function concatBytes(chunks: Uint8Array[]): Uint8Array { + // Convert Uint8Array to Buffer for concatenation, then back to Uint8Array + const buffers = chunks.map((chunk) => Buffer.from(chunk)); + return new Uint8Array(Buffer.concat(buffers)); +} + +/** + * Fetches and formats material from key shares + * @param shares - Array of Buffer containing key share data + * @returns Array of formatted share material + */ +export function fetchMaterial(shares: Buffer[]) { + return shares.map((share) => { + const material = decode(share) as unknown as KeyShareReadable; + return { + threshold: material.threshold, + total_parties: material.total_parties, + party_id: material.party_id, + d_i: Buffer.from(material.d_i).toString('hex'), + public_key: Buffer.from(material.public_key).toString('hex'), + key_id: Buffer.from(material.key_id).toString('hex'), + root_chain_code: Buffer.from(material.root_chain_code).toString('hex'), + final_session_id: Buffer.from(material.final_session_id).toString('hex'), + }; + }); +} diff --git a/modules/sdk-lib-mpc/src/tss/index.ts b/modules/sdk-lib-mpc/src/tss/index.ts index b8d5aecfbe..504d0eb8bf 100644 --- a/modules/sdk-lib-mpc/src/tss/index.ts +++ b/modules/sdk-lib-mpc/src/tss/index.ts @@ -1,2 +1,3 @@ export * from './ecdsa'; export * from './ecdsa-dkls'; +export * from './eddsa-mps'; diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts new file mode 100644 index 0000000000..1bb9d4a390 --- /dev/null +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts @@ -0,0 +1,526 @@ +import assert from 'assert'; +import { concatBytes } from '../../../../src/tss/eddsa-mps/util'; +import { generateEdDsaDKGKeyShares } from './util'; +import { decode } from 'cbor-x'; +import { MPSDkg, MPSUtil } from '../../../../src/tss/eddsa-mps'; +import { DkgState } from '../../../../src/tss/eddsa-mps/types'; + +describe('EdDSA MPS DKG', function () { + let user: MPSDkg.DKG; + let backup: MPSDkg.DKG; + let bitgo: MPSDkg.DKG; + let publicKeyConcat: Uint8Array; + + beforeEach(async function () { + user = new MPSDkg.DKG(3, 2, 0); + backup = new MPSDkg.DKG(3, 2, 1); + bitgo = new MPSDkg.DKG(3, 2, 2); + + const publicKeys = await Promise.all([user.getPublicKey(), backup.getPublicKey(), bitgo.getPublicKey()]); + publicKeyConcat = concatBytes(publicKeys); + }); + + afterEach(function () { + user.endSession(); + backup.endSession(); + bitgo.endSession(); + }); + + describe('DKG Initialization', function () { + it('should initialize DKG sessions for all parties', async function () { + await Promise.all([ + user.initDkg(publicKeyConcat), + backup.initDkg(publicKeyConcat), + bitgo.initDkg(publicKeyConcat), + ]); + + // Verify that all parties can create first messages + const userMessage = user.getFirstMessage(); + const backupMessage = backup.getFirstMessage(); + const bitgoMessage = bitgo.getFirstMessage(); + + assert(userMessage.payload.length > 0, 'User first message should have payload'); + assert(backupMessage.payload.length > 0, 'Backup first message should have payload'); + assert(bitgoMessage.payload.length > 0, 'BitGo first message should have payload'); + + assert.strictEqual(userMessage.from, 0, 'User message should be from party 0'); + assert.strictEqual(backupMessage.from, 1, 'Backup message should be from party 1'); + assert.strictEqual(bitgoMessage.from, 2, 'BitGo message should be from party 2'); + }); + + it('should throw error when DKG session is not initialized', function () { + assert.throws(() => { + user.getFirstMessage(); + }, /DKG session not initialized/); + + assert.throws(() => { + user.handleIncomingMessages([]); + }, /DKG session not initialized/); + + assert.throws(() => { + user.getKeyShare(); + }, /DKG session not initialized/); + }); + }); + + describe('DKG Protocol Execution', function () { + beforeEach(async function () { + await Promise.all([ + user.initDkg(publicKeyConcat), + backup.initDkg(publicKeyConcat), + bitgo.initDkg(publicKeyConcat), + ]); + }); + + it('should complete full DKG protocol and generate key shares', async function () { + // Round 1: Create first messages + const r1Messages = [user.getFirstMessage(), backup.getFirstMessage(), bitgo.getFirstMessage()]; + + // Verify round 1 messages + assert.strictEqual(r1Messages.length, 3, 'Should have 3 round 1 messages'); + r1Messages.forEach((msg, index) => { + assert.strictEqual(msg.from, index, `Message ${index} should be from party ${index}`); + assert(msg.payload.length > 0, `Message ${index} should have payload`); + }); + + // Round 2: Handle round 1 messages and create round 2 messages + const r2Messages = [ + ...user.handleIncomingMessages(r1Messages), + ...backup.handleIncomingMessages(r1Messages), + ...bitgo.handleIncomingMessages(r1Messages), + ]; + + // Verify round 2 messages + assert(r2Messages.length > 0, 'Should have round 2 messages'); + r2Messages.forEach((msg) => { + assert(msg.payload.length > 0, 'Round 2 message should have payload'); + assert(typeof msg.from === 'number', 'Round 2 message should have from field'); + }); + + // Round 3: Handle round 2 messages + const r3Messages = [ + ...user.handleIncomingMessages(r2Messages), + ...backup.handleIncomingMessages(r2Messages), + ...bitgo.handleIncomingMessages(r2Messages), + ]; + + // Verify round 3 messages + assert(r3Messages.length >= 0, 'Round 3 messages should be valid'); + + // Get key shares + const userKeyShare = user.getKeyShare(); + const backupKeyShare = backup.getKeyShare(); + const bitgoKeyShare = bitgo.getKeyShare(); + + // Verify key shares + assert(Buffer.isBuffer(userKeyShare), 'User key share should be a Buffer'); + assert(Buffer.isBuffer(backupKeyShare), 'Backup key share should be a Buffer'); + assert(Buffer.isBuffer(bitgoKeyShare), 'BitGo key share should be a Buffer'); + + assert(userKeyShare.length > 0, 'User key share should not be empty'); + assert(backupKeyShare.length > 0, 'Backup key share should not be empty'); + assert(bitgoKeyShare.length > 0, 'BitGo key share should not be empty'); + }); + + it('should generate consistent key shares across all parties', async function () { + // Complete the DKG protocol + const r1Messages = [user.getFirstMessage(), backup.getFirstMessage(), bitgo.getFirstMessage()]; + + const r2Messages = [ + ...user.handleIncomingMessages(r1Messages), + ...backup.handleIncomingMessages(r1Messages), + ...bitgo.handleIncomingMessages(r1Messages), + ]; + + user.handleIncomingMessages(r2Messages); + backup.handleIncomingMessages(r2Messages); + bitgo.handleIncomingMessages(r2Messages); + + // Get key shares + const userKeyShare = user.getKeyShare(); + const backupKeyShare = backup.getKeyShare(); + const bitgoKeyShare = bitgo.getKeyShare(); + + // Parse key shares using fetchMaterial + const keyShareData = MPSUtil.fetchMaterial([userKeyShare, backupKeyShare, bitgoKeyShare]); + + // Verify all parties have the same public key + const publicKeys = keyShareData.map((share) => share.public_key); + assert.strictEqual(publicKeys[0], publicKeys[1], 'User and backup should have same public key'); + assert.strictEqual(publicKeys[1], publicKeys[2], 'Backup and BitGo should have same public key'); + + // Verify all parties have the same root chain code + const rootChainCodes = keyShareData.map((share) => share.root_chain_code); + assert.strictEqual(rootChainCodes[0], rootChainCodes[1], 'User and backup should have same root chain code'); + assert.strictEqual(rootChainCodes[1], rootChainCodes[2], 'Backup and BitGo should have same root chain code'); + + // Verify threshold and total parties + keyShareData.forEach((share, index) => { + assert.strictEqual(share.threshold, 2, `Party ${index} should have threshold 2`); + assert.strictEqual(share.total_parties, 3, `Party ${index} should have total parties 3`); + assert.strictEqual(share.party_id, index, `Party ${index} should have correct party ID`); + }); + + // Verify each party has unique private key material + const privateKeys = keyShareData.map((share) => share.d_i); + const uniquePrivateKeys = new Set(privateKeys); + assert.strictEqual(uniquePrivateKeys.size, 3, 'Each party should have unique private key material'); + }); + }); + + describe('Seed-based Key Generation', function () { + it('should create key shares with deterministic seeds', async function () { + const seedUser = Buffer.from('a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270', 'hex'); + const seedBackup = Buffer.from('9d91c2e6353202cf61f8f275158b3468e9a00f7872fc2fd310b72cd026e2e2f9', 'hex'); + const seedBitgo = Buffer.from('33c749b635cdba7f9fbf51ad0387431cde47e20d8dc13acd1f51a9a0ad06ebfe', 'hex'); + + const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(seedUser, seedBackup, seedBitgo); + + const userKeyShare = user.getKeyShare(); + const backupKeyShare = backup.getKeyShare(); + const bitgoKeyShare = bitgo.getKeyShare(); + const userReducedKeyShare = user.getReducedKeyShare(); + + // Verify key shares are generated + assert(Buffer.isBuffer(userKeyShare), 'User key share should be a Buffer'); + assert(Buffer.isBuffer(backupKeyShare), 'Backup key share should be a Buffer'); + assert(Buffer.isBuffer(bitgoKeyShare), 'BitGo key share should be a Buffer'); + assert(Buffer.isBuffer(userReducedKeyShare), 'User reduced key share should be a Buffer'); + + // Verify all parties have the same public key + const [userKeyData, backupKeyData, bitgoKeyData] = MPSUtil.fetchMaterial([ + userKeyShare, + backupKeyShare, + bitgoKeyShare, + ]); + + assert.deepEqual(userKeyData.public_key, bitgoKeyData.public_key, 'User and BitGo should have same public key'); + assert.deepEqual( + backupKeyData.public_key, + bitgoKeyData.public_key, + 'Backup and BitGo should have same public key' + ); + + // Verify deterministic behavior - running again with same seeds should produce same results + const [user2, backup2, bitgo2] = await generateEdDsaDKGKeyShares(seedUser, seedBackup, seedBitgo); + + const [userKeyShare2, backupKeyShare2, bitgoKeyShare2] = MPSUtil.fetchMaterial([ + user2.getKeyShare(), + backup2.getKeyShare(), + bitgo2.getKeyShare(), + ]); + + // Key shares should be identical when using same seeds + assert.deepEqual(userKeyData.d_i, userKeyShare2.d_i, 'User key shares should be identical with same seed'); + assert.deepEqual(backupKeyData.d_i, backupKeyShare2.d_i, 'Backup key shares should be identical with same seed'); + assert.deepEqual(bitgoKeyData.d_i, bitgoKeyShare2.d_i, 'BitGo key shares should be identical with same seed'); + + // Clean up + user.endSession(); + backup.endSession(); + bitgo.endSession(); + user2.endSession(); + backup2.endSession(); + bitgo2.endSession(); + }); + + it('should create different key shares with different seeds', async function () { + const seedUser1 = Buffer.from('a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270', 'hex'); + const seedBackup1 = Buffer.from('9d91c2e6353202cf61f8f275158b3468e9a00f7872fc2fd310b72cd026e2e2f9', 'hex'); + const seedBitgo1 = Buffer.from('33c749b635cdba7f9fbf51ad0387431cde47e20d8dc13acd1f51a9a0ad06ebfe', 'hex'); + + const seedUser2 = Buffer.from('b415844d27dd9320f282d6d8ecd8387f0e9fbf9198664e28a2f66e6f5b87c381', 'hex'); + const seedBackup2 = Buffer.from('ae02d3f7464313d0f72f9f3862694579fa11f8983fc3fe42183cd137e3f3f30a', 'hex'); + const seedBitgo2 = Buffer.from('44d85ab746decb8f0f0c62be0498542ddf58f31d9ed24bd1f62b1b1be17fce0f', 'hex'); + + const [user1, backup1, bitgo1] = await generateEdDsaDKGKeyShares(seedUser1, seedBackup1, seedBitgo1); + const [user2, backup2, bitgo2] = await generateEdDsaDKGKeyShares(seedUser2, seedBackup2, seedBitgo2); + + const userKeyShare1 = user1.getKeyShare(); + const userKeyShare2 = user2.getKeyShare(); + + // Key shares should be different with different seeds + assert.notDeepEqual(userKeyShare1, userKeyShare2, 'User key shares should be different with different seeds'); + + // Clean up + user1.endSession(); + backup1.endSession(); + bitgo1.endSession(); + user2.endSession(); + backup2.endSession(); + bitgo2.endSession(); + }); + + it('should create key shares without seeds (random)', async function () { + const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(); + + const userKeyShare = user.getKeyShare(); + const backupKeyShare = backup.getKeyShare(); + const bitgoKeyShare = bitgo.getKeyShare(); + + // Verify key shares are generated + assert(Buffer.isBuffer(userKeyShare), 'User key share should be a Buffer'); + assert(Buffer.isBuffer(backupKeyShare), 'Backup key share should be a Buffer'); + assert(Buffer.isBuffer(bitgoKeyShare), 'BitGo key share should be a Buffer'); + + // Verify all parties have the same public key + const userKeyData = decode(userKeyShare); + const backupKeyData = decode(backupKeyShare); + const bitgoKeyData = decode(bitgoKeyShare); + + assert.deepEqual(userKeyData.public_key, bitgoKeyData.public_key, 'User and BitGo should have same public key'); + assert.deepEqual( + backupKeyData.public_key, + bitgoKeyData.public_key, + 'Backup and BitGo should have same public key' + ); + + // Clean up + user.endSession(); + backup.endSession(); + bitgo.endSession(); + }); + + it('should generate valid reduced key shares', async function () { + const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(); + + const userReducedKeyShare = user.getReducedKeyShare(); + const backupReducedKeyShare = backup.getReducedKeyShare(); + const bitgoReducedKeyShare = bitgo.getReducedKeyShare(); + + // Verify reduced key shares are generated + assert(Buffer.isBuffer(userReducedKeyShare), 'User reduced key share should be a Buffer'); + assert(Buffer.isBuffer(backupReducedKeyShare), 'Backup reduced key share should be a Buffer'); + assert(Buffer.isBuffer(bitgoReducedKeyShare), 'BitGo reduced key share should be a Buffer'); + + // Verify reduced key shares have content + assert(userReducedKeyShare.length > 0, 'User reduced key share should not be empty'); + assert(backupReducedKeyShare.length > 0, 'Backup reduced key share should not be empty'); + assert(bitgoReducedKeyShare.length > 0, 'BitGo reduced key share should not be empty'); + + // Decode and verify structure + const userReducedData = decode(userReducedKeyShare); + const backupReducedData = decode(backupReducedKeyShare); + const bitgoReducedData = decode(bitgoReducedKeyShare); + + // Verify all parties have the same public key in reduced shares + assert.deepEqual( + userReducedData.pub, + bitgoReducedData.pub, + 'User and BitGo should have same public key in reduced share' + ); + assert.deepEqual( + backupReducedData.pub, + bitgoReducedData.pub, + 'Backup and BitGo should have same public key in reduced share' + ); + + // Verify all parties have the same root chain code in reduced shares + assert.deepEqual( + userReducedData.rootChainCode, + bitgoReducedData.rootChainCode, + 'User and BitGo should have same root chain code in reduced share' + ); + assert.deepEqual( + backupReducedData.rootChainCode, + bitgoReducedData.rootChainCode, + 'Backup and BitGo should have same root chain code in reduced share' + ); + + // Verify each party has unique private key material in reduced shares + const privateKeys = [userReducedData.prv, backupReducedData.prv, bitgoReducedData.prv]; + const uniquePrivateKeys = new Set(privateKeys.map((key) => Buffer.from(key).toString('hex'))); + assert.strictEqual( + uniquePrivateKeys.size, + 3, + 'Each party should have unique private key material in reduced share' + ); + + // Clean up + user.endSession(); + backup.endSession(); + bitgo.endSession(); + }); + }); + + describe('Session Management', function () { + it('should export and restore DKG session correctly', async function () { + const user = new MPSDkg.DKG(3, 2, 0); + const backup = new MPSDkg.DKG(3, 2, 1); + const bitgo = new MPSDkg.DKG(3, 2, 2); + + const publicKeys = await Promise.all([user.getPublicKey(), backup.getPublicKey(), bitgo.getPublicKey()]); + const publicKeyConcat = MPSUtil.concatBytes(publicKeys); + + // Initialize DKG sessions + await Promise.all([ + user.initDkg(publicKeyConcat), + backup.initDkg(publicKeyConcat), + bitgo.initDkg(publicKeyConcat), + ]); + + // Get first messages + user.getFirstMessage(); + backup.getFirstMessage(); + bitgo.getFirstMessage(); + + // Export sessions at this point + const userSession = user.getSession(); + const backupSession = backup.getSession(); + const bitgoSession = bitgo.getSession(); + + // Verify sessions are exported as base64 strings + assert(typeof userSession === 'string', 'User session should be a string'); + assert(typeof backupSession === 'string', 'Backup session should be a string'); + assert(typeof bitgoSession === 'string', 'BitGo session should be a string'); + assert(userSession.length > 0, 'User session should not be empty'); + assert(backupSession.length > 0, 'Backup session should not be empty'); + assert(bitgoSession.length > 0, 'BitGo session should not be empty'); + + // Create new DKG instances and restore sessions + const restoredUser = new MPSDkg.DKG(3, 2, 0); + const restoredBackup = new MPSDkg.DKG(3, 2, 1); + const restoredBitgo = new MPSDkg.DKG(3, 2, 2); + + await restoredUser.restoreSession(userSession); + await restoredBackup.restoreSession(backupSession); + await restoredBitgo.restoreSession(bitgoSession); + + // Verify restored sessions have the same state + assert.strictEqual(restoredUser.getState(), user.getState(), 'Restored user state should match original'); + assert.strictEqual(restoredBackup.getState(), backup.getState(), 'Restored backup state should match original'); + assert.strictEqual(restoredBitgo.getState(), bitgo.getState(), 'Restored bitgo state should match original'); + + // Clean up + user.endSession(); + backup.endSession(); + bitgo.endSession(); + restoredUser.endSession(); + restoredBackup.endSession(); + restoredBitgo.endSession(); + }); + + it('should throw error when trying to export session after completion', async function () { + const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(); + + // Now try to export session - should throw error + assert.throws(() => { + user.getSession(); + }, /DKG session is complete. Exporting the session is not allowed./); + + assert.throws(() => { + backup.getSession(); + }, /DKG session is complete. Exporting the session is not allowed./); + + assert.throws(() => { + bitgo.getSession(); + }, /DKG session is complete. Exporting the session is not allowed./); + + // Clean up + user.endSession(); + backup.endSession(); + bitgo.endSession(); + }); + + it('should safely end session multiple times without throwing errors', async function () { + const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(); + + // End session first time + user.endSession(); + assert.strictEqual(user.getState(), DkgState.Uninitialized, 'User should be in Uninitialized state'); + + // End session second time - should not throw + assert.doesNotThrow(() => { + user.endSession(); + }, 'Calling endSession multiple times should not throw'); + + // Verify still in Uninitialized state + assert.strictEqual(user.getState(), DkgState.Uninitialized, 'User should still be in Uninitialized state'); + + // Clean up other instances + backup.endSession(); + bitgo.endSession(); + }); + }); + + describe('DKG State Management', function () { + it('should track state transitions correctly throughout the protocol', async function () { + const user = new MPSDkg.DKG(3, 2, 0); + const backup = new MPSDkg.DKG(3, 2, 1); + const bitgo = new MPSDkg.DKG(3, 2, 2); + + // Initial state should be Uninitialized + assert.strictEqual(user.getState(), DkgState.Uninitialized, 'Initial state should be Uninitialized'); + assert.strictEqual(backup.getState(), DkgState.Uninitialized, 'Initial state should be Uninitialized'); + assert.strictEqual(bitgo.getState(), DkgState.Uninitialized, 'Initial state should be Uninitialized'); + + const publicKeys = await Promise.all([user.getPublicKey(), backup.getPublicKey(), bitgo.getPublicKey()]); + const publicKeyConcat = MPSUtil.concatBytes(publicKeys); + + // After initialization, state should be Init + await Promise.all([ + user.initDkg(publicKeyConcat), + backup.initDkg(publicKeyConcat), + bitgo.initDkg(publicKeyConcat), + ]); + assert.strictEqual(user.getState(), DkgState.Init, 'State should be Init after initDkg()'); + assert.strictEqual(backup.getState(), DkgState.Init, 'State should be Init after initDkg()'); + assert.strictEqual(bitgo.getState(), DkgState.Init, 'State should be Init after initDkg()'); + + // After getting first message, state should be WaitMsg1 + const r1Messages = [user.getFirstMessage(), backup.getFirstMessage(), bitgo.getFirstMessage()]; + assert.strictEqual(user.getState(), DkgState.WaitMsg1, 'State should be WaitMsg1 after getFirstMessage()'); + assert.strictEqual(backup.getState(), DkgState.WaitMsg1, 'State should be WaitMsg1 after getFirstMessage()'); + assert.strictEqual(bitgo.getState(), DkgState.WaitMsg1, 'State should be WaitMsg1 after getFirstMessage()'); + + // After handling round 1 messages, state should be WaitMsg2 + const r2Messages = [ + ...user.handleIncomingMessages(r1Messages), + ...backup.handleIncomingMessages(r1Messages), + ...bitgo.handleIncomingMessages(r1Messages), + ]; + assert.strictEqual(user.getState(), DkgState.WaitMsg2, 'State should be WaitMsg2 after handling round 1'); + assert.strictEqual(backup.getState(), DkgState.WaitMsg2, 'State should be WaitMsg2 after handling round 1'); + assert.strictEqual(bitgo.getState(), DkgState.WaitMsg2, 'State should be WaitMsg2 after handling round 1'); + + // After handling round 2 messages, state should be Complete + user.handleIncomingMessages(r2Messages); + backup.handleIncomingMessages(r2Messages); + bitgo.handleIncomingMessages(r2Messages); + assert.strictEqual(user.getState(), DkgState.Complete, 'State should be Complete after handling all messages'); + assert.strictEqual(backup.getState(), DkgState.Complete, 'State should be Complete after handling all messages'); + assert.strictEqual(bitgo.getState(), DkgState.Complete, 'State should be Complete after handling all messages'); + + // After ending session, state should be Uninitialized + user.endSession(); + backup.endSession(); + bitgo.endSession(); + assert.strictEqual(user.getState(), DkgState.Uninitialized, 'State should be Uninitialized after endSession()'); + assert.strictEqual(backup.getState(), DkgState.Uninitialized, 'State should be Uninitialized after endSession()'); + assert.strictEqual(bitgo.getState(), DkgState.Uninitialized, 'State should be Uninitialized after endSession()'); + }); + + it('should throw errors when trying to use methods after session is ended', async function () { + const [user, backup, bitgo] = await generateEdDsaDKGKeyShares(); + + // End the session + user.endSession(); + assert.strictEqual(user.getState(), DkgState.Uninitialized, 'Session should be in Uninitialized state'); + + // Try to use various methods - all should throw + assert.throws(() => user.getFirstMessage(), /DKG session not initialized/); + + assert.throws(() => user.handleIncomingMessages([]), /DKG session not initialized/); + + assert.throws(() => user.getKeyShare(), /DKG session not initialized/); + + assert.throws(() => user.getReducedKeyShare(), /DKG session not initialized/); + + assert.throws(() => user.getSession(), /DKG session not initialized/); + + // Clean up other instances + backup.endSession(); + bitgo.endSession(); + }); + }); +}); diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts new file mode 100644 index 0000000000..38fa856290 --- /dev/null +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/eddsa-utils.ts @@ -0,0 +1,17 @@ +import assert from 'assert'; +import { concatBytes } from '../../../../src/tss/eddsa-mps/util'; + +describe('EdDSA Utility Functions', function () { + describe('concatBytes', function () { + it('should concatenate Uint8Array arrays correctly', function () { + const arr1 = new Uint8Array([1, 2, 3]); + const arr2 = new Uint8Array([4, 5, 6]); + const arr3 = new Uint8Array([7, 8, 9]); + + const result = concatBytes([arr1, arr2, arr3]); + const expected = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]); + + assert.deepStrictEqual(result, expected, 'concatBytes should concatenate arrays correctly'); + }); + }); +}); diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts new file mode 100644 index 0000000000..6521410a16 --- /dev/null +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts @@ -0,0 +1,37 @@ +import { MPSDkg, MPSUtil } from '../../../../src/tss/eddsa-mps'; + +/** + * Generates EdDSA DKG key shares for 3 parties with optional seeds + * @param seedUser - Optional seed for user party (party 0) + * @param seedBackup - Optional seed for backup party (party 1) + * @param seedBitgo - Optional seed for BitGo party (party 2) + * @returns Promise resolving to array of Dkg instances [user, backup, bitgo] + */ +export async function generateEdDsaDKGKeyShares( + seedUser?: Buffer, + seedBackup?: Buffer, + seedBitgo?: Buffer +): Promise<[MPSDkg.DKG, MPSDkg.DKG, MPSDkg.DKG]> { + const user = new MPSDkg.DKG(3, 2, 0, seedUser); + const backup = new MPSDkg.DKG(3, 2, 1, seedBackup); + const bitgo = new MPSDkg.DKG(3, 2, 2, seedBitgo); + + const publicKeys = await Promise.all([user.getPublicKey(), backup.getPublicKey(), bitgo.getPublicKey()]); + const publicKeyConcat = MPSUtil.concatBytes(publicKeys); + + await Promise.all([user.initDkg(publicKeyConcat), backup.initDkg(publicKeyConcat), bitgo.initDkg(publicKeyConcat)]); + // Complete the DKG protocol + const r1Messages = [user.getFirstMessage(), backup.getFirstMessage(), bitgo.getFirstMessage()]; + + const r2Messages = [ + ...user.handleIncomingMessages(r1Messages), + ...backup.handleIncomingMessages(r1Messages), + ...bitgo.handleIncomingMessages(r1Messages), + ]; + + user.handleIncomingMessages(r2Messages); + backup.handleIncomingMessages(r2Messages); + bitgo.handleIncomingMessages(r2Messages); + + return [user, backup, bitgo]; +} diff --git a/yarn.lock b/yarn.lock index f5206face0..a02f7ce91b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5061,6 +5061,21 @@ resolved "https://registry.npmjs.org/@silencelaboratories/dkls-wasm-ll-web/-/dkls-wasm-ll-web-1.2.0-pre.4.tgz" integrity sha512-RDyGVX6nyABPchnucl4IOV78LWzXBV9QucRiitRNONo3pfO4z375T00lI/wPiId13wXb8YNkB1Ej90hBNUK25A== +"@silencelaboratories/eddsa-wasm-ll-bundler@1.0.0-pre.3": + version "1.0.0-pre.3" + resolved "https://registry.npmjs.org/@silencelaboratories/eddsa-wasm-ll-bundler/-/eddsa-wasm-ll-bundler-1.0.0-pre.3.tgz#cdc8afaffdd9b2c7a3c99f9da3bdb982d51d31a0" + integrity sha512-RKzUijwOr4PGDb8/i9A3eFxnuecSnp4M1ILoW5Vl0U1LT096pSpQEkzvzfK61HoFBQMxJjaK45tNcgkCG2a6Pg== + +"@silencelaboratories/eddsa-wasm-ll-node@1.0.0-pre.3": + version "1.0.0-pre.3" + resolved "https://registry.npmjs.org/@silencelaboratories/eddsa-wasm-ll-node/-/eddsa-wasm-ll-node-1.0.0-pre.3.tgz#b3d5a4cf9e1afe0866d3b13d6ed67ad0fec858f9" + integrity sha512-siVHrc1ixWpqQTPj0V9BXsbg4SLUPF6N0kZgPp+QRTvXxf4MwmmehEZfUdFFEBpUeI8GSduHjAnwLSpyQMyu/g== + +"@silencelaboratories/eddsa-wasm-ll-web@1.0.0-pre.3": + version "1.0.0-pre.3" + resolved "https://registry.npmjs.org/@silencelaboratories/eddsa-wasm-ll-web/-/eddsa-wasm-ll-web-1.0.0-pre.3.tgz#5ad6149fe312db331cc2d428feaa0360f170099b" + integrity sha512-WB8VizRaPmImYY4BXT9FRemky9WyR65nrEMj7A5EkRl7MPBmnVdpNGVkXuOvtcRB5hchq5HnqEFvrO5aAjZbZQ== + "@sinclair/typebox@^0.34.0": version "0.34.41" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz#aa51a6c1946df2c5a11494a2cdb9318e026db16c"