PHP WebShell

Текущая директория: /opt/BitGoJS/modules/bitgo/test/v2/unit/internal/tssUtils

Просмотр файла: eddsa.ts

import * as sodium from 'libsodium-wrappers-sumo';
import * as _ from 'lodash';
import * as nock from 'nock';
import * as openpgp from 'openpgp';
import * as should from 'should';
import * as sinon from 'sinon';

import { TestableBG, TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../../../../src/bitgo';
import {
  common,
  Keychain,
  RequestTracer,
  SignatureShareRecord,
  SignatureShareType,
  TssUtils,
  TxRequest,
  Wallet,
  Eddsa,
  KeyShare,
  Ed25519BIP32,
  createSharedDataProof,
  CommitmentShareRecord,
  CommitmentType,
  ExchangeCommitmentResponse,
  EncryptedSignerShareType,
  BaseCoin,
} from '@bitgo/sdk-core';
import { createWalletSignatures } from '../../tss/helpers';
import {
  nockSendSignatureShare,
  nockGetTxRequest,
  nockCreateTxRequest,
  nockDeleteSignatureShare,
  nockSendTxRequest,
  nockExchangeCommitments,
} from './common';

openpgp.config.rejectCurves = new Set();

describe('TSS Utils:', async function () {
  let sandbox: sinon.SinonSandbox;
  let MPC: Eddsa;
  let bgUrl: string;
  let tssUtils: TssUtils;
  let userGpgKey;
  let backupGpgKey;
  let bitgoGpgKey;
  let bitgo: TestableBG & BitGo;
  let baseCoin: BaseCoin;
  let wallet: Wallet;
  let bitgoKeyShare;
  const reqId = new RequestTracer();
  const coinName = 'tsol';
  const validUserSigningMaterial = {
    uShare: {
      i: 1,
      t: 2,
      n: 3,
      y: '093c8603ad86c41d5ee25a814b88185b435dd3a9ceccf9c9fd691a465ac4a8b0',
      seed: 'ca40c789813250c334ddd2ba19050f6ed20b5a08853ceca492358f2711ad4b15',
      chaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca',
    },
    commonChaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca',
    bitgoYShare: {
      i: 1,
      j: 3,
      y: '59d8000ba5e85fa402f39382960e7d5ede82b1b6e22b146a18b7df238c3a3225',
      v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517',
      u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b',
      chaincode: 'd21dbd8eae5d4789292ecea2efa53e0165b2439d57f5158eb4dd57dc26b59236',
    },
    backupYShare: {
      i: 1,
      j: 2,
      y: 'e0ae75077715686a121acb41b29a55bde426971154f40a41fc317f7f774a9424',
      v: 'f76ef629dfc15ab5e4531e532b5d67f2176637ca752b195876b7e3172459c969',
      u: 'fe6b89fb6acfcd7392c35c084f58bde0846b888c4df57e466caf0a3271b06a05',
      chaincode: '1c34e5dfbbd4a870f4479caaa5e6a46e3438f976ad5aefd4905b8fe8bca1101e',
    },
  };

  const validUserSignShare = {
    xShare: {
      i: 1,
      y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296',
      u: '1e159d6a0ae3a8dccc74615113e7c3e25d3080e5e0ffeb0ae04dd6a967268102',
      r: 'c8f64cc48926216c3f60e1d8ff1e24eba060d7c1ff020d0fc1d735d4564efd03',
      R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c',
    },
    rShares: {
      3: {
        i: 3,
        j: 1,
        u: 'd675f9099fbef03aa9fcdca4009286f435e56369c374d0042f03cc60b49e690a',
        v: '3c090e88ed42da0dd0bade35c8d6b88bc050284536b98e5b27d33ff45da9755b',
        r: '7f16224dbf5b02adb6c21380fcb2a8ee00323daae62cac3575a4d328fd23a905',
        R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c',
        commitment: '445c8cb1dee0166b6bdd5ad1d0a53fbfe86c4d3a470f184745530a863eedff28',
      },
    },
  };

  const validBitgoToUserSignShare = {
    xShare: {
      i: 3,
      y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296',
      u: '1315dbe18069825b4a27188b813eae7ff2917a614499ed553e70d65d4fa4820b',
      r: 'd0539375e6566f2fe540cba48c5e56bd1cdf68cfe1f0d527d2b730fe4e879809',
      R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32',
    },
    rShares: {
      1: {
        i: 1,
        j: 3,
        u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b',
        v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517',
        r: '0375e8c5a5691a73c21df00d49d423e3f83fe08d7b5d5af33c5c6aa9cae59d0a',
        R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32',
        commitment: '62b21f98bf885841ad469145192d4df0697b3f42c581e3e926394eae0b101ecb',
      },
    },
  };

  const txRequest = {
    txRequestId: 'randomId',
    unsignedTxs: [{ signableHex: 'MPC on a Friday night', serializedTxHex: 'MPC on a Friday night' }],
    signatureShares: [
      {
        from: 'bitgo',
        to: 'user',
        share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
      },
    ],
  };

  beforeEach(function () {
    sandbox = sinon.createSandbox();
  });

  afterEach(function () {
    sandbox.restore();
  });

  before('initializes mpc', async function () {
    const hdTree = await Ed25519BIP32.initialize();
    MPC = await Eddsa.initialize(hdTree);
  });

  before(async function () {
    bitgoKeyShare = await MPC.keyShare(3, 2, 3);

    userGpgKey = await openpgp.generateKey({
      userIDs: [
        {
          name: 'test',
          email: 'test@test.com',
        },
      ],
      curve: 'secp256k1',
    });

    backupGpgKey = await openpgp.generateKey({
      userIDs: [
        {
          name: 'testBackup',
          email: 'testBackup@test.com',
        },
      ],
      curve: 'secp256k1',
    });

    bitgoGpgKey = await openpgp.generateKey({
      userIDs: [
        {
          name: 'bitgo',
          email: 'bitgo@test.com',
        },
      ],
      curve: 'secp256k1',
    });
    const constants = {
      mpc: {
        bitgoPublicKey: bitgoGpgKey.publicKey,
      },
    };

    bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
    bitgo.initializeTestVars();

    baseCoin = bitgo.coin(coinName);

    bgUrl = common.Environments[bitgo.getEnv()].uri;

    // TODO(WP-346): sdk-test mocks conflict so we can't use persist
    nock(bgUrl).get('/api/v1/client/constants').times(23).reply(200, { ttl: 3600, constants });

    const walletData = {
      id: '5b34252f1bf349930e34020a00000000',
      coin: 'tsol',
      keys: [
        '5b3424f91bf349930e34017500000000',
        '5b3424f91bf349930e34017600000000',
        '5b3424f91bf349930e34017700000000',
      ],
      coinSpecific: {},
      multisigType: 'tss',
    };
    wallet = new Wallet(bitgo, baseCoin, walletData);
    tssUtils = new TssUtils(bitgo, baseCoin, wallet);
  });

  after(function () {
    nock.cleanAll();
  });

  describe('TSS key chains:', async function () {
    it('should generate TSS key chains', async function () {
      const userKeyShare = MPC.keyShare(1, 2, 3);
      const backupKeyShare = MPC.keyShare(2, 2, 3);

      const nockedBitGoKeychain = await nockBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        backupGpgKey,
        bitgoGpgKey,
      });
      const nockedUserKeychain = await nockUserKeychain({ coin: coinName });
      await nockBackupKeychain({ coin: coinName });

      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
      });
      const userKeychain = await tssUtils.createUserKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
        bitgoKeychain,
        passphrase: 'passphrase',
      });
      const backupKeychain = await tssUtils.createBackupKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
        bitgoKeychain,
        passphrase: 'passphrase',
      });

      bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
      userKeychain.should.deepEqual(nockedUserKeychain);

      // unencrypted `prv` property should exist on backup keychain
      JSON.stringify({
        uShare: backupKeyShare.uShare,
        bitgoYShare: bitgoKeyShare.yShares[2],
        userYShare: userKeyShare.yShares[2],
      }).should.equal(backupKeychain.prv);
      should.exist(backupKeychain.encryptedPrv);
    });

    it('should generate TSS key chains without passphrase', async function () {
      const userKeyShare = MPC.keyShare(1, 2, 3);
      const backupKeyShare = MPC.keyShare(2, 2, 3);

      const nockedBitGoKeychain = await nockBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        // reusing the user gpg key as the backup gpg key, i.e. the user is their own the backup provider
        backupGpgKey,
        bitgoGpgKey,
      });
      const nockedUserKeychain = await nockUserKeychain({ coin: coinName });
      await nockBackupKeychain({ coin: coinName });

      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
      });
      const userKeychain = await tssUtils.createUserKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
        bitgoKeychain,
      });
      const backupKeychain = await tssUtils.createBackupKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
        bitgoKeychain,
      });

      bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
      userKeychain.should.deepEqual(nockedUserKeychain);

      // unencrypted `prv` property should exist on backup keychain
      JSON.stringify({
        uShare: backupKeyShare.uShare,
        bitgoYShare: bitgoKeyShare.yShares[2],
        userYShare: userKeyShare.yShares[2],
      }).should.equal(backupKeychain.prv);
    });

    it('should generate TSS key chains with optional params', async function () {
      const enterprise = 'enterprise';

      const userKeyShare = MPC.keyShare(1, 2, 3);
      const backupKeyShare = MPC.keyShare(2, 2, 3);

      const nockedBitGoKeychain = await nockBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        backupGpgKey,
        bitgoGpgKey,
      });
      const nockedUserKeychain = await nockUserKeychain({ coin: coinName });
      await nockBackupKeychain({ coin: coinName });

      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
        enterprise,
      });
      const userKeychain = await tssUtils.createUserKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
        bitgoKeychain,
        passphrase: 'passphrase',
        originalPasscodeEncryptionCode: 'originalPasscodeEncryptionCode',
      });
      const backupKeychain = await tssUtils.createBackupKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
        bitgoKeychain,
        passphrase: 'passphrase',
      });

      bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
      userKeychain.should.deepEqual(nockedUserKeychain);

      // unencrypted `prv` property should exist on backup keychain
      JSON.stringify({
        uShare: backupKeyShare.uShare,
        bitgoYShare: bitgoKeyShare.yShares[2],
        userYShare: userKeyShare.yShares[2],
      }).should.equal(backupKeychain.prv);
      should.exist(backupKeychain.encryptedPrv);
    });

    it('should fail to generate TSS keychains when received invalid number of wallet signatures', async function () {
      const userKeyShare = MPC.keyShare(1, 2, 3);
      const backupKeyShare = MPC.keyShare(2, 2, 3);

      const bitgoKeychain = await generateBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        backupGpgKey,
        bitgoGpgKey,
      });

      const certsString = await createSharedDataProof(bitgoGpgKey.privateKey, userGpgKey.publicKey, []);
      const certsKey = await openpgp.readKey({ armoredKey: certsString });
      const finalKey = new openpgp.PacketList();
      certsKey.toPacketList().forEach((packet) => finalKey.push(packet));
      // the underlying function only requires two arguments but the according .d.ts file for openpgp has the further
      // arguments marked as mandatory as well.
      // Once the following PR has been merged and released we no longer need the ts-ignore:
      // https://github.com/openpgpjs/openpgpjs/pull/1576
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      bitgoKeychain.walletHSMGPGPublicKeySigs = openpgp.armor(openpgp.enums.armor.publicKey, finalKey.write());

      await tssUtils
        .verifyWalletSignatures(userGpgKey.publicKey, backupGpgKey.publicKey, bitgoKeychain, '', 1)
        .should.be.rejectedWith('Invalid wallet signatures');
    });

    it('should fail to generate TSS keychains when wallet signature fingerprints do not match passed user/backup fingerprints', async function () {
      const userKeyShare = MPC.keyShare(1, 2, 3);
      const backupKeyShare = MPC.keyShare(2, 2, 3);

      const bitgoKeychain = await generateBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        backupGpgKey,
        bitgoGpgKey,
      });

      // using the backup gpg here instead of the user gpg key to simulate that the first signature has a different
      // fingerprint from the passed in first gpg key
      await tssUtils
        .verifyWalletSignatures(backupGpgKey.publicKey, backupGpgKey.publicKey, bitgoKeychain, '', 1)
        .should.be.rejectedWith(
          `first wallet signature's fingerprint does not match passed user gpg key's fingerprint`
        );

      // using the user gpg here instead of the backup gpg key to simulate that the second signature has a different
      // fingerprint from the passed in second gpg key
      await tssUtils
        .verifyWalletSignatures(userGpgKey.publicKey, userGpgKey.publicKey, bitgoKeychain, '', 1)
        .should.be.rejectedWith(
          `second wallet signature's fingerprint does not match passed backup gpg key's fingerprint`
        );
    });

    it('should fail to generate TSS keychains when wallet signature is for different key share', async function () {
      const userKeyShare = MPC.keyShare(1, 2, 3);
      const backupKeyShare = MPC.keyShare(2, 2, 3);
      const customBitgoKeyShare = MPC.keyShare(3, 2, 3);

      const bitgoKeychain1 = await generateBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        backupGpgKey,
        bitgoGpgKey,
      });
      const bitgoKeychain2 = await generateBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare: customBitgoKeyShare,
        userGpgKey,
        backupGpgKey,
        bitgoGpgKey,
      });

      // using the other bitgo keychains common keychain and walletHSMGPGPublicKeySigs so that the verification of the
      // commmon keychain passes but fails for the bitgo to user/ backup shares
      bitgoKeychain1.commonKeychain = bitgoKeychain2.commonKeychain;
      bitgoKeychain1.walletHSMGPGPublicKeySigs = bitgoKeychain2.walletHSMGPGPublicKeySigs;

      await tssUtils
        .createUserKeychain({
          userGpgKey,
          backupGpgKey,
          userKeyShare,
          backupKeyShare,
          bitgoKeychain: bitgoKeychain1,
        })
        .should.be.rejectedWith('bitgo share mismatch');

      await tssUtils
        .createBackupKeychain({
          userGpgKey,
          backupGpgKey,
          userKeyShare,
          backupKeyShare,
          bitgoKeychain: bitgoKeychain1,
        })
        .should.be.rejectedWith('bitgo share mismatch');
    });

    it('should fail to generate TSS key chains when common keychains do not match', async function () {
      const userKeyShare = MPC.keyShare(1, 2, 3);
      const backupKeyShare = MPC.keyShare(2, 2, 3);

      const nockedBitGoKeychain = await nockBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        backupGpgKey,
        bitgoGpgKey,
      });
      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare,
      });
      bitgoKeychain.should.deepEqual(nockedBitGoKeychain);

      await tssUtils
        .createUserKeychain({
          userGpgKey,
          backupGpgKey,
          userKeyShare,
          backupKeyShare: MPC.keyShare(2, 2, 3),
          bitgoKeychain,
          passphrase: 'passphrase',
        })
        .should.be.rejectedWith('Failed to create user keychain - commonKeychains do not match.');
      await tssUtils
        .createUserKeychain({
          userGpgKey,
          backupGpgKey,
          userKeyShare: MPC.keyShare(1, 2, 3),
          backupKeyShare,
          bitgoKeychain,
          passphrase: 'passphrase',
        })
        .should.be.rejectedWith('Failed to create user keychain - commonKeychains do not match.');

      await tssUtils
        .createBackupKeychain({
          userGpgKey,
          backupGpgKey,
          userKeyShare: MPC.keyShare(1, 2, 3),
          backupKeyShare,
          bitgoKeychain,
          passphrase: 'passphrase',
        })
        .should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.');
      await tssUtils
        .createBackupKeychain({
          userGpgKey,
          backupGpgKey,
          userKeyShare,
          backupKeyShare: MPC.keyShare(2, 2, 3),
          bitgoKeychain,
          passphrase: 'passphrase',
        })
        .should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.');
    });
  });

  describe('signTxRequest:', function () {
    const txRequestId = 'randomid';
    const txRequest: TxRequest = {
      txRequestId,
      transactions: [],
      unsignedTxs: [
        {
          serializedTxHex: 'MPC on a Friday night',
          signableHex: 'MPC on a Friday night',
          derivationPath: 'm/0',
        },
      ],
      date: new Date().toISOString(),
      intent: {
        intentType: 'payment',
      },
      latest: true,
      state: 'pendingUserSignature',
      walletType: 'hot',
      walletId: 'walletId',
      policiesChecked: true,
      version: 1,
      userId: 'userId',
    };

    beforeEach(async function () {
      const userSignShare = validUserSignShare;
      const rShare = userSignShare.rShares[3];
      const signatureShare: SignatureShareRecord = {
        from: SignatureShareType.USER,
        to: SignatureShareType.BITGO,
        share: rShare.r + rShare.R,
      };

      await nockSendSignatureShare({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        signatureShare,
      });

      const signatureShare2: SignatureShareRecord = {
        from: SignatureShareType.BITGO,
        to: SignatureShareType.USER,
        share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
      };
      const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] };
      await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response });

      const bitgoToUserCommitmentShare: CommitmentShareRecord = {
        from: SignatureShareType.BITGO,
        to: SignatureShareType.USER,
        type: CommitmentType.COMMITMENT,
        share: validBitgoToUserSignShare.rShares[1].commitment,
      };
      const exchangeCommitResponse: ExchangeCommitmentResponse = { commitmentShare: bitgoToUserCommitmentShare };
      await nockExchangeCommitments({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        response: exchangeCommitResponse,
      });
    });

    it('signTxRequest should succeed with txRequest object as input', async function () {
      const signedTxRequest = await tssUtils.signTxRequest({
        txRequest,
        prv: JSON.stringify(validUserSigningMaterial),
        reqId,
      });
      signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);

      sandbox.verifyAndRestore();
    });

    it('signTxRequest should succeed with txRequest id as input', async function () {
      const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest');
      getTxRequest.resolves(txRequest);
      getTxRequest.calledWith(txRequestId);

      const signedTxRequest = await tssUtils.signTxRequest({
        txRequest: txRequestId,
        prv: JSON.stringify(validUserSigningMaterial),
        reqId,
      });
      signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);

      sandbox.verifyAndRestore();
    });
  });

  describe('signTxRequest With Commitment:', function () {
    const txRequestId = 'randomid';
    const txRequest: TxRequest = {
      txRequestId,
      transactions: [],
      unsignedTxs: [
        {
          serializedTxHex: 'MPC on a Friday night',
          signableHex: 'MPC on a Friday night',
          derivationPath: 'm/0',
        },
      ],
      date: new Date().toISOString(),
      intent: {
        intentType: 'payment',
      },
      latest: true,
      state: 'pendingUserSignature',
      walletType: 'hot',
      walletId: 'walletId',
      policiesChecked: true,
      version: 1,
      userId: 'userId',
    };

    beforeEach(async function () {
      const userSignShare = validUserSignShare;
      const rShare = userSignShare.rShares[3];
      const signatureShare: SignatureShareRecord = {
        from: SignatureShareType.USER,
        to: SignatureShareType.BITGO,
        share: rShare.r + rShare.R,
      };

      await nockSendSignatureShare({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        signatureShare,
      });

      const signatureShare2: SignatureShareRecord = {
        from: SignatureShareType.BITGO,
        to: SignatureShareType.USER,
        share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
      };
      const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] };
      await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response });
      const bitgoToUserCommitmentShare: CommitmentShareRecord = {
        from: SignatureShareType.BITGO,
        to: SignatureShareType.USER,
        type: CommitmentType.COMMITMENT,
        share: validBitgoToUserSignShare.rShares[1].commitment,
      };
      const exchangeCommitResponse: ExchangeCommitmentResponse = { commitmentShare: bitgoToUserCommitmentShare };
      await nockExchangeCommitments({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        response: exchangeCommitResponse,
      });
    });

    it('signTxRequest should succeed with txRequest object as input', async function () {
      const signedTxRequest = await tssUtils.signTxRequest({
        txRequest,
        prv: JSON.stringify(validUserSigningMaterial),
        reqId,
      });
      signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);

      sandbox.verifyAndRestore();
    });

    it('signTxRequest should succeed with txRequest id as input', async function () {
      const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest');
      getTxRequest.resolves(txRequest);
      getTxRequest.calledWith(txRequestId);

      const signedTxRequest = await tssUtils.signTxRequest({
        txRequest: txRequestId,
        prv: JSON.stringify(validUserSigningMaterial),
        reqId,
      });
      signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);

      sandbox.verifyAndRestore();
    });
  });

  describe('prebuildTxWithIntent:', async function () {
    it('should build single recipient tx', async function () {
      const nockedCreateTx = await nockCreateTxRequest({
        walletId: wallet.id(),
        requestBody: {
          apiVersion: 'lite',
          intent: {
            intentType: 'payment',
            recipients: [
              {
                address: {
                  address: 'recipient',
                },
                amount: {
                  value: '10000',
                  symbol: 'tsol',
                },
              },
            ],
          },
        },
        // don't care about the actual response - just need to make sure request body matches
        response: {},
      });

      await tssUtils.prebuildTxWithIntent({
        reqId,
        recipients: [
          {
            address: 'recipient',
            amount: '10000',
          },
        ],
        intentType: 'payment',
      });

      nockedCreateTx.isDone().should.be.true();
    });

    it('should build multiple recipients with memo tx', async function () {
      const nockedCreateTx = await nockCreateTxRequest({
        walletId: wallet.id(),
        requestBody: {
          apiVersion: 'lite',
          intent: {
            intentType: 'payment',
            recipients: [
              {
                address: {
                  address: 'recipient1',
                },
                amount: {
                  value: '10000',
                  symbol: 'tsol',
                },
              },
              {
                address: {
                  address: 'recipient2',
                },
                amount: {
                  value: '20000',
                  symbol: 'tsol',
                },
              },
            ],
            memo: 'memo',
          },
        },
        // don't care about the actual response - just need to make sure request body matches
        response: {},
      });

      await tssUtils.prebuildTxWithIntent({
        reqId,
        recipients: [
          {
            address: 'recipient1',
            amount: '10000',
          },
          {
            address: 'recipient2',
            amount: '20000',
          },
        ],
        memo: {
          value: 'memo',
          type: 'text',
        },
        intentType: 'payment',
      });

      nockedCreateTx.isDone().should.be.true();
    });
  });

  describe('delete SignatureShare:', async function () {
    it('should succeed to delete Signature Share', async function () {
      const signatureShare = { from: 'user', to: 'bitgo', share: '128bytestring' } as SignatureShareRecord;
      const nock = await nockDeleteSignatureShare({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        signatureShare,
      });
      const response = await tssUtils.deleteSignatureShares(txRequest.txRequestId);
      response.should.deepEqual([signatureShare]);
      response.should.length(1);
      nock.isDone().should.equal(true);
    });

    it('should call setRequestTracer', async function () {
      const signatureShare = { from: 'user', to: 'bitgo', share: '128bytestring' } as SignatureShareRecord;
      const nock = await nockDeleteSignatureShare({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        signatureShare,
      });
      const reqId = new RequestTracer();
      const setRequestTracerSpy = sinon.spy(bitgo, 'setRequestTracer');
      setRequestTracerSpy.withArgs(reqId);
      const response = await tssUtils.deleteSignatureShares(txRequest.txRequestId, reqId);
      response.should.deepEqual([signatureShare]);
      response.should.length(1);
      nock.isDone().should.equal(true);
      sinon.assert.calledOnce(setRequestTracerSpy);
      setRequestTracerSpy.restore();
    });
  });

  describe('sendTxRequest:', async function () {
    it('should succeed to send tx request', async function () {
      const nock = await nockSendTxRequest({
        coin: coinName,
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
      });
      await tssUtils.sendTxRequest(txRequest.txRequestId).should.be.fulfilled();
      nock.isDone().should.equal(true);
    });

    it('should call setRequestTracer', async function () {
      const nock = await nockSendTxRequest({
        coin: coinName,
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
      });
      const reqId = new RequestTracer();
      const setRequestTracerSpy = sinon.spy(bitgo, 'setRequestTracer');
      setRequestTracerSpy.withArgs(reqId);
      await tssUtils.sendTxRequest(txRequest.txRequestId, reqId).should.be.fulfilled();
      nock.isDone().should.equal(true);
      sinon.assert.calledOnce(setRequestTracerSpy);
      setRequestTracerSpy.restore();
    });
  });

  describe('createUserToBitgoCommitmentShare', function () {
    it('should create a valid commitmentShare', async function () {
      const value = 'randomstring';
      const validUserToBitgoCommitmentShare = {
        from: SignatureShareType.USER,
        to: SignatureShareType.BITGO,
        type: CommitmentType.COMMITMENT,
        share: value,
      };
      const commitmentShare = tssUtils.createUserToBitgoCommitmentShare(value);
      commitmentShare.should.deepEqual(validUserToBitgoCommitmentShare);
    });
  });

  describe('createUserToBitgoEncryptedSignerShare', function () {
    it('should create a valid encryptedSignerShare', async function () {
      const value = 'randomstring';
      const validUserToBitgoEncryptedSignerShare = {
        from: SignatureShareType.USER,
        to: SignatureShareType.BITGO,
        type: EncryptedSignerShareType.ENCRYPTED_SIGNER_SHARE,
        share: value,
      };
      const encryptedSignerShare = tssUtils.createUserToBitgoEncryptedSignerShare(value);
      encryptedSignerShare.should.deepEqual(validUserToBitgoEncryptedSignerShare);
    });
  });

  describe('supportedTxRequestVersions', function () {
    it('should return full for custodial wallets', async function () {
      const custodialWallet = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'custodial' });
      const custodialTssUtils = new TssUtils(bitgo, baseCoin, custodialWallet);
      custodialTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
    });
    it('should return full for cold wallets', async function () {
      const coldWallet = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'cold' });
      const coldWalletTssUtils = new TssUtils(bitgo, baseCoin, coldWallet);
      coldWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
    });
    it('should return full and lite for hot wallets', async function () {
      const hotWallet = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'hot' });
      const hotTssUtils = new TssUtils(bitgo, baseCoin, hotWallet);
      const supportedTxRequestVersions = hotTssUtils.supportedTxRequestVersions();
      supportedTxRequestVersions.should.deepEqual(['lite', 'full']);
    });
    it('should return empty for trading wallets', function () {
      const tradingWallets = new Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'trading' });
      const tradingWalletTssUtils = new TssUtils(bitgo, baseCoin, tradingWallets);
      const supportedTxRequestVersions = tradingWalletTssUtils.supportedTxRequestVersions();
      supportedTxRequestVersions.should.deepEqual([]);
    });
    it('should return empty for non-tss wallets', function () {
      const nonTssWalletData = { coin: 'btc', multisigType: 'onchain' };
      const btcCoin = bitgo.coin('tbtc');
      const nonTssWallet = new Wallet(bitgo, btcCoin, nonTssWalletData);
      const nonTssWalletTssUtils = new TssUtils(bitgo, btcCoin, nonTssWallet);
      nonTssWalletTssUtils.supportedTxRequestVersions().should.deepEqual([]);
    });
  });

  describe('isPendingApprovalTxRequestFull', () => {
    it('should return true for full apiVersion and pendingApproval state', async () => {
      const txRequest = {
        apiVersion: 'full',
        state: 'pendingApproval',
      } as TxRequest;
      const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest);
      result.should.be.true();
    });

    it('should return false for non-full apiVersion', async () => {
      const txRequest = {
        apiVersion: 'lite',
        state: 'pendingApproval',
      } as TxRequest;
      const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest);
      result.should.be.false();
    });

    it('should return false for non-pendingApproval state', async () => {
      const txRequest = {
        apiVersion: 'full',
        state: 'pendingDelivery',
      } as TxRequest;
      const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest);
      result.should.be.false();
    });
  });

  // #region Nock helpers
  async function generateBitgoKeychain(params: {
    coin: string;
    userKeyShare: KeyShare;
    backupKeyShare: KeyShare;
    bitgoKeyShare: KeyShare;
    userGpgKey: openpgp.SerializedKeyPair<string>;
    backupGpgKey: openpgp.SerializedKeyPair<string>;
    bitgoGpgKey: openpgp.SerializedKeyPair<string>;
  }): Promise<Keychain> {
    const bitgoCombined = MPC.keyCombine(params.bitgoKeyShare.uShare, [
      params.userKeyShare.yShares[3],
      params.backupKeyShare.yShares[3],
    ]);
    const userGpgKeyActual = await openpgp.readKey({ armoredKey: params.userGpgKey.publicKey });
    const backupGpgKeyActual = await openpgp.readKey({ armoredKey: params.backupGpgKey.publicKey });

    const bitgoToUserMessage = await openpgp.createMessage({
      text: Buffer.concat([
        Buffer.from(params.bitgoKeyShare.yShares[1].u, 'hex'),
        Buffer.from(params.bitgoKeyShare.yShares[1].chaincode, 'hex'),
      ]).toString('hex'),
    });
    const encryptedBitgoToUserMessage = await openpgp.encrypt({
      message: bitgoToUserMessage,
      encryptionKeys: [userGpgKeyActual.toPublic()],
      format: 'armored',
    });

    const bitgoToBackupMessage = await openpgp.createMessage({
      text: Buffer.concat([
        Buffer.from(params.bitgoKeyShare.yShares[2].u, 'hex'),
        Buffer.from(params.bitgoKeyShare.yShares[2].chaincode, 'hex'),
      ]).toString('hex'),
    });
    const encryptedBitgoToBackupMessage = await openpgp.encrypt({
      message: bitgoToBackupMessage,
      encryptionKeys: [backupGpgKeyActual.toPublic()],
      format: 'armored',
    });

    const bitgoKeychain: Keychain = {
      id: '3',
      pub: '',
      commonKeychain: bitgoCombined.pShare.y + bitgoCombined.pShare.chaincode,
      keyShares: [
        {
          from: 'bitgo',
          to: 'user',
          publicShare: params.bitgoKeyShare.yShares[1].y + params.bitgoKeyShare.yShares[1].chaincode,
          privateShare: encryptedBitgoToUserMessage.toString(),
          vssProof: params.bitgoKeyShare.yShares[1].v,
        },
        {
          from: 'bitgo',
          to: 'backup',
          publicShare: params.bitgoKeyShare.yShares[2].y + params.bitgoKeyShare.yShares[2].chaincode,
          privateShare: encryptedBitgoToBackupMessage.toString(),
          vssProof: params.bitgoKeyShare.yShares[2].v,
        },
      ],
      type: 'tss',
    };

    const userKeyId = userGpgKeyActual.keyPacket.getFingerprint();
    const backupKeyId = backupGpgKeyActual.keyPacket.getFingerprint();
    const bitgoToUserPublicShare =
      Buffer.from(
        await sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(params.bitgoKeyShare.yShares[1].u, 'hex'))
      ).toString('hex') + params.bitgoKeyShare.yShares[1].chaincode;
    const bitgoToBackupPublicShare =
      Buffer.from(
        await sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(params.bitgoKeyShare.yShares[2].u, 'hex'))
      ).toString('hex') + params.bitgoKeyShare.yShares[2].chaincode;

    bitgoKeychain.walletHSMGPGPublicKeySigs = await createWalletSignatures(
      params.bitgoGpgKey.privateKey,
      params.userGpgKey.publicKey,
      params.backupGpgKey.publicKey,
      [
        { name: 'commonKeychain', value: bitgoCombined.pShare.y + bitgoCombined.pShare.chaincode },
        { name: 'userKeyId', value: userKeyId },
        { name: 'backupKeyId', value: backupKeyId },
        { name: 'bitgoToUserPublicShare', value: bitgoToUserPublicShare },
        { name: 'bitgoToBackupPublicShare', value: bitgoToBackupPublicShare },
      ]
    );

    return bitgoKeychain;
  }

  async function nockBitgoKeychain(params: {
    coin: string;
    userKeyShare: KeyShare;
    backupKeyShare: KeyShare;
    bitgoKeyShare: KeyShare;
    userGpgKey: openpgp.SerializedKeyPair<string>;
    backupGpgKey: openpgp.SerializedKeyPair<string>;
    bitgoGpgKey: openpgp.SerializedKeyPair<string>;
  }): Promise<Keychain> {
    const bitgoKeychain = await generateBitgoKeychain(params);

    nock(bgUrl)
      .post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'bitgo' }))
      .reply(200, bitgoKeychain);

    return bitgoKeychain;
  }

  async function nockUserKeychain(params: { coin: string }): Promise<Keychain> {
    const userKeychain: Keychain = {
      id: '1',
      pub: '',
      type: 'tss',
    };

    nock('https://bitgo.fakeurl')
      .post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'user' }))
      .reply(200, userKeychain);

    return userKeychain;
  }

  async function nockBackupKeychain(params: { coin: string }): Promise<Keychain> {
    const backupKeychain: Keychain = {
      id: '2',
      pub: '',
      type: 'tss',
    };

    nock('https://bitgo.fakeurl')
      .post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'backup' }))
      .reply(200, backupKeychain);

    return backupKeychain;
  }

  // #endregion Nock helpers
});

Выполнить команду


Для локальной разработки. Не используйте в интернете!