1+ import { payments , networks } from '@bitgo/utxo-lib' ;
2+
13// Source: https://docs.coredao.org/docs/Learn/products/btc-staking/design
24export const CORE_DAO_TESTNET_CHAIN_ID = Buffer . from ( '045b' , 'hex' ) ;
35export const CORE_DAO_MAINNET_CHAIN_ID = Buffer . from ( '045c' , 'hex' ) ;
46export const CORE_DAO_SATOSHI_PLUS_IDENTIFIER = Buffer . from ( '5341542b' , 'hex' ) ;
57// https://github.com/bitcoin/bitcoin/blob/5961b23898ee7c0af2626c46d5d70e80136578d3/src/script/script.h#L47
68const 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
1110export 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-
6523type 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 */
185142export 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
0 commit comments