PHP WebShell

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

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

import * as assert from 'assert';
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 {
  mockSerializedChallengeWithProofs,
  mockSerializedChallengeWithProofs2,
  TestableBG,
  TestBitGo,
} from '@bitgo/sdk-test';
import { BitGo, createSharedDataProof, TssUtils, RequestType } from '../../../../../src';
import {
  BackupGpgKey,
  BackupKeyShare,
  BaseCoin,
  BitgoGPGPublicKey,
  BitgoHeldBackupKeyShare,
  common,
  Ecdsa,
  ECDSA,
  ECDSAMethods,
  ECDSAUtils,
  EnterpriseData,
  Keychain,
  RequestTracer,
  SignatureShareRecord,
  SignatureShareType,
  TxRequest,
  Wallet,
} from '@bitgo/sdk-core';
import { EcdsaPaillierProof, EcdsaRangeProof, EcdsaTypes, hexToBigInt } from '@bitgo/sdk-lib-mpc';
import { keyShares, otherKeyShares } from '../../../fixtures/tss/ecdsaFixtures';
import { nockSendSignatureShareWithResponse } from './common';
import {
  createWalletSignatures,
  nockGetChallenge,
  nockGetChallenges,
  nockGetEnterprise,
  nockGetSigningKey,
  nockGetTxRequest,
} from '../../tss/helpers';
import { bip32, ecc } from '@bitgo/utxo-lib';
import { Hash } from 'crypto';
import { mockChallengeA, mockChallengeB, mockChallengeC } from './mocks/ecdsaNtilde';

import { loadWebAssembly } from '@bitgo/sdk-opensslbytes';

const openSSLBytes = loadWebAssembly().buffer;

const createKeccakHash = require('keccak');

const encryptNShare = ECDSAMethods.encryptNShare;
type KeyShare = ECDSA.KeyShare;

openpgp.config.rejectCurves = new Set();

describe('TSS Ecdsa Utils:', async function () {
  const coinName = 'hteth';
  const reqId = new RequestTracer();
  const walletId = '5b34252f1bf349930e34020a00000000';
  const enterpriseId = '6449153a6f6bc20006d66771cdbe15d3';
  const enterpriseData = { id: enterpriseId, name: 'Test Enterprise' };

  let sandbox: sinon.SinonSandbox;
  let MPC: Ecdsa;
  let bgUrl: string;
  let tssUtils: ECDSAUtils.EcdsaUtils;
  let wallet: Wallet;
  let bitgo: TestableBG & BitGo;
  let baseCoin: BaseCoin;
  let bitgoKeyShare;
  let userKeyShare: KeyShare;
  let backupKeyShare: KeyShare;
  let bitgoPublicKey: openpgp.Key;

  let userGpgKey: openpgp.SerializedKeyPair<string> & {
    revocationCertificate: string;
  };
  let userLocalBackupGpgKey: openpgp.SerializedKeyPair<string> & {
    revocationCertificate: string;
  };
  let bitGoGPGKeyPair: openpgp.SerializedKeyPair<string> & {
    revocationCertificate: string;
  };
  let nockedBitGoKeychain: Keychain;
  let nockedUserKeychain: Keychain;

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

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

  before(async function () {
    nock.cleanAll();
    MPC = new Ecdsa();
    userKeyShare = keyShares.userKeyShare;
    backupKeyShare = keyShares.backupKeyShare;
    bitgoKeyShare = keyShares.bitgoKeyShare;

    const gpgKeyPromises = [
      openpgp.generateKey({
        userIDs: [
          {
            name: 'test',
            email: 'test@test.com',
          },
        ],
        curve: 'secp256k1',
      }),
      openpgp.generateKey({
        userIDs: [
          {
            name: 'backup',
            email: 'backup@test.com',
          },
        ],
        curve: 'secp256k1',
      }),
      openpgp.generateKey({
        userIDs: [
          {
            name: 'bitgo',
            email: 'bitgo@test.com',
          },
        ],
        curve: 'secp256k1',
      }),
    ];
    [userGpgKey, userLocalBackupGpgKey, bitGoGPGKeyPair] = await Promise.all(gpgKeyPromises);
    bitgoPublicKey = await openpgp.readKey({ armoredKey: bitGoGPGKeyPair.publicKey });
    const constants = {
      mpc: {
        bitgoPublicKey: bitGoGPGKeyPair.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(16).reply(200, { ttl: 3600, constants });

    const nockPromises = [
      nockBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        userLocalBackupGpgKey,
        bitgoGpgKey: bitGoGPGKeyPair,
      }),
      nockKeychain({ coin: coinName, keyChain: { id: '1', pub: '', type: 'tss' }, source: 'user' }),
      nockKeychain({ coin: coinName, keyChain: { id: '2', pub: '', type: 'tss' }, source: 'backup' }),
    ];
    [nockedBitGoKeychain, nockedUserKeychain] = await Promise.all(nockPromises);

    const walletData = {
      id: walletId,
      enterprise: enterpriseId,
      coin: coinName,
      coinSpecific: {},
      multisigType: 'tss',
    };
    wallet = new Wallet(bitgo, baseCoin, walletData);
    tssUtils = new ECDSAUtils.EcdsaUtils(bitgo, baseCoin, wallet);
  });

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

  describe('TSS key chains', async function () {
    it('should create backup key share held by BitGo', async function () {
      const enterpriseId = 'enterprise id';
      const expectedKeyShare = await nockCreateBitgoHeldBackupKeyShare(
        coinName,
        enterpriseId,
        userGpgKey,
        backupKeyShare,
        bitGoGPGKeyPair
      );
      const result = await tssUtils.createBitgoHeldBackupKeyShare(userGpgKey, enterpriseId);
      result.should.eql(expectedKeyShare);
    });

    it('should finalize backup key share held by BitGo', async function () {
      const commonKeychain = '4428';
      const originalKeyShare = await createIncompleteBitgoHeldBackupKeyShare(
        userGpgKey,
        backupKeyShare,
        bitGoGPGKeyPair
      );
      const expectedFinalKeyShare = await nockFinalizeBitgoHeldBackupKeyShare(
        coinName,
        originalKeyShare,
        commonKeychain,
        userKeyShare,
        bitGoGPGKeyPair,
        nockedBitGoKeychain
      );

      const result = await tssUtils.finalizeBitgoHeldBackupKeyShare(
        originalKeyShare.id,
        commonKeychain,
        userKeyShare,
        nockedBitGoKeychain,
        userGpgKey,
        bitgoPublicKey
      );
      result.should.eql(expectedFinalKeyShare);
    });

    it('should get the respective backup key shares based on provider', async function () {
      const enterpriseId = 'enterprise id';
      await nockCreateBitgoHeldBackupKeyShare(coinName, enterpriseId, userGpgKey, backupKeyShare, bitGoGPGKeyPair);
      const backupKeyShares = await tssUtils.createBackupKeyShares();
      should.exist(backupKeyShares.userHeldKeyShare);
      should.not.exist(backupKeyShares.bitGoHeldKeyShares);
    });

    it('should get the correct bitgo gpg key based on coin and feature flags', async function () {
      const nitroGPGKeypair = await openpgp.generateKey({
        userIDs: [
          {
            name: 'bitgo nitro',
            email: 'bitgo@test.com',
          },
        ],
      });
      const nockGPGKey = await nockGetBitgoPublicKeyBasedOnFeatureFlags(coinName, 'enterprise_id', nitroGPGKeypair);
      const bitgoGpgPublicKey = await tssUtils.getBitgoGpgPubkeyBasedOnFeatureFlags('enterprise_id');
      should.equal(nockGPGKey.publicKey, bitgoGpgPublicKey.armor());
    });

    it('getBackupEncryptedNShare should get valid encrypted n shares based on provider', async function () {
      const bitgoGpgKeyPubKey = await tssUtils.getBitgoPublicGpgKey();
      // Backup key held by user
      const backupShareHolderNew: BackupKeyShare = {
        userHeldKeyShare: backupKeyShare,
      };
      const backupToBitgoEncryptedNShare = await tssUtils.getBackupEncryptedNShare(
        backupShareHolderNew,
        3,
        bitgoGpgKeyPubKey.armor(),
        userGpgKey
      );
      const encryptedNShare = await encryptNShare(backupKeyShare, 3, bitgoGpgKeyPubKey.armor(), userGpgKey);
      // cant verify the encrypted shares, since they will be encrypted with diff. values
      should.equal(backupToBitgoEncryptedNShare.publicShare, encryptedNShare.publicShare);
    });

    it('should generate TSS key chains', async function () {
      const backupShareHolder: BackupKeyShare = {
        userHeldKeyShare: backupKeyShare,
      };
      const backupGpgKey: BackupGpgKey = userLocalBackupGpgKey;
      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare: backupShareHolder,
        bitgoPublicGpgKey: bitgoPublicKey,
      });
      const usersKeyChainPromises = [
        tssUtils.createParticipantKeychain(
          userGpgKey,
          userLocalBackupGpgKey,
          bitgoPublicKey,
          1,
          userKeyShare,
          backupKeyShare,
          bitgoKeychain,
          'passphrase'
        ),
        tssUtils.createParticipantKeychain(
          userGpgKey,
          userLocalBackupGpgKey,
          bitgoPublicKey,
          2,
          userKeyShare,
          backupKeyShare,
          bitgoKeychain,
          'passphrase'
        ),
      ];
      const [userKeychain, backupKeychain] = await Promise.all(usersKeyChainPromises);

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

      // unencrypted `prv` property should exist on backup keychain
      const keyChainPrv = JSON.parse(backupKeychain.prv ?? '');
      _.isEqual(keyChainPrv.pShare, backupKeyShare.pShare).should.be.true();
      _.isEqual(keyChainPrv.bitgoNShare, bitgoKeyShare.nShares[2]).should.be.true();
      _.isEqual(keyChainPrv.userNShare, userKeyShare.nShares[2]).should.be.true();
      should.exist(backupKeychain.encryptedPrv);
    });

    it('should generate TSS key chains with optional params', async function () {
      const enterprise = 'enterprise_id';
      const backupShareHolder: BackupKeyShare = {
        userHeldKeyShare: backupKeyShare,
      };
      const backupGpgKey: BackupGpgKey = userLocalBackupGpgKey;
      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare: backupShareHolder,
        enterprise,
        bitgoPublicGpgKey: bitgoPublicKey,
      });
      const usersKeyChainPromises = [
        tssUtils.createParticipantKeychain(
          userGpgKey,
          userLocalBackupGpgKey,
          bitgoPublicKey,
          1,
          userKeyShare,
          backupKeyShare,
          bitgoKeychain,
          'passphrase',
          'originalPasscodeEncryptionCode'
        ),
        tssUtils.createParticipantKeychain(
          userGpgKey,
          userLocalBackupGpgKey,
          bitgoPublicKey,
          2,
          userKeyShare,
          backupKeyShare,
          bitgoKeychain,
          'passphrase'
        ),
      ];

      const [userKeychain, backupKeychain] = await Promise.all(usersKeyChainPromises);
      bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
      userKeychain.should.deepEqual(nockedUserKeychain);

      // unencrypted `prv` property should exist on backup keychain
      const keyChainPrv = JSON.parse(backupKeychain.prv ?? '');
      _.isEqual(keyChainPrv.pShare, backupKeyShare.pShare).should.be.true();
      _.isEqual(keyChainPrv.bitgoNShare, bitgoKeyShare.nShares[2]).should.be.true();
      _.isEqual(keyChainPrv.userNShare, userKeyShare.nShares[2]).should.be.true();
      should.exist(backupKeychain.encryptedPrv);
    });

    it('should fail to generate TSS key chains', async function () {
      const backupShareHolder: BackupKeyShare = {
        userHeldKeyShare: backupKeyShare,
      };
      const backupGpgKey: BackupGpgKey = userLocalBackupGpgKey;
      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare,
        backupKeyShare: backupShareHolder,
        bitgoPublicGpgKey: bitgoPublicKey,
      });
      bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
      const testKeyShares = otherKeyShares;
      const testCasesPromises = [
        tssUtils
          .createParticipantKeychain(
            userGpgKey,
            userLocalBackupGpgKey,
            bitgoPublicKey,
            1,
            userKeyShare,
            testKeyShares[0],
            bitgoKeychain,
            'passphrase'
          )
          .should.be.rejectedWith('Common keychains do not match'),
        tssUtils
          .createParticipantKeychain(
            userGpgKey,
            userLocalBackupGpgKey,
            bitgoPublicKey,
            1,
            testKeyShares[1],
            backupKeyShare,
            bitgoKeychain,
            'passphrase'
          )
          .should.be.rejectedWith('Common keychains do not match'),
        tssUtils
          .createParticipantKeychain(
            userGpgKey,
            userLocalBackupGpgKey,
            bitgoPublicKey,
            2,
            testKeyShares[2],
            backupKeyShare,
            bitgoKeychain,
            'passphrase'
          )
          .should.be.rejectedWith('Common keychains do not match'),
        tssUtils
          .createParticipantKeychain(
            userGpgKey,
            userLocalBackupGpgKey,
            bitgoPublicKey,
            2,
            userKeyShare,
            testKeyShares[3],
            bitgoKeychain,
            'passphrase'
          )
          .should.be.rejectedWith('Common keychains do not match'),
      ];
      await Promise.all(testCasesPromises);
    });

    it('should fail to generate TSS keychains when received invalid number of wallet signatures', async function () {
      const bitgoKeychain = await generateBitgoKeychain({
        coin: coinName,
        userKeyShare,
        backupKeyShare,
        bitgoKeyShare,
        userGpgKey,
        userLocalBackupGpgKey,
        bitgoGpgKey: bitGoGPGKeyPair,
      });

      const certsString = await createSharedDataProof(bitGoGPGKeyPair.privateKey, userGpgKey.publicKey, []);
      const certsKey = await openpgp.readKey({ armoredKey: certsString });
      const finalKey = new openpgp.PacketList();
      certsKey.toPacketList().forEach((packet) => finalKey.push(packet));
      // 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(userLocalBackupGpgKey.publicKey, userLocalBackupGpgKey.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 customUserKeyShare = await MPC.keyShare(1, 2, 3);
      const customBackupKeyShare = await MPC.keyShare(2, 2, 3);
      const backupShareHolder: BackupKeyShare = {
        userHeldKeyShare: customBackupKeyShare,
      };
      const backupGpgKey: BackupGpgKey = userLocalBackupGpgKey;

      const bitgoKeychain = await tssUtils.createBitgoKeychain({
        userGpgKey,
        backupGpgKey,
        userKeyShare: customUserKeyShare,
        backupKeyShare: backupShareHolder,
        bitgoPublicGpgKey: bitgoPublicKey,
      });

      // 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(userLocalBackupGpgKey.publicKey, userLocalBackupGpgKey.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`
        );
    });
  });

  describe('signTxRequest:', () => {
    const txRequestId = 'randomidEcdsa';
    const txRequest: TxRequest = {
      txRequestId,
      transactions: [
        {
          unsignedTx: {
            // hteth txid: 0xc5a7bfe6b13ceae563da0f9feaa9c4ad1c101a15366a2a488828a5dd27cb9da3
            serializedTxHex:
              '02f38242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0808080',
            signableHex:
              '02f08242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0',
            derivationPath: '', // Needs this when key derivation is supported
          },
          state: 'pendingSignature',
          signatureShares: [],
        },
      ],
      unsignedTxs: [
        {
          // hteth txid: 0xc5a7bfe6b13ceae563da0f9feaa9c4ad1c101a15366a2a488828a5dd27cb9da3
          serializedTxHex:
            '02f38242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0808080',
          signableHex:
            '02f38242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0808080',
          derivationPath: '', // Needs this when key derivation is supported
        },
      ],
      date: new Date().toISOString(),
      intent: {
        intentType: 'payment',
      },
      latest: true,
      state: 'pendingUserSignature',
      walletType: 'hot',
      walletId: 'walletId',
      policiesChecked: true,
      version: 1,
      userId: 'userId',
    };
    let aShare, dShare, wShare, oShare, userSignShare, bitgoChallenges, enterpriseChallenges;

    beforeEach(async () => {
      // Initializing user and bitgo for creating shares for nocks
      const userSigningKey = MPC.keyCombine(userKeyShare.pShare, [bitgoKeyShare.nShares[1], backupKeyShare.nShares[1]]);
      const bitgoSigningKey = MPC.keyCombine(bitgoKeyShare.pShare, [
        userKeyShare.nShares[3],
        backupKeyShare.nShares[3],
      ]);

      const serializedEntChallenge = mockChallengeA;
      const serializedBitgoChallenge = mockChallengeB;

      const deserializedEntChallenge = EcdsaTypes.deserializeNtildeWithProofs(serializedEntChallenge);
      sinon.stub(EcdsaRangeProof, 'generateNtilde').resolves(deserializedEntChallenge);

      const [userToBitgoPaillierChallenge, bitgoToUserPaillierChallenge] = await Promise.all([
        EcdsaPaillierProof.generateP(hexToBigInt(userSigningKey.yShares[3].n)),
        EcdsaPaillierProof.generateP(hexToBigInt(bitgoSigningKey.yShares[1].n)),
      ]);

      bitgoChallenges = {
        ...serializedBitgoChallenge,
        p: EcdsaTypes.serializePaillierChallenge({ p: bitgoToUserPaillierChallenge }).p,
        n: bitgoSigningKey.xShare.n,
      };
      enterpriseChallenges = {
        ...serializedEntChallenge,
        p: EcdsaTypes.serializePaillierChallenge({ p: userToBitgoPaillierChallenge }).p,
        n: bitgoSigningKey.xShare.n,
      };
      sinon.stub(ECDSAUtils.EcdsaUtils.prototype, 'getEcdsaSigningChallenges').resolves({
        enterpriseChallenge: enterpriseChallenges,
        bitgoChallenge: bitgoChallenges,
      });

      const [userXShare, bitgoXShare] = [
        MPC.appendChallenge(
          userSigningKey.xShare,
          serializedEntChallenge,
          EcdsaTypes.serializePaillierChallenge({ p: userToBitgoPaillierChallenge })
        ),
        MPC.appendChallenge(
          bitgoSigningKey.xShare,
          serializedBitgoChallenge,
          EcdsaTypes.serializePaillierChallenge({ p: bitgoToUserPaillierChallenge })
        ),
      ];
      const bitgoYShare = MPC.appendChallenge(
        userSigningKey.yShares[3],
        serializedBitgoChallenge,
        EcdsaTypes.serializePaillierChallenge({ p: bitgoToUserPaillierChallenge })
      );
      /**
       * START STEP ONE
       * 1) User creates signShare, saves wShare and sends kShare to bitgo
       * 2) Bitgo performs signConvert operation using its private xShare , yShare
       *  and KShare from user and responds back with aShare and saves bShare for later use
       */
      userSignShare = await ECDSAMethods.createUserSignShare(userXShare, bitgoYShare);
      wShare = userSignShare.wShare;
      const signatureShareOneFromUser: SignatureShareRecord = {
        from: SignatureShareType.USER,
        to: SignatureShareType.BITGO,
        share: ECDSAMethods.convertKShare(userSignShare.kShare).share.replace(ECDSAMethods.delimeter, ''),
      };
      const getBitgoAandBShare = await MPC.signConvertStep1({
        xShare: bitgoXShare,
        yShare: bitgoSigningKey.yShares[1], // corresponds to the user
        kShare: userSignShare.kShare,
      });
      const bitgoAshare = getBitgoAandBShare.aShare;
      aShare = bitgoAshare;
      const aShareBitgoResponse = ECDSAMethods.convertAShare(bitgoAshare).share.replace(ECDSAMethods.delimeter, '');
      const signatureShareOneFromBitgo: SignatureShareRecord = {
        from: SignatureShareType.BITGO,
        to: SignatureShareType.USER,
        share: aShareBitgoResponse,
      };
      await nockSendSignatureShareWithResponse({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        signatureShare: signatureShareOneFromUser,
        response: signatureShareOneFromBitgo,
        tssType: 'ecdsa',
      });
      /**  END STEP ONE */

      /**
       * START STEP TWO
       * 1) Using the aShare got from bitgo and wShare from previous step,
       * user creates gShare and muShare and sends muShare to bitgo
       * 2) Bitgo using the signConvert step using bShare from previous step
       * and muShare from user generates its gShare.
       * 3) Using the signCombine operation using gShare, Bitgo generates oShare
       * which it saves and dShare which is send back to the user.
       */
      const userGammaAndMuShares = await ECDSAMethods.createUserGammaAndMuShare(userSignShare.wShare, bitgoAshare);
      const signatureShareTwoFromUser: SignatureShareRecord = {
        from: SignatureShareType.USER,
        to: SignatureShareType.BITGO,
        share: ECDSAMethods.convertMuShare(userGammaAndMuShares.muShare!).share.replace(ECDSAMethods.delimeter, ''),
      };
      const getBitGoGShareAndSignerIndexes = await MPC.signConvertStep3({
        bShare: getBitgoAandBShare.bShare,
        muShare: userGammaAndMuShares.muShare,
      });

      const getBitgoOShareAndDShares = MPC.signCombine({
        gShare: getBitGoGShareAndSignerIndexes.gShare as ECDSA.GShare,
        signIndex: {
          i: 1,
          j: 3,
        },
      });
      const bitgoDshare = getBitgoOShareAndDShares.dShare as ECDSA.DShare;
      dShare = bitgoDshare;
      const dShareBitgoResponse = (bitgoDshare.delta as string) + (bitgoDshare.Gamma as string);
      const signatureShareTwoFromBitgo: SignatureShareRecord = {
        from: SignatureShareType.BITGO,
        to: SignatureShareType.USER,
        share: dShareBitgoResponse,
      };
      await nockSendSignatureShareWithResponse({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        signatureShare: signatureShareTwoFromUser,
        response: signatureShareTwoFromBitgo,
        tssType: 'ecdsa',
      });
      /**  END STEP TWO */

      /**
       * START STEP THREE
       * 1) User creates its oShare and  dShare using the  private gShare
       * from step two
       * 2) User uses the private oShare and dShare from bitgo from step
       * two to generate its signature share which it sends back along with dShare that
       * user generated from the above step
       * 3) Bitgo using its private oShare from step two and dShare from bitgo creates
       * its signature share. Using the Signature Share received from user from the above
       * step, bitgo constructs the final signature and is returned to the user
       */
      const userOmicronAndDeltaShare = await ECDSAMethods.createUserOmicronAndDeltaShare(
        userGammaAndMuShares.gShare as ECDSA.GShare
      );
      oShare = userOmicronAndDeltaShare.oShare;
      const signablePayload = Buffer.from(txRequest.unsignedTxs[0].signableHex, 'hex');
      const userSShare = await ECDSAMethods.createUserSignatureShare(
        userOmicronAndDeltaShare.oShare,
        bitgoDshare,
        signablePayload
      );
      const signatureShareThreeFromUser: SignatureShareRecord = {
        from: SignatureShareType.USER,
        to: SignatureShareType.BITGO,
        share:
          userSShare.R +
          userSShare.s +
          userSShare.y +
          userOmicronAndDeltaShare.dShare.delta +
          userOmicronAndDeltaShare.dShare.Gamma,
      };
      const getBitGoSShare = MPC.sign(
        signablePayload,
        getBitgoOShareAndDShares.oShare,
        userOmicronAndDeltaShare.dShare,
        createKeccakHash('keccak256') as Hash
      );
      const getBitGoFinalSignature = MPC.constructSignature([getBitGoSShare, userSShare]);
      const finalSigantureBitgoResponse =
        getBitGoFinalSignature.r + getBitGoFinalSignature.s + getBitGoFinalSignature.y;
      const signatureShareThreeFromBitgo: SignatureShareRecord = {
        from: SignatureShareType.BITGO,
        to: SignatureShareType.USER,
        share: finalSigantureBitgoResponse,
      };
      await nockSendSignatureShareWithResponse({
        walletId: wallet.id(),
        txRequestId: txRequest.txRequestId,
        signatureShare: signatureShareThreeFromUser,
        response: signatureShareThreeFromBitgo,
        tssType: 'ecdsa',
      });
      /* END STEP THREE */
      const signature = MPC.constructSignature([userSShare, getBitGoSShare]);
      MPC.verify(signablePayload, signature, createKeccakHash('keccak256') as Hash).should.be.true;
    });

    afterEach(async () => {
      sinon.restore();
    });

    it('signTxRequest should fail if wallet is in pendingEcdsaTssInitialization', async function () {
      sandbox.stub(wallet, 'coinSpecific').returns({
        customChangeWalletId: '',
        pendingEcdsaTssInitialization: true,
      });
      await tssUtils
        .signTxRequest({
          txRequest,
          prv: JSON.stringify({
            pShare: userKeyShare.pShare,
            bitgoNShare: bitgoKeyShare.nShares[1],
            backupNShare: backupKeyShare.nShares[1],
          }),
          reqId,
        })
        .should.be.rejectedWith(
          'Wallet is not ready for TSS ECDSA signing. Please contact your enterprise admin to finish the enterprise TSS initialization.'
        );
    });

    it('signTxRequest should succeed with txRequest object as input', async function () {
      const sendShareSpy = sinon.spy(ECDSAMethods, 'sendShareToBitgo' as any);
      await setupSignTxRequestNocks(false, userSignShare, aShare, dShare, enterpriseData);
      const signedTxRequest = await tssUtils.signTxRequest({
        txRequest,
        prv: JSON.stringify({
          pShare: userKeyShare.pShare,
          bitgoNShare: bitgoKeyShare.nShares[1],
          backupNShare: backupKeyShare.nShares[1],
        }),
        reqId,
      });
      signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);
      const userGpgActual = sendShareSpy.getCalls()[0].args[10] as string;
      userGpgActual.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
    });

    it('signTxRequest should succeed with txRequest id as input', async function () {
      const sendShareSpy = sinon.spy(ECDSAMethods, 'sendShareToBitgo' as any);
      await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData);
      const signedTxRequest = await tssUtils.signTxRequest({
        txRequest: txRequestId,
        prv: JSON.stringify({
          pShare: userKeyShare.pShare,
          bitgoNShare: bitgoKeyShare.nShares[1],
          backupNShare: backupKeyShare.nShares[1],
        }),
        reqId,
      });
      signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);
      const userGpgActual = sendShareSpy.getCalls()[0].args[10] as string;
      userGpgActual.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
    });

    it('signTxRequest should fail with wrong recipient', async function () {
      await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData);
      await tssUtils
        .signTxRequest({
          txRequest: txRequestId,
          prv: JSON.stringify({
            pShare: userKeyShare.pShare,
            bitgoNShare: bitgoKeyShare.nShares[1],
            backupNShare: backupKeyShare.nShares[1],
          }),
          reqId,
          txParams: { recipients: [{ address: '0x1234', amount: '100000000000000' }], type: 'transfer' },
        })
        .should.be.rejectedWith('destination address does not match with the recipient address');
    });

    it('signTxRequest should fail with incorrect value', async function () {
      await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData);
      await tssUtils
        .signTxRequest({
          txRequest: txRequestId,
          prv: JSON.stringify({
            pShare: userKeyShare.pShare,
            bitgoNShare: bitgoKeyShare.nShares[1],
            backupNShare: backupKeyShare.nShares[1],
          }),
          reqId,
          txParams: {
            recipients: [{ address: '0xa1cfb9d51c0af191ff21c5f0f01723e056f7dc12', amount: '1' }],
            type: 'transfer',
          },
        })
        .should.be.rejectedWith('the transaction amount in txPrebuild does not match the value given by client');
    });

    it('signTxRequest should fail with incorrect value for token txn', async function () {
      const signableHex =
        '02f86d8242681083122c9e83122cae8301e04994ebe8b46a42f05072b723b00013ff822b2af1b5cb80b844a9059cbb0000000000000000000000002b0d6cb2f8c388757f4d7ad857fccab18290dbc900000000000000000000000000000000000000000000000000000000000186a0c0';
      const serializedTxHex =
        '02f8708242681083122c9e83122cae8301e04994ebe8b46a42f05072b723b00013ff822b2af1b5cb80b844a9059cbb0000000000000000000000002b0d6cb2f8c388757f4d7ad857fccab18290dbc900000000000000000000000000000000000000000000000000000000000186a0c0808080';
      await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData, {
        signableHex,
        serializedTxHex,
        apiVersion: 'full',
      });
      await tssUtils
        .signTxRequest({
          txRequest: txRequestId,
          prv: JSON.stringify({
            pShare: userKeyShare.pShare,
            bitgoNShare: bitgoKeyShare.nShares[1],
            backupNShare: backupKeyShare.nShares[1],
          }),
          reqId,
          txParams: {
            recipients: [{ address: '0x2b0d6cb2f8c388757f4d7ad857fccab18290dbc9', amount: '707' }],
            type: 'transfer',
          },
        })
        .should.be.rejectedWith('the transaction amount in txPrebuild does not match the value given by client');
    });

    it('getOfflineSignerPaillierModulus should succeed', async function () {
      const paillierModulus = tssUtils.getOfflineSignerPaillierModulus({
        prv: JSON.stringify({
          pShare: userKeyShare.pShare,
          bitgoNShare: bitgoKeyShare.nShares[1],
          backupNShare: backupKeyShare.nShares[1],
        }),
      });
      paillierModulus.userPaillierModulus.should.equal(userKeyShare.pShare.n);
    });

    it('createOfflineKShare should succeed', async function () {
      const mockPassword = 'password';
      const step1SigningMaterial = await tssUtils.createOfflineKShare({
        tssParams: {
          txRequest,
          reqId: reqId,
        },
        challenges: {
          enterpriseChallenge: enterpriseChallenges,
          bitgoChallenge: bitgoChallenges,
        },
        prv: JSON.stringify({
          pShare: userKeyShare.pShare,
          bitgoNShare: bitgoKeyShare.nShares[1],
          backupNShare: backupKeyShare.nShares[1],
        }),
        requestType: RequestType.tx,
        walletPassphrase: mockPassword,
      });
      step1SigningMaterial.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
      step1SigningMaterial.vssProof?.length.should.equal(userKeyShare.nShares[3].v?.length);
      step1SigningMaterial.publicShare.length.should.equal(
        userKeyShare.nShares[3].y.length + userKeyShare.nShares[3].chaincode.length
      );
      step1SigningMaterial.encryptedSignerOffsetShare.should.startWith('-----BEGIN PGP MESSAGE-----');
      step1SigningMaterial.userPublicGpgKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
      step1SigningMaterial.kShare.n.should.equal(userKeyShare.pShare.n);
      step1SigningMaterial.wShare.should.startWith('{"iv":');
    });

    it('createOfflineKShare should fail with txId passed', async function () {
      const mockPassword = 'password';
      await tssUtils
        .createOfflineKShare({
          tssParams: {
            txRequest: txRequest.txRequestId,
            reqId: reqId,
          },
          challenges: {
            enterpriseChallenge: enterpriseChallenges,
            bitgoChallenge: bitgoChallenges,
          },
          prv: JSON.stringify({
            pShare: userKeyShare.pShare,
            bitgoNShare: bitgoKeyShare.nShares[1],
            backupNShare: backupKeyShare.nShares[1],
          }),
          requestType: RequestType.tx,
          walletPassphrase: mockPassword,
        })
        .should.be.rejectedWith('Invalid txRequest type');
    });

    // Seems to be flaky on CI, failed here: https://github.com/BitGo/BitGoJS/actions/runs/5902489990/job/16010623888?pr=3822
    xit('createOfflineMuDeltaShare should succeed', async function () {
      const mockPassword = 'password';
      const alphaLength = 1536;
      const deltaLength = 64;
      const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
      const step2SigningMaterial = await tssUtils.createOfflineMuDeltaShare({
        aShareFromBitgo: aShare,
        bitgoChallenge: bitgoChallenges,
        encryptedWShare: bitgo.encrypt({ input: JSON.stringify(wShare), password: mockPassword }),
        walletPassphrase: mockPassword,
      });
      step2SigningMaterial.muDShare.muShare.alpha.length.should.equal(alphaLength);
      step2SigningMaterial.muDShare.dShare.delta.length.should.equal(deltaLength);
      step2SigningMaterial.oShare.should.startWith('{"iv":');
    });

    it('createOfflineMuDeltaShare should fail with incorrect password', async function () {
      const mockPassword = 'password';
      const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
      await tssUtils
        .createOfflineMuDeltaShare({
          aShareFromBitgo: aShare,
          bitgoChallenge: bitgoChallenges,
          encryptedWShare: bitgo.encrypt({ input: JSON.stringify(wShare), password: mockPassword }),
          walletPassphrase: 'password1',
        })
        .should.be.rejectedWith("password error - ccm: tag doesn't match");
    });

    it('createOfflineSShare should succeed', async function () {
      const mockPassword = 'password';
      const pubKeyLength = 66;
      const privKeyLength = 64;
      const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
      const step3SigningMaterial = await tssUtils.createOfflineSShare({
        tssParams: {
          txRequest: txRequest,
          reqId: reqId,
        },
        dShareFromBitgo: dShare,
        encryptedOShare: bitgo.encrypt({ input: JSON.stringify(oShare), password: mockPassword }),
        walletPassphrase: mockPassword,
        requestType: RequestType.tx,
      });
      step3SigningMaterial.R.length.should.equal(pubKeyLength);
      step3SigningMaterial.y.length.should.equal(pubKeyLength);
      step3SigningMaterial.s.length.should.equal(privKeyLength);
    });

    it('createOfflineSShare should fail with txId passed', async function () {
      const mockPassword = 'password';
      const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
      await tssUtils
        .createOfflineSShare({
          tssParams: {
            txRequest: txRequest.txRequestId,
            reqId: reqId,
          },
          dShareFromBitgo: dShare,
          encryptedOShare: bitgo.encrypt({ input: JSON.stringify(oShare), password: mockPassword }),
          walletPassphrase: mockPassword,
          requestType: RequestType.tx,
        })
        .should.be.rejectedWith('Invalid txRequest type');
    });

    it('signTxRequest should fail with invalid user prv', async function () {
      const invalidUserKey = { ...userKeyShare, pShare: { ...userKeyShare.pShare, i: 2 } };
      await tssUtils
        .signTxRequest({
          txRequest: txRequestId,
          prv: JSON.stringify({
            pShare: invalidUserKey.pShare,
            bitgoNShare: bitgoKeyShare.nShares[1],
            backupNShare: backupKeyShare.nShares[1],
          }),
          reqId,
        })
        .should.be.rejectedWith('Invalid user key');
    });

    it('signTxRequest should fail with no backupNShares', async function () {
      const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest');
      getTxRequest.resolves(txRequest);
      getTxRequest.calledWith(txRequestId);
      setupSignTxRequestNocks(false, userSignShare, aShare, dShare, enterpriseData);
      await tssUtils
        .signTxRequest({
          txRequest: txRequestId,
          prv: JSON.stringify({
            pShare: userKeyShare.pShare,
            bitgoNShare: bitgoKeyShare.nShares[1],
          }),
          reqId,
        })
        .should.be.rejectedWith('Invalid user key - missing backupNShare');
    });

    async function setupSignTxRequestNocks(
      isTxRequest = true,
      userSignShare: ECDSA.SignShareRT,
      aShare: ECDSA.AShare,
      dShare: ECDSA.DShare,
      enterpriseData: EnterpriseData,
      {
        signableHex,
        serializedTxHex,
        apiVersion,
      }: { signableHex?: string; serializedTxHex?: string; apiVersion?: 'full' | 'lite' } = {}
    ) {
      if (enterpriseData) {
        await nockGetEnterprise({ enterpriseId: enterpriseData.id, response: enterpriseData, times: 1 });
      }
      const derivationPath = '';
      sinon.stub(ECDSAMethods, 'createUserSignShare').resolves(userSignShare);
      let response = {
        txRequests: [
          {
            ...txRequest,
            transactions: [
              {
                ...txRequest,
                unsignedTx: {
                  signableHex: signableHex ?? txRequest.unsignedTxs[0].signableHex,
                  serializedTxHex: serializedTxHex ?? txRequest.unsignedTxs[0].serializedTxHex,
                  derivationPath,
                },
              },
            ],
            apiVersion: apiVersion,
          },
        ],
      };
      if (isTxRequest) {
        await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response });
      }
      const aRecord = ECDSAMethods.convertAShare(aShare);
      const signatureShares = [aRecord];
      txRequest.signatureShares = signatureShares;
      response = {
        txRequests: [
          {
            ...txRequest,
            transactions: [
              {
                ...txRequest,
                unsignedTx: {
                  signableHex: txRequest.unsignedTxs[0].signableHex,
                  serializedTxHex: txRequest.unsignedTxs[0].serializedTxHex,
                  derivationPath,
                },
              },
            ],
            apiVersion: apiVersion,
          },
        ],
      };
      await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response });
      const dRecord = ECDSAMethods.convertDShare(dShare);
      signatureShares.push(dRecord);
      response = {
        txRequests: [
          {
            ...txRequest,
            transactions: [
              {
                ...txRequest,
                unsignedTx: {
                  signableHex: txRequest.unsignedTxs[0].signableHex,
                  serializedTxHex: txRequest.unsignedTxs[0].serializedTxHex,
                  derivationPath,
                },
              },
            ],
            apiVersion: apiVersion,
          },
        ],
      };
      await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response });
      await nockGetTxRequest({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response });
    }
  });

  describe('getEcdsaSigningChallenges', function () {
    const mockWalletPaillierKey = {
      n: 'f47be4c2d8bc1e28f88c6c4da634da97d92a1c279a7b0fe7b87c337c36a27b32ce0ff0c45f16e4e15bbd20e4e640de12047eff9b1a2b98144f9a268d406bd000d192a35b6847a17e40fb85f55b314d001ff87393481cafe391807d0eb83eff9e38614b38e5f25fc4449cb01caed805584d026b5d866c723f3d4d4f1e462662f2113b1561eb2bf755b4b91d0308d8eacc439167da8b7d6e108524f226960360af00215d9614457414ebdbe8834999689e2e903208c8713ff5d9901f9eaba3aa81d705323cbbba61ba7fa9f3228f30853fb55da1b3d3ed7db1dfc6545bc96aa8d2eb848931c1b807fdfe8f65af72f68638a82fe9e22ac1f0f032e621066806a1f144b5719a5f091986867b384be6c34146c8241cbfbd781966ebbcd19e6caa27fab040e62e5a162888aa8624d046c8fe3b72244f04a7264c4a36b6366dbe7da98afb201d34be2c0d6dd11982af35bf7535582b263914725aaec280d52290527382d3ab297d746c41aacd8de98c09fcfb85a95e02de1b34d4933e51045e2f1ce8af',
      lambda:
        'f47be4c2d8bc1e28f88c6c4da634da97d92a1c279a7b0fe7b87c337c36a27b32ce0ff0c45f16e4e15bbd20e4e640de12047eff9b1a2b98144f9a268d406bd000d192a35b6847a17e40fb85f55b314d001ff87393481cafe391807d0eb83eff9e38614b38e5f25fc4449cb01caed805584d026b5d866c723f3d4d4f1e462662f2113b1561eb2bf755b4b91d0308d8eacc439167da8b7d6e108524f226960360af00215d9614457414ebdbe8834999689e2e903208c8713ff5d9901f9eaba3aa7fc3d0c0bcc5bff644156ab887146d51bcee1eef70f45c486147d687ee37def1f8a16bc945eff22dd4dca3614a99158823acd9492e347f7ec79a7771024205d07f27b30cd20340e330411da8fa2da209e5cc688da94d1dbef54bfd9c69b4e99cf06d67309a3420b82c78a0fe0dd0b9c31382eae38746cfdd27fa90022a50532246c8ae1339c93e183c03bf6fd7014be3658abc73baae1fa5b86dab94b9f125395a818e54dde6235c45d3dbc032b3078e9df1cad69d8ac19a7cb6405a558b7bfba8',
    };
    const mockBitgoPaillierKey = {
      n: 'f010d294effceb8c4f96af1978ab367c4fbb272c2169317e41ae87220652cae2ce929696ee55ec6831aa6b4b3b931babc2bac9c1a20fddbca925cc99680791f7c3157b3d31256ee72c47d47db567e0f070dce121c3a4d9e003c1f1389073acb252c65d2b0723e86e3265f67a137cb1e23f4551544405644d0ae63d35f25f40becd2b693879f3bdbec3f7250791a3f3c975a5ac78a0e81dcd1a87eb2ca67010dff880b2338556275de23d9e88d21b77da0d524ddc2b394f8de00b1af0ce85f6eee2e05a184e05494d66d2c636045bf70ed15ebd0f41a8eea2920af85e6d68a0ce11fc2abbcb3cebcc3c23ec2e148c318683a5426e15b5207efd3b9b05cb919ec4340f74dff336986d0c923df10a789007b1da9daddf8edf3014e93989f30243f27f9a307d55d630cbfcd16cd6a95a41dee10c31acc293df6834ce0e3ea5b68f170bd7938ea0c2eeb788e16f30af57b3f0888fb44d3610e7eeba60e7fd8cc4a8f044718dfc6174bf4a380690dc1dc77472a48892eb3e81775540ea0acc9e89b639',
      lambda:
        'f010d294effceb8c4f96af1978ab367c4fbb272c2169317e41ae87220652cae2ce929696ee55ec6831aa6b4b3b931babc2bac9c1a20fddbca925cc99680791f7c3157b3d31256ee72c47d47db567e0f070dce121c3a4d9e003c1f1389073acb252c65d2b0723e86e3265f67a137cb1e23f4551544405644d0ae63d35f25f40becd2b693879f3bdbec3f7250791a3f3c975a5ac78a0e81dcd1a87eb2ca67010dff880b2338556275de23d9e88d21b77da0d524ddc2b394f8de00b1af0ce85f6ecdef8a4bc955a28ecef7d97cded079d390e77c80998d78ad9510cbabfeb8f0a157dbfc590b4d59ee8c0b088f9d89473b557320078a117478624f5d1df36e30f320b6722a4217dcb46b978cc6c8f1a21c8a6c74bce84d82c481402c99a69b798e3c05f23350b4aade4f79784b1c09692b6a33cfba7f145597d82b799cccef620c36f1fbbe2cb4ac0ea395c476e381bc475d41722320f541ae9bf56aa4a12dff3ea7ab11174fb5b8df7429c9f57d36f8fc51e1a8c647d5b8fa0189fb8acdbd0a780',
    };
    const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
    const txRequestId = 'fakeTxRequestId';
    const rawEntChallengeWithProofs: EcdsaTypes.SerializedNtildeWithProofs = mockSerializedChallengeWithProofs;
    let rawBitgoChallenge: EcdsaTypes.SerializedEcdsaChallenges & { n: string };
    const adminEcdhKey = bitgo.keychains().create();
    const fakeAdminEcdhKey = bitgo.keychains().create();
    const derivationPath = 'm/0/0';
    const mockedSigningKey = {
      userId: 'id',
      userEmail: 'user@bitgo.com',
      derivedPubkey: bip32.fromBase58(adminEcdhKey.xpub).derivePath(derivationPath).publicKey.toString('hex'),
      derivationPath: derivationPath,
      ecdhKeychain: 'my keychain',
    };

    before(async function () {
      const p = await EcdsaPaillierProof.generateP(hexToBigInt(mockWalletPaillierKey.n));
      rawBitgoChallenge = {
        ...EcdsaTypes.serializeNtilde(EcdsaTypes.deserializeNtilde(mockSerializedChallengeWithProofs2)),
        p: EcdsaTypes.serializePaillierChallenge({ p }).p,
        n: mockBitgoPaillierKey.n,
      };
    });

    afterEach(function () {
      sinon.restore();
      nock.cleanAll();
    });

    it('should fetch static ent and bitgo challenges with the ent feature flag and verify them', async function () {
      await nockGetChallenge({ walletId, txRequestId, addendum: '/transactions/0', response: rawBitgoChallenge });
      await nockGetSigningKey({ enterpriseId, userId: mockedSigningKey.userId, response: mockedSigningKey, times: 1 });
      const adminSignatureEntChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        rawEntChallengeWithProofs,
        adminEcdhKey.xprv,
        derivationPath
      );
      const adminSignatureBitGoChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        rawBitgoChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );
      const mockChallengesResponse = {
        enterpriseChallenge: {
          ...rawEntChallengeWithProofs,
          verifiers: {
            adminSignature: adminSignatureEntChallenge.toString('hex'),
          },
        },
        bitgoChallenge: {
          ...rawBitgoChallenge,
          verifiers: {
            adminSignature: adminSignatureBitGoChallenge.toString('hex'),
          },
        },
        createdBy: 'id',
      };

      await nockGetChallenges({ walletId: walletId, response: mockChallengesResponse });
      const challenges = await tssUtils.getEcdsaSigningChallenges(txRequestId, 0, mockWalletPaillierKey.n, 0);
      should.exist(challenges);
      const expectedRangeProofChallenges = {
        enterpriseChallenge: {
          ntilde: challenges.enterpriseChallenge.ntilde,
          h1: challenges.enterpriseChallenge.h1,
          h2: challenges.enterpriseChallenge.h2,
        },
        bitgoChallenge: rawBitgoChallenge,
      };
      expectedRangeProofChallenges.should.deepEqual({
        enterpriseChallenge: {
          ntilde: rawEntChallengeWithProofs.ntilde,
          h1: rawEntChallengeWithProofs.h1,
          h2: rawEntChallengeWithProofs.h2,
        },
        bitgoChallenge: rawBitgoChallenge,
      });
    });

    it('Fails if the enterprise challenge signature is different from the admin ecdh key', async function () {
      await nockGetChallenge({ walletId, txRequestId, addendum: '/transactions/0', response: rawBitgoChallenge });
      await nockGetEnterprise({
        enterpriseId: enterpriseData.id,
        response: {
          ...enterpriseData,
          featureFlags: ['useEnterpriseEcdsaTssChallenge'],
        },
        times: 1,
      });
      await nockGetSigningKey({ enterpriseId, userId: mockedSigningKey.userId, response: mockedSigningKey, times: 1 });
      // Bad sign
      const adminSignedEntChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        rawEntChallengeWithProofs,
        fakeAdminEcdhKey.xprv,
        derivationPath
      );
      const adminSignedBitGoChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        rawBitgoChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );
      const mockChallengesResponse = {
        enterpriseChallenge: {
          ...rawEntChallengeWithProofs,
          verifiers: {
            adminSignature: adminSignedEntChallenge.toString('hex'),
          },
        },
        bitgoChallenge: {
          ...rawBitgoChallenge,
          verifiers: {
            adminSignature: adminSignedBitGoChallenge.toString('hex'),
          },
        },
        createdBy: 'id',
      };
      await nockGetChallenges({ walletId: walletId, response: mockChallengesResponse });
      await tssUtils
        .getEcdsaSigningChallenges(txRequestId, 0, mockWalletPaillierKey.n)
        .should.be.rejectedWith(
          'Admin signature for enterprise challenge is not valid. Please contact your enterprise admin.'
        );
    });

    it('Fails if the bitgo challenge signature is different from the admin ecdh key', async function () {
      await nockGetChallenge({ walletId, txRequestId, addendum: '/transactions/0', response: rawBitgoChallenge });
      await nockGetEnterprise({
        enterpriseId: enterpriseData.id,
        response: {
          ...enterpriseData,
          featureFlags: ['useEnterpriseEcdsaTssChallenge'],
        },
        times: 1,
      });
      await nockGetSigningKey({ enterpriseId, userId: mockedSigningKey.userId, response: mockedSigningKey, times: 1 });
      const adminSignedEntChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        rawEntChallengeWithProofs,
        adminEcdhKey.xprv,
        derivationPath
      );
      // Bad sign
      const adminSignedBitGoChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        rawBitgoChallenge,
        fakeAdminEcdhKey.xprv,
        derivationPath
      );
      const mockChallengesResponse = {
        enterpriseChallenge: {
          ...rawEntChallengeWithProofs,
          verifiers: {
            adminSignature: adminSignedEntChallenge.toString('hex'),
          },
        },
        bitgoChallenge: {
          ...rawBitgoChallenge,
          verifiers: {
            adminSignature: adminSignedBitGoChallenge.toString('hex'),
          },
        },
        createdBy: 'id',
      };
      await nockGetChallenges({ walletId: walletId, response: mockChallengesResponse });
      await tssUtils
        .getEcdsaSigningChallenges(txRequestId, 0, mockWalletPaillierKey.n)
        .should.be.rejectedWith(
          "Admin signature for BitGo's challenge is not valid. Please contact your enterprise admin."
        );
    });
  });

  describe('getVerifyAndSignBitGoChallenges', function () {
    const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
    const adminEcdhKey = bitgo.keychains().create();
    const derivationPath = 'm/0/0';
    const bitgoInstChallenge = mockChallengeA;
    const bitgoNitroChallenge = mockChallengeB;
    const userPassword = 'password123';
    const encryptedXprv = bitgo.encrypt({
      password: userPassword,
      input: adminEcdhKey.xprv,
    });

    beforeEach(async function () {
      sinon.stub(bitgo, 'getSigningKeyForUser').resolves({
        userId: 'id',
        userEmail: 'user@bitgo.com',
        derivedPubkey: bip32.fromBase58(adminEcdhKey.xpub).derivePath(derivationPath).publicKey.toString('hex'),
        derivationPath: derivationPath,
        ecdhKeychain: 'my keychain',
      });
      sinon.stub(bitgo, 'getECDHKeychain').resolves({
        encryptedXprv: encryptedXprv,
      });
    });

    afterEach(async function () {
      sinon.restore();
      nock.cleanAll();
    });

    function nockGetBitgoChallenges(response: unknown): nock.Scope {
      return nock(bgUrl)
        .get(`/api/v2/tss/ecdsa/challenges`)
        .times(1)
        .reply(() => [200, response]);
    }

    it('succeeds for valid bitgo proofs', async function () {
      const nockGetBitgoChallengesApi = nockGetBitgoChallenges({
        bitgoNitroHsm: bitgoNitroChallenge,
        bitgoInstitutionalHsm: bitgoInstChallenge,
      });

      await ECDSAUtils.EcdsaUtils.getVerifyAndSignBitGoChallenges(
        bitgo,
        'ent_id',
        userPassword
      ).should.not.be.rejected();
      nockGetBitgoChallengesApi.isDone().should.be.true();
    });

    it('Fails if bitgo challenge proofs are not present', async function () {
      const nockGetBitgoChallengesApi = nockGetBitgoChallenges({
        bitgoNitroHsm: {
          ...bitgoNitroChallenge,
          ntildeProof: undefined,
        },
        bitgoInstitutionalHsm: bitgoInstChallenge,
      });
      await ECDSAUtils.EcdsaUtils.getVerifyAndSignBitGoChallenges(bitgo, 'ent_id', userPassword).should.be.rejectedWith(
        'Expected BitGo challenge proof to be present. Contact support@bitgo.com.'
      );
      nockGetBitgoChallengesApi.isDone().should.be.true();
    });

    it('Fails if the user password to decrypt the ecdhkeychain is wrong', async function () {
      const nockGetBitgoChallengesApi = nockGetBitgoChallenges({
        bitgoNitroHsm: bitgoNitroChallenge,
        bitgoInstitutionalHsm: bitgoInstChallenge,
      });
      await ECDSAUtils.EcdsaUtils.getVerifyAndSignBitGoChallenges(bitgo, 'ent_id', 'bro').should.be.rejectedWith(
        'Incorrect password. Please try again.'
      );
      nockGetBitgoChallengesApi.isDone().should.be.true();
    });

    it('Fails bitgo challenge proofs for faulty nitro h2WrtH1 proof', async function () {
      const nockGetBitgoChallengesApi = nockGetBitgoChallenges({
        bitgoNitroHsm: {
          ...bitgoNitroChallenge,
          ntildeProof: {
            ...bitgoNitroChallenge.ntildeProof,
            h2WrtH1: bitgoNitroChallenge.ntildeProof.h1WrtH2,
          },
        },
        bitgoInstitutionalHsm: bitgoInstChallenge,
      });
      await ECDSAUtils.EcdsaUtils.getVerifyAndSignBitGoChallenges(bitgo, 'ent_id', userPassword).should.be.rejectedWith(
        "Failed to verify BitGo's challenge needed to enable ECDSA signing. Please contact support@bitgo.com"
      );
      nockGetBitgoChallengesApi.isDone().should.be.true();
    });

    it('Fails bitgo challenge proofs for faulty nitro h1WrtH2 proof', async function () {
      const nockGetBitgoChallengesApi = nockGetBitgoChallenges({
        bitgoNitroHsm: {
          ...bitgoNitroChallenge,
          ntildeProof: {
            ...bitgoNitroChallenge.ntildeProof,
            h1WrtH2: bitgoNitroChallenge.ntildeProof.h2WrtH1,
          },
        },
        bitgoInstitutionalHsm: bitgoInstChallenge,
      });
      await ECDSAUtils.EcdsaUtils.getVerifyAndSignBitGoChallenges(bitgo, 'ent_id', userPassword).should.be.rejectedWith(
        "Failed to verify BitGo's challenge needed to enable ECDSA signing. Please contact support@bitgo.com"
      );
      nockGetBitgoChallengesApi.isDone().should.be.true();
    });

    it('Fails bitgo challenge proofs for faulty inst h2WrtH1 proof', async function () {
      const nockGetBitgoChallengesApi = nock(bgUrl)
        .get(`/api/v2/tss/ecdsa/challenges`)
        .times(1)
        .reply(200, {
          bitgoNitroHsm: bitgoNitroChallenge,
          bitgoInstitutionalHsm: {
            ...bitgoInstChallenge,
            ntildeProof: {
              ...bitgoInstChallenge.ntildeProof,
              h2WrtH1: bitgoInstChallenge.ntildeProof.h1WrtH2,
            },
          },
        });
      await ECDSAUtils.EcdsaUtils.getVerifyAndSignBitGoChallenges(bitgo, 'ent_id', userPassword).should.be.rejectedWith(
        "Failed to verify BitGo's challenge needed to enable ECDSA signing. Please contact support@bitgo.com"
      );
      nockGetBitgoChallengesApi.isDone().should.be.true();
    });

    it('Fails bitgo challenge proofs for faulty inst h1WrtH2 proof', async function () {
      const nockGetBitgoChallengesApi = nock(bgUrl)
        .get(`/api/v2/tss/ecdsa/challenges`)
        .times(1)
        .reply(200, {
          bitgoNitroHsm: bitgoNitroChallenge,
          bitgoInstitutionalHsm: {
            ...bitgoInstChallenge,
            ntildeProof: {
              ...bitgoInstChallenge.ntildeProof,
              h1WrtH2: bitgoInstChallenge.ntildeProof.h2WrtH1,
            },
          },
        });
      await ECDSAUtils.EcdsaUtils.getVerifyAndSignBitGoChallenges(bitgo, 'ent_id', userPassword).should.be.rejectedWith(
        "Failed to verify BitGo's challenge needed to enable ECDSA signing. Please contact support@bitgo.com"
      );
      nockGetBitgoChallengesApi.isDone().should.be.true();
    });
  });

  describe('supportedTxRequestVersions', function () {
    it('returns only full for hot wallets', function () {
      const hotWallet = new Wallet(bitgo, baseCoin, { type: 'hot', multisigType: 'tss' });
      const hotWalletTssUtils = new ECDSAUtils.EcdsaUtils(bitgo, baseCoin, hotWallet);
      hotWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
    });
    it('returns only full for cold wallets', function () {
      const coldWallet = new Wallet(bitgo, baseCoin, {
        type: 'cold',
        multisigType: 'tss',
      });
      const coldWalletTssUtils = new ECDSAUtils.EcdsaUtils(bitgo, baseCoin, coldWallet);

      coldWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
    });
    it('returns only full for custodial wallets', function () {
      const custodialWallet = new Wallet(bitgo, baseCoin, { type: 'custodial', multisigType: 'tss' });
      const custodialWalletTssUtils = new ECDSAUtils.EcdsaUtils(bitgo, baseCoin, custodialWallet);
      custodialWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
    });
    it('returns empty for trading wallets', function () {
      const tradingWallet = new Wallet(bitgo, baseCoin, { type: 'trading', multisigType: 'tss' });
      const tradingWalletTssUtils = new ECDSAUtils.EcdsaUtils(bitgo, baseCoin, tradingWallet);
      tradingWalletTssUtils.supportedTxRequestVersions().should.deepEqual([]);
    });
    it('returns empty for non-tss wallets', function () {
      const nonTssWalletData = { coin: 'tbtc', 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('initiateChallengesForEnterprise', function () {
    const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
    const adminEcdhKey = bitgo.keychains().create();
    const derivationPath = 'm/0/0';
    const bitgoInstChallenge = mockChallengeA;
    const bitgoNitroChallenge = mockChallengeB;
    const serializedEntChallenge = mockChallengeC;
    const userPassword = 'password123';
    const encryptedXprv = bitgo.encrypt({
      password: userPassword,
      input: adminEcdhKey.xprv,
    });

    beforeEach(async function () {
      sinon.stub(bitgo, 'getSigningKeyForUser').resolves({
        userId: 'id',
        userEmail: 'user@bitgo.com',
        derivedPubkey: bip32.fromBase58(adminEcdhKey.xpub).derivePath(derivationPath).publicKey.toString('hex'),
        derivationPath: derivationPath,
        ecdhKeychain: 'my keychain',
      });

      sinon.stub(bitgo, 'getECDHKeychain').resolves({
        encryptedXprv: encryptedXprv,
      });
    });

    afterEach(async function () {
      sinon.restore();
    });

    it('should upload challenge without generating if passed in', async function () {
      const stubUploadChallenge = sinon.stub(ECDSAUtils.EcdsaUtils, 'uploadChallengesToEnterprise');
      const deserializedEntChallenge = EcdsaTypes.deserializeNtildeWithProofs(serializedEntChallenge);

      const signedEntChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        serializedEntChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );
      const signedInstChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        bitgoInstChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );
      const signedNitroChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        bitgoNitroChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );

      await ECDSAUtils.EcdsaUtils.initiateChallengesForEnterprise(
        bitgo,
        'ent_id',
        userPassword,
        signedInstChallenge,
        signedNitroChallenge,
        openSSLBytes,
        deserializedEntChallenge
      ).should.not.be.rejected();
      stubUploadChallenge.calledWith(
        bitgo,
        'ent_id',
        serializedEntChallenge,
        signedEntChallenge.toString('hex'),
        signedInstChallenge.toString('hex'),
        signedNitroChallenge.toString('hex')
      );
    });

    it('should generate a challenge and if one is not provided', async function () {
      const stubUploadChallenge = sinon.stub(ECDSAUtils.EcdsaUtils, 'uploadChallengesToEnterprise');
      const deserializedEntChallenge = EcdsaTypes.deserializeNtildeWithProofs(serializedEntChallenge);
      sinon.stub(EcdsaRangeProof, 'generateNtilde').resolves(deserializedEntChallenge);

      const signedEntChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        serializedEntChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );
      const signedInstChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        bitgoInstChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );
      const signedNitroChallenge = ECDSAUtils.EcdsaUtils.signChallenge(
        bitgoNitroChallenge,
        adminEcdhKey.xprv,
        derivationPath
      );

      await ECDSAUtils.EcdsaUtils.initiateChallengesForEnterprise(
        bitgo,
        'ent_id',
        userPassword,
        signedInstChallenge,
        signedNitroChallenge,
        openSSLBytes
      ).should.not.be.rejected();
      stubUploadChallenge.calledWith(
        bitgo,
        'ent_id',
        serializedEntChallenge,
        signedEntChallenge.toString('hex'),
        signedInstChallenge.toString('hex'),
        signedNitroChallenge.toString('hex')
      );
    });
  });

  it('getMessageToSignFromChallenge concatenates the challenge values only', function () {
    const challenge = mockChallengeA;
    const expectedMessageToSign = challenge.ntilde.concat(challenge.h1).concat(challenge.h2);
    const message = ECDSAUtils.EcdsaUtils.getMessageToSignFromChallenge(challenge);
    message.should.equal(expectedMessageToSign);
  });
  describe('validateCommonKeychainPublicKey', function () {
    it('validateCommonKeychainPublicKey returns correct public key', function () {
      const commonKeychain =
        '03f40c70545b519bb7bbc7195fd4b7d5bbfc873bfd38b18596e4b47a05b6a88d552e2e8319cb31e279b99dbe54115a983d35e86679af96d81b7478d1df368f76a8';
      const expectedPubKeyResult = `f40c70545b519bb7bbc7195fd4b7d5bbfc873bfd38b18596e4b47a05b6a88d556a10d6ab8055dc0b3a9af9dc4e42f4f9773c590afcc298d017c1b1ce29a88041`;
      const actualPubKey = ECDSAUtils.EcdsaUtils.validateCommonKeychainPublicKey(commonKeychain);
      actualPubKey.should.equal(expectedPubKeyResult);
    });
    it('validateCommonKeychainPublicKey throws correctly with invalid length', function () {
      const commonKeychain = '03f40c70548';
      should(() => ECDSAUtils.EcdsaUtils.validateCommonKeychainPublicKey(commonKeychain)).throwError(
        'Invalid commonKeychain length, expected 130, got 11'
      );
    });
    it('validateCommonKeychainPublicKey throws correctly with invalid commonKeychain', function () {
      const commonKeychainWithInvalidCharacters =
        '!@#$^0c70545b519bb7bbc7195fd4b7d5bfc873bfd38b18596e4b47a05b6a88d552e2e8319cb31e279b99dbe54115a983d35e86679af96d81b7478d1df368f76a8'; // 129 chars
      should(() =>
        ECDSAUtils.EcdsaUtils.validateCommonKeychainPublicKey(commonKeychainWithInvalidCharacters)
      ).throwError(/^Invalid commonKeychain, error:/);
    });
  });

  // #region Nock helpers
  async function createIncompleteBitgoHeldBackupKeyShare(
    userGpgKey: openpgp.SerializedKeyPair<string>,
    backupKeyShare: KeyShare,
    bitgoGpgKey: openpgp.SerializedKeyPair<string>
  ): Promise<BitgoHeldBackupKeyShare> {
    const nSharePromises = [
      encryptNShare(backupKeyShare, 1, userGpgKey.publicKey, userGpgKey, false),
      encryptNShare(backupKeyShare, 3, bitgoGpgKey.publicKey, userGpgKey, false),
    ];

    const backupToUserPublicShare = Buffer.concat([
      Buffer.from(backupKeyShare.nShares[1].y, 'hex'),
      Buffer.from(backupKeyShare.nShares[1].chaincode, 'hex'),
    ]).toString('hex');

    const backupToBitgoPublicShare = Buffer.concat([
      Buffer.from(backupKeyShare.nShares[3].y, 'hex'),
      Buffer.from(backupKeyShare.nShares[3].chaincode, 'hex'),
    ]).toString('hex');

    return {
      id: '4711',
      keyShares: [
        {
          from: 'backup',
          to: 'user',
          publicShare: backupToUserPublicShare,
          privateShare: (await nSharePromises[0]).encryptedPrivateShare,
        },
        {
          from: 'backup',
          to: 'bitgo',
          publicShare: backupToBitgoPublicShare,
          privateShare: (await nSharePromises[1]).encryptedPrivateShare,
        },
      ],
    };
  }

  async function nockGetBitgoPublicKeyBasedOnFeatureFlags(
    coin: string,
    enterpriseId: string,
    bitgoGpgKeyPair: openpgp.SerializedKeyPair<string>
  ): Promise<BitgoGPGPublicKey> {
    const bitgoGPGPublicKeyResponse: BitgoGPGPublicKey = {
      name: 'irrelevant',
      publicKey: bitgoGpgKeyPair.publicKey,
      enterpriseId,
    };
    nock(bgUrl).get(`/api/v2/${coin}/tss/pubkey`).query({ enterpriseId }).reply(200, bitgoGPGPublicKeyResponse);

    return bitgoGPGPublicKeyResponse;
  }

  async function nockCreateBitgoHeldBackupKeyShare(
    coin: string,
    enterpriseId: string,
    userGpgKey: openpgp.SerializedKeyPair<string>,
    backupKeyShare: KeyShare,
    bitgoGpgKey: openpgp.SerializedKeyPair<string>
  ): Promise<BitgoHeldBackupKeyShare> {
    const keyShare = await createIncompleteBitgoHeldBackupKeyShare(userGpgKey, backupKeyShare, bitgoGpgKey);

    nock(bgUrl)
      .post(
        `/api/v2/${coin}/krs/backupkeys`,
        _.matches({ enterprise: enterpriseId, userGPGPublicKey: userGpgKey.publicKey })
      )
      .reply(201, keyShare);

    return keyShare;
  }

  async function nockFinalizeBitgoHeldBackupKeyShare(
    coin: string,
    originalKeyShare: BitgoHeldBackupKeyShare,
    commonKeychain: string,
    userKeyShare: KeyShare,
    userLocalBackupGpgKey: openpgp.SerializedKeyPair<string>,
    bitgoKeychain: Keychain
  ): Promise<BitgoHeldBackupKeyShare> {
    const encryptedUserToBackupKeyShare = await encryptNShare(
      userKeyShare,
      2,
      userLocalBackupGpgKey.publicKey,
      userGpgKey,
      false
    );

    assert(bitgoKeychain.keyShares);
    const bitgoToBackupKeyShare = bitgoKeychain.keyShares.find(
      (keyShare) => keyShare.from === 'bitgo' && keyShare.to === 'backup'
    );
    assert(bitgoToBackupKeyShare);

    const userPublicShare = Buffer.concat([
      Buffer.from(userKeyShare.nShares[2].y, 'hex'),
      Buffer.from(userKeyShare.nShares[2].chaincode, 'hex'),
    ]).toString('hex');

    const expectedKeyShares = [
      {
        from: 'user',
        to: 'backup',
        publicShare: userPublicShare,
        // Omitting the private share, the actual encryption happens inside the function where we make the matching call
        // to this nock. We cannot recreate the same encrypted value here because gpg encryption is not deterministic
      },
      bitgoToBackupKeyShare,
    ];

    const updatedKeyShare: BitgoHeldBackupKeyShare = {
      id: originalKeyShare.id,
      commonKeychain,
      keyShares: [
        ...originalKeyShare.keyShares,
        {
          from: 'user',
          to: 'backup',
          publicShare: userPublicShare,
          privateShare: encryptedUserToBackupKeyShare.encryptedPrivateShare,
        },
        bitgoToBackupKeyShare,
      ],
    };

    nock(bgUrl)
      .put(
        `/api/v2/${coin}/krs/backupkeys/${originalKeyShare.id}`,
        _.matches({ commonKeychain, keyShares: expectedKeyShares })
      )
      .reply(200, updatedKeyShare);

    return updatedKeyShare;
  }

  /**
   * Helper function to generate a bitgo keychain given the full set of keyshares and GPG keys.
   * Also mocks the wallet signatures added by the HSM.
   * @param params
   */
  async function generateBitgoKeychain(params: {
    coin: string;
    userKeyShare: KeyShare;
    backupKeyShare: KeyShare;
    bitgoKeyShare: KeyShare;
    userGpgKey: openpgp.SerializedKeyPair<string>;
    userLocalBackupGpgKey: openpgp.SerializedKeyPair<string>;
    bitgoGpgKey: openpgp.SerializedKeyPair<string>;
  }): Promise<Keychain> {
    const bitgoCombined = MPC.keyCombine(params.bitgoKeyShare.pShare, [
      params.userKeyShare.nShares[3],
      params.backupKeyShare.nShares[3],
    ]);
    const userGpgKeyActual = await openpgp.readKey({ armoredKey: params.userGpgKey.publicKey });
    const backupGpgKeyActual = await openpgp.readKey({ armoredKey: params.userLocalBackupGpgKey.publicKey });

    const nSharePromises = [
      encryptNShare(params.bitgoKeyShare, 1, params.userGpgKey.publicKey, params.userGpgKey, false),
      encryptNShare(
        params.bitgoKeyShare,
        2,
        params.userLocalBackupGpgKey.publicKey,
        params.userLocalBackupGpgKey,
        false
      ),
    ];
    const [userToBitgoShare, backupToBitgoShare] = await Promise.all(nSharePromises);
    const bitgoKeychain: Keychain = {
      id: '3',
      pub: '',
      commonKeychain: bitgoCombined.xShare.y + bitgoCombined.xShare.chaincode,
      keyShares: [
        {
          from: 'bitgo',
          to: 'user',
          publicShare: userToBitgoShare.publicShare,
          privateShare: userToBitgoShare.encryptedPrivateShare,
          n: userToBitgoShare.n,
          vssProof: userToBitgoShare.vssProof,
          privateShareProof: userToBitgoShare.privateShareProof,
        },
        {
          from: 'bitgo',
          to: 'backup',
          publicShare: backupToBitgoShare.publicShare,
          privateShare: backupToBitgoShare.encryptedPrivateShare,
          n: backupToBitgoShare.n,
          vssProof: backupToBitgoShare.vssProof,
          privateShareProof: backupToBitgoShare.privateShareProof,
        },
      ],
      type: 'tss',
    };

    const userKeyId = userGpgKeyActual.keyPacket.getFingerprint();
    const backupKeyId = backupGpgKeyActual.keyPacket.getFingerprint();
    const bitgoToUserPublicU =
      Buffer.from(
        ecc.pointFromScalar(Buffer.from(params.bitgoKeyShare.nShares[1].u, 'hex'), true) as Uint8Array
      ).toString('hex') + params.bitgoKeyShare.nShares[1].chaincode;
    const bitgoToBackupPublicU =
      Buffer.from(
        ecc.pointFromScalar(Buffer.from(params.bitgoKeyShare.nShares[2].u, 'hex'), true) as Uint8Array
      ).toString('hex') + params.bitgoKeyShare.nShares[2].chaincode;

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

    return bitgoKeychain;
  }

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

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

    return bitgoKeychain;
  }

  async function nockKeychain(params: {
    coin: string;
    keyChain: Keychain;
    source: 'user' | 'backup';
  }): Promise<Keychain> {
    nock('https://bitgo.fakeurl')
      .persist()
      .post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: params.source }))
      .reply(200, params.keyChain);

    return params.keyChain;
  }
  // #endregion Nock helpers
});

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


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