Re-checking the derived graph against on-chain truth — and routing durable drift to action. ~14 min.
Everything you've studied builds the graph from derived signals — RPC probes, heuristics, decoders. Any of them can be wrong: a stale balance, a missed admin, an edge that no longer reflects the chain. So the system does what a careful engineer would — it re-reads the chain and checks its own work. That's the chain-reference harness, the home of the "freshness verifier" L26 kept pointing at, and the closed loop that makes discovery's honest gaps actionable.
HOLDS edge for a token should equal its
totalSupply(); an Aave market's collateral in the graph should match the market's actual on-chain state; a
Safe's owners in the graph should match getOwners() today. Each is a re-derivable invariant. The harness ships
one Verifier per class that re-computes the truth from chain and compares.
The interface (verifier.go) is strikingly small. Each verifier supplies the chain truth-set, the
stored set, and a per-instance recheck:
type Verifier interface { Class() string // e.g. "HOLDS", "LENDING_COLLATERAL" EnumerateOnChain(ctx, block) ([]Ref, error) // C — the canonical truth set EnumerateGraph(ctx) ([]Ref, error) // G — what we currently store VerifyInstance(ctx, ref, block) (pass bool, []Diff) // recheck one ref's VALUES }
From C and G the runner derives three kinds of finding — and that taxonomy is the whole mental
model:
On chain, missing from the graph. We failed to discover something. This is the machine form of L19/L25's discovery_gap.
In the graph, gone on chain. A stale edge/node we should prune (e.g. an owner removed, a market closed).
Present both sides, but the stored value ≠ chain value beyond tolerance. The right shape, the wrong number.
AbsErr > AbsTolerance AND RelErr > RelTolerance. Requiring
both means a tiny absolute wiggle on a huge balance, or a large relative swing on a dust amount, won't trip a
finding — only a mismatch that's big in both senses counts. Same "distinguish a real miss from rounding noise"
discipline as L19's ×1.001 cap slack and L16's minDelta.
The verify_*.go files (one per class) sort into three jobs — and you've met all three before:
| Job | Asks | Examples |
|---|---|---|
| Coverage | "did we find everything?" (gaps) | verify_at_risk_coverage, verify_multisig_coverage, verify_oracle_discovery_coverage |
| Correctness | "is what we stored right?" (drift) | verify_balance_conservation, verify_lending_aave / compound_v3 / morpho / euler_v2, verify_admin |
| Freshness | "is it recent enough?" | verify_lending_freshness, the NavlinkFreshnessVerifier from L26 |
verify_multisig_coverage gap means a
Safe whose owners the probes (L25) missed. The NavlinkFreshnessVerifier is the safety net L26 said replaced the
whitelist gate — here's where it lives. And chainref's per-instance VALUE checks are the on-chain cousin of L23's
in-process CheckInvariants: same instinct, different reference (the chain vs. internal logic).
A unit test fails loudly and stops. A production graph drifts constantly and partially — you can't page a human on every transient blip. So findings flow through a streak-based actuator:
streak counter: re-detected next run → streak++; resolved (no longer found) → streak = 0. Only findings with streak >= threshold are FetchPromotable — so a one-cycle mid-block balance or an RPC blip never escalates, but a persistent discrepancy does.LinearPromoter opens a Linear issue and stamps linear_issue_id back on the finding (so it's filed once, not every run). Drift the system can't self-correct becomes tracked engineering work — automatically.runner_healer.go, healers/) auto-fix what they can — re-enrich a node, reclassify it, prune a stale edge — instead of ticketing. The harness fixes the cheap stuff and escalates the rest.Config with sampling/budget knobs, and the runner verifies a sampled subset per cycle
(runner_sampling.go). Coverage trends over many cycles rather than a full sweep each time — quality is
measured statistically, not exhaustively.
Each run writes a ClassReport (coverage %, gap/excess/drift counts) persisted as a :QualityReport node —
read by the admin-panel /quality page (L17). So an operator sees, per class, "HOLDS coverage 99.2%, 14 drifts, 3
gaps" at a glance, with the durable findings behind it in OpenSearch and the escalated ones linked to Linear. The harness
turns "is our graph any good?" from a vibe into a dashboard.
EnumerateOnChain (C) and EnumerateGraph (G). What is a "gap" finding?AbsErr > AbsTolerance AND RelErr > RelTolerance. Requiring both bounds means…verify_balance_conservation verifier checks which on-chain invariant?streak counter rather than acted on immediately?Grounded in: pkg/quality/chainref/verifier.go (Verifier interface — EnumerateOnChain C / EnumerateGraph G / VerifyInstance, Kind gap/excess/drift, Tolerance Abs∧Rel, Config sampling, ClassReport→:QualityReport), findings.go (OpenSearch FindingDoc + streak upsert/reset, FetchPromotable streak≥threshold), linear_promoter.go (LinearPromoter + linear_issue_id write-back), runner_healer.go/healers/, runner_sampling.go, the ~30 verify_*.go classes. Verify against source — the code is the truth.