1- import { Transaction , UTXO } from './bitcoin/schema' ;
21import pLimit from 'p-limit' ;
32import asyncRetry from 'async-retry' ;
4- import { Cradle } from '../container ' ;
3+ import * as Sentry from '@sentry/node ' ;
54import {
65 IndexerCell ,
7- btcTxIdFromBtcTimeLockArgs ,
6+ leToU128 ,
7+ isScriptEqual ,
8+ buildPreLockArgs ,
89 buildRgbppLockArgs ,
910 genRgbppLockScript ,
11+ getRgbppLockScript ,
12+ genBtcTimeLockArgs ,
1013 getBtcTimeLockScript ,
11- isScriptEqual ,
12- leToU128 ,
14+ btcTxIdFromBtcTimeLockArgs ,
15+ calculateCommitment ,
16+ BTCTimeLock ,
17+ RGBPP_TX_ID_PLACEHOLDER ,
18+ RGBPP_TX_INPUTS_MAX_LENGTH ,
1319} 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 ' ;
1723import { 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' ;
1828import { Cell , XUDTBalance } from '../routes/rgbpp/types' ;
29+ import { Transaction , UTXO } from './bitcoin/schema' ;
1930import BaseQueueWorker from './base/queue-worker' ;
2031import 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' ;
2433import { TestnetTypeMap } from '../constants' ;
25- import { TransactionWithStatus } from '@ckb-lumos/base ' ;
34+ import { tryGetCommitmentFromBtcTx } from '../utils/commitment ' ;
2635
2736type GetCellsParams = Parameters < RPC [ 'getCells' ] > ;
2837export type SearchKey = GetCellsParams [ 0 ] ;
@@ -59,10 +68,10 @@ class RgbppCollectorError extends Error {
5968/**
6069 * RgbppCollector is used to collect the cells for the utxos.
6170 * 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.
6372 */
6473export default class RgbppCollector extends BaseQueueWorker < IRgbppCollectRequest , IRgbppCollectJobReturn > {
65- private limit : pLimit . Limit ;
74+ private readonly limit : pLimit . Limit ;
6675 private dataCache : DataCache < IRgbppCollectJobReturn > ;
6776
6877 constructor ( private cradle : Cradle ) {
@@ -83,6 +92,30 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
8392 this . limit = pLimit ( 100 ) ;
8493 }
8594
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+
86119 /**
87120 * Capture the exception to the sentry scope with the btc address and utxos
88121 * @param job - the job that failed
@@ -151,21 +184,20 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
151184 * @param typeScript - the type script to filter the cells
152185 */
153186 public async getRgbppCellsByBatchRequest ( utxos : UTXO [ ] , typeScript ?: Script ) {
154- const network = this . cradle . env . NETWORK ;
155187 const batchRequest : CKBBatchRequest = this . cradle . ckb . rpc . createBatchRequest (
156188 utxos . map ( ( utxo : UTXO ) => {
157189 const { txid, vout } = utxo ;
158190 const args = buildRgbppLockArgs ( vout , txid ) ;
159191 const searchKey : SearchKey = {
160- script : genRgbppLockScript ( args , network === 'mainnet' , TestnetTypeMap [ network ] ) ,
192+ script : genRgbppLockScript ( args , this . isMainnet , this . testnetType ) ,
161193 scriptType : 'lock' ,
162194 } ;
163195 if ( typeScript ) {
164196 searchKey . filter = {
165197 script : typeScript ,
166198 } ;
167199 }
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
169201 // Priority: Low
170202 const params : GetCellsParams = [ searchKey , 'desc' , '0x64' ] ;
171203 return [ 'getCells' , ...params ] ;
@@ -240,55 +272,55 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
240272 }
241273
242274 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 ) } ` ;
246277 const batchRequest = this . cradle . ckb . rpc . createBatchRequest (
247278 btcTx . vout . map ( ( _ , index ) => {
248279 const args = buildRgbppLockArgs ( index , btcTx . txid ) ;
249- const lock = genRgbppLockScript ( args , isMainnet , TestnetTypeMap [ network ] ) ;
280+ const lock = genRgbppLockScript ( args , this . isMainnet , this . testnetType ) ;
250281 const searchKey : SearchKey = {
251282 script : lock ,
252283 scriptType : 'lock' ,
253284 } ;
254- return [ 'getTransactions' , searchKey , 'desc ' , '0x1' ] ;
285+ return [ 'getTransactions' , searchKey , 'asc ' , maxRecords ] ;
255286 } ) ,
256287 ) ;
257288 type getTransactionsResult = ReturnType < typeof this . cradle . ckb . rpc . getTransactions < false > > ;
258289 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+ }
263298 }
264299 }
265300 return null ;
266301 }
267302
268303 public async queryBtcTimeLockTxByBtcTxId ( btcTxId : string ) {
269- const isMainnet = this . cradle . env . NETWORK === 'mainnet' ;
270304 // XXX: unstable, need to be improved: https://github.com/ckb-cell/btc-assets-api/issues/45
271- const btcTimeLockScript = getBtcTimeLockScript ( isMainnet ) ;
272305 const btcTimeLockTxs = await this . cradle . ckb . indexer . getTransactions ( {
273306 script : {
274- ...btcTimeLockScript ,
307+ ...this . btcTimeLockScript ,
275308 args : '0x' ,
276309 } ,
277310 scriptType : 'lock' ,
278311 } ) ;
279312
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 ] ) ) ;
283315 const transactions : TransactionWithStatus [ ] = await batchRequest . exec ( ) ;
284316 if ( transactions . length > 0 ) {
285317 for ( const tx of transactions ) {
286318 const isBtcTimeLockTx = tx . transaction . outputs . some ( ( output ) => {
287- if ( ! isScriptEqual ( output . lock , btcTimeLockScript ) ) {
319+ if ( ! isScriptEqual ( output . lock , this . btcTimeLockScript ) ) {
288320 return false ;
289321 }
290- const btcTxid = btcTxIdFromBtcTimeLockArgs ( output . lock . args ) ;
291- return remove0x ( btcTxid ) === btcTxId ;
322+ const outputBtcTxId = btcTxIdFromBtcTimeLockArgs ( output . lock . args ) ;
323+ return remove0x ( outputBtcTxId ) === btcTxId ;
292324 } ) ;
293325 if ( isBtcTimeLockTx ) {
294326 return tx ;
@@ -298,9 +330,112 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
298330 return null ;
299331 }
300332
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+
301437 /**
302438 * Enqueue a collect job to the queue
303- * @param utxos - the utxos to collect
304439 */
305440 public async enqueueCollectJob ( btcAddress : string , allowDuplicate ?: boolean ) : Promise < Job < IRgbppCollectRequest > > {
306441 let jobId = btcAddress ;
0 commit comments