Skip to content

Stellar Index v0.39.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 19:07
· 123 commits to main since this release

[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 dfees fee 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 new defindex_fees
    table, with full sink/projector/reconcile registry parity. The ~12.8K
    historical events backfill via projected-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 (shared useLedgerFollow); 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-rebuild clamps -workers to 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:
    1. PrivateTmp=true gives the unit its own empty /tmp and /var/tmp,
      so DRILL_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. ReadWritePaths on that path failed
      namespace setup and systemd aborted the unit with 226/NAMESPACE
      BEFORE ExecStart. Every passing drill on record was run by hand,
      which has no namespace.
    2. With that cleared, NoNewPrivileges=true blocked sudo's setuid
      transition ("unable to open /etc/sudoers: Operation not permitted").
      The unit runs as root by design and DROPS privilege to postgres;
      no-new-privs protects nothing on an already-root unit while
      disabling the one mechanism it uses to run with LESS privilege.
    3. Then pgbackrest, running as postgres, 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 by PrivateTmp, stays writable
      under ProtectSystem=full, and already hosts history-archive. The
      ZFS role gained optional per-dataset dir_owner/dir_group
      (default(omit), so every other dataset is untouched).
  • tip_lag was measuring the backup's AGE, not recoverability (BDR-05).
    The scratch instance runs hot_standby = on and 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, while archive-get was demonstrably still streaming segments
    in ~10ms each minutes later. On a daily-diff schedule that made the
    < 5000 threshold 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 ReadWritePaths directive is gone entirely rather than repointed:
    ProtectSystem=full already leaves /var and /srv writable, and the
    directive's only effect here was to make a missing path a hard start
    failure.