fix(bridgeservice): correct l1-info-tree-index fallback and cross-syncer error codes - #1794
Conversation
…eeSyncer interface Introduces the interface method used by the fallback fix (next commit) to look up the latest L1 info tree leaf at or before a given L1 block, and regenerates the corresponding mock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
Corrects the getFirstL1InfoTreeIndexForL1Bridge fallback to clamp on GetLatestL1InfoLeafUntilBlock(root.BlockNum) instead of feeding a bridge exit-tree deposit count into an L1 info tree index lookup (INC-129 root cause). Adds a shared httpStatusForSyncerError/respondSyncerError mapping so endpoints joining syncers at different heights return 404 for not-yet-indexed data and 503 for a genuinely halted syncer, applied to L1InfoTreeIndexForBridgeHandler, ClaimProofHandler (also fixing its statusCode never being propagated to reportMetrics), and the L1 path of InjectedL1InfoLeafHandler. Wraps the processor's getInfoByIndexWithTx not-found in db.ReturnErrNotFound so the 404 mapping is reachable instead of leaking a raw sql: no rows in result set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
Switches GetL1InfoTreeIndex and GetClaimProof from doRequest to doRequestAllowNotFound so callers can detect the new 404 responses via ErrNotFound and retry, matching the existing GetInjectedL1InfoLeaf and GetL1InfoTreeLeafByGER convention. Signatures are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
…or statuses Adds a regression test reproducing the kinexys skew (GetLastRoot.Index a deposit count orders of magnitude apart from BlockNum), rewrites the four existing fallback subtests that previously pinned the buggy behaviour, and adds table-driven coverage for httpStatusForSyncerError, respondSyncerError, and each in-scope handler's status mapping including ClaimProofHandler's reportMetrics propagation. Strengthens the bridge client's 404/503 tests for GetL1InfoTreeIndex and GetClaimProof. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
…swagger Regenerates bridgeservice/docs and docs/assets/swagger/bridge_service to reflect the new @failure 404/503 annotations. Adds notes to docs/bridge_service.md documenting that /l1-info-tree-index, /claim-proof, and the L1 path of /injected-l1-info-leaf now return 404 (not-yet-indexed, retry later) and 503 (syncer halted) instead of 500, and describes the corrected fallback behaviour. The regeneration also incidentally picks up pre-existing /l1-info-tree-leaf-by-ger and /root-by-ler annotations that were already in source but never previously regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
An exit path that forgets to assign statusCode now reports a fault to reportMetrics instead of silently reporting success, which is the failure mode that hid the ClaimProofHandler metrics bug. Success paths set 200 explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
The proof preparer feeds a bridge-exit-tree deposit count into an L1 info tree lookup, exactly as bridgeservice did before this branch. Tracked in #1795. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
…dge syncer's block The GetLastRoot fallback in firstL1InfoTreeIndexForL1Bridge fed root.Index -- a position in the L1 bridge exit tree, i.e. a deposit count -- into l1InfoTree.GetInfoByIndex, which expects an L1 info tree index. Both are bare uint32 so nothing caught the mix-up at compile time; the lookup failed with "sql: no rows in result set" whenever bridgesync L1 trailed l1infotreesync (the INC-129 skew). Clamp on root.BlockNum via GetLatestL1InfoLeafUntilBlock instead, which is the one index space both syncers agree on, and guard root.BlockNum == 0 by returning bridgeservice.ErrNotOnL1Info so the request stays pending rather than hitting l1infotreesync.ErrNoBlock0. Mirrors the fix already applied to the bridge service; removes the TODO(#1795) marker. Tests keep the deposit count (1_146_035) and the L1 block number (8_412) orders of magnitude apart and assert that skew, so a future mix-up cannot accidentally satisfy a fixture. Fixes #1795 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
|
Claude finished @arnaubennassar's task in 2m 30s —— View job Code Review Complete ✅Reviewing PR #1794: fix(bridgeservice): correct l1-info-tree-index fallback and cross-syncer error codes Todo List
🚀 What's NewThis PR fixes the INC-129 root cause — a critical index-space confusion bug where the L1 bridge exit tree deposit count (~1.1M) was incorrectly passed to L1 info tree index queries, causing HTTP 500 errors. The fix introduces proper index space clamping via 🐛 Bug Fixes
📋 Config UpdatesNone.
|
…gainst real rows The existing table test only asserted the three error sentinels, so the "newest leaf at or before blockNum" semantics the INC-129 fallback relies on were never exercised against actual DB rows. Block numbers are kept orders of magnitude above the info tree indexes so an index-space mix-up cannot pass unnoticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
🔄 Changes Summary
getFirstL1InfoTreeIndexForL1Bridgetookroot.IndexfrombridgeL1.GetLastRoot— a position in the L1 bridge exit tree, i.e. a deposit count (~1,146,035) — and passed it tol1InfoTree.GetInfoByIndex, which queriesSELECT * FROM l1info_leaf WHERE position = $1, an L1 info tree index. Two different trees, two different counters, both bareuint32. The row could not exist, so/bridge/v1/l1-info-tree-indexreturned HTTP 500sql: no rows in result setwhenever the L1 bridge syncer trailed the L1 info tree syncer.GetLatestL1InfoLeafUntilBlock(ctx, root.BlockNum), clamping to the most recent info-tree leaf at or before the last block the bridge syncer indexed — the clamp the original code intended, in the correct index space.root.BlockNum == 0is guarded. The tip-anchored binary search, theGetRootByLERjoin, andErrNotOnL1Infoare all retained; the search'supperLimit/bestResultinherit the clamp from the reassignedlastInfowithout a second assignment.httpStatusForSyncerErrorhelper:ErrBlockNotProcessed,l1infotreesync.ErrNotFound,ErrNoBlock0,db.ErrNotFound,ErrNotOnL1Info→ 404;sync.ErrInconsistentState→ 503; anything else → 500.ClaimProofHandlermetrics fix.statusCodewas initialised to200and never reassigned, so all 13 exit paths reported200toreportMetrics— every failure was invisible in metrics. All exits now set it correctly.GetInfoByIndexnot-found was unwrapped.getInfoByIndexWithTxskippeddb.ReturnErrNotFound, returning a rawsql: no rows in result set. That was the literal 500 body in INC-129, and it meanterrors.Is(err, db.ErrNotFound)was dead code at three call sites. One-line wrap added.Debug(404) /Warn(503) instead ofError, so the INC-129 alert volume does not simply migrate from HTTP 500s into ERROR logs.autoclaim(closes fix(autoclaim): proof preparer reuses the INC-129 index-space bug (deposit count used as L1 info tree index) #1795).autoclaim/proof/preparer.gocarried a verbatim copy of the index-space confusion in its ownGetRootByLERfallback. It now clamps onroot.BlockNumtoo. After this,grep -rn "GetInfoByIndex(ctx, .*\.Index)"over non-test Go returns zero hits — that was the last one./bridge/v1/l1-info-tree-index,/bridge/v1/claim-proof, and the L1 path of/bridge/v1/injected-l1-info-leafnow return404when a syncer has not indexed the requested data yet (callers should treat it as not ready yet, retry later) and503when a syncer is halted / in an inconsistent state. Previously all of these were500./injected-l1-info-leaf's L2 path already returned404; only its L1 path changes.bridgeservice/client:GetL1InfoTreeIndexandGetClaimProofswitch fromdoRequesttodoRequestAllowNotFound, so they now return theErrNotFoundsentinel on404instead of a generic error. Signatures and return types are unchanged, but callers that treated any error as fatal may now want to retry onErrNotFound. A503deliberately stays a plain non-ErrNotFounderror so a halted syncer is not mistaken for "not indexed yet".📋 Config Updates
✅ Testing
make lint→0 issues.make test-unit→ exit 0 (whole repo).TestGetFirstL1InfoTreeIndexForL1Bridge_KinexysIndexSpaceSkewreproduces the exact skew:GetRootByLERcannot resolve the tip leaf's MER, andGetLastRootreturnsIndex = 1_146_035(a deposit count) withBlockNum = 8_412. The fixtures keep the two ~136× apart on purpose — and assert that skew — because every pre-existing fixture setIndex == BlockNum == depositCount, which is precisely the coincidence that let an index-space confusion hide for so long.GetInfoByIndex(ctx, root.Index)makes both subtests fail on the unexpected mock call forGetLatestL1InfoLeafUntilBlock; restoring it byte-for-byte returns them to green."failed to get last info for L1"; it now assertsErrorIs(l1infotreesync.ErrNotFound), a 404 mapping, andNotContainsthat old message. A new subtest covers theroot.BlockNum == 0guard.TestHTTPStatusForSyncerError(14 rows — every sentinel bare and wrapped) andTestRespondSyncerErrorcover the mapping directly, includingdb.ErrNotFoundandl1infotreesync.ErrNotFoundas separate 404 cases, since the new fallback returns the latter while its siblings return the former.ClaimProofHandler's exits are asserted through the Prometheus counter, and every failure row also asserts the200counter did not move — pinning the metrics bug shut.TestGetL1InfoTreeIndex's weakrequire.Errorbecamerequire.ErrorIs(err, ErrNotFound);TestGetClaimProofgained its first404subtest; both gained a503subtest.TestFirstL1InfoTreeIndexForL1BridgeIndexSpaceSkewreproduces the same skew against the proof preparer (GetLastRoot().Index = 1_146_035vsBlockNum = 8_412, skew asserted), plus tests for theBlockNum == 0guard and the clamp-lookup error wrap. Its probed block (6_200, the midpoint of[4_000, 8_400]) doubles as proof the clamp actually tightenedupperLimit— unclamped, the search would probe6_250and miss the fixture. Proven red by reverting the one line: the test fails withl1infotreesync has no L1 info tree leaf at or before L1 block 8412.🐞 Issues
/l1-info-tree-indexHTTP 500, 89 alerts).autoclaim/proof/preparer.go).🔗 Related PRs
📝 Notes
Scope widening —
l1infotreesync/processor.go. The diff intentionally reaches one line outsidebridgeservice/:getInfoByIndexWithTxnow wraps its return indb.ReturnErrNotFound(...). Without it, the handlers'errors.Is(err, db.ErrNotFound)checks are dead code and the endpoint keeps returning a rawsql: no rows in result set. All 9 non-test callers were audited; the decisive evidence that this is the right layer is that threeautoclaimcall sites already work around the asymmetry by checkingerrors.Is(db.ErrNotFound) || errors.Is(sql.ErrNoRows).translateErrorwas deliberately not added at the syncer wrapper, so the sentinel staysdb.ErrNotFound, matching every sibling method.Incidental swagger drift (pre-existing, not new). Regenerating the swagger artefacts also picks up
/l1-info-tree-leaf-by-gerand/root-by-lerentries plus thetypes.RootByLERResponseschema. These were already annotated inbridge.goondevelopbut had never been regenerated into the committed docs. They are not new functionality introduced here — verified withgit show origin/develop:bridgeservice/bridge.go.The
autoclaimcopy of the bug is now fixed here too (#1795).autoclaim/proof/preparer.gohad a verbatim copy — a bridge-exit-treeroot.IndexfromGetLastRootfed intoGetInfoByIndex, with the same"failed to get last info for L1"wrapper. It was first flagged with aTODO, then fixed in this PR. Two details worth a reviewer's eye:root.BlockNum == 0guard reuses the existingbridgeservice.ErrNotOnL1Inforather than introducing a sentinel. Its only caller,selectL1InfoTreeIndex, already treats that sentinel as retry next cycle (Result{Ready: false}), which is the correct reading of "bridgesync L1 has indexed nothing yet" — a transient startup state, not a fault. Lettingl1infotreesync.ErrNoBlock0escape instead would leak an l1infotreesync-internal sentinel into autoclaim's error contract.GetLastRootfallback inautoclaim/proofhad zero test coverage — no pre-existing test ever setlastRootorrootErrson the fake. The copied bug came with a copied coverage gap, which is why it survived.preparer.go:111retains a legitimateGetInfoByIndexcall whose argument is already in info-tree space; that one is correct and should not be "fixed".Deliberate non-changes. The deposit-anchored rewrite (
GetBridgeByDepositCount+ a(block_num, block_pos) >query) is the better long-term design and is not attempted here — this fix is deliberately minimal and in place.getFirstL1InfoTreeIndexForL2Bridgeis untouched beyond error mapping, and the syncers are not merged.L1InfoTreeLeafByGERHandleralready matched the taxonomy and was left alone. One pre-existing inconsistency remains:L1InfoTreeIndexForBridgeHandler's nil-syncer path still answers500while the same condition answers503inClaimProofHandler; fixing it cleanly needs a new sentinel, so it is left as-is.🤖 Generated with Claude Code
https://claude.ai/code/session_01WvzbGpjp6VeiW6ccThguGC
🔍 Review follow-up
Applied from the automated review:
Pessimistic
statusCodedefault (suggestion 2).L1InfoTreeIndexForBridgeHandler,InjectedL1InfoLeafHandler, andClaimProofHandlernow initialisestatusCodeto500and set200explicitly on the success path. Worth noting the suggestion as literally written would have introduced a bug: each of these handlers' success responses usedc.JSON(statusCode, ...)relying on thehttp.StatusOKinitialiser, so flipping the default alone would have returned HTTP 500 on success. Setting 200 explicitly is what makes the pessimistic default safe.Autoclaim duplicate bug (suggestion 3). Filed as fix(autoclaim): proof preparer reuses the INC-129 index-space bug (deposit count used as L1 info tree index) #1795, and then fixed in this PR rather than deferred — see the Notes section. The
TODOcomment is gone; the audit the issue asked for is clean.DB-level coverage of the clamp (integration-coverage suggestion). Added
TestGetLatestL1InfoLeafUntilBlockClampinl1infotreesync/processor_test.go. The pre-existing table test for that method only asserted the three error sentinels (ErrNoBlock0,ErrBlockNotProcessed,ErrNotFound), so the actual "newest leaf at or beforeblockNum" semantics the fallback now depends on were never exercised against real rows. This was the one genuinely untested link, and it is the link a mocked syncer cannot cover: a mock returns whatever it is told, so it can never catch an index-space error — it only proves the handler passesroot.BlockNumto the clamp, not that the query answers correctly.The new test inserts leaves at L1 blocks
1_000 / 5_000 / 9_000(indexes0 / 1 / 2— kept orders of magnitude apart on purpose, same discipline as the handler fixtures) and asserts index, block, andleaf.BlockNumber <= requestedacross eight cases: exact-boundary, above-all, between-leaves, ±1 around a leaf, and below-all →ErrNotFound. Verified non-vacuous by mutation: flippingORDER BY … DESCtoASCandblock_num <= $1to>= $1each make it fail (source restored, md5 verified).Declined, with reasons:
Nil-syncer taxonomy unification (suggestion 1) — the review itself notes this may be intentional, and it is: a nil syncer is a configuration fault, not a syncer state, and routing it through
respondSyncerErrorwould require a new sentinel. Left as documented under Deliberate non-changes.GetLatestL1InfoLeafUntilBlockpointer parameter (suggestion 4) — the premise is incorrect. The*uint64is the pre-existing processor signature, and itsnilpath is exercised:l1infotreesync/l1infotreesync.go:301passesnilfor the unbounded "latest leaf" variant, so making it non-pointer would break that caller. The public wrapper this PR consumes already takes a plainuint64, which is exactly what theL1InfoTreeSyncerinterface addition mirrors.Test constant renaming (suggestion 5) — marked optional by the review, and the proposed names (e.g.
fallbackL1Block_DepositCountBelowBlock) use underscores in mixedCaps identifiers, which Go style and this repo's linters discourage.A full HTTP → handler → syncer → database wired test (the rest of the integration-coverage suggestion). Two of its three bullets were already satisfied before the suggestion was made: the handler status tests go through a real gin router via
performRequestand assertw.Codeandw.Body, andassertHandlerStatusreads the actual Prometheus counter before and after each request, asserting both that the expected status was reported toreportMetricsand that the200counter did not move. So "verify the full HTTP response (status + body + metrics)" and "test thereportMetricscalls with the correct status codes" are covered. The mocks are also already at the syncer-interface seam — there is no higher seam short of a real database.What remained was standing up real
l1infotreesync+bridgesyncinstances with populated sqlite behind a liveBridgeService. That is a substantial bespoke harness, and it duplicates what the repo's kurtosis/docker e2e suites already do end-to-end (they are excluded frommake test-unitby-short). The targeted DB-level test above closes the actual coverage hole at a fraction of the cost and maintenance surface. Happy to add the wired harness if reviewers want it, but it did not look like the right trade here.