1
- import { Transaction , UTXO } from './bitcoin/schema' ;
2
1
import pLimit from 'p-limit' ;
3
2
import asyncRetry from 'async-retry' ;
4
- import { Cradle } from '../container ' ;
3
+ import * as Sentry from '@sentry/node ' ;
5
4
import {
6
5
IndexerCell ,
7
- btcTxIdFromBtcTimeLockArgs ,
6
+ leToU128 ,
7
+ isScriptEqual ,
8
+ buildPreLockArgs ,
8
9
buildRgbppLockArgs ,
9
10
genRgbppLockScript ,
11
+ getRgbppLockScript ,
12
+ genBtcTimeLockArgs ,
10
13
getBtcTimeLockScript ,
11
- isScriptEqual ,
12
- leToU128 ,
14
+ btcTxIdFromBtcTimeLockArgs ,
15
+ calculateCommitment ,
16
+ BTCTimeLock ,
17
+ RGBPP_TX_ID_PLACEHOLDER ,
18
+ RGBPP_TX_INPUTS_MAX_LENGTH ,
13
19
} from '@rgbpp-sdk/ckb' ;
14
- import * as Sentry from '@sentry/node ' ;
15
- import { BI , RPC , Script } from '@ckb-lumos/lumos ' ;
16
- import { Job } from 'bullmq ' ;
20
+ import { remove0x } from '@rgbpp-sdk/btc ' ;
21
+ import { unpackRgbppLockArgs } from '@rgbpp-sdk/btc/lib/ckb/molecule ' ;
22
+ import { groupBy , cloneDeep , uniq } from 'lodash ' ;
17
23
import { z } from 'zod' ;
24
+ import { Job } from 'bullmq' ;
25
+ import { BI , RPC , Script } from '@ckb-lumos/lumos' ;
26
+ import { TransactionWithStatus } from '@ckb-lumos/base' ;
27
+ import { computeScriptHash } from '@ckb-lumos/lumos/utils' ;
18
28
import { Cell , XUDTBalance } from '../routes/rgbpp/types' ;
29
+ import { Transaction , UTXO } from './bitcoin/schema' ;
19
30
import BaseQueueWorker from './base/queue-worker' ;
20
31
import DataCache from './base/data-cache' ;
21
- import { groupBy } from 'lodash' ;
22
- import { computeScriptHash } from '@ckb-lumos/lumos/utils' ;
23
- import { remove0x } from '@rgbpp-sdk/btc' ;
32
+ import { Cradle } from '../container' ;
24
33
import { TestnetTypeMap } from '../constants' ;
25
- import { TransactionWithStatus } from '@ckb-lumos/base ' ;
34
+ import { tryGetCommitmentFromBtcTx } from '../utils/commitment ' ;
26
35
27
36
type GetCellsParams = Parameters < RPC [ 'getCells' ] > ;
28
37
export type SearchKey = GetCellsParams [ 0 ] ;
@@ -59,10 +68,10 @@ class RgbppCollectorError extends Error {
59
68
/**
60
69
* RgbppCollector is used to collect the cells for the utxos.
61
70
* The cells are stored in the cache with the btc address as the key,
62
- * will be recollect when the utxos are updated or new collect job is enqueued.
71
+ * will be recollected when the utxos are updated or new collect job is enqueued.
63
72
*/
64
73
export default class RgbppCollector extends BaseQueueWorker < IRgbppCollectRequest , IRgbppCollectJobReturn > {
65
- private limit : pLimit . Limit ;
74
+ private readonly limit : pLimit . Limit ;
66
75
private dataCache : DataCache < IRgbppCollectJobReturn > ;
67
76
68
77
constructor ( private cradle : Cradle ) {
@@ -83,6 +92,30 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
83
92
this . limit = pLimit ( 100 ) ;
84
93
}
85
94
95
+ private get isMainnet ( ) {
96
+ return this . cradle . env . NETWORK === 'mainnet' ;
97
+ }
98
+
99
+ private get testnetType ( ) {
100
+ return TestnetTypeMap [ this . cradle . env . NETWORK ] ;
101
+ }
102
+
103
+ private get rgbppLockScript ( ) {
104
+ return getRgbppLockScript ( this . isMainnet , this . testnetType ) ;
105
+ }
106
+
107
+ private get btcTimeLockScript ( ) {
108
+ return getBtcTimeLockScript ( this . isMainnet , this . testnetType ) ;
109
+ }
110
+
111
+ private isRgbppLock ( lock : CKBComponents . Script ) {
112
+ return lock . codeHash === this . rgbppLockScript . codeHash && lock . hashType === this . rgbppLockScript . hashType ;
113
+ }
114
+
115
+ private isBtcTimeLock ( lock : CKBComponents . Script ) {
116
+ return lock . codeHash === this . btcTimeLockScript . codeHash && lock . hashType === this . btcTimeLockScript . hashType ;
117
+ }
118
+
86
119
/**
87
120
* Capture the exception to the sentry scope with the btc address and utxos
88
121
* @param job - the job that failed
@@ -151,21 +184,20 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
151
184
* @param typeScript - the type script to filter the cells
152
185
*/
153
186
public async getRgbppCellsByBatchRequest ( utxos : UTXO [ ] , typeScript ?: Script ) {
154
- const network = this . cradle . env . NETWORK ;
155
187
const batchRequest : CKBBatchRequest = this . cradle . ckb . rpc . createBatchRequest (
156
188
utxos . map ( ( utxo : UTXO ) => {
157
189
const { txid, vout } = utxo ;
158
190
const args = buildRgbppLockArgs ( vout , txid ) ;
159
191
const searchKey : SearchKey = {
160
- script : genRgbppLockScript ( args , network === 'mainnet' , TestnetTypeMap [ network ] ) ,
192
+ script : genRgbppLockScript ( args , this . isMainnet , this . testnetType ) ,
161
193
scriptType : 'lock' ,
162
194
} ;
163
195
if ( typeScript ) {
164
196
searchKey . filter = {
165
197
script : typeScript ,
166
198
} ;
167
199
}
168
- // TOOD : In extreme cases, the num of search target cells may be more than limit=0x64=100
200
+ // TODO : In extreme cases, the num of search target cells may be more than limit=0x64=100
169
201
// Priority: Low
170
202
const params : GetCellsParams = [ searchKey , 'desc' , '0x64' ] ;
171
203
return [ 'getCells' , ...params ] ;
@@ -240,55 +272,55 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
240
272
}
241
273
242
274
public async queryRgbppLockTxByBtcTx ( btcTx : Transaction ) {
243
- const network = this . cradle . env . NETWORK ;
244
- const isMainnet = this . cradle . env . NETWORK === 'mainnet' ;
245
-
275
+ // Only query the first RGBPP_TX_INPUTS_MAX_LENGTH transactions for performance reasons
276
+ const maxRecords = `0x${ RGBPP_TX_INPUTS_MAX_LENGTH . toString ( 16 ) } ` ;
246
277
const batchRequest = this . cradle . ckb . rpc . createBatchRequest (
247
278
btcTx . vout . map ( ( _ , index ) => {
248
279
const args = buildRgbppLockArgs ( index , btcTx . txid ) ;
249
- const lock = genRgbppLockScript ( args , isMainnet , TestnetTypeMap [ network ] ) ;
280
+ const lock = genRgbppLockScript ( args , this . isMainnet , this . testnetType ) ;
250
281
const searchKey : SearchKey = {
251
282
script : lock ,
252
283
scriptType : 'lock' ,
253
284
} ;
254
- return [ 'getTransactions' , searchKey , 'desc ' , '0x1' ] ;
285
+ return [ 'getTransactions' , searchKey , 'asc ' , maxRecords ] ;
255
286
} ) ,
256
287
) ;
257
288
type getTransactionsResult = ReturnType < typeof this . cradle . ckb . rpc . getTransactions < false > > ;
258
289
const transactions : Awaited < getTransactionsResult > [ ] = await batchRequest . exec ( ) ;
259
- for ( const { objects } of transactions ) {
260
- if ( objects . length > 0 ) {
261
- const [ tx ] = objects ;
262
- return tx ;
290
+ for ( const tx of transactions ) {
291
+ for ( const indexerTx of tx . objects ) {
292
+ const ckbTx = await this . cradle . ckb . rpc . getTransaction ( indexerTx . txHash ) ;
293
+ const isIsomorphic = await this . isIsomorphicTx ( btcTx , ckbTx . transaction ) ;
294
+ // console.log('isIsomorphic', btcTx.txid, ckbTx.transaction.hash, isIsomorphic);
295
+ if ( isIsomorphic ) {
296
+ return indexerTx ;
297
+ }
263
298
}
264
299
}
265
300
return null ;
266
301
}
267
302
268
303
public async queryBtcTimeLockTxByBtcTxId ( btcTxId : string ) {
269
- const isMainnet = this . cradle . env . NETWORK === 'mainnet' ;
270
304
// XXX: unstable, need to be improved: https://github.com/ckb-cell/btc-assets-api/issues/45
271
- const btcTimeLockScript = getBtcTimeLockScript ( isMainnet ) ;
272
305
const btcTimeLockTxs = await this . cradle . ckb . indexer . getTransactions ( {
273
306
script : {
274
- ...btcTimeLockScript ,
307
+ ...this . btcTimeLockScript ,
275
308
args : '0x' ,
276
309
} ,
277
310
scriptType : 'lock' ,
278
311
} ) ;
279
312
280
- const batchRequest = this . cradle . ckb . rpc . createBatchRequest (
281
- btcTimeLockTxs . objects . map ( ( { txHash } ) => [ 'getTransaction' , txHash ] ) ,
282
- ) ;
313
+ const txHashes = uniq ( btcTimeLockTxs . objects . map ( ( { txHash } ) => txHash ) ) ;
314
+ const batchRequest = this . cradle . ckb . rpc . createBatchRequest ( txHashes . map ( ( txHash ) => [ 'getTransaction' , txHash ] ) ) ;
283
315
const transactions : TransactionWithStatus [ ] = await batchRequest . exec ( ) ;
284
316
if ( transactions . length > 0 ) {
285
317
for ( const tx of transactions ) {
286
318
const isBtcTimeLockTx = tx . transaction . outputs . some ( ( output ) => {
287
- if ( ! isScriptEqual ( output . lock , btcTimeLockScript ) ) {
319
+ if ( ! isScriptEqual ( output . lock , this . btcTimeLockScript ) ) {
288
320
return false ;
289
321
}
290
- const btcTxid = btcTxIdFromBtcTimeLockArgs ( output . lock . args ) ;
291
- return remove0x ( btcTxid ) === btcTxId ;
322
+ const outputBtcTxId = btcTxIdFromBtcTimeLockArgs ( output . lock . args ) ;
323
+ return remove0x ( outputBtcTxId ) === btcTxId ;
292
324
} ) ;
293
325
if ( isBtcTimeLockTx ) {
294
326
return tx ;
@@ -298,9 +330,112 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
298
330
return null ;
299
331
}
300
332
333
+ async isIsomorphicTx ( btcTx : Transaction , ckbTx : CKBComponents . RawTransaction , validateCommitment ?: boolean ) {
334
+ const replaceLockArgsWithPlaceholder = ( cell : CKBComponents . CellOutput , index : number ) => {
335
+ if ( this . isRgbppLock ( cell . lock ) ) {
336
+ cell . lock . args = buildPreLockArgs ( index + 1 ) ;
337
+ }
338
+ if ( this . isBtcTimeLock ( cell . lock ) ) {
339
+ const lockArgs = BTCTimeLock . unpack ( cell . lock . args ) ;
340
+ cell . lock . args = genBtcTimeLockArgs ( lockArgs . lockScript , RGBPP_TX_ID_PLACEHOLDER , lockArgs . after ) ;
341
+ }
342
+ return cell ;
343
+ } ;
344
+
345
+ // Find the commitment from the btc_tx
346
+ const btcTxCommitment = tryGetCommitmentFromBtcTx ( btcTx ) ;
347
+ if ( ! btcTxCommitment ) {
348
+ return false ;
349
+ }
350
+
351
+ // Check inputs:
352
+ // 1. Find the last index of the type inputs
353
+ // 2. Check if all rgbpp_lock inputs can be found in the btc_tx.vin
354
+ // 3. Check if the inputs contain at least one rgbpp_lock cell (as L1-L1 and L1-L2 transactions should have)
355
+ let lastTypeInputIndex = - 1 ;
356
+ let foundRgbppLockInput = false ;
357
+ const outPoints = ckbTx . inputs . map ( ( input ) => input . previousOutput ! ) ;
358
+ const inputs = await this . cradle . ckb . getInputCellsByOutPoint ( outPoints ) ;
359
+ for ( let i = 0 ; i < inputs . length ; i ++ ) {
360
+ if ( inputs [ i ] . type ) {
361
+ lastTypeInputIndex = i ;
362
+ const isRgbppLock = this . isRgbppLock ( inputs [ i ] . lock ) ;
363
+ if ( isRgbppLock ) {
364
+ foundRgbppLockInput = true ;
365
+ const btcInput = btcTx . vin [ i ] ;
366
+ const rgbppLockArgs = unpackRgbppLockArgs ( inputs [ i ] . lock . args ) ;
367
+ if (
368
+ ! btcInput ||
369
+ btcInput . txid !== remove0x ( rgbppLockArgs . btcTxid ) ||
370
+ btcInput . vout !== rgbppLockArgs . outIndex
371
+ ) {
372
+ return false ;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ // XXX: In some type of RGB++ transactions, the inputs may not contain any rgbpp_lock cells
378
+ // We add this check to ensure this function only validates for L1-L1 and L1-L2 transactions
379
+ if ( ! foundRgbppLockInput ) {
380
+ return false ;
381
+ }
382
+
383
+ // Check outputs:
384
+ // 1. Find the last index of the type outputs
385
+ // 2. Check if all type outputs are rgbpp_lock/btc_time_lock cells
386
+ // 3. Check if each rgbpp_lock cell has an isomorphic UTXO in the btc_tx.vout
387
+ // 4. Check if each btc_time_lock cell contains the corresponding btc_txid in the lock args
388
+ // 5. Check if the outputs contain at least one rgbpp_lock/btc_time_lock cell
389
+ let lastTypeOutputIndex = - 1 ;
390
+ for ( let i = 0 ; i < ckbTx . outputs . length ; i ++ ) {
391
+ const ckbOutput = ckbTx . outputs [ i ] ;
392
+ const isRgbppLock = this . isRgbppLock ( ckbOutput . lock ) ;
393
+ const isBtcTimeLock = this . isBtcTimeLock ( ckbOutput . lock ) ;
394
+ if ( isRgbppLock ) {
395
+ const rgbppLockArgs = unpackRgbppLockArgs ( ckbOutput . lock . args ) ;
396
+ const btcTxId = remove0x ( rgbppLockArgs . btcTxid ) ;
397
+ if ( btcTxId !== RGBPP_TX_ID_PLACEHOLDER && ( btcTxId !== btcTx . txid || ! btcTx . vout [ rgbppLockArgs . outIndex ] ) ) {
398
+ return false ;
399
+ }
400
+ }
401
+ if ( isBtcTimeLock ) {
402
+ const btcTxId = remove0x ( btcTxIdFromBtcTimeLockArgs ( ckbOutput . lock . args ) ) ;
403
+ if ( btcTxId !== RGBPP_TX_ID_PLACEHOLDER && btcTx . txid !== btcTxId ) {
404
+ return false ;
405
+ }
406
+ }
407
+ if ( ckbOutput . type ) {
408
+ lastTypeOutputIndex = i ;
409
+ }
410
+ }
411
+ if ( lastTypeOutputIndex < 0 ) {
412
+ return false ;
413
+ }
414
+
415
+ // Cut the ckb_tx to simulate how the ckb_virtual_tx looks like
416
+ const ckbVirtualTx = cloneDeep ( ckbTx ) ;
417
+ ckbVirtualTx . inputs = ckbVirtualTx . inputs . slice ( 0 , Math . max ( lastTypeInputIndex , 0 ) + 1 ) ;
418
+ ckbVirtualTx . outputs = ckbVirtualTx . outputs . slice ( 0 , lastTypeOutputIndex + 1 ) . map ( replaceLockArgsWithPlaceholder ) ;
419
+
420
+ // Copy ckb_tx and change output lock args to placeholder args
421
+ const ckbPlaceholderTx = cloneDeep ( ckbTx ) ;
422
+ ckbPlaceholderTx . outputs = ckbPlaceholderTx . outputs . map ( replaceLockArgsWithPlaceholder ) ;
423
+ if ( ! validateCommitment ) {
424
+ return true ;
425
+ }
426
+
427
+ // Generate commitment with the ckb_tx/ckb_virtual_tx, then compare it with the btc_tx commitment.
428
+ // If both commitments don't match the btc_tx commitment:
429
+ // 1. The ckb_tx is not the isomorphic transaction of the btc_tx (this is the usual case)
430
+ // 2. The commitment calculation logic differs from the one used in the btc_tx/ckb_tx
431
+ const ckbTxCommitment = calculateCommitment ( ckbPlaceholderTx ) ;
432
+ const ckbVirtualTxCommitment = calculateCommitment ( ckbVirtualTx ) ;
433
+ const btcTxCommitmentHex = btcTxCommitment . toString ( 'hex' ) ;
434
+ return btcTxCommitmentHex === ckbVirtualTxCommitment || btcTxCommitmentHex === ckbTxCommitment ;
435
+ }
436
+
301
437
/**
302
438
* Enqueue a collect job to the queue
303
- * @param utxos - the utxos to collect
304
439
*/
305
440
public async enqueueCollectJob ( btcAddress : string , allowDuplicate ?: boolean ) : Promise < Job < IRgbppCollectRequest > > {
306
441
let jobId = btcAddress ;
0 commit comments