Skip to content

Commit 5f45abf

Browse files
authored
feat: remove optimistic recordIkaSig polling (#324)
* Removed optimistic polling from recordIkaSig into event handler so that when we listen to IKA events, we process a redeem request based on the 2 events we listen Signed-off-by: Rayane Charif <rayane.charif@gonative.cc> * Resolved comments Signed-off-by: Rayane Charif <rayane.charif@gonative.cc> * Resolved comments Signed-off-by: Rayane Charif <rayane.charif@gonative.cc> --------- Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
1 parent 26d926d commit 5f45abf

File tree

8 files changed

+171
-71
lines changed

8 files changed

+171
-71
lines changed

packages/btcindexer/src/btcindexer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Service } from "@cloudflare/workers-types";
66
import type { WorkerEntrypoint } from "cloudflare:workers";
77
import type { SuiIndexerRpc } from "@gonative-cc/sui-indexer/rpc-interface";
88
import { logError, logger } from "@gonative-cc/lib/logger";
9+
import { getMnemonic } from "@gonative-cc/lib/secrets";
910
import { OP_RETURN } from "./opcodes";
1011
import { BitcoinMerkleTree } from "./bitcoin-merkle-tree";
1112
import { SuiClient, type SuiClientI } from "./sui_client";
@@ -54,7 +55,10 @@ export async function indexerFromEnv(env: Env): Promise<Indexer> {
5455
throw new Error("Invalid MAX_NBTC_MINT_TX_RETRIES in config. Must be a number >= 0.");
5556
}
5657

57-
const mnemonic = await env.NBTC_MINTING_SIGNER_MNEMONIC.get();
58+
const mnemonic = await getMnemonic(env.NBTC_MINTING_SIGNER_MNEMONIC);
59+
if (!mnemonic) {
60+
throw new Error("Failed to retrieve mnemonic");
61+
}
5862
const suiClients = new Map<SuiNet, SuiClient>();
5963
for (const p of packageConfigs) {
6064
if (!suiClients.has(p.sui_network))

packages/lib/src/secrets.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { logger } from "./logger";
2+
3+
interface SecretStore {
4+
get(): Promise<string>;
5+
}
6+
7+
/**
8+
* Retrieves the mnemonic from the secrets store with proper error handling.
9+
* Returns the mnemonic string or null if not found/failed.
10+
*/
11+
export async function getMnemonic(secret: SecretStore): Promise<string | null> {
12+
try {
13+
const mnemonic = await secret.get();
14+
if (!mnemonic) {
15+
logger.error({ msg: "Missing NBTC_MINTING_SIGNER_MNEMONIC" });
16+
return null;
17+
}
18+
return mnemonic;
19+
} catch (error) {
20+
logger.error({ msg: "Failed to retrieve NBTC_MINTING_SIGNER_MNEMONIC", error });
21+
return null;
22+
}
23+
}

packages/sui-indexer/src/handler.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ import {
1212
} from "./models";
1313
import { logger } from "@gonative-cc/lib/logger";
1414
import { fromBase64 } from "@mysten/sui/utils";
15+
import type { SuiClient } from "./redeem-sui-client";
1516

1617
export class SuiEventHandler {
1718
private storage: D1Storage;
1819
private setupId?: number;
20+
private suiClient?: SuiClient;
1921

20-
constructor(storage: D1Storage, setupId?: number) {
22+
constructor(storage: D1Storage, setupId?: number, suiClient?: SuiClient) {
2123
this.storage = storage;
2224
this.setupId = setupId;
25+
this.suiClient = suiClient;
2326
}
2427

2528
public async handleEvents(events: SuiEventNode[]) {
@@ -121,14 +124,64 @@ export class SuiEventHandler {
121124
}
122125

123126
private async handleCompletedSign(e: SuiEventNode) {
124-
const { sign_id } = e.json as CompletedSignEventRaw;
125-
logger.info({ msg: "IKA sign completed", type: e.type, signId: sign_id });
126-
//TODO: will handle the sign in the redeem-service in next PR
127+
const data = e.json as CompletedSignEventRaw;
128+
const signId = data.sign_id as string;
129+
logger.info({
130+
msg: "Ika signature completed",
131+
sign_id: signId,
132+
is_future_sign: data.is_future_sign, // true if it's Ika future transaction signature type
133+
signature_length: data.signature.length,
134+
txDigest: e.txDigest,
135+
});
136+
137+
// IKA coordinator is shared across protocols, so we only process sign IDs that match our redeems.
138+
// The final signature is recorded via SignatureRecordedEvent from nbtc.move (handled above).
139+
const redeemInfo = await this.storage.getRedeemInfoBySignId(signId);
140+
if (!redeemInfo) {
141+
logger.debug({ msg: "Sign ID not found in our redeems, ignoring", sign_id: signId });
142+
return;
143+
}
144+
145+
if (!this.suiClient) {
146+
logger.warn({ msg: "No SuiClient available to record signature", sign_id: signId });
147+
return;
148+
}
149+
150+
await this.suiClient.validateSignature(
151+
redeemInfo.redeem_id,
152+
redeemInfo.input_index,
153+
signId,
154+
redeemInfo.nbtc_pkg,
155+
redeemInfo.nbtc_contract,
156+
);
157+
await this.storage.markRedeemInputVerified(redeemInfo.redeem_id, redeemInfo.utxo_id);
158+
159+
logger.info({
160+
msg: "Recorded Ika signature",
161+
redeem_id: redeemInfo.redeem_id,
162+
utxo_id: redeemInfo.utxo_id,
163+
sign_id: signId,
164+
});
127165
}
128166

129167
private async handleRejectedSign(e: SuiEventNode) {
130-
const { sign_id } = e.json as RejectedSignEventRaw;
131-
logger.warn({ msg: "IKA sign rejected", type: e.type, signId: sign_id });
132-
//TODO: will handle the sign in the redeem-service in next PR
168+
const data = e.json as RejectedSignEventRaw;
169+
const signId = data.sign_id as string;
170+
const redeemInfo = await this.storage.getRedeemInfoBySignId(signId);
171+
if (!redeemInfo) {
172+
logger.warn({
173+
msg: "Rejected sign ID not found in our redeems, ignoring",
174+
sign_id: signId,
175+
});
176+
return;
177+
}
178+
179+
logger.debug({
180+
msg: "Ika signature rejected, clearing sign_id for retry",
181+
sign_id: signId,
182+
redeem_id: redeemInfo.redeem_id,
183+
utxo_id: redeemInfo.utxo_id,
184+
});
185+
await this.storage.clearRedeemInputSignId(redeemInfo.redeem_id, redeemInfo.utxo_id);
133186
}
134187
}

packages/sui-indexer/src/index.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type { NetworkConfig } from "./models";
44
import { Processor } from "./processor";
55
import { D1Storage } from "./storage";
66
import { logError, logger } from "@gonative-cc/lib/logger";
7+
import { getMnemonic } from "@gonative-cc/lib/secrets";
78
import { RedeemService } from "./redeem-service";
8-
import { createSuiClients } from "./redeem-sui-client";
9+
import { createSuiClients, type SuiClient } from "./redeem-sui-client";
910
import type { Service } from "@cloudflare/workers-types";
1011
import type { WorkerEntrypoint } from "cloudflare:workers";
1112
import type { BtcIndexerRpc } from "@gonative-cc/btcindexer/rpc-interface";
@@ -26,18 +27,26 @@ export default {
2627
const storage = new D1Storage(env.DB);
2728
const activeNetworks = await storage.getActiveNetworks();
2829

30+
const mnemonic = await getMnemonic(env.NBTC_MINTING_SIGNER_MNEMONIC);
31+
if (!mnemonic) return;
32+
const suiClients = await createSuiClients(activeNetworks, mnemonic);
33+
2934
// Run both indexer and redeem solver tasks in parallel
3035
const results = await Promise.allSettled([
31-
runSuiIndexer(storage, env, activeNetworks),
32-
runRedeemSolver(storage, env, activeNetworks),
36+
runSuiIndexer(storage, activeNetworks, suiClients),
37+
runRedeemSolver(storage, env, suiClients),
3338
]);
3439

3540
// Check for any rejected promises and log errors
3641
reportErrors(results, "scheduled", "Scheduled task error", ["SuiIndexer", "RedeemSolver"]);
3742
},
3843
} satisfies ExportedHandler<Env>;
3944

40-
async function runSuiIndexer(storage: D1Storage, env: Env, activeNetworks: SuiNet[]) {
45+
async function runSuiIndexer(
46+
storage: D1Storage,
47+
activeNetworks: SuiNet[],
48+
suiClients: Map<SuiNet, SuiClient>,
49+
) {
4150
if (activeNetworks.length === 0) {
4251
logger.info({ msg: "No active packages/networks found in database." });
4352
return;
@@ -61,7 +70,9 @@ async function runSuiIndexer(storage: D1Storage, env: Env, activeNetworks: SuiNe
6170
networks: networksToProcess.map((n) => n.name),
6271
});
6372

64-
const networkJobs = networksToProcess.map((netCfg) => poolAndProcessEvents(netCfg, storage));
73+
const networkJobs = networksToProcess.map((netCfg) =>
74+
poolAndProcessEvents(netCfg, storage, suiClients),
75+
);
6576
const results = await Promise.allSettled(networkJobs);
6677
reportErrors(
6778
results,
@@ -72,9 +83,14 @@ async function runSuiIndexer(storage: D1Storage, env: Env, activeNetworks: SuiNe
7283
);
7384
}
7485

75-
async function poolAndProcessEvents(netCfg: NetworkConfig, storage: D1Storage) {
86+
async function poolAndProcessEvents(
87+
netCfg: NetworkConfig,
88+
storage: D1Storage,
89+
suiClients: Map<SuiNet, SuiClient>,
90+
) {
7691
const client = new SuiGraphQLClient(netCfg.url);
77-
const p = new Processor(netCfg, storage, client);
92+
const suiClient = suiClients.get(netCfg.name);
93+
const p = new Processor(netCfg, storage, client, suiClient);
7894

7995
const nbtcPkgs = await storage.getActiveNbtcPkgs(netCfg.name);
8096
if (nbtcPkgs.length > 0) {
@@ -98,23 +114,11 @@ async function poolAndProcessEvents(netCfg: NetworkConfig, storage: D1Storage) {
98114
}
99115
}
100116

101-
async function runRedeemSolver(storage: D1Storage, env: Env, activeNetworks: SuiNet[]) {
117+
async function runRedeemSolver(storage: D1Storage, env: Env, suiClients: Map<SuiNet, SuiClient>) {
102118
logger.info({ msg: "Running scheduled redeem solver task..." });
103-
let mnemonic: string;
104-
try {
105-
mnemonic = (await env.NBTC_MINTING_SIGNER_MNEMONIC.get()) || "";
106-
} catch (error) {
107-
logger.error({ msg: "Failed to retrieve NBTC_MINTING_SIGNER_MNEMONIC", error });
108-
return;
109-
}
110-
if (!mnemonic) {
111-
logger.error({ msg: "Missing NBTC_MINTING_SIGNER_MNEMONIC" });
112-
return;
113-
}
114-
const clients = await createSuiClients(activeNetworks, mnemonic);
115119
const service = new RedeemService(
116120
storage,
117-
clients,
121+
suiClients,
118122
env.BtcIndexer as unknown as Service<BtcIndexerRpc & WorkerEntrypoint>,
119123
env.UTXO_LOCK_TIME,
120124
env.REDEEM_DURATION_MS,

packages/sui-indexer/src/models.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,12 @@ export interface IkaCursorUpdate {
158158

159159
export type CompletedSignEventRaw = typeof CoordinatorInnerModule.CompletedSignEvent.$inferInput;
160160
export type RejectedSignEventRaw = typeof CoordinatorInnerModule.RejectedSignEvent.$inferInput;
161+
162+
export interface RedeemSignInfo {
163+
redeem_id: number;
164+
utxo_id: number;
165+
input_index: number;
166+
nbtc_pkg: string;
167+
nbtc_contract: string;
168+
sui_network: SuiNet;
169+
}

packages/sui-indexer/src/processor.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@ import { D1Storage } from "./storage";
33
import { logError, logger } from "@gonative-cc/lib/logger";
44
import { SuiEventHandler } from "./handler";
55
import type { EventFetcher } from "./graphql-client";
6+
import type { SuiClient } from "./redeem-sui-client";
67

78
export class Processor {
89
netCfg: NetworkConfig;
910
storage: D1Storage;
1011
eventFetcher: EventFetcher;
11-
12-
constructor(netCfg: NetworkConfig, storage: D1Storage, eventFetcher: EventFetcher) {
12+
suiClient?: SuiClient;
13+
14+
constructor(
15+
netCfg: NetworkConfig,
16+
storage: D1Storage,
17+
eventFetcher: EventFetcher,
18+
suiClient?: SuiClient,
19+
) {
1320
this.netCfg = netCfg;
1421
this.storage = storage;
1522
this.eventFetcher = eventFetcher;
23+
this.suiClient = suiClient;
1624
}
1725

1826
// TODO: Refactor pollAllNbtcEvents and pollIkaEvents into a single generic pollEvents function
@@ -113,7 +121,11 @@ export class Processor {
113121
});
114122

115123
if (result.events.length > 0) {
116-
const handler = new SuiEventHandler(this.storage);
124+
const handler = new SuiEventHandler(
125+
this.storage,
126+
undefined,
127+
this.suiClient,
128+
);
117129
await handler.handleEvents(result.events);
118130
}
119131

packages/sui-indexer/src/redeem-service.ts

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export class RedeemService {
5555
// NOTE: here we are processing only 50 redeems every minute (every cron), we are not
5656
// looping thought all the solved redeems to avoid cloudflare timeout, since we are
5757
// already waiting for ika to sign, when calling ikaSdk.getPresignInParicularState
58+
// Signature verification (recordIkaSig) has been moved to the event indexer handler,
59+
// which reacts to IKA CompletedSignEvent / RejectedSignEvent.
5860
const solved = await this.storage.getSolvedRedeems();
5961
if (solved.length === 0) return;
6062

@@ -116,18 +118,14 @@ export class RedeemService {
116118
try {
117119
if (!input.sign_id) {
118120
await this.requestIkaSig(client, req, input);
119-
} else if (input.sign_id && !input.verified) {
120-
// TODO: this should be triggered when getting the event from ika
121-
await this.recordIkaSig(client, req, input);
122121
}
123122
} catch (e) {
124123
logError(
125124
{
126-
msg: "Failed to process input",
125+
msg: "Failed to request signature for input",
127126
method: "processSolvedRedeem",
128127
redeemId: req.redeem_id,
129128
utxoId: input.utxo_id,
130-
step: !input.sign_id ? "request_signature" : "verify_signature",
131129
},
132130
e,
133131
);
@@ -260,39 +258,6 @@ export class RedeemService {
260258
}
261259
}
262260

263-
private async recordIkaSig(
264-
client: SuiClient,
265-
req: RedeemRequestWithInputs,
266-
input: RedeemInput,
267-
) {
268-
logger.info({
269-
msg: "Verifying signature for input",
270-
redeemId: req.redeem_id,
271-
utxoId: input.utxo_id,
272-
inputIdx: input.input_index,
273-
signId: input.sign_id,
274-
});
275-
276-
if (!input.sign_id) {
277-
throw new Error("Input signature ID is missing");
278-
}
279-
280-
await client.validateSignature(
281-
req.redeem_id,
282-
input.input_index,
283-
input.sign_id,
284-
req.nbtc_pkg,
285-
req.nbtc_contract,
286-
);
287-
288-
await this.storage.markRedeemInputVerified(req.redeem_id, input.utxo_id);
289-
logger.info({
290-
msg: "Signature verified",
291-
redeemId: req.redeem_id,
292-
utxoId: input.utxo_id,
293-
});
294-
}
295-
296261
private getSuiClient(suiNet: SuiNet): SuiClient {
297262
const c = this.clients.get(suiNet);
298263
if (c === undefined) throw new Error("No SuiClient for the sui network = " + suiNet);

0 commit comments

Comments
 (0)