Skip to content

Commit f32729d

Browse files
feat(utxo-coredao): parse and verify op-return
TICKET: BTC-1578
1 parent 56157b3 commit f32729d

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed

modules/utxo-coredao/src/transaction.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,80 @@ export function createCoreDaoOpReturnOutputScript({
176176
timelockBuffer,
177177
]);
178178
}
179+
180+
/**
181+
* Parse a CoreDAO OP_RETURN output script into the constituent parts
182+
* @param script
183+
* @returns OpReturnParams
184+
*/
185+
export function parseCoreDaoOpReturnOutputScript(script: Buffer): OpReturnParams {
186+
// OP_RETURN
187+
let offset = 0;
188+
if (!script.subarray(0, 1).equals(OP_RETURN_IDENTIFIER)) {
189+
throw new Error('First byte must be an OP_RETURN');
190+
}
191+
offset += 1;
192+
193+
// Decode Length
194+
const { length, offset: lengthOffset } = decodeOpReturnLength(script.subarray(offset));
195+
// Do not include the OP_RETURN identifier and the length bytes itself in the length
196+
if (script.length - lengthOffset - 1 !== length) {
197+
throw new Error(`Length ${length} does not match script length (${script.length})`);
198+
}
199+
offset += lengthOffset;
200+
201+
// Decode satoshi+ identifier
202+
if (!script.subarray(offset, offset + 4).equals(CORE_DAO_SATOSHI_PLUS_IDENTIFIER)) {
203+
throw new Error('Invalid satoshi+ identifier');
204+
}
205+
offset += 4;
206+
207+
// Decode version
208+
const version = script[offset];
209+
offset += 1;
210+
211+
// Decode chainId
212+
const chainId = Buffer.from(script.subarray(offset, offset + 2));
213+
if (!(chainId.equals(CORE_DAO_TESTNET_CHAIN_ID) || chainId.equals(CORE_DAO_MAINNET_CHAIN_ID))) {
214+
throw new Error(
215+
`Invalid ChainID: ${chainId.toString('hex')}. Must be either 0x1115 (testnet) or 0x1116 (mainnet).`
216+
);
217+
}
218+
offset += 2;
219+
220+
// Decode delegator
221+
const delegator = Buffer.from(script.subarray(offset, offset + 20));
222+
offset += 20;
223+
224+
// Decode validator
225+
const validator = Buffer.from(script.subarray(offset, offset + 20));
226+
offset += 20;
227+
228+
// Decode fee
229+
const fee = script[offset];
230+
offset += 1;
231+
232+
const baseParams = { version, chainId, delegator, validator, fee };
233+
234+
// Decode redeemScript or timelock
235+
if (offset === script.length - 4) {
236+
return { ...baseParams, timelock: decodeTimelock(script.subarray(offset)) };
237+
} else {
238+
return { ...baseParams, redeemScript: Buffer.from(script.subarray(offset)) };
239+
}
240+
}
241+
242+
/**
243+
* Check that the params are valid and that they make the given script
244+
* @param script
245+
* @param params
246+
*/
247+
export function verifyCoreDaoOpReturnOutputScript(script: Buffer, params: OpReturnParams): boolean {
248+
try {
249+
parseCoreDaoOpReturnOutputScript(script);
250+
const inferredScript = createCoreDaoOpReturnOutputScript(params);
251+
return inferredScript.equals(script);
252+
} catch (e) {
253+
return false;
254+
}
255+
}

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

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import * as assert from 'assert';
22
import {
33
CORE_DAO_MAINNET_CHAIN_ID,
44
CORE_DAO_SATOSHI_PLUS_IDENTIFIER,
5+
CORE_DAO_TESTNET_CHAIN_ID,
56
createCoreDaoOpReturnOutputScript,
67
decodeTimelock,
78
encodeTimelock,
9+
parseCoreDaoOpReturnOutputScript,
10+
verifyCoreDaoOpReturnOutputScript,
811
} from '../../src';
912
import { testutil } from '@bitgo/utxo-lib';
1013

@@ -232,4 +235,121 @@ describe('OP_RETURN', function () {
232235
assert.strictEqual(scriptPushdata4.readInt32BE(2), scriptPushdata4.length - 6);
233236
});
234237
});
238+
239+
describe('parseCoreDaoOpReturnOutputScript', function () {
240+
it('should parse a valid script with a timelock', function () {
241+
const script = createCoreDaoOpReturnOutputScript({
242+
version: validVersion,
243+
chainId: validChainId,
244+
delegator: validDelegator,
245+
validator: validValidator,
246+
fee: validFee,
247+
timelock: validTimelock,
248+
});
249+
const parsed = parseCoreDaoOpReturnOutputScript(script);
250+
assert.strictEqual(parsed.version, validVersion);
251+
assert.deepStrictEqual(parsed.chainId, validChainId);
252+
assert.deepStrictEqual(parsed.delegator, validDelegator);
253+
assert.deepStrictEqual(parsed.validator, validValidator);
254+
assert.strictEqual(parsed.fee, validFee);
255+
assert('timelock' in parsed);
256+
assert.deepStrictEqual(parsed.timelock, validTimelock);
257+
});
258+
259+
it('should parse a valid script with a redeem script', function () {
260+
const script = createCoreDaoOpReturnOutputScript({
261+
version: validVersion,
262+
chainId: validChainId,
263+
delegator: validDelegator,
264+
validator: validValidator,
265+
fee: validFee,
266+
redeemScript: validRedeemScript,
267+
});
268+
const parsed = parseCoreDaoOpReturnOutputScript(script);
269+
assert.strictEqual(parsed.version, validVersion);
270+
assert.deepStrictEqual(parsed.chainId, validChainId);
271+
assert.deepStrictEqual(parsed.delegator, validDelegator);
272+
assert.deepStrictEqual(parsed.validator, validValidator);
273+
assert.strictEqual(parsed.fee, validFee);
274+
assert('redeemScript' in parsed);
275+
assert.deepStrictEqual(parsed.redeemScript, validRedeemScript);
276+
});
277+
278+
it('should fail if there is an invalid op-return', function () {
279+
const script = defaultScript.replace('6a4c50', '6b4c50');
280+
assert.throws(() => parseCoreDaoOpReturnOutputScript(Buffer.from(script, 'hex')));
281+
});
282+
283+
it('should fail if the length is incorrect', function () {
284+
const script = defaultScript.replace('4c50', '4c51');
285+
assert.throws(() => parseCoreDaoOpReturnOutputScript(Buffer.from(script, 'hex')));
286+
});
287+
288+
it('should fail if the satoshi+ identifier is incorrect', function () {
289+
const script = defaultScript.replace('5341542b', '5341532b');
290+
assert.throws(() => parseCoreDaoOpReturnOutputScript(Buffer.from(script, 'hex')));
291+
});
292+
293+
it('should fail if the chainId is incorrect', function () {
294+
const script = defaultScript.replace('045b', '0454');
295+
assert.throws(() => parseCoreDaoOpReturnOutputScript(Buffer.from(script, 'hex')));
296+
});
297+
});
298+
299+
describe('verifyCoreDaoOpReturnOutputScript', function () {
300+
it('should return true for a valid script with a redeem script', function () {
301+
const params = {
302+
version: validVersion,
303+
chainId: validChainId,
304+
delegator: validDelegator,
305+
validator: validValidator,
306+
fee: validFee,
307+
redeemScript: validRedeemScript,
308+
};
309+
const script = createCoreDaoOpReturnOutputScript(params);
310+
assert.strictEqual(verifyCoreDaoOpReturnOutputScript(script, params), true);
311+
});
312+
313+
it('should return true for a valid script with a timelock', function () {
314+
const params = {
315+
version: validVersion,
316+
chainId: validChainId,
317+
delegator: validDelegator,
318+
validator: validValidator,
319+
fee: validFee,
320+
timelock: validTimelock,
321+
};
322+
const script = createCoreDaoOpReturnOutputScript(params);
323+
assert.strictEqual(verifyCoreDaoOpReturnOutputScript(script, params), true);
324+
});
325+
326+
it('should return false when they are not equivalent', function () {
327+
const params = {
328+
version: validVersion,
329+
chainId: validChainId,
330+
delegator: validDelegator,
331+
validator: validValidator,
332+
fee: validFee,
333+
redeemScript: validRedeemScript,
334+
};
335+
const script = createCoreDaoOpReturnOutputScript(params);
336+
// Change the version
337+
params.version = 3;
338+
assert.strictEqual(verifyCoreDaoOpReturnOutputScript(script, params), false);
339+
});
340+
341+
it('should fail if they one is mainnet and one is testnet', function () {
342+
const params = {
343+
version: validVersion,
344+
chainId: CORE_DAO_MAINNET_CHAIN_ID,
345+
delegator: validDelegator,
346+
validator: validValidator,
347+
fee: validFee,
348+
timelock: validTimelock,
349+
};
350+
const script = createCoreDaoOpReturnOutputScript(params);
351+
params.chainId = CORE_DAO_TESTNET_CHAIN_ID;
352+
assert.strictEqual(verifyCoreDaoOpReturnOutputScript(script, params), false);
353+
});
354+
});
235355
});

0 commit comments

Comments
 (0)