Skip to content

[BWC/BWS] Add TSS to BWC & BWS #3895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: v11
Choose a base branch
from
Open

Conversation

kajoseph
Copy link
Collaborator

@kajoseph kajoseph commented Apr 28, 2025

This PR:

  • Does a lot of BWC style cleanup and typing. None of it should break anything.
  • Adds TSS classes to BWC and corresponding endpoints to BWS. Again, should just be additive - nothing breaking.
  • More typing, cleanup, and organizing in BWS

Note: I tried to be careful NOT to import the TSS classes into any existing files. The reason for this is so that it doesn't break any frontend webpack/vite/etc compilations with the underlying TSS WASM dependencies...which might be a pain to configure.

@kajoseph kajoseph marked this pull request as draft April 28, 2025 19:54
@kajoseph kajoseph changed the base branch from master to v11 May 21, 2025 16:30
const { value, algo } = params;
switch (String(algo).toUpperCase()) {
switch (algo?.toUpperCase?.()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optional chaining after toUpperCase is extraneous

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it's not because algo could be a non-string if being called from JS (read: no type checking) as is the case in tests.

const { value, algo } = params;
switch (String(algo).toUpperCase()) {
switch (algo?.toUpperCase?.()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraneous optional chaining after toUpperCase

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

switch (String(params?.algo).toUpperCase()) {
#getPrivKey(params: { algo?: KeyAlgorithm; } = {}) {
const { algo } = params;
switch (algo?.toUpperCase?.()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraneous optional chaining after upperCase()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

switch (String(params?.algo).toUpperCase()) {
#getPrivKeyEncrypted(params: { algo?: KeyAlgorithm; } = {}) {
const { algo } = params;
switch (algo?.toUpperCase?.()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraneous optional chaining after upperCase()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

switch (String(params?.algo).toUpperCase()) {
#getFingerprint(params: { algo?: KeyAlgorithm; } = {}) {
const { algo } = params;
switch (algo?.toUpperCase?.()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraneous optional chaining after upperCase()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

supportStaffWalletId: any;
timeout: any;
r: Request;
credentials: CredT;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could lead to type errors since it's initialized as null.

@@ -59,13 +68,13 @@ export class Request {
useSession?: boolean
) {
if (this.credentials) {
headers['x-identity'] = this.credentials.copayerId;
headers['x-identity'] = (this.credentials as Credentials).copayerId;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be ensuring that this.credentials is a Credentials, and not some other 'CredT'?

/**
* Threshold Signature Scheme (TSS) client class
* @param {ITssKeyGenConstructorParams} params Constructor parameters
* @param {EventEmitterOptions} eventOpts Options object for EventEmitter

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unresolved

$.checkArgument(params.key instanceof Key, 'key must be an instance of Key');

this.#request = new Request(params.baseUrl, {
r: params.request, // For testing only

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving this in here?

const prevRound = thisRound - 1; // Get previous round's messages
const { body } = await this.#request.get(`/v1/tss/keygen/${this.id}/${prevRound}`) as RequestResponse;

if (body.messages?.length === this.n - 1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a clarifying comment above this conditional

},
metadata: {
id: string;
m: number;
Copy link

@MichaelAJay MichaelAJay Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n is number of participants, and m is number of required signers - right? Brief inline documentation in this file would be good, especially for ambiguous fields

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment in TssKeyGen class below

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is documented elsewhere (e.g. on input params) and m of n is the generally understood terminology for signers of participants (https://github.com/bitcoin/bips/blob/master/bip-0129.mediawiki#coordinator-1 see first bullet under Coordinator)

}): string {
const { partyId, partyPubKey, opts } = params;
const extra = params.extra || '';
const data = [this.id, partyId, this.m, this.n, extra].join(':');
Copy link

@MichaelAJay MichaelAJay Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use the default value of extra (or if it's undefined), then you end up with a ':' at the end of your string.

const myParams = {
	id: '123',
  partyId: '456',
  m: 3,
  n: 5
};

const myStr = [myParams.id, myParams.partyId, myParams.m, myParams.n, myParams.extra].join(':');
const myStr2 = [myParams.id, myParams.partyId, myParams.m, myParams.n, ''].join(':');
console.log(myStr);
console.log(myStr2);

If that's not the intended behavior -
Suggestion:
const myJoinCodeArr = [this.id, partyId, this.m, this.n];
if (params.extra) { myJoinCodeArr.push(params.extra };
const code = myJoinCodeArr.join(':');

privateKey: this.#requestPrivateKey,
opts
});
return code.toString(opts?.encoding || 'hex');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If opts.encoding, then is it ensured to be in your list of provided values? Would validation be useful here?

const encodingSupported = ['hex', 'base64', 'utf8', 'binary'].includes(opts?.encoding);
const encoding = encodingSupported ? opts.encoding : 'hex';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, as an args check?

$.checkArgument(code, 'Missing required param: code');
$.checkArgument(typeof code === 'string' || Buffer.isBuffer(code), '`code` must be a string or buffer');

code = Buffer.isBuffer(code) ? code : Buffer.from(code, opts?.encoding || 'hex');
Copy link

@MichaelAJay MichaelAJay Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see note in createJoinCode comment above

this.partyId = parseInt(partyId);

const msg = await keygen.initJoin();
password = password || extra;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should happen if password isn't provided in opts, and there's no 'more'?

It will be an empty string on the reassign - is that alright?


/**
* Export the session for storage
* @returns {string} Session string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string format would be nice: 'id:partyId:m:n:keygenSession'
And in the restoreSession below it.

/**
* Threshold Signature Scheme (TSS) client class
* @param {ITssConstructorParams} params Constructor parameters
* @param {EventEmitterOptions} eventOpts Options object for EventEmitter

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unresolved

const prevRound = thisRound - 1; // Get previous round's messages
const { body } = await this.#request.get(`/v1/tss/sign/${this.id}/${prevRound}`) as RequestResponse;

if (body.messages?.length === this.#tssKey.metadata.m - 1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment would be really useful here

If the DKG ceremony is executed asynchronously (meaning all parties are not required to be online at the same time during the DKG), the `keyGen.export()` function should be used between each step to export the local session state. The session state should be securely stored and used with `KeyGen.restore()` for following steps.

### Flow Diagram
![DKG Flow](https://drive.google.com/uc?export=view&id=1A-zzWamhDmFzPY7GUqYNvc8BFHMUM22c)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broken link. You could change this to DKG-flow.png & it would show up in an md preview as the image.

If the DSG ceremony is executed asynchronously (meaning all signing parties are not required to be online at the same time during the DSG), the `sign.export()` function should be used between each step to export the local session state. The session state should be securely stored and used with `Sign.restore()` for following steps.

### Flow Diagram
![DKG Flow](https://drive.google.com/uc?export=view&id=1cU_m5wzpPUCcrhTIS0-gWWopdOIa_wYx)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DSG Flow. And also, as above, you can reference the DSG-flow.png from the markdown file to embed the image into it.

<summary style="font-size:17px"><span style="font-weight:bold">POST</span> /v1/tss/keygen/:id/store</summary>


### Request body:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't see the request body on markdown preview

@@ -71,7 +75,7 @@ export class ExpressApp {
const POST_LIMIT_LARGE = 2 * 1024 * 1024; // Max POST 2 MB

this.app.use((req, res, next) => {
if (req.path.includes('/txproposals')) {
if (req.path.includes('/txproposals') || req.path.includes('/tss/')) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking: is the difference in form intended - note the trailing slash for tss

x.copayers = _.map(obj.copayers, copayer => {
return Copayer.fromObj(copayer);
});
x.copayers = obj.copayers?.map(copayer => Copayer.fromObj(copayer));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If obj.copayers is undefined, x.copayers will be undefined.
To get array behavior, consider:
x.copayers = (obj.copayers || []).map(...) or use ternary

}

async processMessage(params: { id: string; message: ITssKeyMessageObject; n?: string | number; password?: string; copayerId: string; }) {
const { id, message, n, password, copayerId } = params;
Copy link

@MichaelAJay MichaelAJay Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's TS problem here. Here, n is typed as string | number, and no longer considered to potentially be undefined.

The documentation for this method, in the md file in this PR, also considers it optional. _initSession expects a defined value.

Invalid n gets handled in _initSession - do you want it to make it that far?

}

const otherPartyMsgs = session.rounds[round].filter(m => m.fromPartyId != partyId);
if (otherPartyMsgs.length === session.n - 1) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment as to why it's all or nothing for otherPartyMsgs length would be useful

throw Errors.TSS_GENERIC_ERROR.withMessage('Invalid n provided: ' + n);
}

let passwordHash: string = null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make the assignment stricter by setting as an empty string, or defining the type as string | null?

Also, consider declaring as const and using ternary operator on password.

throw Errors.TSS_INVALID_MESSAGE.withMessage('Invalid message provided');
}

const isParticipant = !!session.participants.find(p => p.copayerId === copayerId && p.partyId === message.partyId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.prototype.some is semantically more appropriate


let result = false;
while (!result) {
result = await this._pushMessage({ id, session, message, storage });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment ensuring a guarantee that we'll break out of this loop would be nice, and provide context to the work at large

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add bitcore-tss to BWS Add bitcore-tss to BWC
2 participants