From 65bab5650472ca4dc2464b36efab48de483b3663 Mon Sep 17 00:00:00 2001 From: Morenikeoa Date: Fri, 26 Jun 2026 21:10:24 +0100 Subject: [PATCH] fix(discovery): remove stale isV2 branch misrouting V12_15/17/19 in parseEngineLight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseEngineLight's isV2 branch (gated on layout.version === 2) used hardcoded fixed offsets written for an old "V2 BPF intermediate" layout (e.g. currentSlot at a fixed base+352). That same version: 2 discriminant is also set by buildLayoutV12_15/17/19 (V12_19 inherits it by spreading V12_17's base layout) — an unrelated reuse of the same enum value — which meant V12_15/17/19 (the currently-deployed mainnet tier line) were silently routed through this stale branch's wrong offsets instead of their own correct per-field offsets. Verified directly: V12_19's real engineCurrentSlotOff is 200, not 352. This affected every field parseEngineLight returns for V12_15/17/19, not just one. parseEngineLight backs discoverMarkets() and getMarketsByAddress(), the SDK's primary market-listing entry points. The layout-driven branch immediately below the removed code already documents that it's meant to cover V_ADL, V12_1, V12_15, V12_17, V12_19, V1M, V1M2, V_SETDEXPOOL and any future layout — the isV2 branch was dead weight left over from a prior refactor that was never cleaned up. While moving V12_15/17/19 onto the layout-driven branch, also added the engineFundingIndexOff >= 0 guard that the heavy parser (slab.ts parseEngine) already has — V12_15/17/19 set this offset to -1 (the field doesn't exist in those engine structs), and the unguarded read would otherwise read 16 bytes starting one byte before the engine region. Exported parseEngineLight (was internal) so it can be unit tested directly, and added two regression tests for V12_19 proving (1) the real per-field offset is used instead of the dead branch's hardcoded one, and (2) fundingIndexQpbE6 is 0n instead of an off-by-one read. Both tests fail against the prior code (parseEngineLight not exported) and pass against this fix. Full suite: 853 passed | 31 skipped (882 -> 884, +2 new tests). --- src/solana/discovery.ts | 73 +++++++++++----------------------------- test/drift-check.test.ts | 41 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/solana/discovery.ts b/src/solana/discovery.ts index e49c1fe..f47e2db 100644 --- a/src/solana/discovery.ts +++ b/src/solana/discovery.ts @@ -286,7 +286,7 @@ function readI128LE(buf: Uint8Array, offset: number): bigint { * @param layout — SlabLayout from detectSlabLayout(actualDataSize). If null, falls back to V0. * @param maxAccounts — tier's max accounts for bitmap offset calculation */ -function parseEngineLight( +export function parseEngineLight( data: Uint8Array, layout: SlabLayout | null, maxAccounts: number = 4096, @@ -363,56 +363,16 @@ function parseEngineLight( }; } - // V2 engine struct (BPF intermediate): ENGINE_OFF=600, BITMAP_OFF=432 - // No mark_price, long_oi, short_oi, emergency OI fields. - // Field offsets relative to engineOff are different from V1. - const isV2 = layout?.version === 2; - if (isV2) { - return { - vault: readU128LE(data, base + 0), - insuranceFund: { - balance: readU128LE(data, base + 16), - feeRevenue: readU128LE(data, base + 32), - isolatedBalance: readU128LE(data, base + 48), - isolationBps: readU16LE(data, base + 64), - }, - currentSlot: readU64LE(data, base + 352), - fundingIndexQpbE6: readI128LE(data, base + 360), - lastFundingSlot: readU64LE(data, base + 376), - fundingRateBpsPerSlotLast: readI64LE(data, base + 384), - fundingRateE9: 0n, - marketMode: null, - lastCrankSlot: readU64LE(data, base + 392), - maxCrankStalenessSlots: readU64LE(data, base + 400), - totalOpenInterest: readU128LE(data, base + 408), - longOi: 0n, // V2 has no long_oi - shortOi: 0n, // V2 has no short_oi - cTot: readU128LE(data, base + 424), - pnlPosTot: readU128LE(data, base + 440), - pnlMaturedPosTot: 0n, - liqCursor: readU16LE(data, base + 456), - gcCursor: readU16LE(data, base + 458), - lastSweepStartSlot: readU64LE(data, base + 464), - lastSweepCompleteSlot: readU64LE(data, base + 472), - crankCursor: readU16LE(data, base + 480), - sweepStartIdx: readU16LE(data, base + 482), - lifetimeLiquidations: readU64LE(data, base + 488), - lifetimeForceCloses: readU64LE(data, base + 496), - netLpPos: readI128LE(data, base + 504), - lpSumAbs: readU128LE(data, base + 520), - lpMaxAbs: readU128LE(data, base + 536), - lpMaxAbsSweep: readU128LE(data, base + 552), - emergencyOiMode: false, // V2 has no emergency OI fields - emergencyStartSlot: 0n, - lastBreakerSlot: 0n, - markPriceE6: 0n, // V2 has no mark_price - oraclePriceE6: 0n, - fLongNum: 0n, fShortNum: 0n, negPnlAccountCount: 0n, fundPxLast: 0n, - resolvedKLongTerminalDelta: 0n, resolvedKShortTerminalDelta: 0n, resolvedLivePrice: 0n, - numUsedAccounts: canReadNumUsed ? readU16LE(data, base + numUsedOff) : 0, - nextAccountId: canReadNextId ? readU64LE(data, base + nextAccountIdOff) : 0n, - }; - } + // NOTE: a hardcoded "V2 engine struct (BPF intermediate)" branch used to live here, + // gated on `layout?.version === 2`. It was dead/stale: `SlabLayout.version === 2` is + // also set by buildLayoutV12_15/17/19 (V12_19 inherits it by spreading V12_17's base + // layout) — an unrelated reuse of the same discriminant — which meant V12_15/17/19 + // (the currently-deployed mainnet tier line) were being routed through this branch's + // long-stale hardcoded offsets (e.g. currentSlot at a fixed `base+352`) instead of + // their own correct per-field offsets (V12_19's real engineCurrentSlotOff is 200). + // Every field this branch returned was potentially wrong for V12_15/17/19. Removed + // per the layout-driven branch's own comment below, which already documents that it + // covers V12_15/17/19 — that was the intended path all along. // Layout-driven engine parse: covers V_ADL (engineOff=624, accountSize=312), V12_1, V12_15, // V12_17, V12_19, V1M, V1M2, V_SETDEXPOOL and any future layout registered in slab.ts. @@ -433,7 +393,14 @@ function parseEngineLight( isolationBps: hasInsuranceIsolation ? readU16LE(data, base + l.engineInsuranceIsolationBpsOff) : 0, }, currentSlot: readU64LE(data, base + l.engineCurrentSlotOff), - fundingIndexQpbE6: readI128LE(data, base + l.engineFundingIndexOff), + // engineFundingIndexOff is -1 on V12_15/17/19 (this field doesn't exist in those + // engine structs) — guard the same way the heavy parser does (slab.ts parseEngine) + // or `base + (-1)` reads 16 bytes starting one byte before the engine region. + fundingIndexQpbE6: l.engineFundingIndexOff >= 0 + ? ((l.engineLastFundingSlotOff >= 0 && l.engineLastFundingSlotOff - l.engineFundingIndexOff === 8) + ? BigInt(readI64LE(data, base + l.engineFundingIndexOff)) + : readI128LE(data, base + l.engineFundingIndexOff)) + : 0n, lastFundingSlot: readU64LE(data, base + l.engineLastFundingSlotOff), fundingRateBpsPerSlotLast: readI64LE(data, base + l.engineFundingRateBpsOff), fundingRateE9: 0n, @@ -477,7 +444,7 @@ function parseEngineLight( // layout === null: unrecognized slab format — callers should have skipped via the // layout !== null guard in discoverMarkets before calling parseEngineLight. - throw new Error(`parseEngineLight: unrecognized slab layout (isV0=${isV0}, isV2=${isV2})`); + throw new Error(`parseEngineLight: unrecognized slab layout (isV0=${isV0})`); } /** Options for `discoverMarkets`. */ diff --git a/test/drift-check.test.ts b/test/drift-check.test.ts index 71fe8b5..766662a 100644 --- a/test/drift-check.test.ts +++ b/test/drift-check.test.ts @@ -45,7 +45,9 @@ import { detectLayout, parseAccount, SLAB_TIERS_V12_1, + SLAB_TIERS_V12_19, } from "../src/solana/slab.js"; +import { parseEngineLight } from "../src/solana/discovery.js"; // --------------------------------------------------------------------------- // Helpers @@ -569,6 +571,45 @@ describe("V12_1 slab — layout detection and field offsets", () => { }); }); +// =========================================================================== +// 3b. parseEngineLight — V12_19 must use its own layout offsets, not the stale +// hardcoded "isV2" branch (both shared SlabLayout.version === 2 by coincidence) +// =========================================================================== + +describe("parseEngineLight — V12_19 uses layout-driven offsets (not stale isV2 branch)", () => { + it("reads currentSlot from the layout's own engineCurrentSlotOff, not a hardcoded offset", () => { + const dataSize = SLAB_TIERS_V12_19.small.dataSize; + const layout = detectSlabLayout(dataSize); + expect(layout).not.toBeNull(); + expect(layout!.version).toBe(2); // shares the discriminant with the old dead isV2 branch + expect(layout!.engineCurrentSlotOff).toBe(200); // NOT 352, which the dead branch hardcoded + + const buf = new Uint8Array(dataSize); + const dv = new DataView(buf.buffer); + dv.setBigUint64(layout!.engineOff + layout!.engineCurrentSlotOff, 12345n, true); + // Plant a decoy at the old dead branch's hardcoded offset to prove it's not read. + dv.setBigUint64(layout!.engineOff + 352, 999999n, true); + + const engine = parseEngineLight(buf, layout, layout!.maxAccounts); + expect(engine.currentSlot).toBe(12345n); + }); + + it("returns 0n for fundingIndexQpbE6 instead of an off-by-one read (engineFundingIndexOff = -1)", () => { + const dataSize = SLAB_TIERS_V12_19.small.dataSize; + const layout = detectSlabLayout(dataSize); + expect(layout).not.toBeNull(); + expect(layout!.engineFundingIndexOff).toBe(-1); + + const buf = new Uint8Array(dataSize); + // Plant a nonzero marker at exactly the byte `base + engineFundingIndexOff` + // (engineOff + (-1)) would read from without the `>= 0` guard. + buf[layout!.engineOff - 1] = 0xab; + + const engine = parseEngineLight(buf, layout, layout!.maxAccounts); + expect(engine.fundingIndexQpbE6).toBe(0n); + }); +}); + // =========================================================================== // 4. STAKE_PROGRAM_ID — mainnet/devnet address verification // ===========================================================================