@bitpay-labs/bitcore-wallet-service 11.11.1 → 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.
- package/Dockerfile +1 -1
- package/package.json +5 -5
- package/src/lib/chain/btc/index.ts +3 -2
- package/src/lib/chain/eth/index.ts +134 -117
- package/src/lib/chain/sol/index.ts +1 -1
- package/src/lib/chain/xrp/index.ts +1 -1
- package/src/lib/common/constants.ts +1 -1
- package/src/lib/common/utils.ts +18 -1
- package/src/lib/model/tsskeygen.ts +16 -0
- package/src/lib/routes/maintenance.ts +11 -2
- package/src/lib/routes/middleware/authTssRequest.ts +28 -16
- package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
- package/src/lib/routes/services.ts +7 -1
- package/src/lib/routes/tss.ts +33 -30
- package/src/lib/routes/wallets.ts +3 -1
- package/src/lib/server.ts +198 -105
- package/src/lib/storage.ts +129 -20
- package/src/lib/tss.ts +111 -7
- package/test/chain/eth.test.ts +70 -0
- package/test/chain/sol.test.ts +1 -1
- package/test/chain/xrp.test.ts +1 -1
- package/test/expressapp.test.ts +243 -0
- package/test/integration/addressReads.test.ts +503 -0
- package/test/integration/bcmonitor.test.ts +44 -0
- package/test/integration/server.test.ts +251 -49
- package/test/integration/tss.test.ts +428 -0
- package/test/integration/walletReads.test.ts +488 -0
- package/ts_build/package.json +4 -4
- package/ts_build/src/lib/chain/btc/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +3 -2
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.d.ts +8 -1
- package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +114 -102
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +1 -1
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +1 -1
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/constants.d.ts +1 -1
- package/ts_build/src/lib/common/constants.js +1 -1
- package/ts_build/src/lib/common/index.d.ts +2 -1
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.d.ts +1 -0
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +16 -1
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
- package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.js +3 -0
- package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
- package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
- package/ts_build/src/lib/routes/maintenance.js +12 -4
- package/ts_build/src/lib/routes/maintenance.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
- package/ts_build/src/lib/routes/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +6 -1
- package/ts_build/src/lib/routes/services.js.map +1 -1
- package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
- package/ts_build/src/lib/routes/tss.js +32 -30
- package/ts_build/src/lib/routes/tss.js.map +1 -1
- package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
- package/ts_build/src/lib/routes/wallets.js +2 -1
- package/ts_build/src/lib/routes/wallets.js.map +1 -1
- package/ts_build/src/lib/server.d.ts +24 -3
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +148 -97
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +26 -10
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +99 -20
- package/ts_build/src/lib/storage.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts +12 -0
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +64 -5
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/chain/eth.test.js +59 -0
- package/ts_build/test/chain/eth.test.js.map +1 -1
- package/ts_build/test/chain/sol.test.js +1 -1
- package/ts_build/test/chain/sol.test.js.map +1 -1
- package/ts_build/test/chain/xrp.test.js +1 -1
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/expressapp.test.js +222 -0
- package/ts_build/test/expressapp.test.js.map +1 -1
- package/ts_build/test/integration/addressReads.test.d.ts +2 -0
- package/ts_build/test/integration/addressReads.test.d.ts.map +1 -0
- package/ts_build/test/integration/addressReads.test.js +465 -0
- package/ts_build/test/integration/addressReads.test.js.map +1 -0
- package/ts_build/test/integration/bcmonitor.test.js +40 -0
- package/ts_build/test/integration/bcmonitor.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +228 -48
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.d.ts.map +1 -1
- package/ts_build/test/integration/tss.test.js +344 -0
- package/ts_build/test/integration/tss.test.js.map +1 -1
- package/ts_build/test/integration/walletReads.test.d.ts +2 -0
- package/ts_build/test/integration/walletReads.test.d.ts.map +1 -0
- package/ts_build/test/integration/walletReads.test.js +436 -0
- package/ts_build/test/integration/walletReads.test.js.map +1 -0
|
@@ -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() {
|