Skip to content

Commit 7d7a32f

Browse files
committed
fix: validate L1-L1 and L1-L2 isomorphic transactions in depth for the activity API
1 parent 50ff6c4 commit 7d7a32f

File tree

2 files changed

+987
-2587
lines changed

2 files changed

+987
-2587
lines changed

src/services/rgbpp.ts

+171-36
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
1-
import { Transaction, UTXO } from './bitcoin/schema';
21
import pLimit from 'p-limit';
32
import asyncRetry from 'async-retry';
4-
import { Cradle } from '../container';
3+
import * as Sentry from '@sentry/node';
54
import {
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';
1723
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';
1828
import { Cell, XUDTBalance } from '../routes/rgbpp/types';
29+
import { Transaction, UTXO } from './bitcoin/schema';
1930
import BaseQueueWorker from './base/queue-worker';
2031
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';
2433
import { TestnetTypeMap } from '../constants';
25-
import { TransactionWithStatus } from '@ckb-lumos/base';
34+
import { tryGetCommitmentFromBtcTx } from '../utils/commitment';
2635

2736
type GetCellsParams = Parameters<RPC['getCells']>;
2837
export 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
*/
6473
export 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

Comments
 (0)