@bitpay-labs/bitcore-wallet-service 11.11.2 → 11.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/package.json +2 -2
  2. package/src/lib/common/constants.ts +1 -1
  3. package/src/lib/common/utils.ts +18 -1
  4. package/src/lib/model/tsskeygen.ts +16 -0
  5. package/src/lib/routes/maintenance.ts +11 -2
  6. package/src/lib/routes/middleware/authTssRequest.ts +28 -16
  7. package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
  8. package/src/lib/routes/services.ts +7 -1
  9. package/src/lib/routes/tss.ts +33 -30
  10. package/src/lib/server.ts +62 -17
  11. package/src/lib/storage.ts +39 -0
  12. package/src/lib/tss.ts +111 -7
  13. package/test/expressapp.test.ts +99 -0
  14. package/test/integration/server.test.ts +53 -1
  15. package/test/integration/tss.test.ts +428 -0
  16. package/ts_build/package.json +4 -4
  17. package/ts_build/src/lib/common/constants.d.ts +1 -1
  18. package/ts_build/src/lib/common/constants.js +1 -1
  19. package/ts_build/src/lib/common/index.d.ts +2 -1
  20. package/ts_build/src/lib/common/index.d.ts.map +1 -1
  21. package/ts_build/src/lib/common/utils.d.ts +1 -0
  22. package/ts_build/src/lib/common/utils.d.ts.map +1 -1
  23. package/ts_build/src/lib/common/utils.js +16 -1
  24. package/ts_build/src/lib/common/utils.js.map +1 -1
  25. package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
  26. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  27. package/ts_build/src/lib/model/tsskeygen.js +3 -0
  28. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  29. package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
  30. package/ts_build/src/lib/routes/maintenance.js +12 -4
  31. package/ts_build/src/lib/routes/maintenance.js.map +1 -1
  32. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
  33. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  34. package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
  35. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  36. package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
  37. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
  38. package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
  39. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  40. package/ts_build/src/lib/routes/services.js +6 -1
  41. package/ts_build/src/lib/routes/services.js.map +1 -1
  42. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  43. package/ts_build/src/lib/routes/tss.js +32 -30
  44. package/ts_build/src/lib/routes/tss.js.map +1 -1
  45. package/ts_build/src/lib/server.d.ts +23 -2
  46. package/ts_build/src/lib/server.d.ts.map +1 -1
  47. package/ts_build/src/lib/server.js +20 -3
  48. package/ts_build/src/lib/server.js.map +1 -1
  49. package/ts_build/src/lib/storage.d.ts +9 -0
  50. package/ts_build/src/lib/storage.d.ts.map +1 -1
  51. package/ts_build/src/lib/storage.js +26 -0
  52. package/ts_build/src/lib/storage.js.map +1 -1
  53. package/ts_build/src/lib/tss.d.ts +12 -0
  54. package/ts_build/src/lib/tss.d.ts.map +1 -1
  55. package/ts_build/src/lib/tss.js +64 -5
  56. package/ts_build/src/lib/tss.js.map +1 -1
  57. package/ts_build/test/expressapp.test.js +90 -0
  58. package/ts_build/test/expressapp.test.js.map +1 -1
  59. package/ts_build/test/integration/server.test.js +47 -0
  60. package/ts_build/test/integration/server.test.js.map +1 -1
  61. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  62. package/ts_build/test/integration/tss.test.js +344 -0
  63. package/ts_build/test/integration/tss.test.js.map +1 -1
@@ -1,10 +1,13 @@
1
+ import util from 'util';
1
2
  import sinon from 'sinon';
2
3
  import 'chai/register-should';
4
+ import { utils as tssUtils } from '@bitpay-labs/bitcore-tss';
3
5
  import { BitcoreLib } from '@bitpay-labs/crypto-wallet-core';
4
6
  import request from 'supertest';
5
7
  import * as twoOfThree from './data/tss/2of3';
6
8
  import BWS from '../../src/index';
7
9
  import helpers from './helpers';
10
+ import * as TestData from '../testdata';
8
11
  import { TssKeyGen } from '../../src/lib/tss';
9
12
  import { TssSigGenModel } from '../../src/lib/model/tsssign';
10
13
  import { TssKeyGenModel } from '../../src/lib/model/tsskeygen';
@@ -472,6 +475,14 @@ describe('TSS', function() {
472
475
  const res = await post(path, { message }, partyAuth(0));
473
476
  expectError(res, 400, 'TSS_INVALID_MESSAGE');
474
477
  });
478
+
479
+ it('should error if messages are empty arrays for an existing session', async function() {
480
+ // keygen sessions have no `m`, so the 1-of-N signing exemption must not apply here
481
+ await createKeyGenSession({ joined: [1, 2] });
482
+ const message = { ...round1Message(0), broadcastMessages: [], p2pMessages: [] };
483
+ const res = await post(path, { message }, partyAuth(0));
484
+ expectError(res, 400, 'TSS_INVALID_MESSAGE');
485
+ });
475
486
  });
476
487
 
477
488
  describe('joining a v1.2 session', function() {
@@ -839,6 +850,329 @@ describe('TSS', function() {
839
850
  });
840
851
  });
841
852
  });
853
+
854
+ describe('BWS join secrets', function() {
855
+ const path = `/v1/tss/keygen/${SESSION_ID}/secret`;
856
+ const ethSecrets = { 1: 'ethSecretForParty1', 2: 'ethSecretForParty2' };
857
+ const storeSecrets = (body: object, auth: Auth = partyAuth(0)) => post(path, body, auth);
858
+ const getSecret = (auth: Auth, query: { chain?: string; network?: string } = { chain: 'eth', network: 'livenet' }) => {
859
+ const qs = new URLSearchParams(query).toString();
860
+ return get(path + (qs ? '?' + qs : ''), auth);
861
+ };
862
+ const fetchSession = () => storage.fetchTssKeyGenSession({ id: SESSION_ID });
863
+
864
+ describe('POST /v1/tss/keygen/:id/secret', function() {
865
+ it('should store the secrets per chain and network', async function() {
866
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
867
+
868
+ (await storeSecrets({ chain: 'eth', network: 'livenet', secrets: ethSecrets })).status.should.equal(200);
869
+ (await storeSecrets({ chain: 'arb', network: 'livenet', secrets: { 1: 'arb1', 2: 'arb2' } })).status.should.equal(200);
870
+
871
+ const session = await fetchSession();
872
+ session.bwsJoinSecrets.should.deep.equal({
873
+ 'eth:livenet': ethSecrets,
874
+ 'arb:livenet': { 1: 'arb1', 2: 'arb2' }
875
+ });
876
+ });
877
+
878
+ it('should key the secrets by partyId when given keygen session copayer IDs', async function() {
879
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
880
+
881
+ const res = await storeSecrets({ chain: 'eth', network: 'livenet', secrets: { [copayerIds[2]]: 'two', [copayerIds[1]]: 'one' } });
882
+ res.status.should.equal(200);
883
+
884
+ const session = await fetchSession();
885
+ session.bwsJoinSecrets['eth:livenet'].should.deep.equal({ 1: 'one', 2: 'two' });
886
+ });
887
+
888
+ it('should normalize the chain and network', async function() {
889
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
890
+
891
+ (await storeSecrets({ chain: 'ETH', network: 'testnet', secrets: ethSecrets })).status.should.equal(200);
892
+ (await storeSecrets({ chain: 'btc', network: 'mainnet', secrets: ethSecrets })).status.should.equal(200);
893
+
894
+ const session = await fetchSession();
895
+ Object.keys(session.bwsJoinSecrets).should.deep.equal(['eth:sepolia', 'btc:livenet']);
896
+ });
897
+
898
+ it('should overwrite the secrets for the same chain and network', async function() {
899
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
900
+
901
+ (await storeSecrets({ chain: 'eth', network: 'livenet', secrets: { 1: 'old1', 2: 'old2' } })).status.should.equal(200);
902
+ (await storeSecrets({ chain: 'eth', network: 'livenet', secrets: ethSecrets })).status.should.equal(200);
903
+
904
+ const session = await fetchSession();
905
+ session.bwsJoinSecrets.should.deep.equal({ 'eth:livenet': ethSecrets });
906
+ });
907
+
908
+ it('should store the secrets after the session has expired', async function() {
909
+ await createKeyGenSession({ version: 1.3, joined: [1, 2], timeLimit: 1 });
910
+ expireSession();
911
+
912
+ const res = await storeSecrets({ chain: 'eth', network: 'livenet', secrets: ethSecrets });
913
+ res.status.should.equal(200);
914
+ (await fetchSession()).bwsJoinSecrets['eth:livenet'].should.deep.equal(ethSecrets);
915
+ });
916
+
917
+ it('should store the secret sent by a legacy (< v1.3) client', async function() {
918
+ await createKeyGenSession({ version: 1.2, joined: [1, 2] });
919
+
920
+ const res = await storeSecrets({ secret: 'legacySecret' });
921
+ res.status.should.equal(200);
922
+
923
+ const session = await fetchSession();
924
+ session.bwsJoinSecret.should.equal('legacySecret');
925
+ session.bwsJoinSecrets.should.deep.equal({});
926
+ });
927
+
928
+ it('should error if the requester is not the session leader', async function() {
929
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
930
+
931
+ const res = await storeSecrets({ chain: 'eth', network: 'livenet', secrets: ethSecrets }, partyAuth(1));
932
+ expectError(res, 400, 'TSS_GENERIC_ERROR', 'Only the session creator can store the BWS join secret');
933
+ });
934
+
935
+ it('should error if the copayer is not in the session', async function() {
936
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
937
+
938
+ const res = await storeSecrets({ chain: 'eth', network: 'livenet', secrets: ethSecrets }, { copayerId: 'notInSession', privKey: randomKey() });
939
+ expectError(res, 401, 'NOT_AUTHORIZED');
940
+ });
941
+
942
+ for (const [description, body] of [
943
+ ['a party is missing a secret', { chain: 'eth', network: 'livenet', secrets: { 1: 'one' } }],
944
+ ['a secret is for the leader', { chain: 'eth', network: 'livenet', secrets: { ...ethSecrets, 0: 'zero' } }],
945
+ ['a secret is for a partyId >= n', { chain: 'eth', network: 'livenet', secrets: { ...ethSecrets, 3: 'three' } }],
946
+ ['a secret is for a copayer that is not in the session', { chain: 'eth', network: 'livenet', secrets: { 1: 'one', notInSession: 'two' } }],
947
+ ['a secret is not a string', { chain: 'eth', network: 'livenet', secrets: { 1: 'one', 2: { $ne: null } } }],
948
+ ['a secret is empty', { chain: 'eth', network: 'livenet', secrets: { 1: 'one', 2: '' } }],
949
+ ['secrets is not an object', { chain: 'eth', network: 'livenet', secrets: 'abc' }],
950
+ ['secrets is an array', { chain: 'eth', network: 'livenet', secrets: ['zero', 'one', 'two'] }],
951
+ ['secrets is missing', { chain: 'eth', network: 'livenet' }],
952
+ ['chain is missing', { network: 'livenet', secrets: ethSecrets }],
953
+ ['chain is not supported', { chain: 'foo', network: 'livenet', secrets: ethSecrets }],
954
+ ['chain is a Mongo path', { chain: 'eth.livenet', network: 'livenet', secrets: ethSecrets }],
955
+ ['network is missing', { chain: 'eth', secrets: ethSecrets }],
956
+ ['network is not supported', { chain: 'eth', network: 'foo', secrets: ethSecrets }],
957
+ ] as Array<[string, object]>) {
958
+ it(`should error if ${description}`, async function() {
959
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
960
+
961
+ const res = await storeSecrets(body);
962
+ expectError(res, 400, 'TSS_GENERIC_ERROR');
963
+ (await fetchSession()).bwsJoinSecrets.should.deep.equal({});
964
+ });
965
+ }
966
+ });
967
+
968
+ describe('GET /v1/tss/keygen/:id/secret', function() {
969
+ beforeEach(async function() {
970
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
971
+ await storage.storeTssKeyBwsJoinSecrets({ id: SESSION_ID, chain: 'eth', network: 'livenet', secrets: ethSecrets });
972
+ });
973
+
974
+ it('should return each party its own secret for the chain and network', async function() {
975
+ for (const partyId of [1, 2]) {
976
+ const res = await getSecret(partyAuth(partyId));
977
+ res.status.should.equal(200);
978
+ res.body.secret.should.equal(ethSecrets[partyId]);
979
+ }
980
+ });
981
+
982
+ it('should normalize the chain and network', async function() {
983
+ const res = await getSecret(partyAuth(1), { chain: 'ETH', network: 'mainnet' });
984
+ res.status.should.equal(200);
985
+ res.body.secret.should.equal(ethSecrets[1]);
986
+ });
987
+
988
+ it('should return the secret after the session has expired', async function() {
989
+ // Use a short time limit so that expireSession() really expires the session (the default limit is longer)
990
+ await storage.db.collection(BWS.Storage.collections.TSS_KEYGEN).deleteMany({});
991
+ await createKeyGenSession({ version: 1.3, joined: [1, 2], timeLimit: 1 });
992
+ await storage.storeTssKeyBwsJoinSecrets({ id: SESSION_ID, chain: 'eth', network: 'livenet', secrets: ethSecrets });
993
+ const roundPath = `/v1/tss/keygen/${SESSION_ID}/0`;
994
+
995
+ // Before expiring: the session is live, so expiry-guarded endpoints accept the party and the secret is returned
996
+ (await fetchSession()).isExpired().should.equal(false);
997
+ const live = await get(roundPath, partyAuth(1));
998
+ live.status.should.equal(200);
999
+ (await getSecret(partyAuth(1))).body.secret.should.equal(ethSecrets[1]);
1000
+
1001
+ expireSession();
1002
+
1003
+ // After expiring: the session is expired and expiry-guarded endpoints reject it...
1004
+ (await fetchSession()).isExpired().should.equal(true);
1005
+ expectError(await get(roundPath, partyAuth(1)), 400, 'TSS_SESSION_EXPIRED');
1006
+
1007
+ // ...but the secret is still available
1008
+ const res = await getSecret(partyAuth(1));
1009
+ res.status.should.equal(200);
1010
+ res.body.secret.should.equal(ethSecrets[1]);
1011
+ });
1012
+
1013
+ it('should error if there is no secret for the chain', async function() {
1014
+ const res = await getSecret(partyAuth(1), { chain: 'arb', network: 'livenet' });
1015
+ expectError(res, 400, 'TSS_BWS_JOIN_SECRET_NOT_FOUND');
1016
+ });
1017
+
1018
+ it('should error if there is no secret for the network', async function() {
1019
+ const res = await getSecret(partyAuth(1), { chain: 'eth', network: 'testnet' });
1020
+ expectError(res, 400, 'TSS_BWS_JOIN_SECRET_NOT_FOUND');
1021
+ });
1022
+
1023
+ it('should error if the chain is not supported', async function() {
1024
+ const res = await getSecret(partyAuth(1), { chain: 'foo', network: 'livenet' });
1025
+ expectError(res, 400, 'TSS_GENERIC_ERROR', 'Invalid chain provided');
1026
+ });
1027
+
1028
+ it('should error if only one of chain and network is given', async function() {
1029
+ const res = await getSecret(partyAuth(1), { chain: 'eth' });
1030
+ expectError(res, 400, 'TSS_GENERIC_ERROR', 'Invalid network provided');
1031
+ });
1032
+
1033
+ it('should error if the copayer is not in the session', async function() {
1034
+ const res = await getSecret({ copayerId: 'notInSession', privKey: randomKey() });
1035
+ expectError(res, 401, 'NOT_AUTHORIZED');
1036
+ });
1037
+
1038
+ it('should reject another party identity', async function() {
1039
+ const res = await getSecret({ copayerId: copayerIds[2], privKey: parties[1].authKey });
1040
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
1041
+ });
1042
+
1043
+ it('should not return the legacy secret to clients that send the chain and network', async function() {
1044
+ await storage.storeTssKeyBwsJoinSecret({ id: SESSION_ID, secret: 'legacySecret' });
1045
+ const res = await getSecret(partyAuth(1), { chain: 'arb', network: 'livenet' });
1046
+ expectError(res, 400, 'TSS_BWS_JOIN_SECRET_NOT_FOUND');
1047
+ });
1048
+
1049
+ describe('legacy (v1.2) session', function() {
1050
+ beforeEach(async function() {
1051
+ await storage.db.collection(BWS.Storage.collections.TSS_KEYGEN).deleteMany({});
1052
+ await createKeyGenSession({ version: 1.2, joined: [1, 2] });
1053
+ await storage.storeTssKeyBwsJoinSecret({ id: SESSION_ID, secret: 'legacySecret' });
1054
+ });
1055
+
1056
+ it('should return the legacy secret to legacy clients', async function() {
1057
+ const res = await getSecret(partyAuth(1), {});
1058
+ res.status.should.equal(200);
1059
+ res.body.secret.should.equal('legacySecret');
1060
+ });
1061
+
1062
+ it('should return the chain secret for a wallet created with the existing key', async function() {
1063
+ await storage.storeTssKeyBwsJoinSecrets({ id: SESSION_ID, chain: 'arb', network: 'livenet', secrets: { 1: 'arb1', 2: 'arb2' } });
1064
+ const res = await getSecret(partyAuth(1), { chain: 'arb', network: 'livenet' });
1065
+ res.status.should.equal(200);
1066
+ res.body.secret.should.equal('arb1');
1067
+ });
1068
+
1069
+ it('should error if there is no secret for the chain', async function() {
1070
+ const res = await getSecret(partyAuth(1), { chain: 'arb', network: 'livenet' });
1071
+ expectError(res, 400, 'TSS_BWS_JOIN_SECRET_NOT_FOUND');
1072
+ });
1073
+ });
1074
+ });
1075
+ });
1076
+
1077
+ describe('TSS wallets', function() {
1078
+ // The keygen session copayer IDs are derived with 'btc'. Wallets are created on other chains.
1079
+ const createWallet = (body: object) => app.post(urlPrefix + '/v2/wallets/').send({
1080
+ name: 'tss wallet',
1081
+ m: vector.m,
1082
+ n: vector.n,
1083
+ pubKey: TestData.keyPair.pub,
1084
+ chain: 'arb',
1085
+ network: 'testnet',
1086
+ tssKeyId: SESSION_ID,
1087
+ ...body
1088
+ });
1089
+ const joinWallet = (walletId: string, partyId: number, body: object = {}) => app.post(`${urlPrefix}/v2/wallets/${walletId}/copayers/`).send(
1090
+ helpers.getSignedCopayerOpts({
1091
+ walletId,
1092
+ name: 'party' + partyId,
1093
+ xPubKey: getXPubKey(parties[partyId].seed),
1094
+ requestPubKey: authPubKey(partyId),
1095
+ chain: 'arb',
1096
+ ...body
1097
+ })
1098
+ );
1099
+
1100
+ beforeEach(async function() {
1101
+ await storage.db.collection(BWS.Storage.collections.WALLETS).deleteMany({});
1102
+ await storage.db.collection(BWS.Storage.collections.COPAYERS_LOOKUP).deleteMany({});
1103
+ await createKeyGenSession({ version: 1.3, joined: [1, 2] });
1104
+ await storage.storeTssKeySharedPubKey({ id: SESSION_ID, publicKey: 'sharedPublicKey' });
1105
+ });
1106
+
1107
+ describe('POST /v2/wallets/', function() {
1108
+ it('should create a wallet on another chain than the key generation session', async function() {
1109
+ const res = await createWallet({ tssKeygenCopayerId: copayerIds[0] });
1110
+ res.status.should.equal(200);
1111
+ const wallet = await util.promisify(storage.fetchWallet).call(storage, res.body.walletId);
1112
+ wallet.chain.should.equal('arb');
1113
+ wallet.tssKeyId.should.equal(SESSION_ID);
1114
+ wallet.tssVersion.should.equal(1.3);
1115
+ wallet.m.should.equal(1);
1116
+ wallet.n.should.equal(1);
1117
+ });
1118
+
1119
+ it('should error if the keygen session copayer ID is missing', async function() {
1120
+ const res = await createWallet({});
1121
+ expectError(res, 400, 'BADREQUEST', 'Copayer ID is required for TSS version >= 1.3');
1122
+ });
1123
+
1124
+ it('should error if the keygen session copayer ID is not the session leader', async function() {
1125
+ const res = await createWallet({ tssKeygenCopayerId: copayerIds[1] });
1126
+ expectError(res, 400, 'BADREQUEST', 'Only the TSS session leader can create a wallet');
1127
+ });
1128
+
1129
+ it('should not require the keygen session copayer ID for a legacy (v1.2) session', async function() {
1130
+ await storage.db.collection(BWS.Storage.collections.TSS_KEYGEN).deleteMany({});
1131
+ await createKeyGenSession({ version: 1.2, joined: [1, 2] });
1132
+ await storage.storeTssKeySharedPubKey({ id: SESSION_ID, publicKey: 'sharedPublicKey' });
1133
+
1134
+ const res = await createWallet({});
1135
+ res.status.should.equal(200);
1136
+ });
1137
+ });
1138
+
1139
+ describe('POST /v2/wallets/:id/copayers/', function() {
1140
+ let walletId: string;
1141
+
1142
+ beforeEach(async function() {
1143
+ const res = await createWallet({ tssKeygenCopayerId: copayerIds[0] });
1144
+ walletId = res.body.walletId;
1145
+ });
1146
+
1147
+ it('should let the parties join with their keygen session copayer IDs', async function() {
1148
+ for (const partyId of [0, 1, 2]) {
1149
+ const res = await joinWallet(walletId, partyId, { tssKeygenChain: 'btc' });
1150
+ res.status.should.equal(200);
1151
+ res.body.copayerId.should.equal(xPubToCopayerId('arb', getXPubKey(parties[partyId].seed)));
1152
+ }
1153
+ const wallet = await util.promisify(storage.fetchWallet).call(storage, walletId);
1154
+ wallet.copayers.length.should.equal(3);
1155
+ });
1156
+
1157
+ it('should error if the keygen chain is not given for a different wallet chain', async function() {
1158
+ const res = await joinWallet(walletId, 1);
1159
+ expectError(res, 400, 'TSS_NON_PARTICIPANT');
1160
+ });
1161
+
1162
+ it('should error if the keygen chain is invalid', async function() {
1163
+ const res = await joinWallet(walletId, 1, { tssKeygenChain: 'foo' });
1164
+ expectError(res, 400, 'BADREQUEST', 'Invalid TSS keygen chain');
1165
+ });
1166
+
1167
+ it('should error if the copayer was not in the key generation session', async function() {
1168
+ const res = await joinWallet(walletId, 1, {
1169
+ xPubKey: getXPubKey(Buffer.alloc(32, 1)),
1170
+ tssKeygenChain: 'btc'
1171
+ });
1172
+ expectError(res, 400, 'TSS_NON_PARTICIPANT');
1173
+ });
1174
+ });
1175
+ });
842
1176
  });
843
1177
 
844
1178
  describe('Signing', function() {
@@ -878,6 +1212,16 @@ describe('TSS', function() {
878
1212
  describe('POST /v1/tss/sign/:id', function() {
879
1213
  const path = `/v1/tss/sign/${SESSION_ID}`;
880
1214
 
1215
+ /** A round message with a broadcast signed by the party's auth key, as round 0 always has */
1216
+ const broadcastMessage = (partyId: number, round = 0) => ({
1217
+ ...sigMessage(partyId),
1218
+ round,
1219
+ broadcastMessages: [{
1220
+ from: partyId,
1221
+ payload: tssUtils.detachSignData(Buffer.from(`round ${round} from party ${partyId}`), parties[partyId].authKey)
1222
+ }]
1223
+ });
1224
+
881
1225
  it('should error if session is expired', async function() {
882
1226
  await createSigSession({ timeLimit: 1 });
883
1227
  expireSession();
@@ -886,6 +1230,90 @@ describe('TSS', function() {
886
1230
  const res = await post(path, { message }, partyAuth(1));
887
1231
  expectError(res, 400, 'TSS_SESSION_EXPIRED');
888
1232
  });
1233
+
1234
+ it('should start a new session with party 0\'s round 0 message', async function() {
1235
+ const res = await post(path, { message: broadcastMessage(0), m: 2, version: 1.1 }, partyAuth(0));
1236
+ res.status.should.equal(200);
1237
+
1238
+ const session = await storage.fetchTssSigSession({ id: SESSION_ID });
1239
+ session.m.should.equal(2);
1240
+ session.participants.should.deep.equal([{ partyId: 0, copayerId: copayerIds[0] }]);
1241
+ });
1242
+
1243
+ it('should let another copayer join with its round 0 message', async function() {
1244
+ (await post(path, { message: broadcastMessage(0), m: 2, version: 1.1 }, partyAuth(0))).status.should.equal(200);
1245
+
1246
+ const res = await post(path, { message: broadcastMessage(1), version: 1.1 }, partyAuth(1));
1247
+ res.status.should.equal(200);
1248
+
1249
+ const session = await storage.fetchTssSigSession({ id: SESSION_ID });
1250
+ session.participants.should.deep.equal([
1251
+ { partyId: 0, copayerId: copayerIds[0] },
1252
+ { partyId: 1, copayerId: copayerIds[1] }
1253
+ ]);
1254
+ session.getCurrentRound().should.equal(1);
1255
+ });
1256
+
1257
+ it('should error if a copayer joins a full session', async function() {
1258
+ await createSigSession();
1259
+ const res = await post(path, { message: broadcastMessage(2), version: 1.1 }, partyAuth(2));
1260
+ expectError(res, 400, 'TSS_MAX_PARTICIPANTS_REACHED');
1261
+ });
1262
+
1263
+ it('should error if a non-participant sends a message for a later round', async function() {
1264
+ await createSigSession();
1265
+ const res = await post(path, { message: broadcastMessage(2, 1), version: 1.1 }, partyAuth(2));
1266
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
1267
+ });
1268
+
1269
+ it('should error if messages are empty arrays in an m > 1 session', async function() {
1270
+ await createSigSession();
1271
+ const message = { ...sigMessage(0), round: 1 };
1272
+ const res = await post(path, { message, version: 1.1 }, partyAuth(0));
1273
+ expectError(res, 400, 'TSS_INVALID_MESSAGE');
1274
+ });
1275
+
1276
+ describe('1-of-N session', function() {
1277
+ const start = () => post(path, { message: broadcastMessage(0), m: 1, version: 1.1 }, partyAuth(0));
1278
+
1279
+ it('should accept rounds that have no messages for other parties', async function() {
1280
+ (await start()).status.should.equal(200);
1281
+
1282
+ // A lone signer has no recipients, so only rounds 0 and 3 carry a (broadcast) message
1283
+ for (const round of [1, 2, 3, 4]) {
1284
+ const message = round === 3 ? broadcastMessage(0, round) : { ...sigMessage(0), round };
1285
+ const res = await post(path, { message, version: 1.1 }, partyAuth(0));
1286
+ res.status.should.equal(200, `round ${round}: ${res.text}`);
1287
+ }
1288
+
1289
+ const session = await storage.fetchTssSigSession({ id: SESSION_ID });
1290
+ session.rounds.length.should.equal(5);
1291
+ session.getCurrentRound().should.equal(5);
1292
+
1293
+ const res = await get(`/v1/tss/sign/${SESSION_ID}/4?maxWaitTime=0`, partyAuth(0));
1294
+ res.status.should.equal(200);
1295
+ JSON.parse(res.text).messages.should.deep.equal([]);
1296
+ });
1297
+
1298
+ it('should error if round 0 has no broadcast message', async function() {
1299
+ const res = await post(path, { message: sigMessage(0), m: 1, version: 1.1 }, partyAuth(0));
1300
+ expectError(res, 400, 'TSS_INVALID_MESSAGE');
1301
+ });
1302
+
1303
+ it('should still verify broadcast signatures', async function() {
1304
+ (await start()).status.should.equal(200);
1305
+ const message = broadcastMessage(0, 1);
1306
+ message.broadcastMessages[0].payload = tssUtils.detachSignData(Buffer.from('forged'), randomKey());
1307
+ const res = await post(path, { message, version: 1.1 }, partyAuth(0));
1308
+ expectError(res, 400, 'TSS_INVALID_MESSAGE_SIG');
1309
+ });
1310
+
1311
+ it('should error if another copayer tries to join', async function() {
1312
+ (await start()).status.should.equal(200);
1313
+ const res = await post(path, { message: broadcastMessage(1), version: 1.1 }, partyAuth(1));
1314
+ expectError(res, 400, 'TSS_MAX_PARTICIPANTS_REACHED');
1315
+ });
1316
+ });
889
1317
  });
890
1318
 
891
1319
  describe('GET /v1/tss/sign/:id/:round', function() {
@@ -2,7 +2,7 @@
2
2
  "name": "@bitpay-labs/bitcore-wallet-service",
3
3
  "description": "A service for Mutisig HD Bitcoin Wallets",
4
4
  "author": "BitPay Inc",
5
- "version": "11.11.1",
5
+ "version": "11.11.2",
6
6
  "license": "MIT",
7
7
  "main": "ts_build/src/index.js",
8
8
  "types": "./ts_build/src/index.d.ts",
@@ -41,9 +41,9 @@
41
41
  "cp:templates": "cp -r ./templates ./ts_build/templates"
42
42
  },
43
43
  "dependencies": {
44
- "@bitpay-labs/bitcore-logging": "^11.10.9",
45
- "@bitpay-labs/bitcore-tss": "^11.11.1",
46
- "@bitpay-labs/crypto-wallet-core": "^11.11.1",
44
+ "@bitpay-labs/bitcore-logging": "^11.11.2",
45
+ "@bitpay-labs/bitcore-tss": "^11.11.2",
46
+ "@bitpay-labs/crypto-wallet-core": "^11.11.2",
47
47
  "@sendgrid/mail": "6.5.4",
48
48
  "abi-decoder": "2.4.0",
49
49
  "async": "0.9.2",
@@ -381,7 +381,7 @@ export declare const Constants: {
381
381
  [network: string]: number;
382
382
  };
383
383
  };
384
- readonly TSS_KEYGEN_SCHEME_VERSION_MAX: 1.2;
384
+ readonly TSS_KEYGEN_SCHEME_VERSION_MAX: 1.3;
385
385
  readonly TSS_SIGGEN_SCHEME_VERSION_MAX: 1.1;
386
386
  readonly TSS_KEYGEN_SCHEME_VERSION_MIN: 1;
387
387
  readonly TSS_SIGGEN_SCHEME_VERSION_MIN: 1;
@@ -228,7 +228,7 @@ exports.Constants = {
228
228
  doge: { testnet: 300, livenet: 0 },
229
229
  ltc: { testnet: 300, livenet: 0 }
230
230
  },
231
- TSS_KEYGEN_SCHEME_VERSION_MAX: 1.2,
231
+ TSS_KEYGEN_SCHEME_VERSION_MAX: 1.3,
232
232
  TSS_SIGGEN_SCHEME_VERSION_MAX: 1.1,
233
233
  TSS_KEYGEN_SCHEME_VERSION_MIN: 1,
234
234
  TSS_SIGGEN_SCHEME_VERSION_MIN: 1,
@@ -382,7 +382,7 @@ export declare const Common: {
382
382
  [network: string]: number;
383
383
  };
384
384
  };
385
- readonly TSS_KEYGEN_SCHEME_VERSION_MAX: 1.2;
385
+ readonly TSS_KEYGEN_SCHEME_VERSION_MAX: 1.3;
386
386
  readonly TSS_SIGGEN_SCHEME_VERSION_MAX: 1.1;
387
387
  readonly TSS_KEYGEN_SCHEME_VERSION_MIN: 1;
388
388
  readonly TSS_SIGGEN_SCHEME_VERSION_MIN: 1;
@@ -799,6 +799,7 @@ export declare const Common: {
799
799
  difference(arr1: any, arr2: any): any[];
800
800
  shuffle(array: Array<any>): any[];
801
801
  sleep(ms: any): Promise<unknown>;
802
+ convertXPubKeyForNetwork(xPubKey: string, chain: string, network: string): any;
802
803
  };
803
804
  };
804
805
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/common/index.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKlB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/common/index.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAKlB,CAAC"}
@@ -48,5 +48,6 @@ export declare const Utils: {
48
48
  difference(arr1: any, arr2: any): any[];
49
49
  shuffle(array: Array<any>): any[];
50
50
  sleep(ms: any): Promise<unknown>;
51
+ convertXPubKeyForNetwork(xPubKey: string, chain: string, network: string): any;
51
52
  };
52
53
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/lib/common/utils.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAUtD,eAAO,MAAM,KAAK;4BASQ,UAAU,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI;gCAqB7B,MAAM,OAAO,UAAU,CAAC,GAAG,CAAC,GAAG,OAAO;mBAwBnD,MAAM,GAAG,MAAM;;;;0BA4CR,MAAM,SAAS,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;kBAc7D,MAAM,GAAG,MAAM;;;;;;;;;;;;gBA6IjB,MAAM;gBACN,MAAM;gBACN,MAAM;gBACN,MAAM;;;cAqBR,MAAM;gBACJ,MAAM;gBACN,MAAM;gBACN,MAAM;;4BA4BC,MAAM;;;;;;;;;sBAiGP,GAAG;YAeb,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;aAsDhE,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;SAuBrE,CAAC,OAAO,CAAC,mBAAY,OAAO,CAAC,CAAC,CAAC;qBAiBnB,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;qBAajB,GAAG,QAAQ,GAAG;mBAWhB,KAAK,CAAC,GAAG,CAAC;;CAU1B,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/lib/common/utils.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAUtD,eAAO,MAAM,KAAK;4BASQ,UAAU,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI;gCAqB7B,MAAM,OAAO,UAAU,CAAC,GAAG,CAAC,GAAG,OAAO;mBAwBnD,MAAM,GAAG,MAAM;;;;0BA4CR,MAAM,SAAS,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;kBAc7D,MAAM,GAAG,MAAM;;;;;;;;;;;;gBA6IjB,MAAM;gBACN,MAAM;gBACN,MAAM;gBACN,MAAM;;;cAqBR,MAAM;gBACJ,MAAM;gBACN,MAAM;gBACN,MAAM;;4BA4BC,MAAM;;;;;;;;;sBAiGP,GAAG;YAeb,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;aAsDhE,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;SAuBrE,CAAC,OAAO,CAAC,mBAAY,OAAO,CAAC,CAAC,CAAC;qBAiBnB,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;qBAajB,GAAG,QAAQ,GAAG;mBAWhB,KAAK,CAAC,GAAG,CAAC;;sCAaS,MAAM,SAAS,MAAM,WAAW,MAAM;CAczE,CAAC"}
@@ -423,6 +423,21 @@ exports.Utils = {
423
423
  }
424
424
  return retval;
425
425
  },
426
- sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
426
+ sleep(ms) {
427
+ return new Promise(resolve => setTimeout(resolve, ms));
428
+ },
429
+ convertXPubKeyForNetwork(xPubKey, chain, network) {
430
+ network = !Bitcore_[chain] ? exports.Utils.getGenericName(network) : network;
431
+ const lib = Bitcore_[chain] || crypto_wallet_core_1.BitcoreLib;
432
+ $.checkArgument(xPubKey, 'xPubKey is required at <convertXPubKeyForNetwork()>');
433
+ $.checkArgument(chain, 'chain is required at <convertXPubKeyForNetwork()>');
434
+ $.checkArgument(lib.Networks.get(network), 'invalid network at <convertXPubKeyForNetwork()>: ' + network);
435
+ const hdPubKeyObj = new lib.HDPublicKey(xPubKey).toObject();
436
+ hdPubKeyObj.network = network;
437
+ delete hdPubKeyObj.checksum;
438
+ delete hdPubKeyObj.xpubkey;
439
+ const newHdPubKey = lib.HDPublicKey.fromObject(hdPubKeyObj);
440
+ return newHdPubKey.toString();
441
+ }
427
442
  };
428
443
  //# sourceMappingURL=utils.js.map