Skip to content

Commit 656ec5e

Browse files
feat(utxo-core): add support for taproot script types
Add BIP322 signing support for p2tr and p2trMusig2 script types, allowing message signing for taproot addresses. Update PSBT creation and handling to accommodate taproot-specific requirements. NOTE: we are not supporting backup signing, only user-bitgo. Issue: BTC-2383
1 parent 0257f73 commit 656ec5e

File tree

5 files changed

+79
-54
lines changed

5 files changed

+79
-54
lines changed

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

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Psbt, bitgo } from '@bitgo/utxo-lib';
1+
import { Psbt, bitgo, networks } from '@bitgo/utxo-lib';
2+
import { toXOnlyPublicKey } from '@bitgo/utxo-lib/dist/src/bitgo/outputScripts';
23

3-
import { addBip322ProofMessage, isTaprootChain } from './utils';
4+
import { addBip322ProofMessage } from './utils';
45
import { BIP322_TAG, buildToSpendTransaction } from './toSpend';
56

67
export type AddressDetails = {
@@ -14,9 +15,9 @@ export const MAX_NUM_BIP322_INPUTS = 200;
1415
* Create the base PSBT for the to_sign transaction for BIP322 signing.
1516
* There will be ever 1 output.
1617
*/
17-
export function createBaseToSignPsbt(rootWalletKeys?: bitgo.RootWalletKeys): Psbt {
18+
export function createBaseToSignPsbt(rootWalletKeys?: bitgo.RootWalletKeys): bitgo.UtxoPsbt {
1819
// Create PSBT object for constructing the transaction
19-
const psbt = new Psbt();
20+
const psbt = bitgo.createPsbtForNetwork({ network: networks.bitcoin });
2021
// Set default value for nVersion and nLockTime
2122
psbt.setVersion(0); // nVersion = 0
2223
psbt.setLocktime(0); // nLockTime = 0
@@ -76,31 +77,82 @@ export function addBip322Input(psbt: Psbt, message: string, addressDetails: Addr
7677
}
7778

7879
export function addBip322InputWithChainAndIndex(
79-
psbt: Psbt,
80+
psbt: bitgo.UtxoPsbt,
8081
message: string,
8182
rootWalletKeys: bitgo.RootWalletKeys,
8283
scriptId: bitgo.ScriptId
83-
): Psbt {
84-
if (isTaprootChain(scriptId.chain)) {
85-
throw new Error('BIP322 is not supported for Taproot script types.');
86-
}
84+
): void {
85+
const scriptType = bitgo.scriptTypeForChain(scriptId.chain);
8786
const walletKeys = rootWalletKeys.deriveForChainAndIndex(scriptId.chain, scriptId.index);
8887
const output = bitgo.outputScripts.createOutputScript2of3(
8988
walletKeys.publicKeys,
9089
bitgo.scriptTypeForChain(scriptId.chain)
9190
);
9291

93-
addBip322Input(psbt, message, {
92+
addBip322Input(psbt as Psbt, message, {
9493
scriptPubKey: output.scriptPubKey,
9594
redeemScript: output.redeemScript,
9695
witnessScript: output.witnessScript,
9796
});
9897

9998
const inputIndex = psbt.data.inputs.length - 1;
100-
psbt.updateInput(
101-
inputIndex,
102-
bitgo.getPsbtBip32DerivationOutputUpdate(rootWalletKeys, walletKeys, bitgo.scriptTypeForChain(scriptId.chain))
103-
);
10499

105-
return psbt;
100+
// When adding the taproot metadata, we assume that we are NOT using the backup path
101+
// For script type p2tr, it means that we are using signer and bitgo keys when creating the tap tree
102+
// spending paths. For p2trMusig2, it means that we are using the taproot key path spending
103+
const keyNames = ['user', 'bitgo'] as bitgo.KeyName[];
104+
if (scriptType === 'p2tr') {
105+
const { controlBlock, witnessScript, leafVersion, leafHash } = bitgo.outputScripts.createSpendScriptP2tr(
106+
walletKeys.publicKeys,
107+
[walletKeys.user.publicKey, walletKeys.bitgo.publicKey]
108+
);
109+
psbt.updateInput(inputIndex, {
110+
tapLeafScript: [{ controlBlock, script: witnessScript, leafVersion }],
111+
});
112+
113+
psbt.updateInput(inputIndex, {
114+
tapBip32Derivation: keyNames.map((key) => ({
115+
leafHashes: [leafHash],
116+
pubkey: toXOnlyPublicKey(walletKeys[key].publicKey),
117+
path: rootWalletKeys.getDerivationPath(rootWalletKeys[key], scriptId.chain, scriptId.index),
118+
masterFingerprint: rootWalletKeys[key].fingerprint,
119+
})),
120+
});
121+
} else if (scriptType === 'p2trMusig2') {
122+
const {
123+
internalPubkey: tapInternalKey,
124+
outputPubkey: tapOutputKey,
125+
taptreeRoot,
126+
} = bitgo.outputScripts.createKeyPathP2trMusig2(walletKeys.publicKeys);
127+
128+
const participantsKeyValData = bitgo.musig2.encodePsbtMusig2Participants({
129+
tapOutputKey,
130+
tapInternalKey,
131+
participantPubKeys: [walletKeys.user.publicKey, walletKeys.bitgo.publicKey],
132+
});
133+
bitgo.addProprietaryKeyValuesFromUnknownKeyValues(psbt, 'input', inputIndex, participantsKeyValData);
134+
135+
psbt.updateInput(inputIndex, {
136+
tapInternalKey: tapInternalKey,
137+
});
138+
139+
psbt.updateInput(inputIndex, {
140+
tapMerkleRoot: taptreeRoot,
141+
});
142+
143+
psbt.updateInput(inputIndex, {
144+
tapBip32Derivation: keyNames.map((key) => ({
145+
leafHashes: [],
146+
pubkey: toXOnlyPublicKey(walletKeys[key].publicKey),
147+
path: rootWalletKeys.getDerivationPath(rootWalletKeys[key], scriptId.chain, scriptId.index),
148+
masterFingerprint: rootWalletKeys[key].fingerprint,
149+
})),
150+
});
151+
} else {
152+
// Add bip32 derivation information for the input
153+
psbt.updateInput(
154+
inputIndex,
155+
bitgo.getPsbtBip32DerivationOutputUpdate(rootWalletKeys, walletKeys, bitgo.scriptTypeForChain(scriptId.chain))
156+
);
157+
}
106158
}

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

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

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

86
/**
@@ -76,10 +74,6 @@ export function buildToSpendTransactionFromChainAndIndex(
7674
message: string | Buffer,
7775
tag = BIP322_TAG
7876
): Transaction<bigint> {
79-
if (isTaprootChain(chain)) {
80-
throw new Error('BIP322 is not supported for Taproot script types.');
81-
}
82-
8377
const outputScript = bitgo.outputScripts.createOutputScript2of3(
8478
rootWalletKeys.deriveForChainAndIndex(chain, index).publicKeys,
8579
bitgo.scriptTypeForChain(chain),

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,3 @@ 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: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,23 @@ describe('BIP322 toSign', function () {
4040
describe('buildToSignPsbtForChainAndIndex', function () {
4141
const rootWalletKeys = utxolib.testutil.getDefaultWalletKeys();
4242

43-
function run(chain: utxolib.bitgo.ChainCode, shouldFail: boolean, index: number) {
44-
it(`should${
45-
shouldFail ? ' fail to' : ''
46-
} build and sign a to_sign PSBT for chain ${chain}, index ${index}`, function () {
43+
function run(chain: utxolib.bitgo.ChainCode, index: number) {
44+
const scriptType = utxolib.bitgo.scriptTypeForChain(chain);
45+
it(`should build and sign a to_sign PSBT for chain ${chain}, index ${index}`, function () {
4746
const message = 'I can believe it is not butter';
48-
if (shouldFail) {
49-
assert.throws(() => {
50-
bip322.buildToSpendTransactionFromChainAndIndex(rootWalletKeys, chain, index, message);
51-
}, /BIP322 is not supported for Taproot script types./);
52-
return;
53-
}
47+
5448
const toSpendTx = bip322.buildToSpendTransactionFromChainAndIndex(rootWalletKeys, chain, index, message);
5549
const toSignPsbt = bip322.createBaseToSignPsbt(rootWalletKeys);
5650
bip322.addBip322InputWithChainAndIndex(toSignPsbt, message, rootWalletKeys, { chain, index });
5751

5852
// Can sign the PSBT with the keys
59-
// Should be able to use HD because we have the bip32Derivation information
60-
toSignPsbt.signAllInputsHD(rootWalletKeys.triple[0]);
61-
toSignPsbt.signAllInputsHD(rootWalletKeys.triple[1]);
53+
// Should be able to use HD because we have the (tap)bip32Derivation information
54+
if (scriptType === 'p2trMusig2') {
55+
toSignPsbt.setAllInputsMusig2NonceHD(rootWalletKeys.user);
56+
toSignPsbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo);
57+
}
58+
toSignPsbt.signAllInputsHD(rootWalletKeys.user);
59+
toSignPsbt.signAllInputsHD(rootWalletKeys.bitgo);
6260

6361
// Wrap the PSBT as a UtxoPsbt so that we can use the validateSignaturesOfInputCommon method
6462
const utxopsbt = utxolib.bitgo.createPsbtFromBuffer(toSignPsbt.toBuffer(), utxolib.networks.bitcoin);
@@ -127,7 +125,7 @@ describe('BIP322 toSign', function () {
127125
}
128126

129127
utxolib.bitgo.chainCodes.forEach((chain, i) => {
130-
run(chain, bip322.isTaprootChain(chain), i);
128+
run(chain, i);
131129
});
132130
});
133131
});

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,6 @@ describe('to_spend', function () {
6161
});
6262

6363
describe('buildToSpendTransactionFromChainAndIndex', function () {
64-
it('should throw an error for Taproot chains', function () {
65-
const taprootChains = [...bitgo.chainCodesP2tr, ...bitgo.chainCodesP2trMusig2];
66-
taprootChains.forEach((chain) => {
67-
assert.throws(() => {
68-
buildToSpendTransactionFromChainAndIndex(
69-
testutil.getDefaultWalletKeys(),
70-
chain,
71-
0,
72-
Buffer.from('Hello World')
73-
);
74-
}, /BIP322 is not supported for Taproot script types/);
75-
});
76-
});
77-
7864
describe('should build a to_spend transaction for a non-Taproot chain', function () {
7965
function run(chain: bitgo.ChainCode) {
8066
it(`scriptType: ${bitgo.scriptTypeForChain(chain)}, chain ${chain}`, function () {

0 commit comments

Comments
 (0)