bitcore-tss
Version:
Bitcore Library for Threshold Signature Schemes
199 lines (173 loc) • 7.55 kB
JavaScript
const bitcoreLib = require('bitcore-lib');
const { DklsDkg, DklsTypes } = require('@bitgo/sdk-lib-mpc');
const { DklsComms } = require('./dklsComms');
const { encrypt, decrypt } = require('./utils');
const $ = bitcoreLib.util.preconditions;
class KeyGen {
#partySize;
#minSigners;
#partyId;
#seed;
#authKey;
#dkg;
#round;
/**
*
* @param {Object} params
* @param {number} params.n - number of participants
* @param {number} params.m - minimum number of signers
* @param {number} params.partyId - party id
* @param {Buffer} params.seed - OPTIONAL seed for the DKG
* @param {Buffer} params.authKey - authentication key for the DKG
* @param {number} params.round - round number for the DKG
*/
constructor({ n, m, partyId, seed, authKey, round }) {
$.checkArgument(n != null, 'n is required');
$.checkArgument(m != null, 'm is required');
$.checkArgument(partyId != null, 'partyId is required');
$.checkArgument(authKey != null, 'authKey is required');
$.checkArgument(n > 1, 'n must be greater than 1');
this.#partySize = parseInt(n);
$.checkArgument(m > 0 && m <= n, 'm must be in the range [1, n]');
this.#minSigners = parseInt(m);
$.checkArgument(partyId >= 0 && partyId < n, 'partyId must be in the range [0, n-1]');
this.#partyId = parseInt(partyId);
$.checkArgument(seed == null || Buffer.isBuffer(seed) && seed.length === 32, 'seed must be a 32 byte buffer');
this.#seed = seed;
this.#authKey = new bitcoreLib.PrivateKey(authKey);
$.checkArgument(this.#authKey.toString('hex') === authKey.toString('hex') || this.#authKey.toWIF() === authKey, 'Unrecognized authKey format');
$.checkArgument(round == null || (round >= 0 && round <= 5), 'round must be in the range [0, 5]');
this.#round = parseInt(round) || 0;
this.#dkg = new DklsDkg.Dkg(this.#partySize, this.#minSigners, this.#partyId, this.#seed);
}
/**
* Export the keygen session to a base64 encoded string
* @returns {string} Base64 encoded session string
*/
export() {
$.checkState(this.#round > 0, 'Cannot export a session that has not started');
$.checkState(!this.isKeyChainReady(), 'Cannot export a completed session. The keychain is ready with getKeyChain()');
const chainCodeCommitment = this.#dkg.chainCodeCommitment;
const sessionBytes = this.#dkg.dkgSessionBytes || this.#dkg.dkgSession?.toBytes();
const payload = this.#round +
':' + this.#partySize +
':' + this.#minSigners +
':' + this.#partyId +
':' + Buffer.from(sessionBytes).toString('base64') +
':' + Buffer.from(chainCodeCommitment || []).toString('base64');
const buf = encrypt(Buffer.from(payload, 'utf8'), this.#authKey.publicKey, this.#authKey);
return buf.toString('base64');
}
/**
* Restore a keygen session from an exported session
* @param {Object} params
* @param {string} params.session Base64 encoded session string
* @param {bitcoreLib.PrivateKey} params.authKey Private key to use for decrypting the session
* @param {Buffer} params.seed Seed used for key generation
* @returns {Sign}
*/
static async restore({ session, authKey, seed }) {
const _authKey = new bitcoreLib.PrivateKey(authKey);
$.checkArgument(_authKey.toString('hex') === authKey.toString('hex') || _authKey.toWIF() === authKey, 'Unrecognized authKey format');
session = decrypt(Buffer.from(session, 'base64'), _authKey.publicKey, _authKey).toString('utf8');
const [
round,
partySize,
minSigners,
partyId,
sessionBytes,
chainCodeCommitment
] = session.split(':');
const initParams = {
round: parseInt(round),
n: parseInt(partySize),
m: parseInt(minSigners),
partyId: parseInt(partyId),
authKey,
seed: Buffer.from(seed, 'base64'),
};
const keygen = new KeyGen(initParams);
await keygen.#dkg.loadDklsWasm();
keygen.#dkg.dkgState = parseInt(round);
keygen.#dkg.dkgSessionBytes = new Uint8Array(Buffer.from(sessionBytes, 'base64'));
keygen.#dkg.chainCodeCommitment = chainCodeCommitment ? new Uint8Array(Buffer.from(chainCodeCommitment, 'base64')) : undefined;
return keygen;
}
/**
* @private
* Format the message to be sent to the other parties
* @param {Object} signedMessage
* @returns
*/
_formatMessage(signedMessage) {
return {
round: this.#round++,
partyId: this.#partyId,
publicKey: this.#authKey.publicKey.toString(),
p2pMessages: signedMessage.p2pMessages,
broadcastMessages: signedMessage.broadcastMessages,
};
}
/**
* Initialize the keygen session with a broadcast message to send to the other participants
* @returns {Promise<{round: number, partyId: number, publicKey: string, p2pMessages: Object[], broadcastMessages: Object[]}>}
*/
async initJoin() {
$.checkState(this.#round == 0, 'initJoin must be called before the rounds ');
const unsignedMessageR1 = await this.#dkg.initDkg();
const serializedMsg = DklsTypes.serializeBroadcastMessage(unsignedMessageR1);
const signedMessage = await DklsComms.encryptAndAuthOutgoingMessages(
{ broadcastMessages: [serializedMsg], p2pMessages: [] },
[],
this.#authKey
);
return this._formatMessage(signedMessage);
}
/**
* Call this after receiving the initJoin broadcast messages from the other participants
* and while isKeyChainReady() is false
* @param {Array<Object>} prevRoundMessages
* @returns {{ round: number, partyId: number, publicKey: string, p2pMessages: Object[], broadcastMessage: Object[] }}
*/
nextRound(prevRoundMessages) {
$.checkState(this.#round > 0, 'initJoin must be called before participating in the rounds');
$.checkState(this.#round < 5, 'Signing rounds are over');
$.checkArgument(Array.isArray(prevRoundMessages), 'prevRoundMessages must be an array');
$.checkArgument(prevRoundMessages.length === this.#partySize - 1, 'Not ready to proceed to the next round');
$.checkArgument(prevRoundMessages.every(msg => msg.round === this.#round - 1), 'All messages must be from the previous round');
$.checkArgument(prevRoundMessages.every(msg => msg.partyId !== this.#partyId), 'Messages must not be from the yourself');
const prevRoundIncomingMsgs = DklsComms.decryptAndVerifyIncomingMessages(prevRoundMessages, this.#authKey);
const thisRoundMessages = DklsTypes.serializeMessages(
this.#dkg.handleIncomingMessages(DklsTypes.deserializeMessages(prevRoundIncomingMsgs))
);
const signedMessage = DklsComms.encryptAndAuthOutgoingMessages(
thisRoundMessages,
prevRoundMessages.map(m => ({ partyId: m.partyId, publicKey: m.publicKey })),
this.#authKey
);
return this._formatMessage(signedMessage);
}
/**
* Check if the keychain is ready
* @returns {boolean}
*/
isKeyChainReady() {
return this.#round === 5;
}
/**
* Get the keychain
* @returns {{ privateKeyShare: Buffer, reducedPrivateKeyShare: Buffer, commonKeyChain: string }} Keychain object
*/
getKeyChain() {
$.checkState(this.isKeyChainReady(), 'Key chain is not ready');
const keyShare = this.#dkg.getKeyShare();
const rKeyShare = this.#dkg.getReducedKeyShare();
const commonKeyChain = DklsTypes.getCommonKeychain(keyShare);
return {
privateKeyShare: keyShare,
reducedPrivateKeyShare: rKeyShare,
commonKeyChain: commonKeyChain
};
}
};
module.exports.KeyGen = KeyGen;