PHP WebShell
Текущая директория: /opt/BitGoJS/modules/utxo-lib/test/bitgo/psbt
Просмотр файла: Psbt.ts
import * as assert from 'assert';
import { Network, getNetworkName, networks, getNetworkList, testutil, isMainnet, Transaction } from '../../../src';
import {
getExternalChainCode,
outputScripts,
KeyName,
UtxoPsbt,
ZcashPsbt,
createPsbtFromHex,
parsePsbtInput,
toWalletPsbt,
createPsbtForNetwork,
addReplayProtectionUnspentToPsbt,
addWalletOutputToPsbt,
getInternalChainCode,
UtxoTransaction,
isTransactionWithKeyPathSpendInput,
isPsbt,
psbtIncludesUnspentAtIndex,
updateWalletUnspentForPsbt,
createPsbtFromTransaction,
toPrevOutput,
updateReplayProtectionUnspentToPsbt,
Unspent,
isWalletUnspent,
updateWalletOutputForPsbt,
extractP2msOnlyHalfSignedTx,
toOutput,
createTransactionBuilderFromTransaction,
addXpubsToPsbt,
clonePsbtWithoutNonWitnessUtxo,
deleteWitnessUtxoForNonSegwitInputs,
getPsbtInputScriptType,
withUnsafeNonSegwit,
getTransactionAmountsFromPsbt,
WalletUnspent,
getDefaultSigHash,
isPsbtLite,
} from '../../../src/bitgo';
import {
createOutputScript2of3,
createOutputScriptP2shP2pk,
isSupportedScriptType,
ScriptType2Of3,
ScriptTypeP2shP2pk,
scriptTypes2Of3,
} from '../../../src/bitgo/outputScripts';
import {
getDefaultWalletKeys,
Input,
inputScriptTypes,
mockReplayProtectionUnspent,
Output,
outputScriptTypes,
replayProtectionKeyPair,
signAllTxnInputs,
} from '../../../src/testutil';
import { defaultTestOutputAmount } from '../../transaction_util';
import {
assertEqualTransactions,
constructTransactionUsingTxBuilder,
signPsbt,
toBigInt,
validatePsbtParsing,
} from './psbtUtil';
import { mockUnspents } from '../../../src/testutil';
import { constructPsbt } from './Musig2Util';
const CHANGE_INDEX = 100;
const FEE = BigInt(100);
export type AmountType = 'number' | 'bigint';
export type InputType = outputScripts.ScriptType2Of3;
export type SignatureTargetType = 'unsigned' | 'halfsigned' | 'fullsigned';
const network = networks.bitcoin;
const rootWalletKeys = getDefaultWalletKeys();
function getScriptTypes2Of3() {
// FIXME(BG-66941): p2trMusig2 signing does not work in this test suite yet
// because the test suite is written with TransactionBuilder
return outputScripts.scriptTypes2Of3.filter((scriptType) => scriptType !== 'p2trMusig2');
}
const halfSignedInputs = (['p2sh', 'p2wsh', 'p2shP2wsh'] as const).map((scriptType) => ({
scriptType,
value: BigInt(1000),
}));
const halfSignedOutputs = outputScriptTypes.map((scriptType) => ({ scriptType, value: BigInt(500) }));
const psbtInputs = inputScriptTypes.map((scriptType) => ({ scriptType, value: BigInt(1000) }));
const psbtOutputs = outputScriptTypes.map((scriptType) => ({ scriptType, value: BigInt(900) }));
describe('Psbt Misc', function () {
function getTestPsbt() {
return testutil.constructPsbt(
[{ scriptType: 'p2tr', value: BigInt(1000) }],
[{ scriptType: 'p2sh', value: BigInt(900) }],
network,
rootWalletKeys,
'fullsigned'
);
}
it('fail to finalise p2tr sighash mismatch', function () {
const psbt = getTestPsbt();
assert(psbt.validateSignaturesOfAllInputs());
const tapScriptSig = psbt.data.inputs[0].tapScriptSig;
assert(tapScriptSig);
tapScriptSig[0].signature = Buffer.concat([tapScriptSig[0].signature, Buffer.of(Transaction.SIGHASH_ALL)]);
assert.throws(
() => psbt.finalizeAllInputs(),
(e: any) => e.message === 'signature sighash does not match input sighash type'
);
});
describe('isPsbtLite', function () {
it('no inputs', function () {
const psbt = testutil.constructPsbt([], [], network, rootWalletKeys, 'unsigned');
assert.strictEqual(isPsbtLite(psbt), false);
});
it('all inputs are segwit', function () {
const psbt = testutil.constructPsbt(
psbtInputs.filter((s) => s.scriptType !== 'p2sh' && s.scriptType !== 'p2shP2pk'),
psbtOutputs,
network,
rootWalletKeys,
'unsigned'
);
assert.strictEqual(isPsbtLite(psbt), false);
});
it('some inputs are non-segwit', function () {
const psbt = testutil.constructPsbt(psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned');
assert.strictEqual(isPsbtLite(psbt), false);
});
it('should be true if after clonePsbtWithoutNonWitnessUtxo', function () {
const psbt = testutil.constructPsbt(psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned');
const clonedPsbt = clonePsbtWithoutNonWitnessUtxo(psbt);
assert.strictEqual(isPsbtLite(clonedPsbt), true);
});
});
});
describe('extractP2msOnlyHalfSignedTx failure', function () {
it('invalid signature count', function () {
const psbt = testutil.constructPsbt(halfSignedInputs, halfSignedOutputs, network, rootWalletKeys, 'unsigned');
assert.throws(
() => extractP2msOnlyHalfSignedTx(psbt),
(e: any) => e.message === 'unexpected signature count undefined'
);
});
it('empty inputs', function () {
const psbt = testutil.constructPsbt([], [], network, rootWalletKeys, 'unsigned');
assert.throws(
() => extractP2msOnlyHalfSignedTx(psbt),
(e: any) => e.message === 'empty inputs or outputs'
);
});
it('unsupported script type', function () {
const psbt = testutil.constructPsbt(
[{ scriptType: 'p2tr', value: BigInt(1000) }],
[{ scriptType: 'p2sh', value: BigInt(900) }],
network,
rootWalletKeys,
'halfsigned'
);
assert.throws(
() => extractP2msOnlyHalfSignedTx(psbt),
(e: any) => e.message === 'unsupported script type taprootScriptPathSpend'
);
});
});
function runExtractP2msOnlyHalfSignedTxTest(network: Network, inputs: Input[], outputs: Output[]) {
const coin = getNetworkName(network);
describe(`extractP2msOnlyHalfSignedTx success for ${coin}`, function () {
it(`success for ${coin}`, function () {
const signers: { signerName: KeyName; cosignerName: KeyName } = { signerName: 'user', cosignerName: 'backup' };
const txnOutputs = outputs;
const txnInputs = inputs
.map((v) =>
v.scriptType === 'p2sh' || v.scriptType === 'p2shP2wsh' || v.scriptType === 'p2wsh'
? {
scriptType: v.scriptType,
value: v.value,
}
: undefined
)
.filter((v) => !!v) as testutil.TxnInput<bigint>[];
const psbt = testutil.constructPsbt(inputs, outputs, network, rootWalletKeys, 'halfsigned', { signers });
const halfSignedPsbtTx = extractP2msOnlyHalfSignedTx(psbt);
let txb = testutil.constructTxnBuilder(txnInputs, txnOutputs, network, rootWalletKeys, 'halfsigned', signers);
const halfSignedTxbTx = txb.buildIncomplete();
const unspents = toBigInt(inputs.map((input, i) => testutil.toUnspent(input, i, network, rootWalletKeys)));
assertEqualTransactions(halfSignedPsbtTx, halfSignedTxbTx);
validatePsbtParsing(halfSignedPsbtTx, psbt, unspents, 'halfsigned');
validatePsbtParsing(halfSignedTxbTx, psbt, unspents, 'halfsigned');
testutil.signAllPsbtInputs(psbt, inputs, rootWalletKeys, 'fullsigned', { signers });
const fullySignedPsbt = psbt.clone();
const psbtTx = psbt.finalizeAllInputs().extractTransaction();
const txnUnspents = txnInputs.map((v, i) => testutil.toTxnUnspent(v, i, network, rootWalletKeys));
const prevOutputs = txnUnspents.map((u) => toOutput(u, network));
txb = createTransactionBuilderFromTransaction<bigint>(halfSignedTxbTx, prevOutputs);
signAllTxnInputs(txb, txnInputs, rootWalletKeys, 'fullsigned', signers);
const txbTx = txb.build();
assertEqualTransactions(psbtTx, txbTx);
validatePsbtParsing(psbtTx, fullySignedPsbt, unspents, 'fullsigned');
validatePsbtParsing(txbTx, fullySignedPsbt, unspents, 'fullsigned');
});
});
}
function runBuildSignSendFlowTest(
network: Network,
inputs: Input[],
outputs: Output[],
{ skipNonWitnessUtxo = false } = {}
) {
const coin = getNetworkName(network);
function assertValidate(psbt: UtxoPsbt) {
psbt.data.inputs.forEach((input, i) => {
assert.ok(psbt.validateSignaturesOfInputHD(i, rootWalletKeys['user']));
if (getPsbtInputScriptType(input) !== 'p2shP2pk') {
assert.ok(psbt.validateSignaturesOfInputHD(i, rootWalletKeys['bitgo']));
}
});
assert.ok(psbt.validateSignaturesOfAllInputs());
}
describe(`Build, sign & send flow for ${coin}`, function () {
/**
* Skip adding nonWitnessUtxos to psbts
* ------------------------------------
* In the instance that we want to doing a bulk sweep, for network and client performance reasons we are substituting
* the nonWitnessUtxo for p2sh and p2shP2pk inputs with a witnessUtxo. We need the witnessUtxo so that we can half
* sign the transaction locally with the user key. When we send the half signed to BitGo, the PSBT will be properly
* populated such that the non-segwit inputs have the nonWitnessUtxo. This means when we send it to BitGo we should
* remove the witnessUtxo so that it just has the partialSig and redeemScript.
*/
it(`success for ${coin}${skipNonWitnessUtxo ? ' without nonWitnessUtxo for p2sh' : ''}`, function () {
const parentPsbt = testutil.constructPsbt(inputs, outputs, network, rootWalletKeys, 'unsigned', {
signers: {
signerName: 'user',
cosignerName: 'bitgo',
},
});
let psbt = skipNonWitnessUtxo ? clonePsbtWithoutNonWitnessUtxo(parentPsbt) : parentPsbt;
addXpubsToPsbt(psbt, rootWalletKeys);
psbt.setAllInputsMusig2NonceHD(rootWalletKeys['user']);
let psbtWithoutPrevTx = clonePsbtWithoutNonWitnessUtxo(psbt);
let hex = psbtWithoutPrevTx.toHex();
let psbtAtHsm = createPsbtFromHex(hex, network);
psbtAtHsm.setAllInputsMusig2NonceHD(rootWalletKeys['bitgo'], { deterministic: true });
let hexAtHsm = psbtAtHsm.toHex();
let psbtFromHsm = createPsbtFromHex(hexAtHsm, network);
deleteWitnessUtxoForNonSegwitInputs(psbtFromHsm);
psbt.combine(psbtFromHsm);
testutil.signAllPsbtInputs(psbt, inputs, rootWalletKeys, 'halfsigned', {
signers: {
signerName: 'user',
cosignerName: 'bitgo',
},
skipNonWitnessUtxo,
});
psbtWithoutPrevTx = clonePsbtWithoutNonWitnessUtxo(psbt);
hex = psbtWithoutPrevTx.toHex();
psbtAtHsm = createPsbtFromHex(hex, network);
withUnsafeNonSegwit(psbtAtHsm, () => {
testutil.signAllPsbtInputs(psbtAtHsm, inputs, rootWalletKeys, 'fullsigned', {
signers: {
signerName: 'user',
cosignerName: 'bitgo',
},
deterministic: true,
});
});
withUnsafeNonSegwit(psbtAtHsm, () => {
assertValidate(psbtAtHsm);
});
hexAtHsm = psbtAtHsm.toHex();
psbtFromHsm = createPsbtFromHex(hexAtHsm, network);
deleteWitnessUtxoForNonSegwitInputs(psbtFromHsm);
if (skipNonWitnessUtxo) {
psbt = parentPsbt;
}
psbt.combine(psbtFromHsm);
assertValidate(psbt);
assert.doesNotThrow(() => psbt.finalizeAllInputs().extractTransaction());
});
});
}
function runBuildPsbtWithSDK(network: Network, inputs: Input[], outputs: Output[]) {
const coin = getNetworkName(network);
it(`check that building a PSBT while skipping nonWitnessUtxo works - ${coin}`, async function () {
const psbtWithNonWitness = testutil.constructPsbt(inputs, outputs, network, rootWalletKeys, 'unsigned', {
signers: {
signerName: 'user',
cosignerName: 'bitgo',
},
});
const psbtWithoutNonWitness = testutil.constructPsbt(inputs, outputs, network, rootWalletKeys, 'unsigned', {
signers: {
signerName: 'user',
cosignerName: 'bitgo',
},
skipNonWitnessUtxo: true,
});
const clonedPsbt = clonePsbtWithoutNonWitnessUtxo(psbtWithNonWitness);
assert.deepStrictEqual(psbtWithoutNonWitness.toHex(), clonedPsbt.toHex());
});
}
getNetworkList()
.filter((v) => isMainnet(v) && v !== networks.bitcoinsv)
.forEach((network) => {
runExtractP2msOnlyHalfSignedTxTest(
network,
halfSignedInputs.filter((input) => isSupportedScriptType(network, input.scriptType)),
halfSignedOutputs.filter((output) => isSupportedScriptType(network, output.scriptType))
);
const supportedPsbtInputs = psbtInputs.filter((input) =>
isSupportedScriptType(network, input.scriptType === 'taprootKeyPathSpend' ? 'p2trMusig2' : input.scriptType)
);
const supportedPsbtOutputs = psbtOutputs.filter((output) => isSupportedScriptType(network, output.scriptType));
[false, true].forEach((skipNonWitnessUtxo) =>
runBuildSignSendFlowTest(network, supportedPsbtInputs, supportedPsbtOutputs, { skipNonWitnessUtxo })
);
runBuildPsbtWithSDK(network, supportedPsbtInputs, supportedPsbtOutputs);
});
describe('isTransactionWithKeyPathSpendInput', function () {
describe('transaction input', function () {
it('empty inputs', function () {
const tx = testutil.constructTxnBuilder([], [], network, rootWalletKeys, 'unsigned').buildIncomplete();
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx), false);
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx.ins), false);
});
it('taprootKeyPath inputs successfully triggers', function () {
const psbt = testutil.constructPsbt(
[
{ scriptType: 'taprootKeyPathSpend', value: BigInt(1e8) },
{ scriptType: 'p2sh', value: BigInt(1e8) },
],
[{ scriptType: 'p2sh', value: BigInt(2e8 - 10000) }],
network,
rootWalletKeys,
'fullsigned'
);
assert(psbt.validateSignaturesOfAllInputs());
psbt.finalizeAllInputs();
const tx = psbt.extractTransaction() as UtxoTransaction<bigint>;
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx), true);
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx.ins), true);
});
it('no taprootKeyPath inputs successfully does not trigger', function () {
const psbt = testutil.constructPsbt(
[
{ scriptType: 'p2trMusig2', value: BigInt(1e8) },
{ scriptType: 'p2sh', value: BigInt(1e8) },
],
[{ scriptType: 'p2sh', value: BigInt(2e8 - 10000) }],
network,
rootWalletKeys,
'fullsigned'
);
assert(psbt.validateSignaturesOfAllInputs());
psbt.finalizeAllInputs();
const tx = psbt.extractTransaction();
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx), false);
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx.ins), false);
});
it('unsigned inputs successfully fail', function () {
const psbt = testutil.constructPsbt(
[
{ scriptType: 'p2wsh', value: BigInt(1e8) },
{ scriptType: 'p2sh', value: BigInt(1e8) },
],
[{ scriptType: 'p2sh', value: BigInt(2e8 - 10000) }],
network,
rootWalletKeys,
'unsigned'
);
const tx = psbt.getUnsignedTx();
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx), false);
assert.strictEqual(isTransactionWithKeyPathSpendInput(tx.ins), false);
});
});
describe('psbt input', function () {
it('empty inputs', function () {
const psbt = testutil.constructPsbt([], [], network, rootWalletKeys, 'unsigned');
assert.strictEqual(isTransactionWithKeyPathSpendInput(psbt), false);
assert.strictEqual(isTransactionWithKeyPathSpendInput(psbt.data.inputs), false);
});
it('psbt with taprootKeyPathInputs successfully triggers', function () {
const psbt = testutil.constructPsbt(
[
{ scriptType: 'taprootKeyPathSpend', value: BigInt(1e8) },
{ scriptType: 'p2sh', value: BigInt(1e8) },
],
[{ scriptType: 'p2sh', value: BigInt(2e8 - 10000) }],
network,
rootWalletKeys,
'unsigned'
);
assert.strictEqual(isTransactionWithKeyPathSpendInput(psbt), true);
assert.strictEqual(isTransactionWithKeyPathSpendInput(psbt.data.inputs), true);
});
it('psbt without taprootKeyPathInputs successfully does not trigger', function () {
const psbt = testutil.constructPsbt(
[
{ scriptType: 'p2wsh', value: BigInt(1e8) },
{ scriptType: 'p2sh', value: BigInt(1e8) },
],
[{ scriptType: 'p2sh', value: BigInt(2e8 - 10000) }],
network,
rootWalletKeys,
'halfsigned'
);
assert.strictEqual(isTransactionWithKeyPathSpendInput(psbt), false);
assert.strictEqual(isTransactionWithKeyPathSpendInput(psbt.data.inputs), false);
});
});
});
describe('Parse PSBT', function () {
it('p2shP2pk parsing', function () {
const signer = rootWalletKeys['user'];
const psbt = createPsbtForNetwork({ network: networks.bitcoincash });
const unspent = mockReplayProtectionUnspent(networks.bitcoincash, BigInt(1e8), { key: signer });
const { redeemScript } = createOutputScriptP2shP2pk(signer.publicKey);
assert(redeemScript);
addReplayProtectionUnspentToPsbt(psbt, unspent, redeemScript);
addWalletOutputToPsbt(psbt, rootWalletKeys, getInternalChainCode('p2sh'), 0, BigInt(1e8 - 10000));
const input = psbt.data.inputs[0];
let parsed = parsePsbtInput(input);
assert.strictEqual(parsed.scriptType, 'p2shP2pk');
assert.strictEqual(parsed.signatures, undefined);
assert.strictEqual(parsed.publicKeys.length, 1);
assert.ok(parsed.publicKeys[0].length === 33);
assert.ok(parsed.pubScript.equals(redeemScript));
psbt.signAllInputs(signer);
assert.ok(psbt.validateSignaturesOfAllInputs());
parsed = parsePsbtInput(input);
assert.strictEqual(parsed.scriptType, 'p2shP2pk');
assert.strictEqual(parsed.signatures?.length, 1);
assert.strictEqual(parsed.publicKeys.length, 1);
assert.ok(parsed.publicKeys[0].length === 33);
assert.ok(parsed.pubScript.equals(redeemScript));
const sighash: number = parsed.signatures[0][parsed.signatures[0].length - 1];
assert.strictEqual(sighash, getDefaultSigHash(psbt.network));
});
it('fail to parse finalized psbt', function () {
const unspents = mockUnspents(
rootWalletKeys,
getScriptTypes2Of3().map((inputType) => inputType),
BigInt('10000000000000000'),
network
);
const txBuilderParams = {
signer: 'user',
cosigner: 'bitgo',
amountType: 'bigint',
outputType: 'p2sh',
signatureTarget: 'fullsigned',
network,
changeIndex: CHANGE_INDEX,
fee: FEE,
} as const;
const tx = constructTransactionUsingTxBuilder(unspents, rootWalletKeys, txBuilderParams);
const psbt = toWalletPsbt(tx, toBigInt(unspents), rootWalletKeys);
psbt.validateSignaturesOfAllInputs();
psbt.finalizeAllInputs();
psbt.data.inputs.forEach((input, i) => {
assert.throws(
() => parsePsbtInput(input),
(e: any) => e.message === 'Finalized PSBT parsing is not supported'
);
});
});
it('fail to parse input with more than one script type metadata', function () {
const unspents = mockUnspents(rootWalletKeys, ['p2tr'], BigInt('10000000000000000'), network);
const txBuilderParams = {
signer: 'user',
cosigner: 'bitgo',
amountType: 'bigint',
outputType: 'p2sh',
signatureTarget: 'halfsigned',
network,
changeIndex: CHANGE_INDEX,
fee: FEE,
} as const;
const txP2tr = constructTransactionUsingTxBuilder([unspents[0]], rootWalletKeys, txBuilderParams);
const psbtP2tr = toWalletPsbt(txP2tr, toBigInt([unspents[0]]), rootWalletKeys);
const walletKeys = rootWalletKeys.deriveForChainAndIndex(getExternalChainCode('p2sh'), 0);
const { redeemScript } = createOutputScript2of3(walletKeys.publicKeys, 'p2sh');
psbtP2tr.updateInput(0, { redeemScript });
assert.throws(
() => parsePsbtInput(psbtP2tr.data.inputs[0]),
(e: any) => e.message === 'Found both p2sh and taprootScriptPath PSBT metadata.'
);
});
it('fail to parse more than one tap leaf script per input', function () {
const unspents = mockUnspents(rootWalletKeys, ['p2tr'], BigInt('10000000000000000'), network);
const txBuilderParams = {
signer: 'user',
cosigner: 'bitgo',
amountType: 'bigint',
outputType: 'p2sh',
signatureTarget: 'halfsigned',
network,
changeIndex: CHANGE_INDEX,
fee: FEE,
} as const;
const txP2tr1 = constructTransactionUsingTxBuilder([unspents[0]], rootWalletKeys, txBuilderParams);
const psbtP2tr1 = toWalletPsbt(txP2tr1, toBigInt([unspents[0]]), rootWalletKeys);
const txBuilderParams2 = {
signer: 'user' as KeyName,
cosigner: 'backup' as KeyName,
amountType: 'bigint' as AmountType,
outputType: 'p2sh' as InputType,
signatureTarget: 'halfsigned' as SignatureTargetType,
network,
changeIndex: CHANGE_INDEX,
fee: FEE,
};
const txP2tr2 = constructTransactionUsingTxBuilder([unspents[0]], rootWalletKeys, txBuilderParams2);
const psbtP2tr2 = toWalletPsbt(txP2tr2, toBigInt([unspents[0]]), rootWalletKeys);
const txBuilderParams3 = {
signer: 'user',
cosigner: 'bitgo',
amountType: 'bigint',
outputType: 'p2sh',
signatureTarget: 'unsigned',
network,
changeIndex: CHANGE_INDEX,
fee: FEE,
} as const;
const txP2tr3 = constructTransactionUsingTxBuilder([unspents[0]], rootWalletKeys, txBuilderParams3);
const psbtP2tr3 = toWalletPsbt(txP2tr3, toBigInt([unspents[0]]), rootWalletKeys);
if (psbtP2tr1.data.inputs[0].tapLeafScript && psbtP2tr2.data.inputs[0].tapLeafScript) {
const tapLeafScripts = [psbtP2tr1.data.inputs[0].tapLeafScript[0], psbtP2tr2.data.inputs[0].tapLeafScript[0]];
psbtP2tr3.updateInput(0, { tapLeafScript: tapLeafScripts });
assert.throws(
() => parsePsbtInput(psbtP2tr3.data.inputs[0]),
(e: any) => e.message === 'Bitgo only supports a single tap leaf script per input.'
);
}
});
});
describe('isPsbt', function () {
function isPsbtForNetwork(n: Network) {
describe(`network: ${getNetworkName(n)}`, function () {
const psbt = createPsbtForNetwork({ network: n });
it('should return true for a valid PSBT', function () {
const psbtBuff = psbt.toBuffer();
assert.strictEqual(isPsbt(psbtBuff), true);
assert.strictEqual(isPsbt(psbtBuff.toString('hex')), true);
});
it('should return false for a transaction', function () {
assert.strictEqual(isPsbt(psbt.getUnsignedTx().toBuffer()), false);
});
it('should return false for a truncated magic word', function () {
const hex = psbt.toBuffer().slice(0, 3);
assert.strictEqual(isPsbt(hex), false);
assert.strictEqual(isPsbt(Buffer.from(hex)), false);
});
it('should return false for a valid PSBT with an invalid magic', function () {
const buffer = psbt.toBuffer();
buffer.writeUInt8(0x00, 1);
assert.strictEqual(isPsbt(psbt.getUnsignedTx().toBuffer()), false);
});
it('should return false for a valid PSBT with an invalid separator', function () {
const buffer = psbt.toBuffer();
buffer.writeUInt8(0xfe, 4);
assert.strictEqual(isPsbt(psbt.getUnsignedTx().toBuffer()), false);
});
it('should return false for a random buffer', function () {
const random = 'deadbeaf';
const buffer = Buffer.from(random, 'hex');
assert.strictEqual(isPsbt(random), false);
assert.strictEqual(isPsbt(buffer), false);
});
it('should return true if buffer is changed after the separator', function () {
const buffer = psbt.toBuffer();
buffer.writeUInt8(0x00, 5);
assert.strictEqual(isPsbt(buffer), true);
});
});
}
getNetworkList().forEach((n) => isPsbtForNetwork(n));
});
describe('Update incomplete psbt', function () {
function removeFromPsbt(
psbtHex: string,
network: Network,
remove: { input?: { index: number; fieldToRemove: string }; output?: { index: number; fieldToRemove: string } }
): UtxoPsbt {
const utxoPsbt = createPsbtFromHex(psbtHex, network);
const psbt = createPsbtForNetwork({ network: utxoPsbt.network });
const txInputs = utxoPsbt.txInputs;
utxoPsbt.data.inputs.map((input, ii) => {
const { hash, index } = txInputs[ii];
if (remove.input && ii === remove.input.index) {
delete input[remove.input.fieldToRemove];
}
psbt.addInput({ ...input, hash, index });
});
const txOutputs = utxoPsbt.txOutputs;
utxoPsbt.data.outputs.map((output, ii) => {
if (remove.output && remove.output.index === ii) {
delete output[remove.output.fieldToRemove];
}
psbt.addOutput({ ...output, script: txOutputs[ii].script, value: txOutputs[ii].value });
});
return psbt;
}
function signAllInputs(psbt: UtxoPsbt, { assertValidSignaturesAndExtractable = true } = {}) {
psbt.data.inputs.forEach((input, inputIndex) => {
const parsedInput = parsePsbtInput(input);
if (parsedInput.scriptType === 'taprootKeyPathSpend') {
psbt.setInputMusig2NonceHD(inputIndex, rootWalletKeys[signer]);
psbt.setInputMusig2NonceHD(inputIndex, rootWalletKeys[cosigner]);
}
if (parsedInput.scriptType === 'p2shP2pk') {
psbt.signInput(inputIndex, replayProtectionKeyPair);
} else {
psbt.signInputHD(inputIndex, rootWalletKeys[signer]);
psbt.signInputHD(inputIndex, rootWalletKeys[cosigner]);
}
});
if (assertValidSignaturesAndExtractable) {
assert.ok(psbt.validateSignaturesOfAllInputs());
psbt.finalizeAllInputs();
const txExtracted = psbt.extractTransaction();
assert.ok(txExtracted);
}
}
let psbtHex: string;
let unspents: Unspent<bigint>[];
const signer = 'user';
const cosigner = 'bitgo';
const scriptTypes = [...scriptTypes2Of3, 'p2shP2pk'] as (ScriptType2Of3 | ScriptTypeP2shP2pk)[];
const outputValue = BigInt((2e8 * scriptTypes.length - 100) / 5);
const outputs = [
{ chain: getExternalChainCode('p2sh'), index: 88, value: outputValue },
{ chain: getExternalChainCode('p2shP2wsh'), index: 89, value: outputValue },
{ chain: getExternalChainCode('p2wsh'), index: 90, value: outputValue },
{ chain: getExternalChainCode('p2tr'), index: 91, value: outputValue },
{ chain: getExternalChainCode('p2trMusig2'), index: 92, value: outputValue },
];
before(function () {
unspents = mockUnspents(rootWalletKeys, scriptTypes, BigInt(2e8), network);
const psbt = constructPsbt(unspents, rootWalletKeys, signer, cosigner, outputs);
psbtHex = psbt.toHex();
});
it('can create a sign-able psbt from an unsigned transaction extracted from the psbt', function () {
if (true) {
return;
}
const psbtOrig = createPsbtFromHex(psbtHex, network);
const tx = psbtOrig.getUnsignedTx();
const psbt = createPsbtFromTransaction(
tx,
unspents.map((u) => toPrevOutput(u, network))
);
unspents.forEach((u, inputIndex) => {
if (isWalletUnspent(u)) {
updateWalletUnspentForPsbt(psbt, inputIndex, u, rootWalletKeys, signer, cosigner);
} else {
const { redeemScript } = createOutputScriptP2shP2pk(replayProtectionKeyPair.publicKey);
updateReplayProtectionUnspentToPsbt(psbt, inputIndex, u, redeemScript);
}
});
signAllInputs(psbt);
});
const componentsOnEachInputScriptType = {
p2sh: ['nonWitnessUtxo', 'redeemScript', 'bip32Derivation'],
p2shP2wsh: ['witnessUtxo', 'bip32Derivation', 'redeemScript', 'witnessScript'],
p2wsh: ['witnessUtxo', 'witnessScript', 'bip32Derivation'],
p2tr: ['witnessUtxo', 'tapLeafScript', 'tapBip32Derivation'],
p2trMusig2: ['witnessUtxo', 'tapBip32Derivation', 'tapInternalKey', 'tapMerkleRoot', 'unknownKeyVals'],
p2shP2pk: ['redeemScript', 'nonWitnessUtxo'],
};
const p2trComponents = ['tapTree', 'tapInternalKey', 'tapBip32Derivation'];
const componentsOnEachOutputScriptType = {
p2sh: ['bip32Derivation', 'redeemScript'],
p2shP2wsh: ['bip32Derivation', 'witnessScript', 'redeemScript'],
p2wsh: ['bip32Derivation', 'witnessScript'],
p2tr: p2trComponents,
p2trMusig2: p2trComponents,
p2shP2pk: [],
};
scriptTypes.forEach((scriptType, i) => {
componentsOnEachInputScriptType[scriptType].forEach((inputComponent) => {
it(`[${scriptType}] missing ${inputComponent} on input should succeed in fully signing unsigned psbt after update`, function () {
const psbt = removeFromPsbt(psbtHex, network, { input: { index: i, fieldToRemove: inputComponent } });
const unspent = unspents[i];
if (isWalletUnspent(unspent)) {
updateWalletUnspentForPsbt(psbt, i, unspent, rootWalletKeys, signer, cosigner);
} else {
const { redeemScript } = createOutputScriptP2shP2pk(replayProtectionKeyPair.publicKey);
assert.ok(redeemScript);
updateReplayProtectionUnspentToPsbt(psbt, i, unspent, redeemScript);
}
signAllInputs(psbt);
});
});
componentsOnEachOutputScriptType[scriptType].forEach((outputComponent) => {
it(`[${scriptType}] missing ${outputComponent} on output should produce same hex as fully hydrated after update`, function () {
const psbt = removeFromPsbt(psbtHex, network, { output: { index: i, fieldToRemove: outputComponent } });
updateWalletOutputForPsbt(psbt, rootWalletKeys, i, outputs[i].chain, outputs[i].index);
assert.strictEqual(psbt.toHex(), psbtHex);
});
});
});
});
describe('Psbt from transaction using wallet unspents', function () {
function runTestSignUnspents<TNumber extends number | bigint>({
inputScriptTypes,
outputScriptType,
signer,
cosigner,
amountType,
testOutputAmount,
signatureTarget,
}: {
inputScriptTypes: InputType[];
outputScriptType: outputScripts.ScriptType2Of3;
signer: KeyName;
cosigner: KeyName;
amountType: 'number' | 'bigint';
testOutputAmount: TNumber;
signatureTarget: SignatureTargetType;
}) {
it(`can be signed [inputs=${inputScriptTypes} signer=${signer} cosigner=${cosigner} amountType=${amountType} signatureTarget=${signatureTarget}]`, function () {
const unspents = mockUnspents(rootWalletKeys, inputScriptTypes, testOutputAmount, network);
// const txBuilderParams = { network, changeIndex: CHANGE_INDEX, fee: FEE };
const txBuilderParams = {
signer,
cosigner,
amountType,
outputType: outputScriptType,
signatureTarget: signatureTarget,
network,
changeIndex: CHANGE_INDEX,
fee: FEE,
};
const tx = constructTransactionUsingTxBuilder(unspents, rootWalletKeys, txBuilderParams);
const unspentBigInt = toBigInt(unspents);
const psbt = toWalletPsbt(tx, unspentBigInt, rootWalletKeys);
validatePsbtParsing(tx, psbt, unspentBigInt, signatureTarget);
// Check that the correct unspent corresponds to the input
unspentBigInt.forEach((unspent, inputIndex) => {
const otherUnspent = inputIndex === 0 ? unspentBigInt[1] : unspentBigInt[0];
assert.strictEqual(psbtIncludesUnspentAtIndex(psbt, inputIndex, unspent.id), true);
assert.strictEqual(psbtIncludesUnspentAtIndex(psbt, inputIndex, otherUnspent.id), false);
updateWalletUnspentForPsbt(psbt, inputIndex, unspent, rootWalletKeys, signer, cosigner);
});
if (signatureTarget !== 'fullsigned') {
// Now signing to make it fully signed psbt.
// So it will be easy to verify its validity with another similar tx to be built with tx builder.
signPsbt(psbt, unspentBigInt, rootWalletKeys, signer, cosigner, signatureTarget);
}
assert.deepStrictEqual(psbt.validateSignaturesOfAllInputs(), true);
psbt.finalizeAllInputs();
const txFromPsbt = psbt.extractTransaction();
const txBuilderParams2 = {
signer,
cosigner,
amountType,
outputType: outputScriptType,
signatureTarget: 'fullsigned' as SignatureTargetType,
network,
changeIndex: CHANGE_INDEX,
fee: FEE,
};
// New legacy tx resembles the signed psbt.
const txFromTxBuilder = constructTransactionUsingTxBuilder(unspents, rootWalletKeys, txBuilderParams2);
assert.deepStrictEqual(txFromPsbt.getHash(), txFromTxBuilder.getHash());
});
}
function getInputScripts(): InputType[][] {
return getScriptTypes2Of3().flatMap((t) => {
return getScriptTypes2Of3().flatMap((lastType) => {
return [[t, t, lastType]];
});
});
}
function getSignerPairs(containsTaprootInput: boolean): [signer: KeyName, cosigner: KeyName][] {
const signaturePairs = [['user', 'bitgo'] as [signer: KeyName, cosigner: KeyName]];
if (containsTaprootInput) {
signaturePairs.push(['user', 'backup'] as [signer: KeyName, cosigner: KeyName]);
}
return signaturePairs;
}
(['unsigned', 'halfsigned', 'fullsigned'] as SignatureTargetType[]).forEach((signatureTarget) => {
getInputScripts().forEach((inputScriptTypes) => {
getSignerPairs(inputScriptTypes.includes('p2tr')).forEach(([signer, cosigner]) => {
runTestSignUnspents({
inputScriptTypes,
outputScriptType: 'p2sh',
signer,
cosigner,
amountType: 'number',
testOutputAmount: defaultTestOutputAmount,
signatureTarget,
});
runTestSignUnspents<bigint>({
inputScriptTypes,
outputScriptType: 'p2sh',
signer,
cosigner,
amountType: 'bigint',
testOutputAmount: BigInt('10000000000000000'),
signatureTarget,
});
});
});
});
});
function testUtxoPsbt(coinNetwork: Network) {
describe(`Testing UtxoPsbt (de)serialization for ${getNetworkName(coinNetwork)} network`, function () {
let psbt: UtxoPsbt;
let psbtHex: string;
let unspents: (WalletUnspent<bigint> | Unspent<bigint>)[];
before(async function () {
unspents = mockUnspents(rootWalletKeys, ['p2sh'], BigInt('10000000000000'), coinNetwork);
const txBuilderParams = {
signer: 'user',
cosigner: 'bitgo',
amountType: 'bigint',
outputType: 'p2sh',
signatureTarget: 'fullsigned',
network: coinNetwork,
changeIndex: CHANGE_INDEX,
fee: FEE,
} as const;
const tx = constructTransactionUsingTxBuilder(unspents, rootWalletKeys, txBuilderParams);
psbt = toWalletPsbt(tx, toBigInt(unspents), rootWalletKeys);
if (coinNetwork === networks.zcash) {
(psbt as ZcashPsbt).setDefaultsForVersion(network, 450);
}
psbtHex = psbt.toHex();
});
it('should be able to clone psbt', async function () {
const clone = psbt.clone();
assert(clone instanceof psbt.constructor, `Expected clone to be instance of ${psbt.constructor.name}`);
assert.deepStrictEqual(clone.toBuffer(), psbt.toBuffer());
assert.deepStrictEqual(clone.clone().toBuffer(), psbt.toBuffer());
assert.strictEqual(clone.network, psbt.network);
assert.strictEqual(clone.clone().network, psbt.network);
});
it('should be able to round-trip', async function () {
assert.deepStrictEqual(createPsbtFromHex(psbtHex, coinNetwork, false).toBuffer(), psbt.toBuffer());
});
it('should be able to get transaction info from psbt', function () {
const txInfo = getTransactionAmountsFromPsbt(psbt);
assert.strictEqual(txInfo.fee, FEE);
assert.strictEqual(txInfo.inputCount, unspents.length);
assert.strictEqual(txInfo.inputAmount, BigInt('10000000000000') * BigInt(unspents.length));
assert.strictEqual(txInfo.outputAmount, BigInt('10000000000000') * BigInt(unspents.length) - FEE);
assert.strictEqual(txInfo.outputCount, psbt.data.outputs.length);
});
function deserializeBip32PathsCorrectly(bip32PathsAbsolute: boolean): void {
function checkDerivationPrefix(bip32Derivation: { path: string }): void {
const path = bip32Derivation.path.split('/');
const prefix = bip32PathsAbsolute ? 'm' : '0';
assert(path[0] === prefix);
}
it(`should deserialize PSBT bip32Derivations with paths ${
bip32PathsAbsolute ? '' : 'not '
} absolute`, async function () {
const deserializedPsbt = createPsbtFromHex(psbtHex, coinNetwork, bip32PathsAbsolute);
assert(deserializedPsbt);
deserializedPsbt.data.inputs.forEach((input) => {
input?.bip32Derivation?.forEach((derivation) => checkDerivationPrefix(derivation));
input?.tapBip32Derivation?.forEach((derivation) => checkDerivationPrefix(derivation));
});
});
}
[true, false].forEach((bip32PathsAbsolute) => deserializeBip32PathsCorrectly(bip32PathsAbsolute));
});
}
[networks.bitcoin, networks.zcash, networks.dash, networks.dogecoin, networks.litecoin].forEach((coinNetwork) =>
testUtxoPsbt(coinNetwork)
);
Выполнить команду
Для локальной разработки. Не используйте в интернете!