Skip to content

Commit ee95e5b

Browse files
OttoAllmendingerllm-git
andcommitted
feat(utxo-lib): improve p2tr fixture testing
Adds comprehensive test fixtures for p2tr and p2trMusig2 output scripts to ensure consistent behavior across implementations. Exports createPaymentP2trCommon function to facilitate direct testing and adds new test file specifically for p2tr script validation. The test fixtures validate: - Control block generation for different script types - Key aggregation behavior - Tree structure consistency - Output script generation - Proper handling of key ordering differences between plain and xonly views - Different key combinations in the taproot script tree Issue: BTC-2652 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 27a13f2 commit ee95e5b

File tree

5 files changed

+292
-22
lines changed

5 files changed

+292
-22
lines changed

modules/utxo-lib/src/bitgo/outputScripts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ function getRedeemIndex(keyCombinations: [Buffer, Buffer][], signer: Buffer, cos
268268
throw new Error(`could not find singer/cosigner combination`);
269269
}
270270

271-
function createPaymentP2trCommon(
271+
export function createPaymentP2trCommon(
272272
scriptType: 'p2tr' | 'p2trMusig2',
273273
pubkeys: Triple<Buffer>,
274274
redeemIndex?: number | { signer: Buffer; cosigner: Buffer }
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
[
2+
{
3+
"scriptType": "p2tr",
4+
"pubkeys": [
5+
"02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7",
6+
"028714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2",
7+
"03203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64"
8+
],
9+
"internalPubkey": "cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa",
10+
"controlBlocks": [
11+
{
12+
"redeemIndex": 0,
13+
"controlBlock": "c0cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aad88b89f6f10f490bb6e1e61585cb3e78f8b4993e574b4031cacc6859c5adbc45"
14+
},
15+
{
16+
"redeemIndex": 1,
17+
"controlBlock": "c0cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aab33e39fb32e503897e9cdc949597dac7b156017bf55a4f9802b619db07d3070a62959ac7472a3cd0ea894b23888341247d3c890c711fff8ac9b02177609e3e27"
18+
},
19+
{
20+
"redeemIndex": 2,
21+
"controlBlock": "c0cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa0e87e7b2bddc1e2f2cde702b5cbe51119df98538b35fa91c40a7c74fa9f5d39862959ac7472a3cd0ea894b23888341247d3c890c711fff8ac9b02177609e3e27"
22+
}
23+
],
24+
"tapTree": {
25+
"leaves": [
26+
{
27+
"script": "20d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7ad20203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64ac",
28+
"leafVersion": 192,
29+
"depth": 1
30+
},
31+
{
32+
"script": "20d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7ad208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ac",
33+
"leafVersion": 192,
34+
"depth": 2
35+
},
36+
{
37+
"script": "208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ad20203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64ac",
38+
"leafVersion": 192,
39+
"depth": 2
40+
}
41+
]
42+
},
43+
"taptreeRoot": "b69e64804422cb6cac96df1d742055b41aca27017dfcf79ef68482fad348b5c3",
44+
"output": "5120ef88931a66e09d2777276f13fc99305aa51d38642fd1c01efe461a4c84c8915a"
45+
},
46+
{
47+
"scriptType": "p2tr",
48+
"pubkeys": [
49+
"03203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64",
50+
"028714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2",
51+
"02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7"
52+
],
53+
"internalPubkey": "cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa",
54+
"controlBlocks": [
55+
{
56+
"redeemIndex": 0,
57+
"controlBlock": "c0cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa154989ec963f9639848d336c522641b38bf5540ca0934318ac824e623ffd9e14"
58+
},
59+
{
60+
"redeemIndex": 1,
61+
"controlBlock": "c0cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa9f8d752c1becee80ffd87719934911d9c8aef659fc3ab512ba67f920ffc47545c3a4b27e58190225770a6cf2fb7ee0d9c536951637b3b0cea693d8ba9528853d"
62+
},
63+
{
64+
"redeemIndex": 2,
65+
"controlBlock": "c0cc899cac29f6243ef481be86f0d39e173c075cd57193d46332b1ec0b42c439aa45fc694de6d51e7c6fcd37c35377b99e4e6e9a19adb600256a20dc0dd34561bcc3a4b27e58190225770a6cf2fb7ee0d9c536951637b3b0cea693d8ba9528853d"
66+
}
67+
],
68+
"tapTree": {
69+
"leaves": [
70+
{
71+
"script": "20203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64ad20d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7ac",
72+
"leafVersion": 192,
73+
"depth": 1
74+
},
75+
{
76+
"script": "20203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64ad208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ac",
77+
"leafVersion": 192,
78+
"depth": 2
79+
},
80+
{
81+
"script": "208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ad20d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7ac",
82+
"leafVersion": 192,
83+
"depth": 2
84+
}
85+
]
86+
},
87+
"taptreeRoot": "e4ca158ee6f82dec51f1ecec71665f0735c170bf89c1fe9f9e568ad6257fabc0",
88+
"output": "51209e609b5cbf529784691f5cb92dd2cf0ceb3c13d8b9539d7ba765f62e5e036379"
89+
}
90+
]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
[
2+
{
3+
"scriptType": "p2trMusig2",
4+
"pubkeys": [
5+
"02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7",
6+
"028714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2",
7+
"03203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64"
8+
],
9+
"internalPubkey": "c0e255b4510e041ab81151091d875687a618de314344dff4b73b1bcd366cdbd8",
10+
"controlBlocks": [
11+
{
12+
"redeemIndex": 0,
13+
"controlBlock": "c0c0e255b4510e041ab81151091d875687a618de314344dff4b73b1bcd366cdbd8b33e39fb32e503897e9cdc949597dac7b156017bf55a4f9802b619db07d3070a"
14+
},
15+
{
16+
"redeemIndex": 1,
17+
"controlBlock": "c0c0e255b4510e041ab81151091d875687a618de314344dff4b73b1bcd366cdbd80e87e7b2bddc1e2f2cde702b5cbe51119df98538b35fa91c40a7c74fa9f5d398"
18+
}
19+
],
20+
"tapTree": {
21+
"leaves": [
22+
{
23+
"script": "20d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7ad208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ac",
24+
"leafVersion": 192,
25+
"depth": 1
26+
},
27+
{
28+
"script": "208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ad20203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64ac",
29+
"leafVersion": 192,
30+
"depth": 1
31+
}
32+
]
33+
},
34+
"taptreeRoot": "d88b89f6f10f490bb6e1e61585cb3e78f8b4993e574b4031cacc6859c5adbc45",
35+
"output": "5120b1b559f099d5480951944bb9e5560b1485c51f7d15c9bb2864b2354de739beaf"
36+
},
37+
{
38+
"scriptType": "p2trMusig2",
39+
"pubkeys": [
40+
"03203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64",
41+
"028714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2",
42+
"02d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7"
43+
],
44+
"internalPubkey": "e48d309b535811eb0b148c4b0600a10e82e289899429e40aee05577504eca356",
45+
"controlBlocks": [
46+
{
47+
"redeemIndex": 0,
48+
"controlBlock": "c0e48d309b535811eb0b148c4b0600a10e82e289899429e40aee05577504eca3569f8d752c1becee80ffd87719934911d9c8aef659fc3ab512ba67f920ffc47545"
49+
},
50+
{
51+
"redeemIndex": 1,
52+
"controlBlock": "c0e48d309b535811eb0b148c4b0600a10e82e289899429e40aee05577504eca35645fc694de6d51e7c6fcd37c35377b99e4e6e9a19adb600256a20dc0dd34561bc"
53+
}
54+
],
55+
"tapTree": {
56+
"leaves": [
57+
{
58+
"script": "20203ab799ce28e2cca044f594c69275050af4bb0854ad730a8f74622342300e64ad208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ac",
59+
"leafVersion": 192,
60+
"depth": 1
61+
},
62+
{
63+
"script": "208714039c6866c27eb6885ffbb4085964a603140e5a39b0fa29b1d9839212f9a2ad20d20a62701c54f6eb3abb9f964b0e29ff90ffa3b4e3fcb73e7c67d4950fa6e3c7ac",
64+
"leafVersion": 192,
65+
"depth": 1
66+
}
67+
]
68+
},
69+
"taptreeRoot": "154989ec963f9639848d336c522641b38bf5540ca0934318ac824e623ffd9e14",
70+
"output": "5120b402ebe79e4563cbc4619a7b2af7e1f9ff124edca871f688987221a09f17c4a7"
71+
}
72+
]

modules/utxo-lib/test/bitgo/outputScripts.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as assert from 'assert';
44
import {
55
createOutputScript2of3,
66
createOutputScriptP2shP2pk,
7-
createPaymentP2tr,
87
isSupportedScriptType,
98
scriptTypes2Of3,
109
} from '../../src/bitgo/outputScripts';
@@ -114,23 +113,3 @@ describe('createOutputScriptP2shP2pk', function () {
114113
assert.strictEqual(witnessScript, undefined);
115114
});
116115
});
117-
118-
describe('createPaymentP2tr', () => {
119-
const controlBlocks = [
120-
'c1aa3303d48847f4d54aa02a4ff97448f1f430b07eecd632c41f390e3f8431a166487df024a0eb38aeb56b5263cf22c84a2c9c7daad9a8e55cce2e3cac87c52a0a',
121-
'c1aa3303d48847f4d54aa02a4ff97448f1f430b07eecd632c41f390e3f8431a1660a75f62db677b9c1974741735aa4b0c2c8718796c82578b960e1fa0986d4f25cf0b2127669c12ad75a079c25502a5456764de23f30df1fcdb88418fe970834d7',
122-
'c1aa3303d48847f4d54aa02a4ff97448f1f430b07eecd632c41f390e3f8431a1669c039366a9ce89ad30c9935268a10110cb1a4b6357dcc2c651e9de38639c206af0b2127669c12ad75a079c25502a5456764de23f30df1fcdb88418fe970834d7',
123-
];
124-
125-
it('allows no redeemIndex', () => {
126-
const p2tr = createPaymentP2tr(pubkeys);
127-
assert.strictEqual(p2tr.controlBlock, undefined);
128-
});
129-
130-
for (let i = 0; i < 3; i++) {
131-
it(`creates controlBlock for redeemIndex ${i}`, () => {
132-
const p2tr = createPaymentP2tr(pubkeys, i);
133-
assert.strictEqual(p2tr.controlBlock?.toString('hex'), controlBlocks[i]);
134-
});
135-
}
136-
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as assert from 'assert';
2+
3+
import { musig, ecc, BIP32Interface } from '@bitgo/secp256k1';
4+
import { TapTree } from 'bip174/src/lib/interfaces';
5+
import * as taproot from '../../src/taproot';
6+
import { isTriple, Triple } from '../../src/bitgo/types';
7+
import { createPaymentP2trCommon, toXOnlyPublicKey } from '../../src/bitgo/outputScripts';
8+
import { getKey } from '../../src/testutil';
9+
import { assertEqualFixture } from '../fixture.util';
10+
11+
function searchKey(path: string, predicate: (key: BIP32Interface) => boolean): BIP32Interface {
12+
for (let i = 0; i < 10; i++) {
13+
const key = getKey(`${path}/${i}`);
14+
if (predicate(key)) {
15+
return key;
16+
}
17+
}
18+
throw new Error('could not find key');
19+
}
20+
21+
// for p2tr, we sort according to the xonly keys, so we need to pick user and bitgo keys
22+
// that are ordered differently dependening on the sort method (with or without y coordinate)
23+
// we pick a user key with first byte 0x02 (even y coordinate) and second byte greater than 0x80
24+
const userKey = searchKey('utxo/user', (key) => key.publicKey[0] === 0x02 && key.publicKey[1] > 0x80);
25+
// backup key does not matter
26+
const backupKey = searchKey('utxo/backup', (key) => true);
27+
// we pick a bitgo key with first byte 0x03 (odd y coordinate) and second byte less than 0x80
28+
const bitgoKey = searchKey('utxo/bitgo', (key) => key.publicKey[0] === 0x03 && key.publicKey[1] < 0x80);
29+
30+
const pubkeys = [userKey, backupKey, bitgoKey].map((key) => key.publicKey) as Triple<Buffer>;
31+
32+
type Fixture = {
33+
scriptType: 'p2tr' | 'p2trMusig2';
34+
pubkeys: Buffer[];
35+
internalPubkey: Buffer;
36+
controlBlocks: { redeemIndex: number | undefined; controlBlock: Buffer | undefined }[];
37+
tapTree: TapTree | undefined;
38+
taptreeRoot: Buffer;
39+
output: Buffer;
40+
};
41+
42+
function getFixture(pubkeys: Buffer[], scriptType: 'p2tr' | 'p2trMusig2'): Fixture {
43+
assert.ok(isTriple(pubkeys), 'pubkeys must be a triple');
44+
const { internalPubkey, tapTree, taptreeRoot, output } = createPaymentP2trCommon(scriptType, pubkeys);
45+
assert.ok(taptreeRoot, 'taptreeRoot is required');
46+
assert.ok(internalPubkey, 'internalPubkey is required');
47+
assert.ok(output, 'output is required');
48+
49+
const redeemIndexes = scriptType === 'p2tr' ? [0, 1, 2] : [0, 1];
50+
const controlBlocks = redeemIndexes.map((redeemIndex) => ({
51+
redeemIndex,
52+
controlBlock: createPaymentP2trCommon(scriptType, pubkeys, redeemIndex).controlBlock ?? undefined,
53+
}));
54+
55+
return {
56+
scriptType,
57+
pubkeys,
58+
internalPubkey,
59+
controlBlocks,
60+
tapTree,
61+
taptreeRoot,
62+
output,
63+
};
64+
}
65+
66+
function getFixtures(scriptType: 'p2tr' | 'p2trMusig2'): Fixture[] {
67+
const [user, backup, bitgo] = pubkeys;
68+
return [
69+
getFixture([user, backup, bitgo], scriptType),
70+
// flips user and bitgo key
71+
getFixture([bitgo, backup, user], scriptType),
72+
];
73+
}
74+
75+
function describeWithScriptType(scriptType: 'p2tr' | 'p2trMusig2') {
76+
describe(`createPaymentP2tr ${scriptType}`, () => {
77+
it('creates expected fixtures', async function () {
78+
const fixtures = getFixtures(scriptType);
79+
await assertEqualFixture(`${__dirname}/fixtures/outputScripts/${scriptType}.json`, fixtures);
80+
});
81+
82+
it('key order is different in plain and xonly views', function () {
83+
// plain view has user key before bitgo key
84+
assert.strictEqual(userKey.publicKey.compare(bitgoKey.publicKey), -1);
85+
assert.strictEqual(bitgoKey.publicKey.compare(userKey.publicKey), 1);
86+
// xonly view has bitgo key before user key
87+
assert.strictEqual(userKey.publicKey.subarray(1).compare(bitgoKey.publicKey.subarray(1)), 1);
88+
assert.strictEqual(bitgoKey.publicKey.subarray(1).compare(userKey.publicKey.subarray(1)), -1);
89+
});
90+
91+
it('has expected aggregate key', function () {
92+
const p2tr = createPaymentP2trCommon(scriptType, pubkeys);
93+
const { internalPubkey } = p2tr;
94+
assert.ok(internalPubkey, 'internalPubkey is required');
95+
const [user, , bitgo] = pubkeys;
96+
const keyPathKeys = [user, bitgo];
97+
switch (scriptType) {
98+
case 'p2tr':
99+
const keyPathXonly = keyPathKeys.map(toXOnlyPublicKey);
100+
assert.strictEqual(
101+
internalPubkey.toString('hex'),
102+
Buffer.from(taproot.aggregateMuSigPubkeys(ecc, keyPathXonly)).toString('hex')
103+
);
104+
assert.strictEqual(
105+
internalPubkey.toString('hex'),
106+
Buffer.from(taproot.aggregateMuSigPubkeys(ecc, keyPathXonly.slice().reverse())).toString('hex'),
107+
'aggregation insensitive to key ordering'
108+
);
109+
break;
110+
case 'p2trMusig2':
111+
assert.strictEqual(
112+
internalPubkey.toString('hex'),
113+
Buffer.from(musig.getXOnlyPubkey(musig.keyAgg(keyPathKeys))).toString('hex')
114+
);
115+
assert.notStrictEqual(
116+
internalPubkey.toString('hex'),
117+
Buffer.from(musig.getXOnlyPubkey(musig.keyAgg(keyPathKeys.slice().reverse()))).toString('hex'),
118+
'aggregation is sensitive to key ordering'
119+
);
120+
break;
121+
default:
122+
throw new Error(`unexpected scriptType: ${scriptType}`);
123+
}
124+
});
125+
});
126+
}
127+
128+
describeWithScriptType('p2tr');
129+
describeWithScriptType('p2trMusig2');

0 commit comments

Comments
 (0)