Skip to content

Commit 774bcce

Browse files
chore(utxo-coredao): use payments.embed to make op-return
TICKET: BTC-1578
1 parent 86018a6 commit 774bcce

File tree

2 files changed

+50
-118
lines changed

2 files changed

+50
-118
lines changed

modules/utxo-coredao/src/opReturn.ts

Lines changed: 36 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import { payments, networks } from '@bitgo/utxo-lib';
2+
13
// Source: https://docs.coredao.org/docs/Learn/products/btc-staking/design
24
export const CORE_DAO_TESTNET_CHAIN_ID = Buffer.from('045b', 'hex');
35
export const CORE_DAO_MAINNET_CHAIN_ID = Buffer.from('045c', 'hex');
46
export const CORE_DAO_SATOSHI_PLUS_IDENTIFIER = Buffer.from('5341542b', 'hex');
57
// https://github.com/bitcoin/bitcoin/blob/5961b23898ee7c0af2626c46d5d70e80136578d3/src/script/script.h#L47
68
const OP_RETURN_IDENTIFIER = Buffer.from('6a', 'hex');
7-
const OP_PUSHDATA1_IDENTIFIER = Buffer.from('4c', 'hex');
8-
const OP_PUSHDATA2_IDENTIFIER = Buffer.from('4d', 'hex');
9-
const OP_PUSHDATA4_IDENTIFIER = Buffer.from('4e', 'hex');
109

1110
export function encodeTimelock(timelock: number): Buffer {
1211
const buff = Buffer.alloc(4);
@@ -21,47 +20,6 @@ export function decodeTimelock(buffer: Buffer): number {
2120
return buffer.readUInt32LE();
2221
}
2322

24-
export function encodeOpReturnLength(length: number): Buffer {
25-
/**
26-
* Any bytes with lengths smaller than 0x4c (76) is pushed with 1 byte equal to the size (byte[10] -> 10 + byte[10]; byte[70] -> 70 + byte[70])
27-
* Any bytes bigger than or equal to 0x4c is pushed by using 0x4c (ie. OP_PUSHDATA) followed by the length followed by the data (byte[80] -> OP_PUSHDATA + 80 + byte[80])
28-
* Any bytes with length bigger than 255 uses 0x4d (OP_PUSHDATA2)
29-
* Any bytes with length bigger than 65535 (0xffff) uses 0x4e (OP_PUSHDATA4)
30-
*/
31-
if (length < 76) {
32-
return Buffer.alloc(1, length);
33-
} else if (length < 255) {
34-
return Buffer.concat([OP_PUSHDATA1_IDENTIFIER, Buffer.alloc(1, length)]);
35-
} else if (length < 65535) {
36-
const buff = Buffer.alloc(2);
37-
buff.writeUInt16BE(length);
38-
return Buffer.concat([OP_PUSHDATA2_IDENTIFIER, buff]);
39-
} else {
40-
const buff = Buffer.alloc(4);
41-
buff.writeUInt32BE(length);
42-
return Buffer.concat([OP_PUSHDATA4_IDENTIFIER, buff]);
43-
}
44-
}
45-
46-
/**
47-
* Decode the length of an OP_RETURN output script
48-
* @param buffer
49-
* @returns { length: number, offset: number } Length of the OP_RETURN output script and the offset
50-
*/
51-
export function decodeOpReturnLength(buffer: Buffer): { length: number; offset: number } {
52-
if (buffer[0] < 0x4c) {
53-
return { length: buffer[0], offset: 1 };
54-
} else if (buffer[0] === 0x4c) {
55-
return { length: buffer[1], offset: 2 };
56-
} else if (buffer[0] === 0x4d) {
57-
return { length: buffer.readUInt16BE(1), offset: 3 };
58-
} else if (buffer[0] === 0x4e) {
59-
return { length: buffer.readUInt32BE(1), offset: 5 };
60-
} else {
61-
throw new Error('Invalid length');
62-
}
63-
}
64-
6523
type BaseParams = {
6624
version: number;
6725
chainId: Buffer;
@@ -151,21 +109,7 @@ export function createCoreDaoOpReturnOutputScript({
151109
// encode the number into a 4-byte buffer
152110
// if timelock is provided, write it into 32-bit little-endian
153111
const timelockBuffer = 'timelock' in rest ? encodeTimelock(rest.timelock) : Buffer.from([]);
154-
155-
const lengthBuffer = encodeOpReturnLength(
156-
CORE_DAO_SATOSHI_PLUS_IDENTIFIER.length +
157-
versionBuffer.length +
158-
chainId.length +
159-
delegator.length +
160-
validator.length +
161-
feeBuffer.length +
162-
redeemScriptBuffer.length +
163-
timelockBuffer.length
164-
);
165-
166-
return Buffer.concat([
167-
OP_RETURN_IDENTIFIER,
168-
lengthBuffer,
112+
const data = Buffer.concat([
169113
CORE_DAO_SATOSHI_PLUS_IDENTIFIER,
170114
versionBuffer,
171115
chainId,
@@ -175,6 +119,19 @@ export function createCoreDaoOpReturnOutputScript({
175119
redeemScriptBuffer,
176120
timelockBuffer,
177121
]);
122+
if (data.length > 80) {
123+
throw new Error('OP_RETURN outputs cannot have a length larger than 80 bytes');
124+
}
125+
126+
const payment = payments.embed({
127+
data: [data],
128+
network: chainId.equals(CORE_DAO_TESTNET_CHAIN_ID) ? networks.testnet : networks.bitcoin,
129+
});
130+
if (!payment.output) {
131+
throw new Error('Unable to create OP_RETURN output');
132+
}
133+
134+
return payment.output;
178135
}
179136

180137
/**
@@ -183,33 +140,35 @@ export function createCoreDaoOpReturnOutputScript({
183140
* @returns OpReturnParams
184141
*/
185142
export function parseCoreDaoOpReturnOutputScript(script: Buffer): OpReturnParams {
186-
// OP_RETURN
187-
let offset = 0;
188143
if (!script.subarray(0, 1).equals(OP_RETURN_IDENTIFIER)) {
189144
throw new Error('First byte must be an OP_RETURN');
190145
}
191-
offset += 1;
192146

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})`);
147+
const payment = payments.embed({
148+
output: script,
149+
});
150+
const data = payment.data;
151+
if (!data || data.length !== 1) {
152+
throw new Error('Invalid OP_RETURN output');
198153
}
199-
offset += lengthOffset;
154+
const dataBuffer = data[0];
155+
if (dataBuffer.length > 80) {
156+
throw new Error(`OP_RETURN outputs cannot have a length larger than 80 bytes`);
157+
}
158+
let offset = 0;
200159

201160
// Decode satoshi+ identifier
202-
if (!script.subarray(offset, offset + 4).equals(CORE_DAO_SATOSHI_PLUS_IDENTIFIER)) {
161+
if (!dataBuffer.subarray(offset, offset + 4).equals(CORE_DAO_SATOSHI_PLUS_IDENTIFIER)) {
203162
throw new Error('Invalid satoshi+ identifier');
204163
}
205164
offset += 4;
206165

207166
// Decode version
208-
const version = script[offset];
167+
const version = dataBuffer[offset];
209168
offset += 1;
210169

211170
// Decode chainId
212-
const chainId = Buffer.from(script.subarray(offset, offset + 2));
171+
const chainId = Buffer.from(dataBuffer.subarray(offset, offset + 2));
213172
if (!(chainId.equals(CORE_DAO_TESTNET_CHAIN_ID) || chainId.equals(CORE_DAO_MAINNET_CHAIN_ID))) {
214173
throw new Error(
215174
`Invalid ChainID: ${chainId.toString('hex')}. Must be either 0x045b (testnet) or 0x045c (mainnet).`
@@ -218,24 +177,24 @@ export function parseCoreDaoOpReturnOutputScript(script: Buffer): OpReturnParams
218177
offset += 2;
219178

220179
// Decode delegator
221-
const delegator = Buffer.from(script.subarray(offset, offset + 20));
180+
const delegator = Buffer.from(dataBuffer.subarray(offset, offset + 20));
222181
offset += 20;
223182

224183
// Decode validator
225-
const validator = Buffer.from(script.subarray(offset, offset + 20));
184+
const validator = Buffer.from(dataBuffer.subarray(offset, offset + 20));
226185
offset += 20;
227186

228187
// Decode fee
229-
const fee = script[offset];
188+
const fee = dataBuffer[offset];
230189
offset += 1;
231190

232191
const baseParams = { version, chainId, delegator, validator, fee };
233192

234193
// Decode redeemScript or timelock
235-
if (offset === script.length - 4) {
236-
return { ...baseParams, timelock: decodeTimelock(script.subarray(offset)) };
194+
if (offset === dataBuffer.length - 4) {
195+
return { ...baseParams, timelock: decodeTimelock(dataBuffer.subarray(offset)) };
237196
} else {
238-
return { ...baseParams, redeemScript: Buffer.from(script.subarray(offset)) };
197+
return { ...baseParams, redeemScript: Buffer.from(dataBuffer.subarray(offset)) };
239198
}
240199
}
241200

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

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ describe('OP_RETURN', function () {
1818
const validDelegator = Buffer.alloc(20, testutil.getKey('wasm-possum').publicKey);
1919
const validValidator = Buffer.alloc(20, testutil.getKey('possum-wasm').publicKey);
2020
const validFee = 1;
21-
// p2sh 2-3 script
22-
const validRedeemScript = Buffer.from(
23-
'522103a8295453660d5e212d4aaf82e8254e27e4c6752b2afa36e648537b644a6ca2702103099e28dd8bcb345e655b5312db0a574dded8f740eeef636cf45317bf010452982102a45b464fed0167d175d89bbc31d7ba1c288b52f64270c7eddaafa38803c5a6b553ae',
24-
'hex'
25-
);
21+
const validRedeemScript = Buffer.from('522103a8295453660d5e212d55556666666666666666666666666666666666', 'hex');
2622
const validTimelock = 800800;
2723
// https://docs.coredao.org/docs/Learn/products/btc-staking/design#op_return-output-1
2824
const defaultScript =
@@ -92,6 +88,19 @@ describe('OP_RETURN', function () {
9288
);
9389
});
9490

91+
it('should throw if the length of the script is too long', function () {
92+
assert.throws(() =>
93+
createCoreDaoOpReturnOutputScript({
94+
version: validVersion,
95+
chainId: validChainId,
96+
delegator: validDelegator,
97+
validator: validValidator,
98+
fee: validFee,
99+
redeemScript: Buffer.alloc(100),
100+
})
101+
);
102+
});
103+
95104
it('should return a buffer with the correct length', function () {
96105
const script = createCoreDaoOpReturnOutputScript({
97106
version: validVersion,
@@ -198,42 +207,6 @@ describe('OP_RETURN', function () {
198207
defaultScript
199208
);
200209
});
201-
202-
it('should create a OP_RETURN with the extra long length identifier', function () {
203-
const redeemScriptPushdata2 = Buffer.alloc(265, 0);
204-
const scriptPushdata2 = createCoreDaoOpReturnOutputScript({
205-
version: validVersion,
206-
chainId: validChainId,
207-
delegator: validDelegator,
208-
validator: validValidator,
209-
fee: validFee,
210-
redeemScript: redeemScriptPushdata2,
211-
});
212-
213-
// Make sure that the first byte is the OP_RETURN opcode
214-
assert.strictEqual(scriptPushdata2[0], 0x6a);
215-
// Make sure that there is the OP_PUSHDATA2 identifier
216-
assert.strictEqual(scriptPushdata2[1], 0x4d);
217-
// We do not count the OP_RETURN opcode or the bytes for the length
218-
assert.strictEqual(scriptPushdata2.readInt16BE(2), scriptPushdata2.length - 4);
219-
220-
const redeemScriptPushdata4 = Buffer.alloc(65540, 0);
221-
const scriptPushdata4 = createCoreDaoOpReturnOutputScript({
222-
version: validVersion,
223-
chainId: validChainId,
224-
delegator: validDelegator,
225-
validator: validValidator,
226-
fee: validFee,
227-
redeemScript: redeemScriptPushdata4,
228-
});
229-
230-
// Make sure that the first byte is the OP_RETURN opcode
231-
assert.strictEqual(scriptPushdata4[0], 0x6a);
232-
// Make sure that there is the OP_PUSHDATA4 identifier
233-
assert.strictEqual(scriptPushdata4[1], 0x4e);
234-
// We do not count the OP_RETURN opcode or the bytes for the length
235-
assert.strictEqual(scriptPushdata4.readInt32BE(2), scriptPushdata4.length - 6);
236-
});
237210
});
238211

239212
describe('parseCoreDaoOpReturnOutputScript', function () {

0 commit comments

Comments
 (0)