Skip to content

Commit 3b0f302

Browse files
Merge pull request #6634 from BitGo/BTC-2373
Build to_sign tx for BIP322 signing for fixed script multisig
2 parents ef13495 + 496f98c commit 3b0f302

File tree

4 files changed

+150
-5
lines changed

4 files changed

+150
-5
lines changed

modules/utxo-core/src/bip322/toSign.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Psbt, Transaction } from '@bitgo/utxo-lib';
1+
import { Psbt, Transaction, bitgo } from '@bitgo/utxo-lib';
2+
3+
import { isTaprootChain } from './utils';
24

35
export type AddressDetails = {
46
redeemScript?: Buffer;
@@ -46,3 +48,29 @@ export function buildToSignPsbt(toSpendTx: Transaction<bigint>, addressDetails:
4648
});
4749
return psbt;
4850
}
51+
52+
export function buildToSignPsbtForChainAndIndex(
53+
toSpendTx: Transaction<bigint>,
54+
rootWalletKeys: bitgo.RootWalletKeys,
55+
chain: bitgo.ChainCode,
56+
index: number
57+
): Psbt {
58+
if (isTaprootChain(chain)) {
59+
throw new Error('BIP322 is not supported for Taproot script types.');
60+
}
61+
const output = bitgo.outputScripts.createOutputScript2of3(
62+
rootWalletKeys.deriveForChainAndIndex(chain, index).publicKeys,
63+
bitgo.scriptTypeForChain(chain)
64+
);
65+
66+
const toSpendScriptPubKey = toSpendTx.outs[0].script;
67+
if (!toSpendScriptPubKey.equals(output.scriptPubKey)) {
68+
throw new Error('Output scriptPubKey does not match the expected output script for the chain and index.');
69+
}
70+
71+
return buildToSignPsbt(toSpendTx, {
72+
scriptPubKey: output.scriptPubKey,
73+
redeemScript: output.redeemScript,
74+
witnessScript: output.witnessScript,
75+
});
76+
}

modules/utxo-core/src/bip322/toSpend.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Hash } from 'fast-sha256';
22
import { Psbt, Transaction, bitgo, networks } from '@bitgo/utxo-lib';
33

4+
import { isTaprootChain } from './utils';
5+
46
export const BIP322_TAG = 'BIP0322-signed-message';
57

68
/**
@@ -74,8 +76,7 @@ export function buildToSpendTransactionFromChainAndIndex(
7476
message: string | Buffer,
7577
tag = BIP322_TAG
7678
): Transaction<bigint> {
77-
const taprootChains = [...bitgo.chainCodesP2tr, ...bitgo.chainCodesP2trMusig2];
78-
if (taprootChains.some((tc) => tc === chain)) {
79+
if (isTaprootChain(chain)) {
7980
throw new Error('BIP322 is not supported for Taproot script types.');
8081
}
8182

modules/utxo-core/src/bip322/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,8 @@ export function getBip322ProofInputIndex(psbt: utxolib.Psbt): number | undefined
2828
export function psbtIsBip322Proof(psbt: utxolib.Psbt): boolean {
2929
return getBip322ProofInputIndex(psbt) !== undefined;
3030
}
31+
32+
export function isTaprootChain(chain: utxolib.bitgo.ChainCode): boolean {
33+
const taprootChains = [...utxolib.bitgo.chainCodesP2tr, ...utxolib.bitgo.chainCodesP2trMusig2];
34+
return taprootChains.some((tc) => tc === chain);
35+
}

modules/utxo-core/test/bip322/toSign.ts

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import assert from 'assert';
22

3-
import { Transaction } from '@bitgo/utxo-lib';
3+
import * as utxolib from '@bitgo/utxo-lib';
44

55
import * as bip322 from '../../src/bip322';
66

77
import { BIP322_PAYMENT_P2WPKH_FIXTURE, BIP322_PRV_FIXTURE as prv } from './bip322.utils';
8+
89
describe('BIP322 toSign', function () {
910
describe('buildToSignPsbt', function () {
1011
const scriptPubKey = BIP322_PAYMENT_P2WPKH_FIXTURE.output as Buffer;
@@ -28,12 +29,122 @@ describe('BIP322 toSign', function () {
2829
};
2930
const result = bip322.buildToSignPsbt(toSpendTx, addressDetails);
3031
const computedTxid = result
31-
.signAllInputs(prv, [Transaction.SIGHASH_ALL])
32+
.signAllInputs(prv, [utxolib.Transaction.SIGHASH_ALL])
3233
.finalizeAllInputs()
3334
.extractTransaction()
3435
.getId();
3536
assert.strictEqual(computedTxid, txid, `Transaction ID for message "${message}" does not match expected value`);
3637
});
3738
});
3839
});
40+
41+
describe('buildToSignPsbtForChainAndIndex', function () {
42+
const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys();
43+
44+
it('should fail when scriptPubKey of to_spend is different than to_sign', function () {
45+
const toSpendTx = bip322.buildToSpendTransaction(BIP322_PAYMENT_P2WPKH_FIXTURE.output as Buffer, 'Hello World');
46+
assert.throws(() => {
47+
bip322.buildToSignPsbtForChainAndIndex(toSpendTx, rootWalletKeys, 0, 0);
48+
}, /Output scriptPubKey does not match the expected output script for the chain and index./);
49+
});
50+
51+
function run(chain: utxolib.bitgo.ChainCode, shouldFail: boolean, index: number) {
52+
it(`should${
53+
shouldFail ? ' fail to' : ''
54+
} build and sign a to_sign PSBT for chain ${chain}, index ${index}`, function () {
55+
const message = 'I can believe it is not butter';
56+
if (shouldFail) {
57+
assert.throws(() => {
58+
bip322.buildToSpendTransactionFromChainAndIndex(rootWalletKeys, chain, index, message);
59+
}, /BIP322 is not supported for Taproot script types./);
60+
return;
61+
}
62+
const toSpendTx = bip322.buildToSpendTransactionFromChainAndIndex(rootWalletKeys, chain, index, message);
63+
const toSignPsbt = bip322.buildToSignPsbtForChainAndIndex(toSpendTx, rootWalletKeys, chain, index);
64+
65+
const derivedKeys = rootWalletKeys.deriveForChainAndIndex(chain, index);
66+
const prv1 = derivedKeys.triple[0];
67+
const prv2 = derivedKeys.triple[1];
68+
assert.ok(prv1);
69+
assert.ok(prv2);
70+
71+
// Can sign the PSBT with the keys
72+
toSignPsbt.signAllInputs(prv1, [utxolib.Transaction.SIGHASH_ALL]);
73+
toSignPsbt.signAllInputs(prv2, [utxolib.Transaction.SIGHASH_ALL]);
74+
75+
// Wrap the PSBT as a UtxoPsbt so that we can use the validateSignaturesOfInputCommon method
76+
const utxopsbt = utxolib.bitgo.createPsbtFromBuffer(toSignPsbt.toBuffer(), utxolib.networks.bitcoin);
77+
derivedKeys.publicKeys.forEach((pubkey, i) => {
78+
assert.deepStrictEqual(
79+
utxopsbt.validateSignaturesOfInputCommon(0, pubkey),
80+
i !== 2,
81+
`Signature validation failed for public key at index ${i}`
82+
);
83+
});
84+
85+
// finalize and extract
86+
const tx = toSignPsbt.finalizeAllInputs().extractTransaction();
87+
assert.ok(tx);
88+
89+
// Check that the transaction matches the full BIP322 format
90+
// Source: https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#full
91+
// For the to_spend transaction, verify that all of the properties are set correctly,
92+
// then get the txid and make sure that it matches the value in the `to_sign` tx
93+
assert.deepStrictEqual(toSpendTx.version, 0, 'version must be 0');
94+
assert.deepStrictEqual(toSpendTx.locktime, 0, 'locktime must be 0');
95+
assert.deepStrictEqual(
96+
toSpendTx.ins[0].hash.toString('hex'),
97+
'0000000000000000000000000000000000000000000000000000000000000000',
98+
'input hash must be a 32 byte zero buffer'
99+
);
100+
assert.deepStrictEqual(toSpendTx.ins[0].index, 0xffffffff, 'input index must be 0xFFFFFFFF');
101+
assert.deepStrictEqual(toSpendTx.ins[0].sequence, 0, 'input sequence must be 0');
102+
assert.deepStrictEqual(
103+
toSpendTx.ins[0].script.toString('hex'),
104+
Buffer.concat([Buffer.from([0x00, 0x20]), bip322.hashMessageWithTag(message)]).toString('hex'),
105+
'input script must be OP_0 PUSH32[ message_hash ]'
106+
);
107+
assert.ok(Array.isArray(toSpendTx.ins[0].witness), 'input witness must be an array');
108+
assert.deepStrictEqual(toSpendTx.ins[0].witness.length, 0, 'input witness must be empty');
109+
assert.deepStrictEqual(toSpendTx.ins.length, 1, 'to_spend transaction must have one input');
110+
assert.deepStrictEqual(toSpendTx.outs.length, 1, 'to_spend transaction must have one output');
111+
assert.deepStrictEqual(toSpendTx.outs[0].value, BigInt(0), 'output value must be 0');
112+
assert.deepStrictEqual(
113+
toSpendTx.outs[0].script.toString('hex'),
114+
utxolib.bitgo.outputScripts
115+
.createOutputScript2of3(
116+
derivedKeys.publicKeys,
117+
utxolib.bitgo.scriptTypeForChain(chain),
118+
utxolib.networks.bitcoin
119+
)
120+
.scriptPubKey.toString('hex'),
121+
'the script pubkey of the to_spend output must be the scriptPubKey of the address we are proving ownership of'
122+
);
123+
assert.deepStrictEqual(tx.ins.length, 1, 'to_sign transaction must have one input');
124+
assert.deepStrictEqual(tx.version, 0, 'to_sign transaction version must be 0');
125+
assert.deepStrictEqual(tx.locktime, 0, 'to_sign transaction locktime must be 0');
126+
assert.deepStrictEqual(
127+
utxolib.bitgo.getOutputIdForInput(tx.ins[0]).txid,
128+
toSpendTx.getId(),
129+
'to_sign transaction input must reference the to_spend transaction'
130+
);
131+
assert.deepStrictEqual(tx.ins[0].index, 0, 'to_sign transaction input index must be 0');
132+
assert.deepStrictEqual(tx.ins[0].sequence, 0, 'to_sign transaction input sequence must be 0');
133+
// We are not going to explicitly check the script witness on this transaction because we already verified the
134+
// signatures on the PSBT for the respective public keys. All that would be verified here is that we can assemble
135+
// the script witness correctly, which must be true orelse we would have a much bigger problem.
136+
assert.deepStrictEqual(tx.outs.length, 1, 'to_sign transaction must have one output');
137+
assert.deepStrictEqual(tx.outs[0].value, BigInt(0), 'to_sign transaction output value must be 0');
138+
assert.deepStrictEqual(
139+
tx.outs[0].script.toString('hex'),
140+
'6a',
141+
'to_sign transaction output script must be OP_RETURN'
142+
);
143+
});
144+
}
145+
146+
utxolib.bitgo.chainCodes.forEach((chain, i) => {
147+
run(chain, bip322.isTaprootChain(chain), i);
148+
});
149+
});
39150
});

0 commit comments

Comments
 (0)