Skip to content

Commit 56157b3

Browse files
fix(utxo-coredao): clarifications from the coredao team
TICKET: BTC-1578
1 parent 1d7fba1 commit 56157b3

File tree

2 files changed

+140
-38
lines changed

2 files changed

+140
-38
lines changed

modules/utxo-coredao/src/transaction.ts

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,66 @@
11
// Source: https://docs.coredao.org/docs/Learn/products/btc-staking/design
2-
export const CORE_DAO_TESTNET_CHAIN_ID = Buffer.alloc(2, 0x1115);
3-
export const CORE_DAO_MAINNET_CHAIN_ID = Buffer.alloc(2, 0x1116);
4-
export const CORE_DAO_SATOSHI_PLUS_IDENTIFIER = Buffer.alloc(4, 0x5341542b);
2+
export const CORE_DAO_TESTNET_CHAIN_ID = Buffer.from('045b', 'hex');
3+
export const CORE_DAO_MAINNET_CHAIN_ID = Buffer.from('045c', 'hex');
4+
export const CORE_DAO_SATOSHI_PLUS_IDENTIFIER = Buffer.from('5341542b', 'hex');
5+
// https://github.com/bitcoin/bitcoin/blob/5961b23898ee7c0af2626c46d5d70e80136578d3/src/script/script.h#L47
6+
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');
10+
11+
export function encodeTimelock(timelock: number): Buffer {
12+
const buff = Buffer.alloc(4);
13+
buff.writeUInt32LE(timelock);
14+
return buff;
15+
}
16+
17+
export function decodeTimelock(buffer: Buffer): number {
18+
if (buffer.length !== 4) {
19+
throw new Error('Invalid timelock buffer length');
20+
}
21+
return buffer.readUInt32LE();
22+
}
23+
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+
}
564

665
type BaseParams = {
766
version: number;
@@ -43,7 +102,7 @@ export function createCoreDaoOpReturnOutputScript({
43102
* LENGTH: which represents the total byte length after the OP_RETURN opcode. Note that all data has to be pushed with its appropriate size byte(s). [1]
44103
* Satoshi Plus Identifier: (SAT+) 4 bytes
45104
* Version: (0x01) 1 byte
46-
* Chain ID: (0x1115 for Core Testnet and 0x1116 for Core Mainnet) 2 bytes
105+
* Chain ID: (0x045b (1115) for Core Testnet and 0x045c (1116) for Core Mainnet) 2 bytes
47106
* Delegator: The Core address to receive rewards, 20 bytes
48107
* Validator: The Core validator address to stake to, 20 bytes
49108
* Fee: Fee for relayer, 1 byte, range [0,255], measured in CORE
@@ -88,36 +147,25 @@ export function createCoreDaoOpReturnOutputScript({
88147
if ('timelock' in rest && (rest.timelock < 0 || rest.timelock > 4294967295)) {
89148
throw new Error('Invalid timelock - out of range');
90149
}
91-
const timelockBuffer = 'timelock' in rest ? Buffer.alloc(4, rest.timelock).reverse() : Buffer.from([]);
92150

93-
const totalLength =
151+
// encode the number into a 4-byte buffer
152+
// if timelock is provided, write it into 32-bit little-endian
153+
const timelockBuffer = 'timelock' in rest ? encodeTimelock(rest.timelock) : Buffer.from([]);
154+
155+
const lengthBuffer = encodeOpReturnLength(
94156
CORE_DAO_SATOSHI_PLUS_IDENTIFIER.length +
95-
versionBuffer.length +
96-
chainId.length +
97-
delegator.length +
98-
validator.length +
99-
feeBuffer.length +
100-
redeemScriptBuffer.length +
101-
timelockBuffer.length +
102-
// This is to account for the LENGTH byte
103-
1;
104-
105-
// If the length is >= 0x4c (76), we need to use the OP_PUSHDATA (0x4c) opcode and then the length
106-
const totalLengthBuffer =
107-
totalLength >= 76
108-
? Buffer.concat([
109-
Buffer.from([0x4c]),
110-
Buffer.alloc(
111-
1,
112-
// This is to account for the extra OP_PUSHDATA byte
113-
totalLength + 1
114-
),
115-
])
116-
: Buffer.alloc(1, totalLength);
157+
versionBuffer.length +
158+
chainId.length +
159+
delegator.length +
160+
validator.length +
161+
feeBuffer.length +
162+
redeemScriptBuffer.length +
163+
timelockBuffer.length
164+
);
117165

118166
return Buffer.concat([
119-
Buffer.from([0x6a]),
120-
totalLengthBuffer,
167+
OP_RETURN_IDENTIFIER,
168+
lengthBuffer,
121169
CORE_DAO_SATOSHI_PLUS_IDENTIFIER,
122170
versionBuffer,
123171
chainId,

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

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
CORE_DAO_MAINNET_CHAIN_ID,
44
CORE_DAO_SATOSHI_PLUS_IDENTIFIER,
55
createCoreDaoOpReturnOutputScript,
6+
decodeTimelock,
7+
encodeTimelock,
68
} from '../../src';
79
import { testutil } from '@bitgo/utxo-lib';
810

@@ -19,6 +21,9 @@ describe('OP_RETURN', function () {
1921
'hex'
2022
);
2123
const validTimelock = 800800;
24+
// https://docs.coredao.org/docs/Learn/products/btc-staking/design#op_return-output-1
25+
const defaultScript =
26+
'6a4c505341542b01045bde60b7d0e6b758ca5dd8c61d377a2c5f1af51ec1a9e209f5ea0036c8c2f41078a3cebee57d8a47d501041f5e0e66b17576a914c4b8ae927ff2b9ce218e20bf06d425d6b68424fd88ac';
2227

2328
describe('createCoreDaoOpReturnOutputScript', function () {
2429
it('should throw if invalid parameters are passed', function () {
@@ -97,8 +102,8 @@ describe('OP_RETURN', function () {
97102
assert.strictEqual(script[0], 0x6a);
98103
// Make sure that the length of the script matches what is in the buffer
99104
assert.strictEqual(
100-
// We do not count the OP_RETURN opcode
101-
script.length - 1,
105+
// We do not count the OP_RETURN opcode or the bytes for the length
106+
script.length - 2,
102107
script[1]
103108
);
104109
});
@@ -118,8 +123,8 @@ describe('OP_RETURN', function () {
118123
// Make sure that the length of the script matches what is in the buffer
119124
assert.strictEqual(script[1], 0x4c);
120125
assert.strictEqual(
121-
// We do not count the OP_RETURN opcode
122-
script.length - 1,
126+
// We do not count the OP_RETURN opcode or the length + pushbytes
127+
script.length - 3,
123128
script[2]
124129
);
125130
// Satoshi plus identifier
@@ -155,8 +160,8 @@ describe('OP_RETURN', function () {
155160
assert.strictEqual(script[0], 0x6a);
156161
// Make sure that the length of the script matches what is in the buffer
157162
assert.strictEqual(
158-
// We do not count the OP_RETURN opcode
159-
script.length - 1,
163+
// We do not count the OP_RETURN opcode or the length
164+
script.length - 2,
160165
script[1]
161166
);
162167
// Satoshi plus identifier
@@ -172,10 +177,59 @@ describe('OP_RETURN', function () {
172177
// Make sure that the fee is correct
173178
assert.strictEqual(script[49], validFee);
174179
// Make sure that the redeemScript is correct
180+
assert.deepStrictEqual(script.subarray(50, 54).toString('hex'), encodeTimelock(validTimelock).toString('hex'));
181+
assert.deepStrictEqual(decodeTimelock(script.subarray(50, 54)), validTimelock);
182+
});
183+
184+
it('should recreate the example OP_RETURN correctly', function () {
175185
assert.deepStrictEqual(
176-
script.subarray(50, 54).reverse().toString('hex'),
177-
Buffer.alloc(4, validTimelock).toString('hex')
186+
createCoreDaoOpReturnOutputScript({
187+
version: 1,
188+
chainId: Buffer.from('045b', 'hex'),
189+
delegator: Buffer.from('de60b7d0e6b758ca5dd8c61d377a2c5f1af51ec1', 'hex'),
190+
validator: Buffer.from('a9e209f5ea0036c8c2f41078a3cebee57d8a47d5', 'hex'),
191+
fee: 1,
192+
redeemScript: Buffer.from('041f5e0e66b17576a914c4b8ae927ff2b9ce218e20bf06d425d6b68424fd88ac', 'hex'),
193+
}).toString('hex'),
194+
// Source: https://docs.coredao.org/docs/Learn/products/btc-staking/design#op_return-output-1
195+
defaultScript
178196
);
179197
});
198+
199+
it('should create a OP_RETURN with the extra long length identifier', function () {
200+
const redeemScriptPushdata2 = Buffer.alloc(265, 0);
201+
const scriptPushdata2 = createCoreDaoOpReturnOutputScript({
202+
version: validVersion,
203+
chainId: validChainId,
204+
delegator: validDelegator,
205+
validator: validValidator,
206+
fee: validFee,
207+
redeemScript: redeemScriptPushdata2,
208+
});
209+
210+
// Make sure that the first byte is the OP_RETURN opcode
211+
assert.strictEqual(scriptPushdata2[0], 0x6a);
212+
// Make sure that there is the OP_PUSHDATA2 identifier
213+
assert.strictEqual(scriptPushdata2[1], 0x4d);
214+
// We do not count the OP_RETURN opcode or the bytes for the length
215+
assert.strictEqual(scriptPushdata2.readInt16BE(2), scriptPushdata2.length - 4);
216+
217+
const redeemScriptPushdata4 = Buffer.alloc(65540, 0);
218+
const scriptPushdata4 = createCoreDaoOpReturnOutputScript({
219+
version: validVersion,
220+
chainId: validChainId,
221+
delegator: validDelegator,
222+
validator: validValidator,
223+
fee: validFee,
224+
redeemScript: redeemScriptPushdata4,
225+
});
226+
227+
// Make sure that the first byte is the OP_RETURN opcode
228+
assert.strictEqual(scriptPushdata4[0], 0x6a);
229+
// Make sure that there is the OP_PUSHDATA4 identifier
230+
assert.strictEqual(scriptPushdata4[1], 0x4e);
231+
// We do not count the OP_RETURN opcode or the bytes for the length
232+
assert.strictEqual(scriptPushdata4.readInt32BE(2), scriptPushdata4.length - 6);
233+
});
180234
});
181235
});

0 commit comments

Comments
 (0)