Skip to content

Commit

Permalink
Merge pull request #76 from torusresearch/batch_sign
Browse files Browse the repository at this point in the history
feat: batch signing for DKLS
  • Loading branch information
himanshuchawla009 authored Nov 21, 2024
2 parents 2997f98 + 424c50a commit 547d838
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 2 deletions.
102 changes: 101 additions & 1 deletion packages/tss-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { keccak256 } from "ethereum-cryptography/keccak";
import type { Socket } from "socket.io-client";

import { DELIMITERS, WEB3_SESSION_HEADER_KEY } from "./constants";
import { Msg } from "./interfaces";
import { BatchSignParams, Msg } from "./interfaces";
import { getEc } from "./utils";

const MSG_READ_TIMEOUT = 10_000;
Expand Down Expand Up @@ -260,6 +260,106 @@ export class Client {
});
}

async batch_sign(batch: BatchSignParams[], additionalParams?: Record<string, unknown>): Promise<{ r: BN; s: BN; recoveryParam: number }[]> {
if (batch.length <= 1) {
throw new Error("Not a batch");
}

if (batch.length > 5) {
throw new Error("Batch size is too large");
}

if (this._consumed === true) {
throw new Error("This instance has already signed a message and cannot be reused");
}

if (this._ready === false) {
throw new Error("client is not ready");
}

// check message hashing
for (let i = 0; i < batch.length; i++) {
if (!batch[i].hash_only) {
if (batch[i].hash_algo === "keccak256") {
if (Buffer.from(keccak256(Buffer.from(batch[i].original_message))).toString("base64") !== batch[i].msg) {
throw new Error("hash of original message does not match msg");
}
} else {
throw new Error(`hash algo ${batch[i].hash_algo} not supported`);
}
}
}

this._startSignTime = Date.now();
const sigFragments: Map<number, string[]> = new Map(); // message index, fragment
for (let i = 0; i < batch.length; i++) {
sigFragments.set(i, []);
}

for (let i = 0; i < this.parties.length; i++) {
const party = i;
if (party === this.index) {
// create signature fragment for this client
for (let m = 0; m < batch.length; m++) {
const currentFragments = sigFragments.get(m);
const fragment = await this.tssLib.local_sign(batch[m].msg, batch[m].hash_only, this.precomputed_value);
currentFragments.push(fragment);
sigFragments.set(m, currentFragments);
}
} else {
// collect signature fragment from all peers
const endpoint = this.lookupEndpoint(this.session, party);
const response = await fetch(`${endpoint}/batch_sign`, {
method: "POST",
headers: {
"Content-Type": "application/json",
[WEB3_SESSION_HEADER_KEY]: this.sid,
},
body: JSON.stringify({
session: this.session,
sender: this.index,
recipient: party,
batch,
...additionalParams,
}),
}).then((res) => res.json());

response.sigs.forEach((res: string, m: number) => {
const currentFragments = sigFragments.get(m);
currentFragments.push(res);
sigFragments.set(m, currentFragments);
});
}
}

const R = await this.tssLib.get_r_from_precompute(this.precomputed_value);
const result: { r: BN; s: BN; recoveryParam: number }[] = [];

for (let m = 0; m < batch.length; m++) {
const sig = await this.tssLib.local_verify(batch[m].msg, batch[m].hash_only, R, sigFragments.get(m), this.pubKey);

const sigHex = Buffer.from(sig, "base64").toString("hex");
const r = new BN(sigHex.slice(0, 64), 16);
let s = new BN(sigHex.slice(64), 16);
let recoveryParam = Buffer.from(R, "base64")[63] % 2;
if (this._sLessThanHalf) {
const ec = getEc();
const halfOfSecp256k1n = ec.n.div(new BN(2));
if (s.gt(halfOfSecp256k1n)) {
s = ec.n.sub(s);
recoveryParam = (recoveryParam + 1) % 2;
}
}
result.push({ r, s, recoveryParam });
}
this._endSignTime = Date.now();

this._consumed = true;
this._ready = false;
this._readyResolve = null;
return result;
}

async sign(
msg: string,
hash_only: boolean,
Expand Down
7 changes: 7 additions & 0 deletions packages/tss-client/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export interface Msg {
msg_data: string;
}

export interface BatchSignParams {
msg: string;
hash_only: boolean;
original_message: string;
hash_algo: string;
}

export type PointHex = {
x: string;
y: string;
Expand Down
87 changes: 86 additions & 1 deletion packages/web-example/src/local.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable no-console */
import { privateToAddress } from "@ethereumjs/util";
import { Client } from "@toruslabs/tss-client";
import { BatchSignParams, Client } from "@toruslabs/tss-client";
import { load as loadLib } from "@toruslabs/tss-dkls-lib";
import BN from "bn.js";
import eccrypto, { generatePrivate } from "eccrypto";
Expand Down Expand Up @@ -237,11 +237,96 @@ const runPreNonceTest = async () => {
client.log("client cleaned up");
};

const runBatchTest = async () => {
// this identifier is only required for testing,
// so that clients cannot override shares of actual users incase
// share route is exposed in production, which is exposed only in development/testing
// by default.
const testingRouteIdentifier = "testingShares";
const randomNonce = keccak256(generatePrivate().toString("hex") + Date.now());
const vid = `test_verifier_name${DELIMITERS.Delimiter1}test_verifier_id`;
const session = `${testingRouteIdentifier}${vid}${DELIMITERS.Delimiter2}default${DELIMITERS.Delimiter3}0${
DELIMITERS.Delimiter4
}${randomNonce.toString("hex")}${testingRouteIdentifier}`;

// generate mock signatures.
const signatures = getSignatures();

// const session = `test:${Date.now()}`;

const parties = 4;
const clientIndex = parties - 1;

// generate endpoints for servers
const { endpoints, tssWSEndpoints, partyIndexes } = generateEndpoints(parties, clientIndex);

// setup mock shares, sockets and tss wasm files.
const [{ pubKey, privKey }, sockets] = await Promise.all([setupMockShares(endpoints, partyIndexes, session), setupSockets(tssWSEndpoints)]);

const serverCoeffs: Record<number, string> = {};
const participatingServerDKGIndexes = [1, 2, 3];

for (let i = 0; i < participatingServerDKGIndexes.length; i++) {
const serverIndex = participatingServerDKGIndexes[i];
serverCoeffs[serverIndex] = new BN(1).toString("hex");
}
// get the shares.
const share = await localStorageDB.get(`session-${session}:share`);

// Load WASM lib.
const tssLib = await loadLib();

// initialize client.
const client = new Client(session, clientIndex, partyIndexes, endpoints, sockets, share, pubKey, true, tssLib);
client.log = log;
// initiate precompute
client.precompute({ signatures, server_coeffs: serverCoeffs, _transport: 1 });
await client.ready();

const messages: BatchSignParams[] = [];
messages.push({
msg: msgHash.toString("base64"),
hash_only: true,
original_message: msg,
hash_algo: "keccak256",
});
messages.push({
msg: msgHash.toString("base64"),
hash_only: true,
original_message: msg,
hash_algo: "keccak256",
});
// initiate signature.
const sigs = await client.batch_sign(messages, { signatures });

for (let i = 0; i < sigs.length; i++) {
const signature = sigs[i];
const message = messages[i];
const msgHash = message.msg;
const buffer = Buffer.from(msgHash, "base64");
const hexToDecimal = (x: Buffer) => ec.keyFromPrivate(x).getPrivate().toString(10);
const pubk = ec.recoverPubKey(hexToDecimal(buffer), signature, signature.recoveryParam, "hex");

client.log(`pubkey, ${JSON.stringify(pubKey)}`);
client.log(`msgHash: 0x${buffer.toString("hex")}`);
client.log(`signature: 0x${signature.r.toString(16, 64)}${signature.s.toString(16, 64)}${new BN(27 + signature.recoveryParam).toString(16)}`);
client.log(`address: 0x${Buffer.from(privateToAddress(privKey.toArrayLike(Buffer, "be", 32))).toString("hex")}`);
const passed = ec.verify(buffer, signature, pubk);

client.log(`passed: ${passed}`);
}
client.log(`precompute time: ${client._endPrecomputeTime - client._startPrecomputeTime}`);
client.log(`signing time: ${client._endSignTime - client._startSignTime}`);
await client.cleanup({ signatures });
client.log("client cleaned up");
};

export const runLocalServerTest = async () => {
try {
// for (let i = 0; i < 20; i++) {
await runPreNonceTest();
await runPostNonceTest();
await runBatchTest();
// }
console.log("test succeeded");
document.title = "Test succeeded";
Expand Down

0 comments on commit 547d838

Please sign in to comment.