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
Cycle-level regression test for the v0.21.12 sink-side adaptive
shrink (internal/projector/cycle_wedge_test.go): drives cycleOneSource under an already-expired cycle budget with a sink
fast-failing on context.DeadlineExceeded and pins that the window
pointer halves per cycle, floors at MinBatchLimit, the cursor holds
(no advance-past-loss, no quarantine), and a healthy cycle then
commits + doubles the window back. The fix previously had only the shrinkWindow unit test.
tx_hash_index_parity config assertion
(scripts/ops/config-assertions.sh): hourly sampled parity probe —
500 random tx hashes from the trailing 10k ledgers must all resolve
in stellar.tx_hash_index via primary-key point lookups. The
explorer treats an index miss as an authoritative 404 and the index
was parity-verified genesis→tip only once (2026-07-30); this catches
future divergence (MV drop/recreate windows, ATTACH-style loads that
bypass the MV). Runbook row added to docs/operations/runbooks/config-assertion-failed.md.
Fixed
Served TWAP now folds the two stored market directions by minute
COVERAGE, not trade count (audit finding M-B; internal/storage/timescale/aggregates.go, migration 0126_twap_sample_count). SDEX stores each market in both
orientations, and TWAPPointsInRange merged them with a
trade-count-weighted mean of {twap, 1.0/twap_flipped}. But the twap_1h/twap_1d CAGGs (migration 0081) define twap as avg(prices_1m.twap) — equal per elapsed MINUTE, deliberately NOT per
trade — so the only correct merge weight is each direction's minute
coverage (how many prices_1m minute-buckets it contributed), which
the CAGGs did not store; count-weighting was exact only when trade
count happened to track coverage and wrong by an unbounded factor
otherwise. Migration 0126 adds sample_count (count(*)) to both
hierarchical TWAP CAGGs, and the read now folds in Go with the new combineDirTWAP: Σ(oriented_twap·sample_count)/Σ(sample_count) in
exact big.Rat, flipped rows inverted as an exact 1/twap (no 1.0/twap SQL rounding — ADR-0003). Pinned by internal/storage/timescale/twap_direction_combine_test.go
(cannedConn) and TestTWAPSampleCount_CoverageWeighted (real
TimescaleDB). ⚠ DEPLOY: migration is WITH NO DATA — after applying
it the operator must re-materialize the two views: CALL refresh_continuous_aggregate('twap_1h', NULL, now()); then ('twap_1d', NULL, now()); (run after prices_1m is whole).
Empty tx_hash_index no longer grants authoritative 404s
(internal/storage/clickhouse/explorer_reader.go): the tx-hash
availability probe now requires the index to be NON-EMPTY, not merely
to exist. An existing-but-empty index (the MV-drop / TRUNCATE
pathology: transactions keep flowing, the index silently stops) is
treated as index-unavailable — hash lookups take the bloom-scan path
and the verdict is not cached, so a repopulated index is picked back
up on re-probe ("empty table is not a definitive answer", the DailyActivityAvailable convention). A per-hash miss against a
non-empty index remains an authoritative not-found (the DoS
protection), pinned by new unit tests
(TestTransactionByHashEmptyIndexFallsBackToScan, TestTransactionByHashRepopulatedIndexRegainsAuthority). Complements
the hourly tx_hash_index_parity assertion, which catches PARTIAL
divergence. Un-reds TestClickHouseTxHashIndexProbeFallback in CI.
Explorer scan-settings integration test seeds genuine LedgerKey
XDR (test/integration/explorer_scan_settings_test.go): the
trustline seed carried a synthetic "e5-trustline-key" placeholder
that could never match the trustline/offer readers' real-XDR
PK-prefix LIKE ranges introduced in 85f706e
(accountEntryKeyPrefix), so the test failed while production (real
keys) worked. The seed now marshals real trustline AND offer xdr.LedgerKeys + entries for the seeded account, exercising both
prefix-range paths end-to-end (balance, limit, offer round-trip
asserted).
now mirrors the registry/catalogue — the stale 62,403,000 floor left
each source's true head range invisible to the supporting gap signal
(the authoritative ADR-0033 verdict was already corrected on 07-31).
Security
Unauthenticated metric-cardinality DoS closed (audit W4-obs-1; internal/obs/http_middleware.go): unrecognized HTTP methods passed
through to the Prometheus method label verbatim; net/http accepts any
token, so curl -X <random> in a loop minted unbounded label children.
Unknown verbs now collapse to a bounded "other".
SSE price-bridge payloads validated + re-marshalled (audit F2; internal/api/streaming/redispub): the subscriber fanned the raw Redis
payload to all stream clients with only an asset/quote-non-empty check;
with no-AUTH Redis a host-adjacent PUBLISH could inject a forged price.
Now the decoded event is bounds-validated (exact big.Rat VWAP, sane
window, fresh timestamp) and the validated struct re-marshalled, dropping
injected fields.
SAC/asset label cross-checked on /movements (audit W2-explorer-1):
a hostile token could set a sep0011 label impersonating a trusted asset
(e.g. Circle USDC) on the public feed. The fallback now derives the SAC
address and trusts the label only on a contract-id match (the check its
sibling wasm_view already had); also fixes /v1/accounts/{g}/positions
and the ?asset= filter.
WASM export-parser prealloc capped (audit W6-go-1): a section's
attacker-influenced LEB128 count sized make() before any read, risking a
multi-GB allocation on adversarial WASM; the prealloc hint is now bounded
by the reader's remaining bytes (defensive — reachable only if an
un-validated WASM path is ever added).
Changed
Cache-gate saturation now returns HTTP 503, not 500 (audit recon R3):
a saturated detached-refresh gate on a cold /v1/accounts/{g} read was
indistinguishable from a real error; a distinct ErrRefreshSaturated
sentinel now maps to the retryable 503 (already declared on the route),
while genuine scan failures stay 500.
Anomaly-freeze now releases reliably (audit W3-freeze-1 + W3-freeze-3):
(1) the shared freeze marker's Clear is deferred until the last frozen
window for a pair releases, so a short window auto-releasing no longer
unfreezes still-frozen sibling windows; (2) once a freeze is active Phase 1
stands down so the Phase 2 lifecycle (the sole auto-unfreeze authority) can
release it — a freeze whose price settled at a residual level past FreezePct but statistically healthy could previously stay frozen forever.
Divergence warning debounced (audit W3-guards-2): comparing our
shortest-window VWAP against an instantaneous reference spot produced false
warnings on fast moves; the warning now fires only after the over-threshold
condition persists a full VWAP-window horizon (default 5m), so a transient
VWAP-vs-spot gap self-clears while a sustained genuine divergence still
fires.
Fixed
decimalsguard retries the durable write (audit W3-guards-1): it latched fired before the DB upsert and never retried, so a transient write failure
left a non-7-decimal token serving a 10^(7-decimals)-skewed price until
restart. Split into an in-memory alarm latch (fire-once) and a persisted
latch set only on a successful upsert (retries otherwise).
/v1/price first-ever bucket served as low-confidence (audit W6-fresh-1):
the served-VWAP guard failed open (unbounded) on an empty baseline, so a
thin pair's first minute — a lone manipulated print — was served stale=false. It is now flagged low-confidence/stale (value still served,
never a blackout).
SAC-balance watched-asset keys canonicalized (audit W1-supply-1): the
SAC-balance observer copied the asset_key verbatim, skipping the CanonicalizeWatchedClassic its sibling observers apply, so a dash-form SAC
wrapper never matched the colon-form supply and its balances were
under-reported (same class as the 2026-07-02 trustline miss). (Historical
dash-form rows need a re-derive.)
One-side-zero SDEX fills no longer sink the trades batch (audit W1-defi-1):
a fill with one leg rounded to 0 (decoder-kept, census-counted) violated the base>0 AND quote>0 CHECK, rolling back the whole all-or-nothing batch and
firing a spurious insert_errors alert. They are now filtered before the
batch (consistent with the completeness reconcile, which already excludes
them); genuine Validate failures stay loud.
Duplicate contract events no longer served during the RMT merge window
(audit W4-storage-1): ContractEventsRecent (the /v1/contracts/{id} feed)
and EventsByTx read contract_events (a ReplacingMergeTree) without
dedup, returning re-ingested events twice until a background merge. Added
the package's per-shape idiom: LIMIT 1 BY <sort-tuple> and FINAL on the
bounded read respectively.
Aggregator background workers panic-isolated (audit W4-cmd-1): the
aggregator had zero panic recovery while the API isolates every worker, so
one worker panic crash-looped the price pipeline. Added a shared internal/worker.Recover and wrapped all 14 workers + 3 inner fan-out
goroutines.
Three dead alerts made fireable (audit W5-mon, W5-mon-3): supply_refresh_stalled + config_assertions_stale used timestamp()
(scrape freshness, always ~0) and never fired — rewritten to real age / changes(); the ledgerstream both_missing page's counter was
nil-in-prod (registry-gated) and is now registered unconditionally in obs. Each gained a promtool positive control.
Unbounded asset-detail response cache bounded (audit W6-perf-1): capped
at 4096 with opportunistic expired-purge / oldest-eviction (a crawler over
the real-asset universe could otherwise grow it without limit).
Data race in archive verify-chunks (audit W6-tst-1-race, surfaced by
bringing runVerifyChunks into CI coverage): totalVerified was written
under one mutex and read under another; now atomic.Int64.
frankfurter truncated responses propagate as errors (audit W6-go-2): errors.Is(io.EOF) instead of a substring match that swallowed io.ErrUnexpectedEOF.
WCAG-AA contrast + four LOW frontend fixes (audit W6-acc-1, W6-web-1/2,
W6-acc-2, W6-i18n-1): lifted --color-ink-faint to ≥4.5:1 on all surfaces
(used as text 254×); legible oracle-source chips; the status-page
deploy-skew render-throw guarded; role="img" on the Sparkline; 102 bare .toLocaleString() sites pinned to en-US.
Supply-reader observability + robustness (audit W6-sweep-1, W6-prv-3,
W1-sub-1): the classic-supply reader now emits the promised WARN on a
component-freshness query error; staff actor emails are masked in the log
sink; scval.AsBool nil-guards a zero-value ScvBool instead of panicking.
CI / tooling
verify.sh mirrors all CI import-checks + a parity guard (audit W5-ci-6):
it ran only 5 of 13 gates; now runs all 13 with a self-testing parity check
so the drift can't recur, plus a best-effort main-CI-health warn.
Money-lint + i128-lint blind spots closed (audit W5-ci-1/5): the SQL
money-lint now covers twap/vwap/tvl/wealth names + integer/smallint/money
types; lint-i128 catches int32/16/8 narrowing and is base-restored.
Three false-green gate paths fixed (audit W5-ci-2/3/4): route-sweep
counts curl 000 as a failure; reconcile-vs-horizon exits inconclusive on
an upstream outage; the gitleaks-config allowlists are in the anti-bypass
tripwire.
pnpm advisory-gate skip made visible (audit DEP-1) and a new lint
binds the completeness-verdict staleness consts to the timer cadence
(audit W6-fresh-4).
Test-integrity gaps closed (audit W6-tst-1, W6-tst-2, W2-plat-1): the
i128-truncation guard gained a positive control; runVerifyChunks (archive
hash-chain verify) is now compiled+run by CI; the usage_daily rollup
idempotency invariant (GREATEST-merge — the over-count finding was refuted)
is now tested against real Postgres.