Skip to content

Commit 69e9954

Browse files
committed
fix: validate L1-L1 and L1-L2 isomorphic transactions in depth for the activity API
1 parent a857c78 commit 69e9954

File tree

2 files changed

+987
-2586
lines changed

2 files changed

+987
-2586
lines changed

src/services/rgbpp.ts

Lines changed: 171 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +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,
6+
leToU128,
7+
isScriptEqual,
8+
buildPreLockArgs,
79
buildRgbppLockArgs,
810
genRgbppLockScript,
11+
getRgbppLockScript,
12+
genBtcTimeLockArgs,
913
getBtcTimeLockScript,
10-
isScriptEqual,
11-
leToU128,
14+
btcTxIdFromBtcTimeLockArgs,
15+
calculateCommitment,
16+
BTCTimeLock,
17+
RGBPP_TX_ID_PLACEHOLDER,
18+
RGBPP_TX_INPUTS_MAX_LENGTH,
1219
} from '@rgbpp-sdk/ckb';
13-
import * as Sentry from '@sentry/node';
14-
import { BI, RPC, Script } from '@ckb-lumos/lumos';
15-
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';
1623
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';
1728
import { Cell, XUDTBalance } from '../routes/rgbpp/types';
29+
import { Transaction, UTXO } from './bitcoin/schema';
1830
import BaseQueueWorker from './base/queue-worker';
1931
import DataCache from './base/data-cache';
20-
import { groupBy } from 'lodash';
21-
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
22-
import { remove0x } from '@rgbpp-sdk/btc';
32+
import { Cradle } from '../container';
2333
import { TestnetTypeMap } from '../constants';
24-
import { TransactionWithStatus } from '@ckb-lumos/base';
34+
import { tryGetCommitmentFromBtcTx } from '../utils/commitment';
2535

2636
type GetCellsParams = Parameters<RPC['getCells']>;
2737
type SearchKey = GetCellsParams[0];
@@ -58,10 +68,10 @@ class RgbppCollectorError extends Error {
5868
/**
5969
* RgbppCollector is used to collect the cells for the utxos.
6070
* The cells are stored in the cache with the btc address as the key,
61-
* 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.
6272
*/
6373
export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest, IRgbppCollectJobReturn> {
64-
private limit: pLimit.Limit;
74+
private readonly limit: pLimit.Limit;
6575
private dataCache: DataCache<IRgbppCollectJobReturn>;
6676

6777
constructor(private cradle: Cradle) {
@@ -82,6 +92,30 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
8292
this.limit = pLimit(100);
8393
}
8494

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+
85119
/**
86120
* Capture the exception to the sentry scope with the btc address and utxos
87121
* @param job - the job that failed
@@ -150,21 +184,20 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
150184
* @param typeScript - the type script to filter the cells
151185
*/
152186
public async getRgbppCellsByBatchRequest(utxos: UTXO[], typeScript?: Script) {
153-
const network = this.cradle.env.NETWORK;
154187
const batchRequest: CKBBatchRequest = this.cradle.ckb.rpc.createBatchRequest(
155188
utxos.map((utxo: UTXO) => {
156189
const { txid, vout } = utxo;
157190
const args = buildRgbppLockArgs(vout, txid);
158191
const searchKey: SearchKey = {
159-
script: genRgbppLockScript(args, network === 'mainnet', TestnetTypeMap[network]),
192+
script: genRgbppLockScript(args, this.isMainnet, this.testnetType),
160193
scriptType: 'lock',
161194
};
162195
if (typeScript) {
163196
searchKey.filter = {
164197
script: typeScript,
165198
};
166199
}
167-
// 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
168201
// Priority: Low
169202
const params: GetCellsParams = [searchKey, 'desc', '0x64'];
170203
return ['getCells', ...params];
@@ -239,55 +272,55 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
239272
}
240273

241274
public async queryRgbppLockTxByBtcTx(btcTx: Transaction) {
242-
const network = this.cradle.env.NETWORK;
243-
const isMainnet = this.cradle.env.NETWORK === 'mainnet';
244-
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)}`;
245277
const batchRequest = this.cradle.ckb.rpc.createBatchRequest(
246278
btcTx.vout.map((_, index) => {
247279
const args = buildRgbppLockArgs(index, btcTx.txid);
248-
const lock = genRgbppLockScript(args, isMainnet, TestnetTypeMap[network]);
280+
const lock = genRgbppLockScript(args, this.isMainnet, this.testnetType);
249281
const searchKey: SearchKey = {
250282
script: lock,
251283
scriptType: 'lock',
252284
};
253-
return ['getTransactions', searchKey, 'desc', '0x1'];
285+
return ['getTransactions', searchKey, 'asc', maxRecords];
254286
}),
255287
);
256288
type getTransactionsResult = ReturnType<typeof this.cradle.ckb.rpc.getTransactions<false>>;
257289
const transactions: Awaited<getTransactionsResult>[] = await batchRequest.exec();
258-
for (const { objects } of transactions) {
259-
if (objects.length > 0) {
260-
const [tx] = objects;
261-
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+
}
262298
}
263299
}
264300
return null;
265301
}
266302

267303
public async queryBtcTimeLockTxByBtcTxId(btcTxId: string) {
268-
const isMainnet = this.cradle.env.NETWORK === 'mainnet';
269304
// XXX: unstable, need to be improved: https://github.com/ckb-cell/btc-assets-api/issues/45
270-
const btcTimeLockScript = getBtcTimeLockScript(isMainnet);
271305
const btcTimeLockTxs = await this.cradle.ckb.indexer.getTransactions({
272306
script: {
273-
...btcTimeLockScript,
307+
...this.btcTimeLockScript,
274308
args: '0x',
275309
},
276310
scriptType: 'lock',
277311
});
278312

279-
const batchRequest = this.cradle.ckb.rpc.createBatchRequest(
280-
btcTimeLockTxs.objects.map(({ txHash }) => ['getTransaction', txHash]),
281-
);
313+
const txHashes = uniq(btcTimeLockTxs.objects.map(({ txHash }) => txHash));
314+
const batchRequest = this.cradle.ckb.rpc.createBatchRequest(txHashes.map((txHash) => ['getTransaction', txHash]));
282315
const transactions: TransactionWithStatus[] = await batchRequest.exec();
283316
if (transactions.length > 0) {
284317
for (const tx of transactions) {
285318
const isBtcTimeLockTx = tx.transaction.outputs.some((output) => {
286-
if (!isScriptEqual(output.lock, btcTimeLockScript)) {
319+
if (!isScriptEqual(output.lock, this.btcTimeLockScript)) {
287320
return false;
288321
}
289-
const btcTxid = remove0x(btcTxId);
290-
return remove0x(btcTxid) === btcTxId;
322+
const outputBtcTxId = btcTxIdFromBtcTimeLockArgs(output.lock.args);
323+
return remove0x(outputBtcTxId) === btcTxId;
291324
});
292325
if (isBtcTimeLockTx) {
293326
return tx;
@@ -297,9 +330,112 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
297330
return null;
298331
}
299332

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+
300437
/**
301438
* Enqueue a collect job to the queue
302-
* @param utxos - the utxos to collect
303439
*/
304440
public async enqueueCollectJob(btcAddress: string, allowDuplicate?: boolean): Promise<Job<IRgbppCollectRequest>> {
305441
let jobId = btcAddress;

0 commit comments

Comments
 (0)