Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions packages/btcindexer/src/btc-address-utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { Transaction, payments, script } from "bitcoinjs-lib";
import { Transaction, payments, script, type Network } from "bitcoinjs-lib";
import { logError } from "@gonative-cc/lib/logger";
import { BtcNet } from "@gonative-cc/lib/nbtc";
import { btcNetworkCfg } from "./models";
import type { Input } from "bitcoinjs-lib/src/transaction";

function extractAddressFromInput(input: Input, btcNetwork: BtcNet): string | undefined {
function extractAddressFromInput(input: Input, network: Network): string | undefined {
try {
const network = btcNetworkCfg[btcNetwork];

if (input.witness && input.witness.length >= 2) {
const pubKey = input.witness[1];
const { address } = payments.p2wpkh({ pubkey: pubKey, network });
Expand All @@ -28,12 +24,12 @@ function extractAddressFromInput(input: Input, btcNetwork: BtcNet): string | und
}
}

export function extractSenderAddresses(tx: Transaction, btcNetwork: BtcNet): string[] {
export function extractSenderAddresses(tx: Transaction, btcNet: Network): string[] {
if (tx.ins.length === 0) return [];

const addresses: string[] = [];
for (const input of tx.ins) {
const addr = extractAddressFromInput(input, btcNetwork);
const addr = extractAddressFromInput(input, btcNet);
if (addr) addresses.push(addr);
}
return addresses;
Expand Down
5 changes: 3 additions & 2 deletions packages/btcindexer/src/btcindexer.helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ export async function setupTestIndexerSuite(
} as unknown as Service<SuiIndexerRpc & WorkerEntrypoint>;

const mockComplianceService: ComplianceRpc = {
isBtcBlocked: (_btcAddresses: string[]): Promise<Record<string, boolean>> =>
Promise.resolve({ "0x12btc": false }),
isAnyBtcAddressSanctioned: (addrs: string[]): Promise<boolean> =>
// returns true if there is an address with last character being digit
Promise.resolve(addrs.findIndex((a) => /\d$/.test(a)) >= 0),
};

const indexer = new Indexer(
Expand Down
32 changes: 10 additions & 22 deletions packages/btcindexer/src/btcindexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,29 +392,17 @@ describe("Indexer.processFinalizedTransactions", () => {
});
});

describe("Indexer.processFinalizedTransactions Sanctions Filtering", () => {
describe("isTxSanctioned", () => {
it("should skip sanctioned addresses and not mint", async () => {
const txData = REGTEST_DATA[329]!.txs[1]!;

await suite.insertTx({ txId: txData.id, status: MintTxStatus.Finalized });
await suite.setupBlock(329);

// Mock compliance to block all addresses
const mockIsBtcBlocked = jest.fn().mockImplementation((addresses: string[]) => {
const result: Record<string, boolean> = {};
for (const addr of addresses) {
result[addr] = true; // Block all addresses
}
return Promise.resolve(result);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
indexer.compliance = { isBtcBlocked: mockIsBtcBlocked } as any;

await indexer.processFinalizedTransactions();

expect(mockIsBtcBlocked).toHaveBeenCalled();
expect(suite.mockSuiClient.tryMintNbtcBatch).not.toHaveBeenCalled();
await suite.expectTxStatus(txData.id, MintTxStatus.Finalized);
const block = Block.fromHex(REGTEST_DATA[327]!.rawBlockHex);
const txs = block.transactions!;
// notes: in this block,
// tx1 senders = [ "tb1q4jxws26hkjn6y5qspgqsuwtu5whel39j2xq3n2" ]
// tx1 senders = [ "tb1qw7zqqtpxmptxszythfjs4ugflvm4m358tn3jvw" ]
// const addrs = extractSenderAddresses(txs[2]!, networks.testnet);

expect(await indexer.isTxSanctioned(txs[1]!, networks.testnet)).toBeTrue();
expect(await indexer.isTxSanctioned(txs[2]!, networks.testnet)).toBeFalse();
});
});

Expand Down
74 changes: 21 additions & 53 deletions packages/btcindexer/src/btcindexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,22 @@ export class Indexer {
const nbtcRedeems: string[] = [];

for (const tx of block.transactions ?? []) {
const txDeposits = await this.detectMintingTx(tx, network, blockInfo);
if (txDeposits.length > 0) {
deposits.push(...txDeposits);
// TODO: we should better optimise the check. In detectMintingTx and detectRedeemTx
// we are checking inputs and outputs, so ideally we don't repeat this.
if (await this.isTxSanctioned(tx, network)) {
logger.debug({ msg: "tx with sanctioned address", txId: tx.getId() });
continue;
}

const depositTxs = await this.detectMintingTx(tx, network, blockInfo);
if (depositTxs.length > 0) {
deposits.push(...depositTxs);
continue;
}

if (this.detectRedeemTx(tx, trackedRedeems)) {
nbtcRedeems.push(tx.getId());
continue;
}
}

Expand Down Expand Up @@ -376,6 +385,7 @@ export class Indexer {
}

for (let i = 0; i < tx.outs.length; i++) {
// TODO: we should aggregate the deposits: this is a single TX with a single recipient!
const vout = tx.outs[i];
if (!vout) {
continue;
Expand All @@ -395,8 +405,8 @@ export class Indexer {
});
continue;
}
// TODO: here we process setups, not package configs
const config = this.getPackageConfig(depositInfo.setup_id);

logger.debug({
msg: "Found matching nBTC deposit output",
txId: tx.getId(),
Expand Down Expand Up @@ -439,23 +449,13 @@ export class Indexer {
}

const txsToProcess = await this.filterAlreadyMinted(finalizedTxs);

if (txsToProcess.length === 0) {
logger.info({ msg: "No new deposits to process after front-run check" });
return;
}

if (txsToProcess.length === 0) return;
logger.info({
msg: "Minting: Found deposits to process",
msg: "Minting: Found finalized deposits to process",
count: txsToProcess.length,
});

const filteredTxs = await this.filterSanctionedTxs(txsToProcess);
if (filteredTxs.length === 0) return;
if (filteredTxs.length !== txsToProcess.length) {
logger.debug({ msg: "sanctioned txs: " + filteredTxs.length });
}
const txsByBlock = this.groupTransactionsByBlock(filteredTxs);
const txsByBlock = this.groupTransactionsByBlock(txsToProcess);
const { batches } = await this.prepareMintBatches(txsByBlock);
await this.executeMintBatches(batches);
}
Expand Down Expand Up @@ -525,7 +525,7 @@ export class Indexer {
* Groups a list of blockchain transactions (or any object containing a block_hash) by their block hash.
* This optimization allows fetching and parsing the block data once for all related transactions.
* @param transactions - A list of objects that must include a block_hash.
* @returns A map where each key is a block hash and the value is an array of transactions belonging to that block.
* @returns Mapping block hash -> subset of transactions belonging to that block.
*/
private groupTransactionsByBlock<T extends { block_hash: string }>(
transactions: T[],
Expand Down Expand Up @@ -786,40 +786,9 @@ export class Indexer {
}
}

private async filterSanctionedTxs(txs: FinalizedTxRow[]): Promise<FinalizedTxRow[]> {
// TODO: rework it (@robert-zaremba )
const txsByBlock = this.groupTransactionsByBlock(txs);
const filteredTxs: FinalizedTxRow[] = [];

for (const [blockHash, blockTxs] of txsByBlock) {
const blockData = await this.fetchAndVerifyBlock(blockHash);
if (!blockData) continue;

const { block } = blockData;
for (const txRow of blockTxs) {
const tx = block.transactions?.find((t) => t.getId() === txRow.tx_id);
if (!tx) continue;
const config = this.getPackageConfig(txRow.setup_id);
const senderAddresses = extractSenderAddresses(tx, config.btc_network);
const blockedResults = await this.compliance.isBtcBlocked(senderAddresses);
const blockedAddrs = Object.entries(blockedResults)
.filter(([_, isBlocked]) => isBlocked)
.map(([addr]) => addr);

if (blockedAddrs.length > 0) {
logger.error({
msg: "Sanctioned address detected, skipping transaction",
txId: txRow.tx_id,
senderAddresses: blockedAddrs,
});
continue;
}

filteredTxs.push(txRow);
}
}

return filteredTxs;
isTxSanctioned(tx: Transaction, btcNet: Network): Promise<boolean> {
const senderAddresses = extractSenderAddresses(tx, btcNet);
return this.compliance.isAnyBtcAddressSanctioned(senderAddresses);
}

async detectMintedReorgs(blockHeight: number): Promise<void> {
Expand Down Expand Up @@ -1311,7 +1280,6 @@ export class Indexer {

async getDepositsBySender(btcAddress: string, network: BtcNet): Promise<NbtcTxResp[]> {
const nbtcMintRows = await this.storage.getNbtcMintTxsByBtcSender(btcAddress, network);

const latestHeight = await this.storage.getChainTip(network);

return nbtcMintRows.map((r) => nbtcRowToResp(r, latestHeight));
Expand Down