You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Carried-claim invalidation gap: a projector-replay below the watermark
now forces re-reconciliation (migration 0125 + internal/ops/ingest/projector.go + internal/ops/chops/compute_completeness.go).
The daily completeness driver reconciles each source only from its prior
watermark to tip and CARRIES the prior clean projection claim for the older
prefix (projectionClaim rule 3 / INV-5) — sound only while the served tier
below the watermark is immutable. A projector-replay rewind rewrites
exactly that region, and nothing forced a re-check: the carried claim kept
certifying rows the replay had changed, which is how the 07-30 cctp
replay's 19,366 over-projected event_index-0 twin rows (62.27M–63.55M)
escaped the verifier. projector-replay now records a per-source dirty
window (projection_dirty_windows, widened LEAST/GREATEST on overlap)
BEFORE rewinding the cursor — refusing to rewind if the record fails — projected-rebuild -write records the same window before its first write
(same invalidation, bulk path), and compute-completeness fails closed on reading the table, extends its
projection reconcile floor over any pending window regardless of -from,
and clears the window only after a CLEAN projection verdict whose scope
covered it (bounded delete, race-safe against a concurrent widening replay).
cctp + rozo reconciliation-catalogue genesis floors corrected to the
lake-derived exact first events (internal/ops/chops/reconciliation_catalogue.go),
mirroring the 07-30 protocols_registry.go fix: cctp 62_403_000 →
62_146_641 (410 real served rows sat permanently BELOW the verify floor,
structurally out of every verdict), rozo 62_403_000 → 60_829_397. All other
catalogue entries audited against the registry — no further drift.
(DefaultGapDetectorTargets in per_source_gaps.go still carries the old
floors — supporting signal only, owned by another change.)
cctp bespoke reader post-twin-deletion cleanup
(internal/storage/timescale/protocol_bespoke_cctp.go). The flow CTEs
collapsed per (tx_hash, op_index) with max(amount) — a workaround for the
legacy event_index-0 twin rows deleted from r1 on 2026-07-31 — which would
silently HALVE a future genuine batched double-transfer in one op (the 0112
class: same-op same-type groups are proven on the wire by admin events).
Value-carrying CTEs (deposit_for_burn / mint_and_withdraw) now read raw
per-event rows; the message_received CTE keeps its one-body-per-op group
(join semantics — a second body row would fan out every joined mint row),
and the mint_and_forward restatement exclusion (a real semantic rule, not
twin dedup) is untouched. Also: the all-time "Unique recipients" KPI now
counts DISTINCT recipient addresses as labelled — it previously counted
recipient-bearing ops.
Explorer honesty wave — cold-audit findings across web/explorer/src.
Eight fabrication/robustness classes fixed in the explorer only (no
API/server changes): (1) /lending pools table: an API failure now
renders an explicit unavailable state instead of the fabricated
"No Blend pools have emitted auction events yet" empty-state, and PoolRealStats (hoisted to module scope — it was re-created every
parent render) computes TVL/utilization/weighted APRs over PRICED
reserves only with the basis labelled, instead of coercing absent supplied_usd/borrowed_usd/APRs to 0 inside sums and weighted
averages; (2) partial trailing daily bucket: a new shared @/lib/series helper (dropPartialTrailingDay) drops today's
accumulating UTC bucket from every daily-grain chart mapper
(BespokeSection standalone + grouped panels, BridgeShowcase lines,
ProtocolView activity, SourceVolumeHistory, SdexVolumeSection;
hourly 24h series keep the live day) and TimeSeriesChart headlines
"Latest" from the last COMPLETE point; (3) /network
protocol-upgrade markers now require BOTH sides of a step to be a
real (>0) version — lake sentinel rows minted phantom "protocol v0/vN"
markers (the field is a uint32 without omitempty, so null-guards never
fired); (4) /anomalies freeze durations render "—" instead of
"NaNd" for missing/garbage timestamps; (5) holder balances render via
a BigInt-divide-first formatBaseUnits/scaleBaseUnits
(@/lib/format, ADR-0003) instead of Number() on >2^53
smallest-unit strings (absent → "—", not NaN); SupplyFlowsBar shares
the same scaler; (6) toChartNumber returns null — the point is
dropped — for non-numeric series values (previously plotted as
fabricated 0s) and for compact-suffixed figures ("1.2M" previously
mis-scaled 10^6× to 1.2); (7) non-parsable point dates are dropped
before setData instead of the epoch-0 fallback that plotted 1970
points and could crash lightweight-charts on duplicate time:0;
(8) small honesty items: SdexVolumeSection renders a served 1-point
series as the point ("insufficient history"), not a false
"unavailable"; the anomalies ReasonHeatmap derives its day columns
from the served data's date range, not the client clock; a bespoke
block's notes no longer caption other windows' data after a failed
window refetch.
Daily-grain protocol/bespoke series exclude the current (partial)
UTC day server-side. Every bespoke daily series (DEX activity/
traders/top-pairs, lending sides/backstop/auctions/per-pool, credit,
oracle, defindex, CCTP flows + cumulative net inflow, rozo) and the
protocol activity series/breakdown included today's still-accumulating
bucket with no marker — the UXP-16 "phantom cliff" class, previously
fixed only on /network. Daily-grain SQL now bounds at the day start
(shared completeDaysOnly fragment; ClickHouse readers bound close_time/day likewise — the event breakdown shares the bound so sum(event_breakdown) == events_total still reconciles). The 24h
window's hourly grain keeps its live edge.
Three small honesty guards (audit 2026-07-31). (a) The SDEX
order-book reader no longer silently skips a non-removed offer change
whose XDR fails to decode — each skip freezes that key's prior served
state, so it now logs a warn line and increments the new stellarindex_sdex_orderbook_undecodable_offers_total counter.
(b) mapFreezeReason no longer defaults unrecognized automated
freeze decisions to manual (the operator-initiated reason) — they
record as the new other reason (migration 0124 extends the CHECK;
spec 1.18.0 adds the enum value).
(c) A soroswap swap with all FOUR amounts non-zero satisfied both
direction arms and silently decoded as 0→1, dropping the 1→0 leg —
now refused whole as ErrAmbiguousSwapDirection (wraps ErrNonDirectionalSwap, so it stays a recognized no-op for ADR-0033
completeness).
Blend bespoke block no longer sums token amounts across assets.
The headline "Net supplied/borrowed" KPIs and the per-pool table's
cross-asset sums + "Util %" ratio added token_amount across tokens
with different decimals — a meaningless number under an authoritative
label (the sibling count-first suite in bespoke_lending.go documents
exactly why). KPIs are now count-first (active users, flash loans,
alongside the existing side-event/pool counts), the per-pool table
serves supply/borrow-side event + user counts, and amount sums survive
only where scoped to a single asset (the per-asset table).
Stale "soroswap has no taker" claims removed from the wire. The
soroswap decoder captures the SwapEvent to recipient since
2026-07-30 (100% taker coverage on new rows), but the /v1/accounts/{g}/trades scope note still excluded soroswap and the
DEX bespoke block carried a hard-coded "0% taker coverage" note. The
scope note now includes soroswap (with the pre-2026-07-30 caveat) and
the bespoke omission note is data-driven — emitted only when the
served window observably has zero taker-stamped trades.
usd_volume no longer hard-codes the off-chain 1e8 amount scale.
The FX pollers stamp trades at 1e6 (registry AmountDecimals: 6),
but both the insert-path stamper (tradeUSDVolume/tradeUSDVolumeViaFX)
and ClassifyUSDVolumeTier (the verify-usd-volume checker, which
would have certified the error) divided by a hard-coded 1e8 — a latent
100× under-valuation the moment FX trades re-enable. Both now consult
the source's registered AmountScaleDecimals (CS-040).
Explorer detached SWR refreshes are now globally bounded. The
stale-while-revalidate caches (account state, asset holders, contracts
directory, contract detail) single-flighted per key but were unbounded
ACROSS keys — and the key space is attacker-chosen on unauthenticated
routes, so churning fabricated G-/C-addresses queued one detached
multi-minute lake scan per key onto the 8-connection serving pool. A
shared semaphore (clickhouse.RefreshGate, limit 4 — half the pool)
now bounds all detached refreshes; on saturation the refresh is
skipped, never queued: stale entries keep serving, cold keys 503 with
the retryable timeout contract.
/v1/assets/{asset_id}/holders normalizes the asset spelling before
the cache and the lake query. ParseAsset admits the Horizon CODE:ISSUER spelling, but the raw request string was used as both
the SWR cache key and the query asset — the colon spelling scanned
for a value the lake never stores and cached an authoritative-looking
empty board under a duplicate key. Both spellings now share one
canonical (CODE-ISSUER) cache entry and query.
Protocol-detail cache: a degraded fast-failing rebuild can no longer
displace a good cached entry. The keep-old-entry guard only checked
the rebuild context's timeout, so a build that failed FAST (ClickHouse
down → every enrich errors in ms) replaced the previously-good view
with an analytics-empty one stamped fresh — one prewarm sweep during a
store outage would blank every protocol page. A non-ok build now only
populates cold/degraded keys (registry-only beats 503) and never
displaces a healthy entry, which stale-serves instead.
Protocol daily-pre-aggregation probe no longer latches a transient
error for the process lifetime. The fast-activity availability probe
was cached with a sync.Once, so one ClickHouse blip at first build
pinned the raw 12B-row scans forever. Per the schemaProbe precedent
(C1-048) only DEFINITIVE answers are cached (table missing = false,
rows found = true); transient errors degrade that call and re-probe.
Protocol analytics window integrity: one shared tip read + fast/raw
decision. Each analytics fill re-read the lake tip discarding the
error — a failed read (tip=0) made the fast path serve a 1-day window
still labeled 90d — and the three concurrent fills could split across
the fast/raw sources, breaking the sum(event_breakdown)==events_total
reconcile. The build now derives one plan from a single checked tip
read; an unreadable tip degrades the analytics honestly (status unavailable).
Security
Redstone attribution chain hardened end-to-end (OpArgs provenance +
cross-corroboration; cold-audit wave 2026-07-31). The dispatcher
attached an op's top-level InvokeContract args to EVERY event the op
produced — including events emitted by OTHER contracts reached as
sub-invocations — so a wrapper contract could call adapter.write_prices with the genuine signed payload while its own
attacker-chosen top-level args supplied the feed_ids redstone zips
prices onto (feed-attribution steering). Defense in depth, all layers
refusal-first (honest-blind beats misattributed):
Provenance gate (dispatcher + lake twin): args now attach only
when the op's invoked contract IS the event's own contract
(internal/dispatcher/dispatcher.go; internal/storage/clickhouse/extract.go applies the identical rule
at lake-write time, so re-derives stay in parity). Wrapper-invoked
adapter events arrive with no args and refuse cleanly via ErrMissingOpArgs, counted through the per-source decode-error
counter.
Body↔args binding: the event body's updater must equal the
op-args updater (ErrUpdaterMismatch), and duplicate feed_ids
are refused outright (ErrDuplicateFeedIDs) — a genuine write_prices cannot carry them (redstone-core Config::try_new
rejects ConfigReoccurringFeedId before the adapter can emit), and
pre-refusal they were a subset-arity inflation lever (audit F2); subsetFromStateWrites additionally counts each written feed once.
Equal-arity corroboration: when the op's value-changed
state-write keys are plumbed, even the equal-arity positional zip
is cross-checked — the changed feed-key set must equal the feed_ids set (order-insensitive), else the whole event refuses
(ErrStateWriteFeedMismatch).
The residual external assumption (redstone-core signer filtering is
not vendored; disagreement lands in the honest-blind class, never a
misattribution) is documented as an accepted risk in internal/sources/redstone/payload.go.
Fixed
CH state-write reader: parse failure now excludes the KEY, not the
row. An unparseable entry_xdr in internal/storage/clickhouse/state_write_keys.go dropped only that
ROW, so a lost pre-image promoted an identical-value rewrite
(adapter-REJECTED feed) to "changed" — violating the documented
"parse failure excludes the key" rule the dispatcher twin already
enforced. The key is now poisoned via a per-key bad-set, mirroring
the dispatcher.
CH state-write reader: ReplacingMergeTree dedup keeps the LATEST
version. Duplicate un-merged rows per (op, change_index) were
deduped first-read-wins; ReplacingMergeTree(ingested_at) semantics
say highest version wins, so a re-ingested correction could lose to
its stale predecessor depending on read order. The lookup now selects ingested_at and keeps the max-version row.
Changed
State-write enrichment is lazy on the ingest hot path. The
dispatcher computed every event-bearing op's value-changed
contract-data keys unconditionally; it now resolves them once per op
and only for events whose contract a registered decoder declared via
the new optional dispatcher.StateWriteKeyConsumer interface
(redstone is the only current implementer).