Skip to content

Commit 4c26416

Browse files
Merge pull request #5087 from BitGo/BTC-1578-descriptor
chore(utxo-coredao): add fixture from testnet3
2 parents a67b54e + bb5be2f commit 4c26416

File tree

9 files changed

+89
-10
lines changed

9 files changed

+89
-10
lines changed

modules/utxo-coredao/src/descriptor.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@ import { BIP32Interface } from '@bitgo/utxo-lib';
55
*/
66
export type ScriptType = 'sh' | 'sh-wsh';
77

8+
function asDescriptorKey(key: BIP32Interface | Buffer, neutered: boolean): string {
9+
if (Buffer.isBuffer(key)) {
10+
return key.toString('hex');
11+
}
12+
return (neutered ? key.neutered() : key).toBase58() + '/*';
13+
}
14+
815
/**
916
* Create a multi-sig descriptor to produce a coredao staking address
1017
* @param scriptType segwit or legacy
1118
* @param locktime locktime for CLTV
1219
* @param m Total number of keys required to unlock
13-
* @param orderedKeys
20+
* @param orderedKeys If Bip32Interfaces, these are xprvs or xpubs and are derivable.
21+
* If they are buffers, then they are pub/prv keys and are not derivable.
1422
* @param neutered If true, neuter the keys. Default to true
1523
*/
1624
export function createMultiSigDescriptor(
1725
scriptType: ScriptType,
1826
locktime: number,
1927
m: number,
20-
orderedKeys: BIP32Interface[],
28+
orderedKeys: (BIP32Interface | Buffer)[],
2129
neutered = true
2230
): string {
2331
if (m > orderedKeys.length || m < 1) {
@@ -28,8 +36,7 @@ export function createMultiSigDescriptor(
2836
if (locktime <= 0) {
2937
throw new Error(`locktime (${locktime}) must be greater than 0`);
3038
}
31-
32-
const xpubs = orderedKeys.map((key) => (neutered ? key.neutered() : key).toBase58() + '/*');
33-
const inner = `and_v(r:after(${locktime}),multi(${m},${xpubs.join(',')}))`;
39+
const keys = orderedKeys.map((key) => asDescriptorKey(key, neutered));
40+
const inner = `and_v(r:after(${locktime}),multi(${m},${keys.join(',')}))`;
3441
return scriptType === 'sh' ? `sh(${inner})` : `sh(wsh(${inner}))`;
3542
}

modules/utxo-coredao/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './opReturn';
2+
export * from './descriptor';

modules/utxo-coredao/src/opReturn.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,15 @@ export function parseCoreDaoOpReturnOutputScript(script: Buffer): OpReturnParams
194194
return { ...baseParams, redeemScript: Buffer.from(dataBuffer.subarray(offset)) };
195195
}
196196
}
197+
198+
export function toString(params: OpReturnParams): string {
199+
return JSON.stringify({
200+
version: params.version,
201+
chainId: params.chainId.toString('hex'),
202+
delegator: params.delegator.toString('hex'),
203+
validator: params.validator.toString('hex'),
204+
fee: params.fee,
205+
...('redeemScript' in params ? { redeemScript: params.redeemScript.toString('hex') } : {}),
206+
...('timelock' in params ? { timelock: params.timelock } : {}),
207+
});
208+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"version": 1,
3+
"chainId": "045b",
4+
"delegator": "2dda5d5d3bec673033b5f21ca07c77695404f491",
5+
"validator": "2953559db5cc88ab20b1960faa9793803d070337",
6+
"fee": 0,
7+
"timelock": 1725503428
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6a345341542b01045b2dda5d5d3bec673033b5f21ca07c77695404f4912953559db5cc88ab20b1960faa9793803d07033700c417d966
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6a4c505341542b01045bde60b7d0e6b758ca5dd8c61d377a2c5f1af51ec1a9e209f5ea0036c8c2f41078a3cebee57d8a47d501041f5e0e66b17576a914c4b8ae927ff2b9ce218e20bf06d425d6b68424fd88ac

modules/utxo-coredao/test/unit/descriptor.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Descriptor } from '@bitgo/wasm-miniscript';
44

55
import { createMultiSigDescriptor } from '../../src/descriptor';
66
import { finalizePsbt, getFixture, updateInputWithDescriptor } from './utils';
7+
import { decodeTimelock } from '../../src';
78

89
describe('descriptor', function () {
910
const baseFixturePath = 'test/fixtures/descriptor/';
@@ -114,4 +115,36 @@ describe('descriptor', function () {
114115
runTestForParams('sh', 2, [key1, key2]);
115116
runTestForParams('sh-wsh', 2, [key1, key2]);
116117
runTestForParams('sh', 3, [key1, key2, key3]);
118+
119+
it('should recreate the script used in testnet staking transaction', function () {
120+
// Source: https://mempool.space/testnet/address/2MxTi2EhHKgdJFKRTBttVGGxir9ZzjmKCXw
121+
// 2 of 2 multisig
122+
const timelock = 'fce4cb66';
123+
const pubkey1 = '03ecb6d4b7f5d56962e547fc52dd588359f5729c0ba856d6978b84723895a16691';
124+
const pubkey2 = '024aaea25d82b1db2be030a05b641d6302e48ed652b1ca9cb08a67267fcbb56747';
125+
const redeemScriptASM = [
126+
'OP_PUSHBYTES_4',
127+
timelock,
128+
'OP_CLTV',
129+
'OP_DROP',
130+
'OP_PUSHNUM_2',
131+
'OP_PUSHBYTES_33',
132+
pubkey1,
133+
'OP_PUSHBYTES_33',
134+
pubkey2,
135+
'OP_PUSHNUM_2',
136+
'OP_CHECKMULTISIG',
137+
].join(' ');
138+
139+
const decodedTimelock = decodeTimelock(Buffer.from(timelock, 'hex'));
140+
const descriptor = createMultiSigDescriptor(
141+
'sh',
142+
decodedTimelock,
143+
2,
144+
[Buffer.from(pubkey1, 'hex'), Buffer.from(pubkey2, 'hex')],
145+
false
146+
);
147+
const descriptorASM = Descriptor.fromString(descriptor, 'definite').toAsmString();
148+
assert.deepStrictEqual(redeemScriptASM, descriptorASM);
149+
});
117150
});

modules/utxo-coredao/test/unit/opReturn.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
decodeTimelock,
77
encodeTimelock,
88
parseCoreDaoOpReturnOutputScript,
9+
toString,
910
} from '../../src';
1011
import { testutil } from '@bitgo/utxo-lib';
12+
import { getFixture } from './utils';
1113

1214
describe('OP_RETURN', function () {
1315
const validVersion = 2;
@@ -18,9 +20,14 @@ describe('OP_RETURN', function () {
1820
const validFee = 1;
1921
const validRedeemScript = Buffer.from('522103a8295453660d5e212d55556666666666666666666666666666666666', 'hex');
2022
const validTimelock = 800800;
21-
// https://docs.coredao.org/docs/Learn/products/btc-staking/design#op_return-output-1
22-
const defaultScript =
23-
'6a4c505341542b01045bde60b7d0e6b758ca5dd8c61d377a2c5f1af51ec1a9e209f5ea0036c8c2f41078a3cebee57d8a47d501041f5e0e66b17576a914c4b8ae927ff2b9ce218e20bf06d425d6b68424fd88ac';
23+
let defaultScript: string;
24+
25+
before(async function () {
26+
// https://docs.coredao.org/docs/Learn/products/btc-staking/design#op_return-output-1
27+
const script = await getFixture('test/fixtures/opReturn/documentation.txt', undefined);
28+
assert(typeof script === 'string');
29+
defaultScript = script;
30+
});
2431

2532
describe('createCoreDaoOpReturnOutputScript', function () {
2633
it('should throw if invalid parameters are passed', function () {
@@ -201,7 +208,6 @@ describe('OP_RETURN', function () {
201208
fee: 1,
202209
redeemScript: Buffer.from('041f5e0e66b17576a914c4b8ae927ff2b9ce218e20bf06d425d6b68424fd88ac', 'hex'),
203210
}).toString('hex'),
204-
// Source: https://docs.coredao.org/docs/Learn/products/btc-staking/design#op_return-output-1
205211
defaultScript
206212
);
207213
});
@@ -246,6 +252,16 @@ describe('OP_RETURN', function () {
246252
assert.deepStrictEqual(parsed.redeemScript, validRedeemScript);
247253
});
248254

255+
it('should parse valid opreturn script from testnet', async function () {
256+
// Source: https://mempool.space/testnet/tx/66ed4cea26a410248a6d87f14b2bca514f33920c54d4af63ed46a903793115d5
257+
const baseFixturePath = 'test/fixtures/opReturn/66ed4cea26a410248a6d87f14b2bca514f33920c54d4af63ed46a903793115d5';
258+
const opReturnHex = await getFixture(baseFixturePath + '.txt', undefined);
259+
assert(typeof opReturnHex === 'string');
260+
const parsed = parseCoreDaoOpReturnOutputScript(Buffer.from(opReturnHex, 'hex'));
261+
const parsedFixture = await getFixture(baseFixturePath + '.json', JSON.parse(toString(parsed)));
262+
assert.deepStrictEqual(toString(parsed), JSON.stringify(parsedFixture));
263+
});
264+
249265
it('should fail if there is an invalid op-return', function () {
250266
const script = defaultScript.replace('6a4c50', '6b4c50');
251267
assert.throws(() => parseCoreDaoOpReturnOutputScript(Buffer.from(script, 'hex')));

modules/utxo-coredao/test/unit/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ function encode(path: string, defaultValue: unknown): string {
77
return String(defaultValue);
88
}
99
if (path.endsWith('.json')) {
10-
return JSON.stringify(defaultValue, null, 2);
10+
return JSON.stringify(defaultValue, null, 2) + '\n';
1111
}
1212
throw new Error(`unrecognized path ${path}`);
1313
}

0 commit comments

Comments
 (0)