From b997f61627292ffb107343deef3d2b5ce618452b Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 9 Apr 2026 17:17:43 +0000 Subject: [PATCH 1/2] fix(FileStore): deduplicate merkle leaves in readMerkleFile to prevent wrong leaf lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JSONL merkle leaves file can accumulate duplicate CID entries when appendMerkleLeaves is called after a process restart re-syncs an overlapping range. Since getMerkleLeaf uses the CID as an array index (rows[cid]), duplicates shift all subsequent lookups — causing buildLocalProofPath to retrieve wrong leaf commitments and produce invalid merkle proofs. After sorting by CID, deduplicate adjacent entries keeping the last occurrence of each CID. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/store/fileStore.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/store/fileStore.ts b/src/store/fileStore.ts index d1bee12..9acda14 100644 --- a/src/store/fileStore.ts +++ b/src/store/fileStore.ts @@ -119,7 +119,21 @@ export class FileStore implements StorageAdapter { } } out.sort((a, b) => a.cid - b.cid); - return out.length ? out : undefined; + // Deduplicate: the JSONL file may contain duplicate cid entries after a process + // restart re-syncs an overlapping range. Since getMerkleLeaf uses cid as an array + // index, duplicates shift all subsequent lookups and cause wrong leaf retrieval. + if (out.length > 0) { + const deduped: typeof out = [out[0]!]; + for (let i = 1; i < out.length; i++) { + if (out[i]!.cid === deduped[deduped.length - 1]!.cid) { + deduped[deduped.length - 1] = out[i]!; + } else { + deduped.push(out[i]!); + } + } + return deduped; + } + return undefined; } catch { return undefined; } From 6f4be16af4b3c367a6f5140b638c7d27875da0f3 Mon Sep 17 00:00:00 2001 From: edison Date: Thu, 9 Apr 2026 17:29:11 +0000 Subject: [PATCH 2/2] fix(FileStore): distinguish ENOENT from transient errors in getMerkleNextCid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getMerkleNextCid catches all errors and returns 0, which tells appendMerkleLeaves the file is empty. On a transient I/O error this causes every incoming leaf to be re-appended, creating duplicate CID entries in the JSONL file. Since getMerkleLeaf uses cid as an array index (rows[cid]), the duplicates shift all lookups — buildLocalProofPath retrieves wrong commitments and produces invalid merkle proofs. Only return 0 for ENOENT (file genuinely missing). Propagate all other errors so appendMerkleLeaves skips the write instead of corrupting the file. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/store/fileStore.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/store/fileStore.ts b/src/store/fileStore.ts index d1bee12..8c272f5 100644 --- a/src/store/fileStore.ts +++ b/src/store/fileStore.ts @@ -143,9 +143,12 @@ export class FileStore implements StorageAdapter { const next = Number.isFinite(cid) ? Math.max(0, Math.floor(cid) + 1) : 0; this.merkleNextCid.set(chainId, next); return next; - } catch { - this.merkleNextCid.set(chainId, 0); - return 0; + } catch (err: unknown) { + if (err && typeof err === 'object' && 'code' in err && (err as { code: string }).code === 'ENOENT') { + this.merkleNextCid.set(chainId, 0); + return 0; + } + throw err; } }