Skip to content

Releases: nuthatch-org/nuthatch

v5.3.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 18:08
b3a8f3f

Nuthatch 5.3.0 proves which blocks a validator proposed (RFC-0063 plan C), whoever received their
fees, so rotki can book a validator's produced blocks and MEV from a nest alone. It also fixes a 5.2.0
defect that stopped rotki reading history from a nest with a start_block. Nothing outside an
[address_history] nest changes, and there is no storage migration.

Proven proposers per validator (Ethereum mainnet)

  • The proof is the chain's own. Block N+1's parent_beacon_block_root (EIP-4788) commits to the
    beacon header that produced block N, whose proposer_index names the proposer. Every row of a span
    is checked before any is filtered by validator, so a tampered row fails the span instead of hiding a
    proposal. Every span hangs from the block the nest's own RPC calls finalized, read by that tag.
  • Coverage: slot 12,777,208 (block 23,551,570) onward. A range starting earlier is refused with
    "proposals are proven from block 23551570". Proofs do not follow start_block or end_block;
    end_block bounds only what is served.
  • Where the proofs come from: catalogue files beside the block partitions on the mirror (published
    for blocks 23,551,570 to 26,159,999), and for the blocks above them, built here from the RPC and a
    beacon node when beacon_url (or NUTHATCH_BEACON_URL) names one. Without one, those blocks stay
    incomplete.
  • API: validators = [...], or POST /api/watch with {"validator": N};
    account/nuthatchValidatorBlocks&validator=N&startblock&endblock&page&offset, complete or refused,
    with supportedFrom and verifiedThrough. Rows carry the block reward (verified from the block's
    receipts, or rewardStatus: unavailable and retried, never zero), the relays' MEV, and the builder's
    payment from the block's traces.
  • nuthatch partitions --proposers --beacon-url URL --beacon-cache DIR builds the catalogue for a
    mirror from its block partitions, verifying every header as a nest does.

Fixes

  • A range before start_block names where history starts (fixes 5.2.0). Every account action
    refused a range starting before start_block, including rotki's usual startblock=0 and a request
    with no start, as "not indexed at block 0", which rotki could not act on. It now says "history starts
    at block N", and rotki asks again from there.
  • A covered answer above the finalized head is served on one hash check instead of waiting for the
    cursor. Measured with the cursor busy backfilling another account: every request timed out at 20 s
    before; 0.08 to 0.53 s after.
  • logs/getLogs fetches a page's receipts and timestamps together, 16 at a time, and answers
    within the request deadline or refuses.
  • A request above what is covered is served inside a long backfill. The cursor serves such
    requests between its windows, and a window could cover 50,000 blocks: on mainnet, tip requests for
    one account while the nest backfilled a busy wallet waited 10.9 to 16.0 s each. Backfill windows now
    start at 128 blocks and are paced to a quarter of the request deadline, and the bridge to the
    finalized block serves waiting requests between its chunks. The same measurement: 0.05 to 2.6 s,
    except a first request right after startup, which can still wait for the first windows.
  • request_deadline defaults to 8 s, down from 20 s, and is one ceiling per request from its
    arrival: the hash check, the queue, a window the cursor is part way through, a trace and a cover all
    count against it. A fully covered range is served if its final read completes within that ceiling;
    otherwise the request is refused. A gap the cursor cannot cover in time at its measured pace is
    refused at once with how far history is covered. The pace is measured per layer (discovery, block
    bodies, produced blocks), the layers' times add up, and owed relay re-asks count as blocks.
  • Recent timestamp lookups search near the chain head first. rotki's usual timestamp just
    behind the tip is bracketed within eight blocks, avoiding a search from genesis that could exhaust
    the four-second search budget. The answer still passes the same neighbour and ancestry checks.
  • An open mirror is read as far as it has published. A [address_history.mirror] without
    to_block follows the mirror's newest partition; coverage it cannot verify is taken back first.

Compatibility and upgrade

Replace the binary and restart. A 5.2.0 store is used as it is. Validator watches start empty.

v5.2.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 11:20
9db50fa

Nuthatch 5.2.0 makes address history (RFC-0063) meet rotki's sync on Ethereum mainnet: history up to
the chain head on demand, reorgs taken back, and answers that are exact or refused. Nothing outside an
[address_history] nest changes. An address-history store written by 5.1 is moved aside intact and
the history read again (see Compatibility).

Two cold tests of 5.1.1, by testers with no build history, said not ready. Every item below answers one
of their findings. RFC-0063 §12 has the reasoning.

Exact or refused

  • An explicit endblock is answered whole or refused. 5.1.1 shortened an endblock above its
    head to the head and answered status 1, so a client recorded history it never received: 17 of
    Etherscan's 65 rows came back as complete. Now the answer is the whole range or
    NUTHATCH_INCOMPLETE. Without endblock the answer runs to the end of the covered run, and a page
    past the first needs an endblock.
  • getblocknobytime without partitions. The 5.1.1 notes said timestamp to block was answered "from
    the RPC or from headers-only partitions". It was not: 5.1.1 answered from verified partitions only,
    and every nest-only rotki sync without them failed. Now a timestamp the partitions do not bracket is
    searched on the nest's own RPC, and the answer is proven against its neighbour block by parent
    hash; above the finalized head the answer must also link, parent hash by parent hash, to the
    chain head the search began from. A timestamp newer than the chain head is refused, and so is any lookup on a chain whose blocks
    can share a second (Arbitrum, BNB Chain).

The tip on demand

rotki ends a sync 15 seconds behind now. The cursor still backfills to the finalized head every poll;
a request above what is covered, and at or below the chain head, is now handed to the cursor, which
covers that address up to the block asked for and answers within request_deadline (new in
[address_history], default 20 s). The cursor stays the only writer: requests wait on a bounded queue
and are served between its windows, so none waits behind another address's backfill. A request past
the chain head is refused. A read reaching above the finalized head also goes through the cursor,
which checks for a reorg first. txhash lookups are traced by the cursor too, up to the chain head,
and refused if the trace names a block the main endpoint does not.

Reorgs

Anything read above the finalized head is read against its window's last block hash, checked before
and after the fetch, together with the newest kept hash under it, and kept with the record. Each poll
and each request above the finalized head compares the newest kept hash with the RPC's, and on a
mismatch takes back everything above the fork: rows, coverage, cached transactions, timestamps,
traces and relay answers. A fetch that began before a reorg keeps nothing above the finalized head,
and a transaction re-included in another block is read again. A fetch is checked against the newest
kept hash under it, so a window read on a fork cannot hide a stale block beneath its own matching
anchor; every block a trace names must be the main endpoint's, finalized or not; and the trace
endpoint must report the main endpoint's hash at each window's last block, so a trace source on
another fork or behind cannot answer an empty range the main chain does not have. Each check is made
twice, so a node that changes fork between comparisons cannot pass. A fetch that fails a check keeps
nothing. A head that fell under a kept block counts as a reorg. Kept hashes survive a restart, and a reorg the chain finalized
while the nest was down is still taken back. Only a block under the
finalized head the store committed is a conflict, which stops the cursor with the reason on /ready.

Finalized history is checked too. The store keeps the hashes of the last 32 finalized heads it
committed, and every poll, the first after a restart included, compares the newest and one older one
in turn with the RPC. A mismatch, which is a reorg under finality or an RPC that came back on another
chain under the same chain id, stops the cursor with the reason on /ready, and every answer is
refused, an answer already being prepared included. The conflict is kept in the store, so a restart
does not clear it; removing the address-history store does. Every fetch is also held against the
newest finalized hash, so a fork switch after a request's own check cannot join two forks. Each
check asks every endpoint in the pool, and the finalized head advances only where they all agree. A new finalized head's
hash is read before and after the checks and committed with the head in one transaction.

Two property tests over random reorg depths gate it, one through the cursor's own window against a
node whose traces follow the chain, with reorgs that cross finality.

rotki's ordinary Etherscan sync can import unfinalized history and does not automatically reconcile
already imported history after a reorg. The nest integration retains this shared limitation.

Produced blocks and MEV

  • Payout contracts. Builders that pay the proposer through a payout contract pay as an internal
    transfer inside the block's last transaction. Those are now found: the cold test's 16 such payments
    over 265,000 blocks had come back as no MEV.
  • unrecorded, not none. The rows come from the chain; MEV is a label: relay, conflict, or
    unrecorded (no delivery at the queried relays when last asked). A relay saves its delivery record
    after the block is published and can fail to, so unrecorded blocks and builder payments without a
    row are asked again daily for 30 days, and a delivery found then upgrades or inserts the row. The
    schedule commits with the coverage, so a crash cannot drop it.
  • Finalized only. Produced blocks are served through the finalized head; a request past it is
    refused with the block it is served through.
  • First in each pass. Produced blocks and the re-asks are covered before the backfill, so a long
    backlog cannot hold them back.
  • Every leg. A builder payment split across several transactions reports every hash in
    paymentTx and their sum in paymentValue.

Smaller fixes

  • logs/getLogs honours page and offset, starts with 16-block spans that grow only while they
    come back sparse, stops once it holds the page, and narrows any span whose answer passes 4 MiB, so
    a dense range no longer holds every log in memory; two run at a time. A range that needs more than 64 calls answers NUTHATCH_INCOMPLETE, as
    does a failed range, rather than an error.
  • The block-body scan keeps each block's miner, hash, timestamp and withdrawals for the newest
    100,000 blocks, so an address watched later reads those rather than the chain.
  • /ready reports stalled: true while the last poll failed; rpc_requests_total is current; a
    refusal always names its reason; addresses = [] is valid; a bind other than loopback warns; and
    unwatching one address no longer throws away another's window in flight.
  • /ready answers 503 until the first finalized head is read, still with chain_id.
  • A span retried after a timeout or a failure logs at warn, and a failed reconcile counts on
    poll_errors_total: a nest whose RPC was failing went sixteen minutes without a log line.
  • The address-history store's page cache is 32 MiB rather than redb's 1 GiB default. It did not
    change the mirrored nest's idle figure on macOS, where the cold test saw 240 MB: see Measured.

Measured

All on Ethereum mainnet over Alchemy.

  • Moving head against Etherscan. One address (0x9522…bafe5) over a 600-block window ending at the
    head. rotki's lookup of now minus 15 seconds returned the same block as Etherscan's, and txlist
    (21 rows), txlistinternal (3) and tokentx (0) up to it were identical to Etherscan's, every
    shared field equal (17 per transaction row, 14 per internal row). Three minutes later the head had
    moved 18 blocks; the same queries were covered on demand and identical again. 687 RPC calls in
    all, 572 of them the block-body scan of the window.
  • Start. 206 ms from a covered store to serving, 36 MB RSS after.
  • Memory with a mirror (26 partitions, 25.9M to 26.16M, one address). On Linux (jemalloc): 5.1.1
    peaked at 108 MB and idled at 36 MB, 5.2.0 peaked at 112 MB and idled at 48 MB. On macOS RSS
    idled at 129 MB (5.1.1) and 140 MB (5.2.0), but 5.2.0's physical footprint was 68 MB (peak 84.5):
    the rest is freed allocator regions macOS keeps resident, and a restart idles at 32 MB. The cold
    test's 240 MB was this macOS figure.
  • At rest a poll costs one more call than before (the RPC's finalized tag) and one more while
    hashes above the finalized head are kept.

Gated on Lodestar's data

Every change is in the address-history path, which no production nest declares, so this release changes
no query path the release gate measures.

Compatibility and upgrade

Replace the binary and restart. An address-history store written by 5.1 has no finalized block hashes,
so it cannot show that its finalized history is the chain's. At the first start it is renamed, intact,
to nuthatch.redb.pre-5.2-<unix time> beside where it was, one warning line says so and where it went,
and the nest reads its history again from start_block, keeping every account the old store watched.
Nothing is deleted: the old file can be kept, inspected or removed by hand. The rebuild costs what the
first sync did; getminedblocks, withdrawals and produced blocks on chains other than Ethereum mainnet
are unchanged.

v5.1.1

Choose a tag to compare

@github-actions github-actions released this 11 Oct 01:03
355a6cc

Nuthatch 5.1.1 fixes address history (RFC-0063) off Ethereum and makes its backfill workable on
providers that cap a call's block range. Nothing outside an [address_history] nest changes. There is
no storage migration.

Fixes

  • Optimism before Bedrock was served as an empty history (#2023). Its RPC answers trace_filter
    there with an empty success, so a pre-Bedrock range read as an address with no transactions. It now
    answers NUTHATCH_UNSUPPORTED and is never scanned. A 5.1.0 nest on Optimism that started before
    block 105,235,063 should be pointed at a fresh directory, since its stored coverage for that range is
    wrong.
  • Timestamp to block off Ethereum (#2023). getblocknobytime was never answered on other chains;
    it now is, from the RPC or from headers-only partitions.
  • Partitions for OP-stack chains (#2023). nuthatch partitions failed on Optimism and Base after
    Isthmus, where the header's withdrawals root is not a withdrawals commitment. Off Ethereum a
    partition now carries verified headers only.
  • A timed-out RPC call reads as a timeout (#2023), so discovery narrows its span instead of
    retrying the same range for good.
  • Discovery's span size (#2024). Several filters sharing one window each halved it on the same
    stale refusal, and a provider whose stated limit changes from call to call (Alchemy on Optimism) had
    its latest figure taken as a permanent ceiling. The window now proves a size before using it, a retry
    never shrinks below what a refusal named, and the ceiling is the largest limit the provider has
    stated.

Faster backfill on capped providers

Once the window has proved a size, discovery sends that many spans at once, eight at a time, and runs
both trace sides and the four log filters together (#2024). Answers are the same bytes. On Optimism,
vitalik.eth over 50,000 blocks on Alchemy went from 396-452 s and 654-1,002 trace_filter calls to
95-105 s and 245-292 calls. Gnosis, where hydration dominates, made the same 282-283 eth_getLogs
calls in the same time.

Checked

Optimism and Gnosis against Blockscout, and Base against Alchemy's transfer API, on vitalik.eth: every
action exact (Optimism 119 transactions, Gnosis 444, with their transfers and 20 of 20 timestamps
each). Base has one internal row Alchemy omits, a failed call Etherscan also lists.

Gated on Lodestar's data

Every change is in the address-history path, which no production nest declares, so this release changes
no query path the release gate measures.

Compatibility and upgrade

Replace the binary and restart. No re-index, except the Optimism case above.

v5.1.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 20:45
fa8c419

Nuthatch 5.1.0 adds address history (RFC-0063): a nest that watches accounts rather than contracts,
and serves their history at /api in Etherscan's shapes, so rotki can use it as an indexer. Nothing
about an existing nest changes. There is no storage migration.

This minor is a month early. 5.x cuts features monthly, and this one was brought forward for rotki,
whose founder agreed to a local indexer source on 2026-10-09 (rotki/rotki#13335). It adds and breaks
nothing, which is all the policy asks of a minor.

Address history (RFC-0063)

A nuthatch.toml with an [address_history] table and no [[contracts]] watches the accounts it
names. It runs on its own light path, one cursor beside the event pipeline, with no SQL engine and no
sealing: 31.6 MB idle, 179 ms to start, 45 MB at its peak during a backfill.

  • Transactions and transfers (#2012, #2013): txlist, tokentx, tokennfttx and token1155tx,
    found by tracing from and to each address and by positional eth_getLogs, each transaction
    hydrated once.
  • Internal transactions (#2016): txlistinternal by address and by hash, by Etherscan's rules
    (value-carrying calls, creates and selfdestructs; reverted frames listed with isError "1").
    --trace-rpc names an RPC whose trace_filter takes a long range.
  • Withdrawals, produced blocks and timestamps (#2018): txsBeaconWithdrawal, getminedblocks
    with the block reward, and getblocknobytime.
  • Verified block partitions (#2019): nuthatch partitions writes 10,000-block Parquet files of
    full headers and withdrawals. A nest that names [address_history.mirror] reads them instead of one
    RPC call per block, hashing every header, linking it to its parent, anchoring the range to a block
    its own RPC calls finalized and recomputing every withdrawals root. A file that fails is refused,
    never served. Ethereum's are published from Shanghai onward; see the hosted page on the website.
  • Proxy passthroughs (#2020): the proxy actions rotki asks for and logs/getLogs, forwarded to
    the nest's RPC in Etherscan's shapes, on a loopback bind only.
  • MEV from the relays (#2021): nuthatchProducedBlocks reports each block an account produced or
    was paid for, with what the MEV-Boost relays say was delivered. Off unless mev_relays = true,
    because the relays see the block numbers asked about. No relay record is no figure, never zero.

A range the nest has not fetched answers NUTHATCH_INCOMPLETE, and an action it does not serve
NUTHATCH_UNSUPPORTED, so a client falls back to its next source rather than reading a gap as an
empty history. The table stays out of the nest's identity; accounts are added at runtime through
POST /api/watch, on a loopback bind.

Checked against Etherscan on vitalik.eth over Ethereum blocks 18M to 20M: 2,318 transactions, 3,334
token, 106 NFT and 53 ERC-1155 transfers, 3,131 withdrawals and 20 of 20 timestamps, every field
equal; internal transactions, block rewards and relay deliveries on pinned cases, to the wei.
scripts/rfc0063-gate.sh re-runs it.

Fixes

  • /ready age assertions in the serve tests allow a clock tick (#2015), and two other flakes that
    turned main red are gone (#2009).

Gated on Lodestar's data

Address history runs on its own path and is reached only by a nest that declares it, so this release
changes no query path the release gate measures. Its commit carries the gate's statuses as usual.

Compatibility and upgrade

Replace the binary and restart. No re-index. A nest without [address_history] behaves exactly as on
5.0.1.

v5.0.1

Choose a tag to compare

@github-actions github-actions released this 09 Oct 15:19
f598d23

Nuthatch 5.0.1 is the first published 5.x release. It is 5.0.0 with one fix to how the release is
built, so everything in the 5.0.0 notes applies to it: the WASM layer is gone, a
[screening] table no longer loads, shipped binaries use fat LTO, and new segments hash differently.

Why there is no 5.0.0 download

v5.0.0 was tagged and never published. Its aarch64 Linux build was shut down twice during the
fat-LTO link of nuthatch, out of memory on the ARM runner, and a release with a missing target
stays a draft. 5.0.1 gives the Linux release runners swap before they build, which changes nothing
the dist profile produces (#2008).

Gated on Lodestar's data

The code is 5.0.0's, which passed the release gate on every production nest (see its notes). This
release's commit carries its own release-gate/<nest> statuses from the same gate run against a
Linux dist build of it.

Compatibility and upgrade

As for 5.0.0: replace the binary and restart, after deleting any [screening] table. From 5.0.0
it is a binary swap.

v4.15.2

Choose a tag to compare

@github-actions github-actions released this 08 Oct 17:02
5b8b0d9

Nuthatch 4.15.2 changes one default: a nest started without --poll-interval now asks for the tip every
5 minutes instead of at the chain's block time. It is 4.15.1 plus that change.

A nest polls every 5 minutes unless told otherwise

Every poll is billed whether or not a block carried an event: a tip call, and when a window commits, a
reorg check, a checkpoint and a finalized probe. An idle nest on a metered endpoint spent most of its
cost polling. Until now an unset --poll-interval meant the chain's block time, never under 2 s, so a
nest polled every 2 to 12 seconds. The openloom-bsc stopgap nest, polling once a minute, costs about
$3 a month on Alchemy at rest; at 5 minutes the polling cost falls to a fifth of that.

Without the flag a caught-up cursor now waits 5 minutes, or the block time if that is longer, which no
registry chain's is. New rows can take up to 5 minutes to appear. A nest whose readers need the
tip closely passes a shorter interval, for example --poll-interval 2s, exactly as before (#1995).

/ready is unchanged: freshness.poll_interval_secs reports 300 for a defaulted interval, and
poll_interval_source stays block_time for a default and flag for an explicit one.

Gated on Lodestar's data

Every production Lodestar nest already passes --poll-interval 5m, so none of them changes behaviour.

Compatibility and upgrade

Replace the binary and restart. No re-index and no configuration change. To keep the old cadence on a
nest that relied on it, add --poll-interval with the chain's block time.

v4.15.1

Choose a tag to compare

@github-actions github-actions released this 08 Oct 13:36
0b63028

Nuthatch 4.15.1 fixes one wrong answer in the GraphQL surface of the graph build: a filter on a
Bytes field with a checksummed address matched nothing. It is 4.15.0 plus that fix. A default build
behaves exactly as 4.15.0 does.

A checksummed address now matches

graph-node decodes a Bytes input as hex, so its case means nothing there, and a nest stores hex in
lower case. Compared as text, cards(where: { owner: "0xFeBD…" }) with the mixed-case address a
wallet hands an app returned an empty list instead of the owner's rows: a wrong answer with no error.

Filter values on a Bytes field, or on a reference to an entity whose id is Bytes, are now lowercased
before they reach SQL, for equality, _in lists and _contains. A singular lookup by a Bytes id is
treated the same way. String and ID values keep their case (#1992).

Found on the subgraph stopgap for openloom-bsc (nuthatch-org/graph-support#53), which has run this
fix since 2026-10-08.

Gated on Lodestar's data

The change is confined to how a GraphQL filter value is written into SQL in a graph build. No query
path of a default build, and nothing the release gate measures, is touched.

Compatibility and upgrade

Replace the binary and restart. No re-index and no configuration change.

v4.15.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 12:41
9a7d2ba

Nuthatch 4.15.0 adds the head count: after a successful nuthatch init, Nuthatch asks once whether it
may count you. The default is no, and nothing about serving, storage or indexing changes. There is no
storage migration.

The head count (RFC-0054)

Non-negotiable 3 is amended to allow exactly this and nothing else: no network call about the operator
that the operator did not explicitly turn on, with this one opt-in as the named exception (#1988).

  • The question appears once per machine, after init has written the nest and printed its last line,
    and only with a terminal on stdin and stdout. Enter is no. The answer is kept in
    ~/.config/nuthatch/count.toml (or under $XDG_CONFIG_HOME) and never asked again.
  • With a yes, each later init sends one object to https://count.nuthatch-indexer.com: version, OS,
    architecture, a built-in chain id (anything else is sent as 0) and which kind of init it was. No
    identifier, no address. Everything a command sends shares one two-second ceiling, and a failure is
    never printed.
  • CI or NUTHATCH_NO_COUNT silences it, question and pings alike.
  • nuthatch count shows where things stand; on, off, payload and forget do what they say.
  • The receiver keeps a tally per UTC day and nothing else. Its source is count/receiver/, and the
    totals are public at https://www.nuthatch-indexer.com/count.

Every line outside src/count.rs that reaches it is tagged, and a new leg of the required CI check
deletes the module and those lines and runs the whole suite on what is left. Deleting the head count
costs a self-hoster nothing, and that is now a job rather than a sentence.

Tooling

  • The subgraph stopgap orphan alert retries a failed fetch twice before paging, honouring Retry-After,
    and a page now carries the HTTP status and the start of the body (#1990).

Gated on Lodestar's data

The head count runs only inside init, which serves nothing, so this release changes no query path the
release gate measures.

Compatibility and upgrade

Replace the binary and restart. No re-index. A scripted init without a terminal is never asked
anything and sends nothing.

v4.14.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 04:57
d7dd6a3

Nuthatch 4.14.0 adds maintained views, keeps a view's own ORDER BY, and fixes two pieces of release
tooling. There is no storage migration, and a nest that declares no maintained view behaves exactly as on
4.13.0.

Maintained views (RFC-0062)

A nest can list views in a new maintained.toml. A maintained view is evaluated exactly as a request would
evaluate it and stored under maintained/, keyed on every input it read: the binary and engine, the
authored files, the schema and sealed segments of the tables it reaches, and their hot rows. A request
reads the stored copy only when its own inputs hash to that key, and otherwise answers from the view as
before, so a copy can never be stale or differ. Copies are rebuilt after a commit or seal reaches the tip,
never on the commit or seal path, and a newer state always wins over a queued older one (#1977, #1979).

  • maintained.toml is outside the data identity, so a nest still seeds from the mirror. Copies of a view
    no longer declared are removed.
  • A view that cannot be maintained (a volatile function, an incremental entity, offchain data, labels,
    a name that is not a plain identifier) stops the nest at load and fails nuthatch check, by name.
  • Metrics nuthatch_maintained_* and per-view counters on /ready appear only on a nest that declares one.

On the BetSwirl stopgap nest (160,802 bets, ThinkPad, 2026-10-07) every SDK query shape answered in
0.02 to 0.76 s from copies, against 0.9 to 3.1 s computed, with byte-identical answers, including through
reorgs 1, 3 and 5 blocks deep. Tip lag was unchanged with a build after every commit. RFC-0062's S3
(check --maintained) and S4 were not pursued.

A view's ORDER BY is kept

An ORDER BY (and LIMIT) at the end of an authored view or a derived table now orders what is read from
it, whenever nothing above reorders the rows. Every earlier release silently dropped it (#1986, #1984).
A view that is joined, aggregated, unioned or windowed plans as before. An ordered view filtered by IN,
EXISTS or a correlated subquery, or read through unnest or row_number() OVER (), is still unordered.

Tooling

  • Lodestar parity now treats a collection by any collector other than GraphTallyCollector as a known
    difference, which is the network subgraph's own rule, and still fails on an unexplained row (#1985, #1983).
  • The nightly mutation check names a baseline entry stale only if that shard actually tested it (#1974).
  • The subgraph stopgap orphan alert also watches arbitrum-nova, soneium, kaia, botanix and monad-testnet (#1978).

Gated on Lodestar's data

The release gate passed on each change against v4.13.0 on the allocations nest copy: 75 of 75 answered,
none regressed, no answer differs, peak RSS inside the 2,048 MiB budget.

Compatibility and upgrade

Replace the binary and restart. No re-index. The view ORDER BY change can reorder rows a client read
from an ordered view without ordering itself; it now matches what the view says.

v4.13.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 14:37
e49e260

Nuthatch 4.13.0 is an engine release: a page taken over a chain of left joins no longer computes every
row first. There is no storage migration and no other change.

A page over left joins reads only the rows it keeps

Burrmill moves to d8f79e4 (#1970, nuthatch-org/burrmill#94).

  • An ORDER BY ... LIMIT (or a plain LIMIT up to 10,000 rows) over a chain of left joins now takes
    its rows from the preserved side first, and every other side is narrowed to the keys those rows
    carry. The rewrite stays out of inner joins, filters over joined columns, shared subqueries, NULL-matching
    joins, mark joins and recursive terms.
  • Joins build on the narrowed side rather than whichever side merely looked smaller.
  • Registering many views no longer rebuilds the name index each time (1,500 views: 31 s to under 1 s).

On the BetSwirl stopgap nest (160,802 bets, cold, 512 MB engine limit, ThinkPad, 20-run p50, 2026-10-07)
the SDK's first bets pages went from 3.0 s to 2.0 s. A deep offset (skip: 150000), a where user
filter, tokens and the single-bet query are within noise of 4.12.0, because nothing in them can be
narrowed. Answers are byte-identical. #1951
records what under 1 s would take.

Gated on Lodestar's data

The release gate ran this build against a copy of the Lodestar allocations nest with 4.12.0 as baseline:
75 of 75 statements answered, none regressed, none differed; p99 2,711 ms against 2,675 ms; peak RSS
1,923 MiB against 1,918 MiB, under the 2,048 MiB budget. Two candidates failed the gate on the way and
were fixed with tests before this one passed.

Compatibility and upgrade

Replace the binary and restart. No re-index. Process RSS on a narrowed page can be up to about 160 MB
higher than on 4.12.0, because the narrowed rows are held outside the engine pool; it stays within the
per-cursor budget on every nest gated.