Stellar Index v0.39.0
[v0.39.0] — 2026-08-21
Tested against Stellar protocol v23. Applies migration 0146 (additive —
creates the defindex_fees hypertable; old-binary-safe, touches no
existing table).
Added
- DeFindex
dfeesfee distributions are now modelled (W5.2, the last
open launch item). Body shape proven from captured on-chain blobs
(Map{"distributed_fees" → Vec[(token, i128)]}, per-asset, empty vec
valid): one row per distributed-fee token into the newdefindex_fees
table, with full sink/projector/reconcile registry parity. The ~12.8K
historical events backfill viaprojected-rebuild -source defindex
after this release deploys. - Explorer feels alive: live data across the whole site. Pool
reserves, pair tables, and lending reserves refresh on every ledger
close (shareduseLedgerFollow); charts advance their forming candle;
the home "live USD price" actually streams and flashes; venue/DEX
last-price cells flash again; the asset History tab is a true live
trade tape over the previously-unused/v1/observations/stream;
rollup panels and activity feeds auto-poll. - Failed transactions are first-class on the explorer and API with
explicit failed status and failure reason (D-PART-FAILEDTX decision),
plus the 2026-08-14 audit's decisions batch. - OpenAPI spec overhaul: exact route parity (129/129 with unique
operationIds), valid OpenAPI 3.1 null unions, 0 Spectral errors,
regenerated Postman + types.
Fixed
- Completeness re-derive counts sweep-rescued outputs at their own
ledger (eventLedgerCarrier): a correlation-buffer rescue (phoenix
7-field era) is now attributed where its served row lives instead of
at the sweep-trigger ledger, removing the CS-084 ± shift noise from
strict per-ledger reconciles. projected-rebuildclamps-workersto 1 for correlation-buffer
decoders — concurrent out-of-order windows starve sweep triggers and
silently drop groups (measured: 4 workers lost ~650 of 5,154 phoenix
era trades in a dry-run; 1 worker lost none).- SQLSTATE class extraction guards malformed codes (
sqlStateClass)
instead of slicing blind. - The pgBackRest restore drill had never once run on its schedule
(BDR-04). CS-110's whole point is evidence that the backups restore,
and the scheduled path produced none — for three stacked reasons, each
hidden behind the one before it:PrivateTmp=truegives the unit its own empty/tmpand/var/tmp,
soDRILL_ROOT=/var/tmp/restore-drill— a provisioned 5.2 TB ZFS
dataset, plainly present on the host — did not exist inside the
service's mount namespace.ReadWritePathson that path failed
namespace setup and systemd aborted the unit with226/NAMESPACE
BEFOREExecStart. Every passing drill on record was run by hand,
which has no namespace.- With that cleared,
NoNewPrivileges=trueblockedsudo's setuid
transition ("unable to open /etc/sudoers: Operation not permitted").
The unit runs as root by design and DROPS privilege topostgres;
no-new-privs protects nothing on an already-root unit while
disabling the one mechanism it uses to run with LESS privilege. - Then
pgbackrest, running aspostgres, could not traverse
/var/lib/stellarindex(drwxr-x---).
The dataset now lives at/srv/restore-drill, postgres-owned —/srv
is world-traversable, is not shadowed byPrivateTmp, stays writable
underProtectSystem=full, and already hostshistory-archive. The
ZFS role gained optional per-datasetdir_owner/dir_group
(default(omit), so every other dataset is untouched).
tip_lagwas measuring the backup's AGE, not recoverability (BDR-05).
The scratch instance runshot_standby = onand is started with
pg_ctl -w, which returns the moment CONSISTENCY is reached — while
replay of the remaining archived WAL continues in the background. The
drill then measured the restored tip immediately, so the number it
reported was "how old was the backup we restored from". Measured
2026-08-19: lag 13,392 ledgers (~18.6h) against a diff taken 21h
earlier, whilearchive-getwas demonstrably still streaming segments
in ~10ms each minutes later. On a daily-diff schedule that made the
< 5000threshold unpassable except by drilling shortly after a diff —
the 2026-07-03 pass (240 ledgers) was exactly that accident, and a
threshold met only by luck is not evidence. The drill now drains the
archive stream to an LSN captured from the live primary before
measuring, treating BOTH terminal states as drained (replay passed the
target, or recovery ended and promoted — the latter returns NULL from
pg_last_wal_replay_lsn()and would otherwise spin to the timeout on
the very run that succeeded). The drain is a reported check of its own,
so a timeout can never masquerade as a clean measurement.- The
ReadWritePathsdirective is gone entirely rather than repointed:
ProtectSystem=fullalready leaves/varand/srvwritable, and the
directive's only effect here was to make a missing path a hard start
failure.