PHP WebShell
Текущая директория: /opt/BitGoJS/modules/bitgo/test/v2/unit/coins/utxo/recovery
Просмотр файла: backupKeyRecovery.ts
/**
* @prettier
*/
import 'should';
import * as mocha from 'mocha';
import * as sinon from 'sinon';
import * as nock from 'nock';
import { BIP32Interface } from '@bitgo/utxo-lib';
import * as utxolib from '@bitgo/utxo-lib';
const { toOutput, outputScripts } = utxolib.bitgo;
type WalletUnspent = utxolib.bitgo.WalletUnspent<bigint>;
type RootWalletKeys = utxolib.bitgo.RootWalletKeys;
type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3;
import { Config } from '../../../../../../src/config';
import {
AbstractUtxoCoin,
backupKeyRecovery,
BackupKeyRecoveryTransansaction,
CoingeckoApi,
FormattedOfflineVaultTxInfo,
} from '@bitgo/abstract-utxo';
import {
defaultBitGo,
encryptKeychain,
getDefaultWalletKeys,
getFixture,
getWalletAddress,
getWalletKeys,
keychains,
shouldEqualJSON,
toKeychainBase58,
utxoCoins,
} from '../util';
import { MockRecoveryProvider } from './mock';
import { krsProviders, Triple } from '@bitgo/sdk-core';
const config = { krsProviders };
nock.disableNetConnect();
function configOverride(f: (config: Config) => void) {
const backup = { ...krsProviders };
before(function () {
f(config);
});
after(function () {
Object.entries(backup).forEach(([k, v]) => {
config[k] = v;
});
});
}
const walletPassphrase = 'lol';
type NamedKeys = {
userKey: string;
backupKey: string;
bitgoKey: string;
};
function getNamedKeys([userKey, backupKey, bitgoKey]: Triple<BIP32Interface>, password: string): NamedKeys {
function encode(k: BIP32Interface): string {
return k.isNeutered() ? k.toBase58() : encryptKeychain(password, toKeychainBase58(k));
}
return {
userKey: encode(userKey),
backupKey: encode(backupKey),
bitgoKey: encode(bitgoKey),
};
}
function getKeysForUnsignedSweep([userKey, backupKey, bitgoKey]: Triple<BIP32Interface>, password: string): NamedKeys {
return getNamedKeys([userKey.neutered(), backupKey.neutered(), bitgoKey.neutered()], password);
}
function getKeysForKeyRecoveryService(
[userKey, backupKey, bitgoKey]: Triple<BIP32Interface>,
password: string
): NamedKeys {
return getNamedKeys([userKey, backupKey.neutered(), bitgoKey.neutered()], password);
}
function getKeysForFullSignedRecovery(
[userKey, backupKey, bitgoKey]: Triple<BIP32Interface>,
password: string
): NamedKeys {
return getNamedKeys([userKey, backupKey, bitgoKey.neutered()], password);
}
function getScriptTypes2Of3() {
return outputScripts.scriptTypes2Of3;
}
function run(
coin: AbstractUtxoCoin,
scriptType: ScriptType2Of3,
walletKeys: RootWalletKeys,
params: {
keys: NamedKeys;
userKeyPath?: string;
krsProvider?: string;
hasUserSignature: boolean;
hasBackupSignature: boolean;
hasKrsOutput?: boolean;
feeRate?: number;
},
tags: string[] = []
) {
if (!coin.supportsAddressType(scriptType)) {
return;
}
describe(`Backup Key Recovery [${[coin.getChain(), ...tags, params.krsProvider].join(',')}]`, function () {
const externalWallet = getWalletKeys('external');
const recoveryDestination = getWalletAddress(coin.network, externalWallet);
let keyRecoveryServiceAddress: string;
let recovery: (BackupKeyRecoveryTransansaction | FormattedOfflineVaultTxInfo) & { txid?: string };
let recoveryTx: utxolib.bitgo.UtxoTransaction<number | bigint> | utxolib.bitgo.UtxoPsbt;
// 1e8 * 9e7 < 9.007e15 but 2e8 * 9e7 > 9.007e15 to test both code paths in queryBlockchainUnspentsPath
const valueMul = coin.amountType === 'bigint' ? BigInt(9e7) : BigInt(1);
const allUnspents = [
utxolib.testutil.toUnspent({ scriptType, value: BigInt(1e8) * valueMul }, 0, coin.network, walletKeys),
utxolib.testutil.toUnspent({ scriptType, value: BigInt(2e8) * valueMul }, 2, coin.network, walletKeys),
utxolib.testutil.toUnspent({ scriptType, value: BigInt(3e8) * valueMul }, 3, coin.network, walletKeys),
// this unspent will not be picked up due to the index gap
utxolib.testutil.toUnspent({ scriptType, value: BigInt(23e8) }, 23, coin.network, walletKeys),
];
const recoverUnspents = allUnspents.slice(0, -1);
// If the coin is bch, convert the mocked unspent address to cashaddr format since that is the format that blockchair
// returns on the /dashboards/addresses response
const mockedApiUnspents =
coin.getChain() === 'bch' || coin.getChain() === 'bcha'
? recoverUnspents.map((u) => ({ ...u, address: coin.canonicalAddress(u.address, 'cashaddr').split(':')[1] }))
: recoverUnspents;
before('mock', function () {
sinon.stub(CoingeckoApi.prototype, 'getUSDPrice').resolves(69_420);
});
configOverride(function (config: Config) {
const configKrsProviders = { ...config.krsProviders };
configKrsProviders.dai.supportedCoins = [coin.getFamily()];
configKrsProviders.keyternal.supportedCoins = [coin.getFamily()];
keyRecoveryServiceAddress = getWalletAddress(coin.network, externalWallet, 0, 100);
configKrsProviders.keyternal.feeAddresses = { [coin.getChain()]: keyRecoveryServiceAddress };
config.krsProviders = configKrsProviders;
});
after(function () {
sinon.restore();
});
before('create recovery data', async function () {
recovery = await backupKeyRecovery(coin, defaultBitGo, {
walletPassphrase,
recoveryDestination,
scan: 5,
ignoreAddressTypes: [],
userKeyPath: params.userKeyPath,
krsProvider: params.krsProvider,
...params.keys,
recoveryProvider: new MockRecoveryProvider(mockedApiUnspents),
});
const txHex =
(recovery as BackupKeyRecoveryTransansaction).transactionHex ?? (recovery as FormattedOfflineVaultTxInfo).txHex;
const isPsbt = utxolib.bitgo.isPsbt(txHex);
recoveryTx = isPsbt
? utxolib.bitgo.createPsbtFromHex(txHex, coin.network)
: utxolib.bitgo.createTransactionFromHex(txHex as string, coin.network, coin.amountType);
recovery.txid =
recoveryTx instanceof utxolib.bitgo.UtxoPsbt ? recoveryTx.getUnsignedTx().getId() : recoveryTx.getId();
});
it('matches fixture', async function () {
shouldEqualJSON(
recovery,
await getFixture(
coin,
`recovery/backupKeyRecovery-${(params.krsProvider ? tags.concat([params.krsProvider]) : tags).join('-')}`,
recovery
)
);
});
it('has expected input count', function () {
(recoveryTx instanceof utxolib.bitgo.UtxoPsbt ? recoveryTx.data.inputs : recoveryTx.ins).length.should.eql(
recoverUnspents.length
);
});
function checkInputsSignedBy(
tx: utxolib.bitgo.UtxoTransaction<number | bigint> | utxolib.bitgo.UtxoPsbt,
rootKey: BIP32Interface,
expectCount: number
) {
if (tx instanceof utxolib.bitgo.UtxoPsbt) {
function validate(tx: utxolib.bitgo.UtxoPsbt, inputIndex: number) {
try {
return tx.validateSignaturesOfInputHD(inputIndex, rootKey);
} catch (e) {
if (e.message === 'No signatures to validate') {
return false;
}
throw e;
}
}
tx.data.inputs.forEach((input, inputIndex) => {
validate(tx, inputIndex).should.eql(!!expectCount);
});
} else {
const prevOutputs = recoverUnspents
.map((u) => toOutput(u, coin.network))
.map((v) => ({ ...v, value: utxolib.bitgo.toTNumber(v.value, coin.amountType) }));
tx.ins.forEach((input, inputIndex) => {
const unspent = recoverUnspents[inputIndex] as WalletUnspent;
const { publicKey } = rootKey.derivePath(walletKeys.getDerivationPath(rootKey, unspent.chain, unspent.index));
const signatures = utxolib.bitgo
.getSignatureVerifications(
tx,
inputIndex,
utxolib.bitgo.toTNumber(unspent.value, coin.amountType),
{ publicKey },
prevOutputs
)
.filter((s) => s.signedBy !== undefined);
signatures.length.should.eql(expectCount);
});
}
}
it((params.hasUserSignature ? 'has' : 'has no') + ' user signature', function () {
checkInputsSignedBy(recoveryTx, walletKeys.user, params.hasUserSignature ? 1 : 0);
});
it((params.hasBackupSignature ? 'has' : 'has no') + ' backup signature', function () {
checkInputsSignedBy(recoveryTx, walletKeys.backup, params.hasBackupSignature ? 1 : 0);
});
if (params.hasUserSignature && params.hasBackupSignature) {
it('has no placeholder signatures', function (this: mocha.Context) {
if (recoveryTx instanceof utxolib.bitgo.UtxoTransaction) {
recoveryTx.ins.forEach((input) => {
const parsed = utxolib.bitgo.parseSignatureScript(input);
switch (parsed.scriptType) {
case 'p2sh':
case 'p2shP2wsh':
case 'p2wsh':
case 'taprootScriptPathSpend':
parsed.signatures.forEach((signature, i) => {
if (utxolib.bitgo.isPlaceholderSignature(signature)) {
throw new Error(`placeholder signature at index ${i}`);
}
});
break;
default:
throw new Error(`unexpected scriptType ${scriptType}`);
}
});
} else {
this.skip();
}
});
}
it((params.hasKrsOutput ? 'has' : 'has no') + ' key recovery service output', function () {
const outs = recoveryTx instanceof utxolib.bitgo.UtxoPsbt ? recoveryTx.getUnsignedTx().outs : recoveryTx.outs;
outs.length.should.eql(1);
const outputAddresses = outs.map((o) => utxolib.address.fromOutputScript(o.script, recoveryTx.network));
outputAddresses
.includes(keyRecoveryServiceAddress)
.should.eql(!!params.hasKrsOutput && params.krsProvider === 'keyternal');
outputAddresses.includes(recoveryDestination).should.eql(true);
});
});
}
utxoCoins.forEach((coin) => {
const walletKeys = getDefaultWalletKeys();
getScriptTypes2Of3().forEach((scriptType) => {
run(
coin,
scriptType,
walletKeys,
{
keys: getKeysForUnsignedSweep(walletKeys.triple, walletPassphrase),
hasUserSignature: false,
hasBackupSignature: false,
},
[scriptType, 'unsignedRecovery']
);
['dai', 'keyternal'].forEach((krsProvider) => {
if (krsProvider === 'keyternal' && !['p2sh', 'p2wsh', 'p2shP2wsh'].includes(scriptType)) {
return;
}
run(
coin,
scriptType,
walletKeys,
{
keys: getKeysForKeyRecoveryService(walletKeys.triple, walletPassphrase),
krsProvider: krsProvider,
hasUserSignature: true,
hasBackupSignature: false,
hasKrsOutput: false,
},
[scriptType, 'keyRecoveryService']
);
});
run(
coin,
scriptType,
walletKeys,
{
keys: getKeysForFullSignedRecovery(walletKeys.triple, walletPassphrase),
hasUserSignature: true,
hasBackupSignature: true,
},
[scriptType, 'fullSignedRecovery']
);
run(
coin,
scriptType,
walletKeys,
{
keys: getKeysForFullSignedRecovery(walletKeys.triple, walletPassphrase),
hasUserSignature: true,
hasBackupSignature: true,
feeRate: 2,
},
[scriptType, 'fullSignedRecovery', 'fixedFeeRate']
);
{
const userKeyPath = '99/99';
const exoticWalletKeys = new utxolib.bitgo.RootWalletKeys(keychains, [
userKeyPath,
utxolib.bitgo.RootWalletKeys.defaultPrefix,
utxolib.bitgo.RootWalletKeys.defaultPrefix,
]);
run(
coin,
scriptType,
exoticWalletKeys,
{
keys: getKeysForFullSignedRecovery(exoticWalletKeys.triple, walletPassphrase),
userKeyPath,
hasUserSignature: true,
hasBackupSignature: true,
},
[scriptType, 'fullSignedRecovery', 'customUserKeyPath']
);
}
});
});
Выполнить команду
Для локальной разработки. Не используйте в интернете!