@@ -255,3 +255,167 @@ Note for whoever picks up (1): `find_value_contracts` is **not defined anywhere
255255this repo** — a repo-wide search matches only this doc and the reference to it in
256256` subgraphs/asset-holdings.ts ` 's header comment. It appears to live in a separate
257257consumer (audit-sentinel); look there, not here.
258+
259+ ---
260+
261+ ## Addendum — 2026-08-04, post-replay-guard reindex-path investigation
262+
263+ > Read-only forensic follow-up. The live-path replay guard (f069, see
264+ > ` live-path-replay-guard.test.ts ` and ` catchup.ts ` 's per-iteration leadership
265+ > check) has since shipped, and a clean ~ 6h47m from-genesis reindex completed
266+ > 2026-08-03 03:07→09:54 UTC (` subgraph_operations ` id ` fae75ad1 ` ). This
267+ > addendum re-scopes to whether the REINDEX path itself (as opposed to the
268+ > since-fixed catch-up replay defect) under-applies events, using
269+ > cursor-bounded per-row reconciliation instead of the superseded fleet-average
270+ > / per-holder-` k ` methods above. Prod access was ` SELECT ` -only plus read-only
271+ > HTTPS GETs against the public Index API throughout; nothing was written,
272+ > redeployed, or reindexed against prod. Local docker Postgres was used for the
273+ > one write-capable reproduction attempt (Step 3).
274+
275+ ### Correction: negative-row count oscillation is normal, not a signal
276+
277+ The row count at ` amount < 0 ` moved 50→51→52→51 across this investigation as
278+ live rows crossed zero in both directions. ** This is expected** — the table
279+ keeps updating live. The only valid reconciliation method is ** cursor-bounded** :
280+ snapshot the subgraph's ` last_processed_block ` , then bound every chain-plane
281+ query to ` block_height <= that height ` . Unbounded or differently-timed
282+ snapshots are not comparable to each other and produce apparent "drift" that
283+ is really just measurement skew. All numbers below are bounded to cursor
284+ ` 8,699,193 ` (captured once, then reused for every query in this pass).
285+
286+ ### Step 1 — cursor-bounded reconciliation, full negative set + positive sample
287+
288+ All 52 negative rows present at the time of the snapshot (49 ` ft ` , 3 ` stx ` )
289+ were reconciled against ` decoded_events ` (` canonical = true ` , event types
290+ matching the deployed handler: ` ft_transfer ` /` ft_mint ` /` ft_burn ` or
291+ ` stx_transfer ` /` stx_mint ` /` stx_burn ` ), credits split by ` recipient ` , debits by
292+ ` sender ` , both bounded to ` block_height <= 8699193 ` .
293+
294+ ** Result: 2 of 52 reconcile exactly (both ` stx ` ) — the same two rows Part B
295+ found genuinely, correctly negative** (` univ2-pool-v1_0_0-0173 ` : stored
296+ ` -29,952,162 ` = chain net exactly; ` hilt ` : stored ` -22,178,700 ` = chain net
297+ exactly). ** The other 50 (49 ` ft ` + 1 new ` stx ` row, ` vgld-vault-v4 ` ,
298+ deficit −17,790,183) do not reconcile** — every one is missing chain-plane
299+ volume the accumulator should have applied (` stored < chain_net ` in every
300+ failing case; never the reverse in this set). This generalizes the two
301+ priority cases from the plan (` gl-fees-bank ` /sbtc-token, deficit −3,699 on a
302+ net of 0; ` alex-vault-v1-1 ` /mega, deficit −6,425 on a net of +201) to the
303+ full 50-row set.
304+
305+ A ** 35-row random positive sample** (9 ` stx ` , 26 ` ft ` ) reconciled the same
306+ way: ** 34 of 35 exact, 1 of 35 fails** —
307+ ` univ2-farming-distributor-1-v1_0_0-0009 ` / ` wen-nakamoto-stxcity::WEN ` ,
308+ stored ` 7,319,861,717,284 ` vs chain net ` 7,308,750,932,822 ` , deficit
309+ ` 11,110,784,462 ` (same under-application shape as the negatives, ~ 0.15% of
310+ gross volume — much smaller in relative terms than the worst negatives).
311+
312+ ** Blast-radius verdict** : ~ 96% of currently-negative rows are wrong, ~ 3% of a
313+ random positive sample is wrong. This is a ** materially smaller blast radius**
314+ than f068's "every unbalanced live-active holder, hundreds to thousands of
315+ rows" finding — because that finding was about the pre-f069 catch-up replay
316+ defect, which is now fixed. What remains post-fix is a real but much
317+ smaller-magnitude under-application, concentrated in (but not limited to) the
318+ negative rows.
319+
320+ ** Deficit-bearing block ranges** (from `GREATEST(max sender block, max
321+ recipient block)` per failing key, bounded to cursor): one genuinely dense
322+ cluster — 17 of the 49 failing ` ft ` rows are all ` SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.alex-vault* `
323+ relationships with last activity between blocks ** 148,315 and 154,976**
324+ (and, per the full per-holder event-height list pulled for ` mega ` , real
325+ activity densely populating ** 133,776–150,373** ) — plus the two priority-case
326+ clusters at block ** 8,465,501** (` gl-fees-bank ` / sbtc-token * and* usdh-token,
327+ same block) and a long tail of isolated single-holder deficits scattered
328+ across the whole chain (296k, 398k–433k, 510k, 599k, 644k, 1.18M, 1.6M–1.61M,
329+ 2.13M, 2.88M, 8.24M, 8.30M, 8.39M, 8.66M–8.67M). The scattered tail does not
330+ cluster into a small number of incident windows the way the f068 catch-up
331+ burst did — it looks like a low, steady background rate rather than one
332+ episode.
333+
334+ ### Step 2 — SDK-consumer comparison: clean
335+
336+ A minimal consumer built ONLY on ` @secondlayer/sdk ` (` Index.events.walk ` ,
337+ hitting the public ` https://api.secondlayer.tools ` Index API — no
338+ ` packages/subgraphs ` runtime code in the path) reconstructed both priority
339+ cases' full credit/debit histories:
340+
341+ ```
342+ mega / alex-vault-v1-1: credits=40,507,496 debits=40,507,295 net=+201
343+ gl-fees-bank / sbtc-token: credits=1,754,308 debits=1,754,308 net=0
344+ ```
345+
346+ Both figures are ** byte-identical** to the ` decoded_events ` SQL reconciliation
347+ above, and to Part B's original numbers. ** Verdict: the SDK read path is
348+ correct; ` holdings ` (the subgraph accumulator) is what's wrong.** This is a
349+ clean, unambiguous pass — no STOP condition. The defect, whatever it is,
350+ is confined to the subgraph-runtime write path; SDK-based consumers reading
351+ the same Index API are unaffected.
352+
353+ ### Step 3 — reindex-path reproduction: discard counter never fired
354+
355+ ` PublicApiBlockSource.loadBlockRange ` (` block-source.ts:226-274 ` ) was
356+ instrumented (locally, uncommitted, reverted after this pass) to count events
357+ whose ` block_height ` was absent from the blocks-walk-seeded map — the exact
358+ mechanism the leading hypothesis named. Reproduction ran against prod's real
359+ public Index API (read-only GETs; no prod writes) with local docker Postgres
360+ as the write target:
361+
362+ 1 . A real ` reindexSubgraph ` call (not a synthetic per-block harness) over
363+ blocks 140,000–142,999 — the real function, real HTTP walks, real local
364+ DB writes, ` SUBGRAPH_SOURCE=streams-index ` — processed ~ 2,000 contiguous
365+ blocks with ** zero discards** .
366+ 2 . A targeted sample of 40 exact block heights spanning the * entire*
367+ ` mega ` /` alex-vault-v1-1 ` relationship's history (133,776→150,373) and 59
368+ exact heights spanning the entire ` gl-fees-bank ` sbtc-token+usdh-token
369+ relationship's history (857,750→8,465,501) were each probed via
370+ ` loadBlockRange(h, h) ` — ** zero discards** across all 99 heights.
371+
372+ ** The discard counter never fired.** Per the plan's STOP condition, this
373+ hypothesis is ** not supported by empirical evidence** — at least not under
374+ current chain/API conditions, replaying via a fresh, low-concurrency local
375+ process. This does not rule out a historical, timing/concurrency-dependent
376+ occurrence during the original production reindex (a reorg landing between
377+ the concurrent blocks/events walks, the leading candidate) that isn't
378+ reproducible from a quiet replay after the fact — but no positive evidence
379+ for it was found either.
380+
381+ ** Pivot per the plan: ` FallbackBlockSource ` log check.** Read-only
382+ ` docker logs secondlayer-subgraph-processor-1 ` was checked for "fallback" /
383+ "using DB tap" warnings during the reindex window (2026-08-03 03:07–09:54
384+ UTC). ** Inconclusive, not clean** : the container was restarted 2026-08-04
385+ 02:40:39 UTC (log history starts there), so no logs from the reindex window
386+ exist to check. This is unmeasured, not ruled out.
387+
388+ ### Step 4/5 — no fix
389+
390+ Per the STOP condition (discard counter never fired), ** no fix was
391+ implemented** . The instrumented ` block-source.ts ` was reverted to its
392+ committed state; no other file in ` packages/subgraphs ` was changed. No
393+ regression test was added, since there is no confirmed mechanism to encode
394+ one against.
395+
396+ ### Where this leaves things
397+
398+ - The pre-f069 catch-up replay defect is fixed and its blast radius (hundreds
399+ to thousands of rows) no longer applies.
400+ - A smaller, real under-application defect remains: ~ 96% of currently-negative
401+ rows and ~ 3% of a random positive sample. Its mechanism is ** not** the
402+ ` loadBlockRange ` blocks/events walk-disagreement hypothesis — that was
403+ tested directly and did not reproduce.
404+ - Two structural facts from the earlier (superseded) investigation still
405+ stand and are worth re-reading before the next attempt: (a) the reindex
406+ era's own ` blocks_processed ` coverage was 95.2% of its range — consistent
407+ with sparse early-chain history, not evidence of loss on its own; (b) the
408+ per-height retry-or-halt logic in both ` reindex.ts:289-308 ` and
409+ ` catchup.ts:302-320 ` means a height * entirely* missing from the blocks walk
410+ either self-heals (single-height refetch succeeds) or fails loudly
411+ (` subgraph_gaps ` row or a halted reindex) — neither of which matches the
412+ silent, gap-free deficits found here. That structural argument, plus this
413+ addendum's empirical null result, together make the ` loadBlockRange `
414+ map-seeding gap an unlikely explanation going forward, not just an
415+ unreproduced one this session.
416+ - Genuine next steps, not attempted here: instrument the reindex * write* path
417+ (` ctx.increment ` / ` context.ts ` 's batching) rather than the read path, since
418+ Step 2 already clears the read path all the way to the Index API; and
419+ retry the ` FallbackBlockSource ` log check with longer-retention logging
420+ (or a metrics counter) in place before the next full reindex, since this
421+ pass found the question unanswerable after the fact.
0 commit comments