Skip to content
6 changes: 3 additions & 3 deletions packages/btcindexer/src/btcindexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,9 +547,9 @@ describe("Indexer.detectMintedReorgs", () => {
.bind(blockData.hash, blockData.height, BtcNet.REGTEST, Date.now(), 1)
.run();

await indexer.detectMintedReorgs(blockData.height);
await indexer.detectMintedReorgs(blockData.height, BtcNet.REGTEST);

const status = await indexer.storage.getTxStatus(txData.id);
const status = await indexer.storage.getTxStatus(txData.id, BtcNet.REGTEST);
expect(status).toEqual(MintTxStatus.Minted);
});
});
Expand Down Expand Up @@ -590,7 +590,7 @@ describe("Indexer.processBlock", () => {
await suite.setupBlock(327);
await indexer.processBlock(reorgBlockInfo);

const status = await indexer.storage.getTxStatus(txData.id);
const status = await indexer.storage.getTxStatus(txData.id, BtcNet.REGTEST);
expect(status).toEqual(MintTxStatus.MintedReorg);
});
});
Expand Down
18 changes: 9 additions & 9 deletions packages/btcindexer/src/btcindexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export class Indexer {
hash: blockInfo.hash,
status: result,
});
await this.detectMintedReorgs(blockInfo.height);
await this.detectMintedReorgs(blockInfo.height, blockInfo.network);
}
return true;
}
Expand Down Expand Up @@ -555,11 +555,14 @@ export class Indexer {
for (const txRow of txs) {
try {
const txId = txRow.tx_id;
const setupId = txRow.setup_id;
const config = this.getPackageConfig(setupId);

const transactions = block.transactions;
const txIndex = transactions?.findIndex((t) => t.getId() === txId) ?? -1;

if (txIndex < 0 || !transactions) {
await this.handleMissingFinalizedMintingTx(txId);
await this.handleMissingFinalizedMintingTx(txId, config.btc_network);
continue;
}
const targetTx = transactions[txIndex];
Expand All @@ -572,9 +575,6 @@ export class Indexer {
throw new Error("Proof generation failed (returned null or undefined)");
}

const setupId = txRow.setup_id;
const config = this.getPackageConfig(setupId);

let batch = batches.get(setupId);
if (!batch) {
batch = { mintArgs: [], processedKeys: [] };
Expand Down Expand Up @@ -656,14 +656,14 @@ export class Indexer {
}

// Marks a transaction as reorged if it is missing from its expected block.
private async handleMissingFinalizedMintingTx(txId: string): Promise<void> {
private async handleMissingFinalizedMintingTx(txId: string, btcNet: BtcNet): Promise<void> {
logger.error({
msg: "Minting: Could not find TX within its block. Detecting reorg.",
method: "handleMissingFinalizedMintingTx",
txId,
});
try {
const currentStatus = await this.storage.getTxStatus(txId);
const currentStatus = await this.storage.getTxStatus(txId, btcNet);
if (
currentStatus !== MintTxStatus.Finalized &&
currentStatus !== MintTxStatus.MintFailed
Expand Down Expand Up @@ -773,14 +773,14 @@ export class Indexer {
}
}

async detectMintedReorgs(blockHeight: number): Promise<void> {
async detectMintedReorgs(blockHeight: number, btcNet: BtcNet): Promise<void> {
logger.debug({
msg: "Checking for reorgs on minted transactions",
method: "detectMintedReorgs",
blockHeight,
});

const reorgedTxs = await this.storage.getReorgedMintedTxs(blockHeight);
const reorgedTxs = await this.storage.getReorgedMintedTxs(blockHeight, btcNet);
if (!reorgedTxs || reorgedTxs.length === 0) {
return;
}
Expand Down
35 changes: 22 additions & 13 deletions packages/btcindexer/src/cf-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
import { MintTxStatus, InsertBlockStatus } from "./models";
import type { Storage } from "./storage";
import type { BlockQueueRecord, BtcNet } from "@gonative-cc/lib/nbtc";
import type { SuiNet } from "@gonative-cc/lib/nsui";

export class CFStorage implements Storage {
private d1: D1Database;
Expand Down Expand Up @@ -239,23 +240,25 @@ export class CFStorage implements Storage {
}

// Returns all Bitcoin deposit transactions in or after the given block, that successfully minted nBTC.
//TODO: We need to query by network
async getMintedTxs(blockHeight: number): Promise<FinalizedTxRow[]> {
async getMintedTxs(
blockHeight: number,
btcNet: BtcNet,
suiNet: SuiNet,
): Promise<FinalizedTxRow[]> {
const txs = await this.d1
.prepare(
`SELECT m.tx_id, m.vout, m.block_hash, m.block_height, p.nbtc_pkg, p.sui_network, p.btc_network
`SELECT m.tx_id, m.vout, m.block_hash, m.block_height, p.nbtc_pkg, p.sui_network, p.btc_network, p.id as setup_id
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN setups p ON a.setup_id = p.id
WHERE m.status = '${MintTxStatus.Minted}' AND m.block_height >= ?`,
WHERE m.status = '${MintTxStatus.Minted}' AND m.block_height >= ? AND p.btc_network = ? AND p.sui_network = ?`,
)
.bind(blockHeight)
.bind(blockHeight, btcNet, suiNet)
.all<FinalizedTxRow>();
return txs.results ?? [];
}

//TODO: We need to query by network
async getReorgedMintedTxs(blockHeight: number): Promise<ReorgedMintedTx[]> {
async getReorgedMintedTxs(blockHeight: number, btcNet: BtcNet): Promise<ReorgedMintedTx[]> {
const reorged = await this.d1
.prepare(
`SELECT
Expand All @@ -264,23 +267,29 @@ export class CFStorage implements Storage {
b.hash as new_block_hash,
m.block_height
FROM nbtc_minting m
INNER JOIN btc_blocks b ON m.block_height = b.height
INNER JOIN btc_blocks b ON m.block_height = b.height AND b.network = ?
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN setups p ON a.setup_id = p.id AND p.btc_network = b.network
WHERE m.status = '${MintTxStatus.Minted}'
AND m.block_height >= ?
AND m.block_hash != b.hash`,
)
.bind(blockHeight)
.bind(btcNet, blockHeight)
.all<ReorgedMintedTx>();
return reorged.results ?? [];
}

//TODO: We need to query by network
async getTxStatus(txId: string): Promise<MintTxStatus | null> {
async getTxStatus(txId: string, btcNet: BtcNet): Promise<MintTxStatus | null> {
const result = await this.d1
.prepare(`SELECT status FROM nbtc_minting WHERE tx_id = ? LIMIT 1`)
.bind(txId)
.prepare(
`SELECT m.status
FROM nbtc_minting m
JOIN nbtc_deposit_addresses a ON m.address_id = a.id
JOIN setups p ON a.setup_id = p.id
WHERE m.tx_id = ? AND p.btc_network = ?
LIMIT 1`,
)
.bind(txId, btcNet)
.first<{ status: MintTxStatus }>();
return result?.status ?? null;
}
Expand Down
177 changes: 174 additions & 3 deletions packages/btcindexer/src/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ describe("CFStorage", () => {
timestamp_ms: 2000,
});

const reorged = await storage.getReorgedMintedTxs(100);
const reorged = await storage.getReorgedMintedTxs(100, BtcNet.REGTEST);
expect(reorged.length).toBe(1);
expect(reorged[0]!.tx_id).toBe("tx1");
expect(reorged[0]!.old_block_hash).toBe("blockHash1");
Expand Down Expand Up @@ -326,11 +326,11 @@ describe("CFStorage", () => {
await storage.insertOrUpdateNbtcTxs([txBase]);
await storage.updateNbtcTxsStatus(["tx1"], MintTxStatus.Minted);

const minted = await storage.getMintedTxs(90);
const minted = await storage.getMintedTxs(90, BtcNet.REGTEST, toSuiNet("devnet"));
expect(minted.length).toBe(1);
expect(minted[0]!.tx_id).toBe("tx1");

const mintedHigh = await storage.getMintedTxs(101);
const mintedHigh = await storage.getMintedTxs(101, BtcNet.REGTEST, toSuiNet("devnet"));
expect(mintedHigh.length).toBe(0);
});

Expand Down Expand Up @@ -372,4 +372,175 @@ describe("CFStorage", () => {
expect(txs[0]!.tx_id).toBe("tx1");
});
});

describe("Network Isolation", () => {
beforeEach(async () => {
const db = await mf.getD1Database("DB");
// Setup additional network: testnet
await db
.prepare(
`
INSERT INTO setups (id, btc_network, sui_network, nbtc_pkg, nbtc_contract, lc_pkg, lc_contract, nbtc_fallback_addr, is_active)
VALUES (2, 'testnet', 'testnet', '0xPkgTestnet', '0xContractTestnet', '0xLC2', '0xLCC2', '0xFB2', 1)
`,
)
.run();
await db
.prepare(
`
INSERT INTO nbtc_deposit_addresses (id, setup_id, deposit_address, is_active)
VALUES (20, 2, 'addr_testnet', 1)
`,
)
.run();
});

it("getMintedTxs should isolate by network pair", async () => {
await storage.insertOrUpdateNbtcTxs([
{
txId: "tx_regtest",
btcNetwork: BtcNet.REGTEST,
suiNetwork: toSuiNet("devnet"),
nbtcPkg: "0xPkg1",
depositAddress: "bcrt1qAddress1",
sender: "sender1",
vout: 0,
blockHash: "block_regtest",
blockHeight: 100,
suiRecipient: "0xSui1",
amount: 1000,
},
]);
await storage.updateNbtcTxsStatus(["tx_regtest"], MintTxStatus.Minted);

await storage.insertOrUpdateNbtcTxs([
{
txId: "tx_testnet",
btcNetwork: BtcNet.TESTNET,
suiNetwork: toSuiNet("testnet"),
nbtcPkg: "0xPkgTestnet",
depositAddress: "addr_testnet",
sender: "sender2",
vout: 0,
blockHash: "block_testnet",
blockHeight: 100,
suiRecipient: "0xSui2",
amount: 2000,
},
]);
await storage.updateNbtcTxsStatus(["tx_testnet"], MintTxStatus.Minted);

const mintedRegtest = await storage.getMintedTxs(
90,
BtcNet.REGTEST,
toSuiNet("devnet"),
);
expect(mintedRegtest.length).toBe(1);
expect(mintedRegtest[0]!.tx_id).toBe("tx_regtest");

const mintedTestnet = await storage.getMintedTxs(
90,
BtcNet.TESTNET,
toSuiNet("testnet"),
);
expect(mintedTestnet.length).toBe(1);
expect(mintedTestnet[0]!.tx_id).toBe("tx_testnet");
});

it("getReorgedMintedTxs should isolate by network", async () => {
await storage.insertOrUpdateNbtcTxs([
{
txId: "tx_regtest",
btcNetwork: BtcNet.REGTEST,
suiNetwork: toSuiNet("devnet"),
nbtcPkg: "0xPkg1",
depositAddress: "bcrt1qAddress1",
sender: "sender1",
vout: 0,
blockHash: "old_block_regtest",
blockHeight: 100,
suiRecipient: "0xSui1",
amount: 1000,
},
]);
await storage.updateNbtcTxsStatus(["tx_regtest"], MintTxStatus.Minted);

await storage.insertOrUpdateNbtcTxs([
{
txId: "tx_testnet",
btcNetwork: BtcNet.TESTNET,
suiNetwork: toSuiNet("testnet"),
nbtcPkg: "0xPkgTestnet",
depositAddress: "addr_testnet",
sender: "sender2",
vout: 0,
blockHash: "old_block_testnet",
blockHeight: 100,
suiRecipient: "0xSui2",
amount: 2000,
},
]);
await storage.updateNbtcTxsStatus(["tx_testnet"], MintTxStatus.Minted);

await storage.insertBlockInfo({
hash: "new_block_testnet",
height: 100,
network: BtcNet.TESTNET,
timestamp_ms: 2000,
});

const reorgedRegtest = await storage.getReorgedMintedTxs(90, BtcNet.REGTEST);
expect(reorgedRegtest.length).toBe(0);

const reorgedTestnet = await storage.getReorgedMintedTxs(90, BtcNet.TESTNET);
expect(reorgedTestnet.length).toBe(1);
expect(reorgedTestnet[0]!.tx_id).toBe("tx_testnet");
});

it("getTxStatus should isolate by network", async () => {
await storage.insertOrUpdateNbtcTxs([
{
txId: "tx_regtest",
btcNetwork: BtcNet.REGTEST,
suiNetwork: toSuiNet("devnet"),
nbtcPkg: "0xPkg1",
depositAddress: "bcrt1qAddress1",
sender: "sender1",
vout: 0,
blockHash: "block1",
blockHeight: 100,
suiRecipient: "0xSui1",
amount: 1000,
},
]);
await storage.updateNbtcTxsStatus(["tx_regtest"], MintTxStatus.Minted);

await storage.insertOrUpdateNbtcTxs([
{
txId: "tx_testnet",
btcNetwork: BtcNet.TESTNET,
suiNetwork: toSuiNet("testnet"),
nbtcPkg: "0xPkgTestnet",
depositAddress: "addr_testnet",
sender: "sender2",
vout: 0,
blockHash: "block2",
blockHeight: 100,
suiRecipient: "0xSui2",
amount: 2000,
},
]);
await storage.updateNbtcTxsStatus(["tx_testnet"], MintTxStatus.Confirming);

expect(await storage.getTxStatus("tx_regtest", BtcNet.REGTEST)).toBe(
MintTxStatus.Minted,
);
expect(await storage.getTxStatus("tx_testnet", BtcNet.TESTNET)).toBe(
MintTxStatus.Confirming,
);

expect(await storage.getTxStatus("tx_regtest", BtcNet.TESTNET)).toBeNull();
expect(await storage.getTxStatus("tx_testnet", BtcNet.REGTEST)).toBeNull();
});
});
});
8 changes: 4 additions & 4 deletions packages/btcindexer/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
} from "./models";
import { D1Database } from "@cloudflare/workers-types";
import type { BlockQueueRecord, BtcNet } from "@gonative-cc/lib/nbtc";
import { toSuiNet } from "@gonative-cc/lib/nsui";
import { toSuiNet, type SuiNet } from "@gonative-cc/lib/nsui";

export interface Storage {
// Block operations
Expand All @@ -31,9 +31,9 @@ export interface Storage {
insertOrUpdateNbtcTxs(txs: NbtcTxInsertion[]): Promise<void>;

getNbtcMintCandidates(maxRetries: number): Promise<FinalizedTxRow[]>;
getMintedTxs(blockHeight: number): Promise<FinalizedTxRow[]>;
getTxStatus(txId: string): Promise<MintTxStatus | null>;
getReorgedMintedTxs(blockHeight: number): Promise<ReorgedMintedTx[]>;
getMintedTxs(blockHeight: number, btcNet: BtcNet, suiNet: SuiNet): Promise<FinalizedTxRow[]>;
getTxStatus(txId: string, btcNet: BtcNet): Promise<MintTxStatus | null>;
getReorgedMintedTxs(blockHeight: number, btcNet: BtcNet): Promise<ReorgedMintedTx[]>;
updateNbtcTxsStatus(txIds: string[], status: MintTxStatus): Promise<void>;
batchUpdateNbtcMintTxs(updates: NbtcTxUpdate[]): Promise<void>;
updateConfirmingTxsToReorg(blockHashes: string[]): Promise<void>;
Expand Down
Loading