diff --git a/packages/tracker/src/common/constants.ts b/packages/tracker/src/common/constants.ts index dd2da66..89d4a50 100644 --- a/packages/tracker/src/common/constants.ts +++ b/packages/tracker/src/common/constants.ts @@ -49,6 +49,10 @@ export class Constants { static readonly TRANSFER_GUARD_MASK_OFFSET = this.TRANSFER_GUARD_AMOUNT_OFFSET + this.CONTRACT_OUTPUT_MAX_COUNT; + static readonly TRANSFER_GUARD_INPUT_TOKEN_SCRIPT_OFFSET = 26; + static readonly TRANSFER_GUARD_INPUT_AMOUNT_OFFSET = 27; + static readonly TOKEN_INPUT_MAX_COUNT = 6; + static readonly QUERY_PAGING_DEFAULT_OFFSET = 0; static readonly QUERY_PAGING_DEFAULT_LIMIT = 100; static readonly QUERY_PAGING_MAX_LIMIT = 500; diff --git a/packages/tracker/src/common/exceptions.ts b/packages/tracker/src/common/exceptions.ts index ab408b5..af8ef61 100644 --- a/packages/tracker/src/common/exceptions.ts +++ b/packages/tracker/src/common/exceptions.ts @@ -4,3 +4,10 @@ export class CatTxError extends Error { this.name = 'CatTxError'; } } + +export class TransferTxError extends Error { + constructor(message: string) { + super(message); + this.name = 'TransferTxError'; + } +} diff --git a/packages/tracker/src/services/tx/tx.module.ts b/packages/tracker/src/services/tx/tx.module.ts index 023ed96..0a2f514 100644 --- a/packages/tracker/src/services/tx/tx.module.ts +++ b/packages/tracker/src/services/tx/tx.module.ts @@ -5,10 +5,11 @@ import { TxEntity } from '../../entities/tx.entity'; import { TokenInfoEntity } from '../../entities/tokenInfo.entity'; import { CommonModule } from '../common/common.module'; import { ScheduleModule } from '@nestjs/schedule'; +import { TxOutEntity } from '../../entities/txOut.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([TxEntity, TokenInfoEntity]), + TypeOrmModule.forFeature([TxEntity, TokenInfoEntity, TxOutEntity]), CommonModule, ScheduleModule.forRoot(), ], diff --git a/packages/tracker/src/services/tx/tx.service.ts b/packages/tracker/src/services/tx/tx.service.ts index 6c4660d..42a4046 100644 --- a/packages/tracker/src/services/tx/tx.service.ts +++ b/packages/tracker/src/services/tx/tx.service.ts @@ -12,7 +12,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Constants } from '../../common/constants'; import { TokenInfoEntity } from '../../entities/tokenInfo.entity'; import { NftInfoEntity } from '../../entities/nftInfo.entity'; -import { CatTxError } from '../../common/exceptions'; +import { CatTxError, TransferTxError } from '../../common/exceptions'; import { parseTokenInfoEnvelope } from '../../common/utils'; import { BlockHeader, @@ -52,6 +52,8 @@ export class TxService { private tokenInfoEntityRepository: Repository, @InjectRepository(TxEntity) private txEntityRepository: Repository, + @InjectRepository(TxOutEntity) + private txOutEntityRepository: Repository, ) { this.dataSource = this.txEntityRepository.manager.connection; } @@ -141,12 +143,19 @@ export class TxService { await queryRunner.commitTransaction(); return Math.ceil(Date.now() - startTs); } catch (e) { - if (e instanceof CatTxError) { - this.logger.log(`skip tx ${tx.getId()}, ${e.message}`); + if (e instanceof TransferTxError) { + // do not rollback for TransferTxError + this.logger.error( + `[502750] invalid transfer tx ${tx.getId()}, ${e.message}`, + ); } else { - this.logger.error(`process tx ${tx.getId()} error, ${e.message}`); + if (e instanceof CatTxError) { + this.logger.log(`skip tx ${tx.getId()}, ${e.message}`); + } else { + this.logger.error(`process tx ${tx.getId()} error, ${e.message}`); + } + await queryRunner.rollbackTransaction(); } - await queryRunner.rollbackTransaction(); } finally { await queryRunner.release(); } @@ -659,6 +668,10 @@ export class TxService { if (this.commonService.isTransferGuard(guardInput)) { const tokenOutputs = this.commonService.parseTransferTxTokenOutputs(guardInput); + + // verify the token amount in guard witness equals to it in token input + await this.verifyGuardTokenAmount(guardInput, tx); + // save tx outputs promises.push( manager.save( @@ -676,6 +689,65 @@ export class TxService { return stateHashes; } + public async verifyGuardTokenAmount( + guardInput: TaprootPayment, + tx: Transaction, + ) { + const timeBefore = Date.now(); + const tokenScript = + guardInput.witness[ + Constants.TRANSFER_GUARD_INPUT_TOKEN_SCRIPT_OFFSET + ].toString('hex'); + const tokenAmounts = guardInput.witness.slice( + Constants.TRANSFER_GUARD_INPUT_AMOUNT_OFFSET, + Constants.TRANSFER_GUARD_INPUT_AMOUNT_OFFSET + + Constants.TOKEN_INPUT_MAX_COUNT, + ); + await Promise.all( + tokenAmounts.map(async (amount, i) => { + const tokenAmount = + amount.length === 0 ? 0n : BigInt(amount.readIntLE(0, amount.length)); + if (tokenAmount === 0n) { + return Promise.resolve(); + } + const input = tx.ins[i]; + const prevTxid = Buffer.from(input.hash).reverse().toString('hex'); + const prevOutputIndex = input.index; + const prevout = `${prevTxid}:${prevOutputIndex}`; + return this.txOutEntityRepository + .findOne({ + where: { + txid: prevTxid, + outputIndex: prevOutputIndex, + }, + }) + .then((prevOutput) => { + if (!prevOutput) { + this.logger.error(`prevout ${prevout} not found`); + throw new TransferTxError( + 'invalid transfer tx, token input prevout is missing', + ); + } + if (prevOutput.lockingScript !== tokenScript) { + this.logger.error(`prevout ${prevout} token script mismatches`); + throw new TransferTxError( + 'invalid transfer tx, token script in guard not equal to it in token input', + ); + } + if (BigInt(prevOutput.tokenAmount) !== tokenAmount) { + this.logger.error( + `prevout ${prevout} token amount mismatches, ${prevOutput.tokenAmount} !== ${tokenAmount}`, + ); + throw new TransferTxError( + 'invalid transfer tx, token amount in guard not equal to it in token input', + ); + } + }); + }), + ); + this.logger.debug(`verifyGuardTokenAmount: ${Date.now() - timeBefore} ms`); + } + /** * Parse state root hash from tx */