Skip to content

Commit

Permalink
fix(utxo-lib): check prevout script against computed for non-taproot …
Browse files Browse the repository at this point in the history
…inputs

Make sure that we are actually signing the right script.

Taproot cases will follow.

Issue: BTC-428
  • Loading branch information
OttoAllmendinger committed Oct 26, 2023
1 parent 923d27b commit eb16e8e
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 10 deletions.
30 changes: 22 additions & 8 deletions modules/utxo-lib/src/bitgo/outputScripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ export function createOutputScriptP2shP2pk(pubkey: Buffer): SpendableScript {
};
}

export function getOutputScript(scriptType: 'p2sh' | 'p2shP2wsh' | 'p2wsh', conditionScript: Buffer): Buffer {
let output;
switch (scriptType) {
case 'p2sh':
({ output } = bitcoinjs.payments.p2sh({ redeem: { output: conditionScript } }));
break;
case 'p2shP2wsh':
({ output } = bitcoinjs.payments.p2sh({
redeem: { output: getOutputScript('p2wsh', conditionScript) },
}));
break;
case 'p2wsh':
({ output } = bitcoinjs.payments.p2wsh({ redeem: { output: conditionScript } }));
break;
}
if (output === undefined) {
throw new Error(`output undefined`);
}
return output;
}

/**
* Return scripts for 2-of-3 multisig output
* @param pubkeys - the key triple for multisig
Expand Down Expand Up @@ -148,32 +169,25 @@ export function createOutputScript2of3(
const script2of3 = bitcoinjs.payments.p2ms({ m: 2, pubkeys });
assert(script2of3.output);

let scriptPubKey: bitcoinjs.Payment;
let redeemScript: bitcoinjs.Payment | undefined;
let witnessScript: bitcoinjs.Payment | undefined;
switch (scriptType) {
case 'p2sh':
redeemScript = script2of3;
scriptPubKey = bitcoinjs.payments.p2sh({ redeem: script2of3 });
break;
case 'p2shP2wsh':
witnessScript = script2of3;
redeemScript = bitcoinjs.payments.p2wsh({ redeem: script2of3 });
scriptPubKey = bitcoinjs.payments.p2sh({ redeem: redeemScript });
break;
case 'p2wsh':
witnessScript = script2of3;
scriptPubKey = bitcoinjs.payments.p2wsh({ redeem: witnessScript });
break;
default:
throw new Error(`unknown multisig script type ${scriptType}`);
}

assert(scriptPubKey);
assert(scriptPubKey.output);

return {
scriptPubKey: scriptPubKey.output,
scriptPubKey: getOutputScript(scriptType, script2of3.output),
redeemScript: redeemScript?.output,
witnessScript: witnessScript?.output,
};
Expand Down
16 changes: 16 additions & 0 deletions modules/utxo-lib/src/bitgo/signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createOutputScript2of3,
createOutputScriptP2shP2pk,
createSpendScriptP2tr,
getOutputScript,
ScriptType,
ScriptType2Of3,
scriptType2Of3AsPrevOutType,
Expand Down Expand Up @@ -102,6 +103,21 @@ export function getSignatureVerifications<TNumber extends number | bigint>(
}
}

if (
parsedScript.scriptType !== 'taprootKeyPathSpend' &&
parsedScript.scriptType !== 'taprootScriptPathSpend' &&
prevOutputs
) {
const prevoutScript = prevOutputs[inputIndex].script;

const output = getOutputScript(parsedScript.scriptType, parsedScript.pubScript);
if (!prevoutScript.equals(output)) {
throw new Error(
`prevout script ${prevoutScript.toString('hex')} does not match computed script ${output.toString('hex')}`
);
}
}

let publicKeys: Buffer[];
if (parsedScript.scriptType === 'taprootKeyPathSpend') {
if (!prevOutputs) {
Expand Down
12 changes: 11 additions & 1 deletion modules/utxo-lib/test/bitgo/signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
getSignKeyCombinations,
getUnsignedTransaction2Of3,
} from '../transaction_util';
import { getTransactionWithHighS } from './signatureModify';
import { getPrevOutsWithInvalidOutputScript, getTransactionWithHighS } from './signatureModify';
import { normDefault } from '../testutil/normalize';

function getScriptTypes2Of3() {
Expand Down Expand Up @@ -291,6 +291,16 @@ function checkSignTransaction<TNumber extends number | bigint>(
signKeys.length - 1
);
});

if (scriptType !== 'p2tr' && scriptType !== 'p2trMusig2') {
assert.throws(
() =>
signatureCount(
verifySignatureWithPublicKeys<TNumber>(tx, i, getPrevOutsWithInvalidOutputScript(prevOutputs, i), pubkeys)
),
/prevout script .* does not match computed script .*/
);
}
}
});
}
Expand Down
17 changes: 16 additions & 1 deletion modules/utxo-lib/test/bitgo/signatureModify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-redeclare */
import { script, ScriptSignature } from 'bitcoinjs-lib';
import { script, ScriptSignature, TxOutput } from 'bitcoinjs-lib';
import { isPlaceholderSignature, parseSignatureScript, UtxoTransaction } from '../../src/bitgo';

const BN = require('bn.js');
Expand Down Expand Up @@ -79,3 +79,18 @@ export function getTransactionWithHighS<TNumber extends number | bigint>(
return [cloned];
});
}

/** Return transaction with script xored with 0xff for the given input */
export function getPrevOutsWithInvalidOutputScript<TNumber extends number | bigint>(
prevOuts: TxOutput<TNumber>[],
inputIndex: number
): TxOutput<TNumber>[] {
return prevOuts.map((prevOut, i) => {
return i === inputIndex
? {
...prevOut,
script: prevOut.script.map((v) => v ^ 0xff) as typeof prevOut.script,
}
: prevOut;
});
}

0 comments on commit eb16e8e

Please sign in to comment.