May 12, 8-9 PM (18)
May 12, 9-10 PM (8)
May 12, 10-11 PM (40)
May 12, 11-12 AM (19)
May 13, 12-1 AM (10)
May 13, 1-2 AM (2)
May 13, 2-3 AM (5)
May 13, 3-4 AM (2)
May 13, 4-5 AM (5)
May 13, 5-6 AM (29)
May 13, 6-7 AM (52)
May 13, 7-8 AM (43)
May 13, 8-9 AM (44)
May 13, 9-10 AM (21)
May 13, 10-11 AM (22)
May 13, 11-12 PM (47)
May 13, 12-1 PM (25)
May 13, 1-2 PM (44)
May 13, 2-3 PM (57)
May 13, 3-4 PM (42)
May 13, 4-5 PM (33)
May 13, 5-6 PM (30)
May 13, 6-7 PM (51)
May 13, 7-8 PM (33)
May 13, 8-9 PM (9)
May 13, 9-10 PM (24)
May 13, 10-11 PM (30)
May 13, 11-12 AM (11)
May 14, 12-1 AM (18)
May 14, 1-2 AM (3)
May 14, 2-3 AM (4)
May 14, 3-4 AM (21)
May 14, 4-5 AM (11)
May 14, 5-6 AM (18)
May 14, 6-7 AM (18)
May 14, 7-8 AM (47)
May 14, 8-9 AM (53)
May 14, 9-10 AM (35)
May 14, 10-11 AM (20)
May 14, 11-12 PM (114)
May 14, 12-1 PM (54)
May 14, 1-2 PM (151)
May 14, 2-3 PM (32)
May 14, 3-4 PM (17)
May 14, 4-5 PM (14)
May 14, 5-6 PM (38)
May 14, 6-7 PM (12)
May 14, 7-8 PM (22)
May 14, 8-9 PM (37)
May 14, 9-10 PM (35)
May 14, 10-11 PM (27)
May 14, 11-12 AM (14)
May 15, 12-1 AM (18)
May 15, 1-2 AM (15)
May 15, 2-3 AM (5)
May 15, 3-4 AM (3)
May 15, 4-5 AM (13)
May 15, 5-6 AM (14)
May 15, 6-7 AM (10)
May 15, 7-8 AM (31)
May 15, 8-9 AM (23)
May 15, 9-10 AM (52)
May 15, 10-11 AM (71)
May 15, 11-12 PM (70)
May 15, 12-1 PM (73)
May 15, 1-2 PM (73)
May 15, 2-3 PM (66)
May 15, 3-4 PM (26)
May 15, 4-5 PM (13)
May 15, 5-6 PM (30)
May 15, 6-7 PM (29)
May 15, 7-8 PM (25)
May 15, 8-9 PM (8)
May 15, 9-10 PM (34)
May 15, 10-11 PM (34)
May 15, 11-12 AM (25)
May 16, 12-1 AM (2)
May 16, 1-2 AM (2)
May 16, 2-3 AM (3)
May 16, 3-4 AM (3)
May 16, 4-5 AM (0)
May 16, 5-6 AM (6)
May 16, 6-7 AM (2)
May 16, 7-8 AM (10)
May 16, 8-9 AM (1)
May 16, 9-10 AM (2)
May 16, 10-11 AM (1)
May 16, 11-12 PM (13)
May 16, 12-1 PM (11)
May 16, 1-2 PM (8)
May 16, 2-3 PM (15)
May 16, 3-4 PM (10)
May 16, 4-5 PM (2)
May 16, 5-6 PM (2)
May 16, 6-7 PM (2)
May 16, 7-8 PM (10)
May 16, 8-9 PM (6)
May 16, 9-10 PM (9)
May 16, 10-11 PM (29)
May 16, 11-12 AM (42)
May 17, 12-1 AM (9)
May 17, 1-2 AM (1)
May 17, 2-3 AM (0)
May 17, 3-4 AM (1)
May 17, 4-5 AM (0)
May 17, 5-6 AM (3)
May 17, 6-7 AM (2)
May 17, 7-8 AM (1)
May 17, 8-9 AM (1)
May 17, 9-10 AM (1)
May 17, 10-11 AM (6)
May 17, 11-12 PM (6)
May 17, 12-1 PM (4)
May 17, 1-2 PM (5)
May 17, 2-3 PM (9)
May 17, 3-4 PM (4)
May 17, 4-5 PM (8)
May 17, 5-6 PM (14)
May 17, 6-7 PM (10)
May 17, 7-8 PM (2)
May 17, 8-9 PM (4)
May 17, 9-10 PM (2)
May 17, 10-11 PM (20)
May 17, 11-12 AM (13)
May 18, 12-1 AM (10)
May 18, 1-2 AM (4)
May 18, 2-3 AM (5)
May 18, 3-4 AM (9)
May 18, 4-5 AM (14)
May 18, 5-6 AM (2)
May 18, 6-7 AM (37)
May 18, 7-8 AM (28)
May 18, 8-9 AM (35)
May 18, 9-10 AM (41)
May 18, 10-11 AM (42)
May 18, 11-12 PM (27)
May 18, 12-1 PM (134)
May 18, 1-2 PM (33)
May 18, 2-3 PM (83)
May 18, 3-4 PM (33)
May 18, 4-5 PM (44)
May 18, 5-6 PM (21)
May 18, 6-7 PM (16)
May 18, 7-8 PM (10)
May 18, 8-9 PM (22)
May 18, 9-10 PM (4)
May 18, 10-11 PM (25)
May 18, 11-12 AM (12)
May 19, 12-1 AM (7)
May 19, 1-2 AM (2)
May 19, 2-3 AM (9)
May 19, 3-4 AM (5)
May 19, 4-5 AM (10)
May 19, 5-6 AM (3)
May 19, 6-7 AM (52)
May 19, 7-8 AM (22)
May 19, 8-9 AM (45)
May 19, 9-10 AM (66)
May 19, 10-11 AM (30)
May 19, 11-12 PM (47)
May 19, 12-1 PM (79)
May 19, 1-2 PM (70)
May 19, 2-3 PM (41)
May 19, 3-4 PM (50)
May 19, 4-5 PM (15)
May 19, 5-6 PM (17)
May 19, 6-7 PM (18)
May 19, 7-8 PM (5)
May 19, 8-9 PM (0)
3,922 commits this week May 12, 2026 - May 19, 2026
perf: switch to stake_address.hash_raw ordering for stable + fast pagination
The previous fix on this branch ordered by the slot_no of the earliest
delegation per (addr, pool). That ordering was correct and chain-stable
but had a fatal perf characteristic: it forced a correlated subquery on
`delegation` for every row in `epoch_stake` (~1.3M rows per recent epoch
on mainnet). Page 1 of /epochs/:number/stakes took ~5s in EXPLAIN ANALYZE
even though the page itself is only 100 rows — top-N sort still requires
the sort key for every input row.

We tried several escape hatches before giving up on seniority ordering:

  - LATERAL subquery with (slot_no, tx_id, cert_index) tie-break to remove
    the replica-local addr_id fallback. Correctness improved, perf
    unchanged: still ~5s, same loop count.

  - 225MB covering index on
    delegation(addr_id, pool_hash_id, active_epoch_no, slot_no, tx_id, cert_index)
    Went to 6.9s — the index turned each lookup into an Index Only Scan
    but the range filter on active_epoch_no (column 3) meant rows still
    came back ordered by active_epoch_no, not by slot_no, so the planner
    re-sorted inside every loop. Reordering to put slot_no before
    active_epoch_no would have helped but only to ~1-2s, still far from
    what the original es.id query (~17ms) delivered.

  - Walking delegation backwards (latest-first) to invert the LATERAL.
    Doesn't work because a stake address can re-delegate to a different
    pool — only the most recent delegation matches epoch_stake, so any
    earlier delegations we see during the walk are noise.

The only paths to sub-second seniority ordering are materializing the
first-deleg-per-(addr, pool) into a helper table (Koios's approach via
grest.stake_distribution_cache) or upstreaming it into db-sync. Both are
large engineering changes for a query traffic of ~2k calls/day where the
common case is already <50ms via HTTP-layer caching.

Instead, drop seniority semantics and order by stake_address.hash_raw —
chain-derived, deterministic across replicas, and lets the planner walk
the existing `unique_stake_address` index, probing epoch_stake via
`unique_epoch_stake` (epoch_no, addr_id) with early-exit on LIMIT.
EXPLAIN ANALYZE on epoch 500 (1.3M rows):

  /epochs/:number/stakes page 1:           5000ms → 138ms

For /epochs/:number/stakes/:pool_id the win is smaller (7s → 3s on the
biggest pool) because that endpoint's bottleneck is the BitmapAnd
finding pool delegators for the epoch, not the sort. A future
bf_idx_epoch_stake_pool_epoch(pool_id, epoch_no) would close that gap
but isn't urgent at current traffic.

Both endpoints now sort by sa.hash_raw so pagination order is consistent
between them. Returned bech32 in `stake_address` is rendered from
sa.view; the binary hash_raw is only the sort key.

Trade-off: offset pagination cost grows linearly with page depth on
/epochs/:number/stakes (page 1 ≈ 138ms, page 10 ≈ 1s, page 100 ≈ 5-10s).
This is the standard "walk N matches, discard" cost of OFFSET on an
index-walk plan; the only escape would be cursor pagination, which would
change the public API. Acceptable for current usage.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
style: auto-fix markdown formatting across the repo
Markdownlint auto-fix resolved formatting issues:
- MD009: trailing spaces
- MD012: multiple consecutive blank lines
- MD022: missing blank lines around headings
- MD031: missing blank lines around fenced code blocks
- MD032: missing blank lines around lists
- MD047: missing final newline

CRLF to LF normalization: DCO.md, MAINTAINERS.md, SECURITY.md

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Yurii Shynbuiev <[email protected]>
ci: add file hygiene workflow and lint configuration
Add reusable file-hygiene workflow caller and canonical lint configs:

- .github/workflows/file-hygiene.yml: caller for lint-files.yml
- .editorconfig: UTF-8 (no BOM), LF line endings, indent rules
  with indent_size=4 override for Kotlin (*.kt, *.kts)
- .gitattributes: LF normalization for text files, binary rules
- .markdownlint.yml + .markdownlint-cli2.yaml: markdown lint rules
  with secp256k1-kmp/native/secp256k1/ excluded
- .yamllint.yml: YAML lint rules with relaxations

Refs: hyperledger-identus/hyperledger-identus#172

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Yurii Shynbuiev <[email protected]>