Repository navigation
v1.19.0
Added
-
tools compare-blocks:--fieldsprints the protobuf field paths that differ for each mismatched block (e.g.header.chain_id,txs[2],unknown_field(12)). Use it when--diffprints nothing: blocks are marked different byproto.Equal(which includes unknown fields), while--diffcompares JSON with unknown fields stripped by default. -
The merger's
maxUnlinkableBlockscircuit breaker (bundleSize*4) can now be overridden with theMERGER_MAX_UNLINKABLE_BLOCKSenv var, for deployments where the default is too tight (e.g. several one-block-file writers per chain) to tolerate an ordinary reorg or brief writer hiccup. Unset or invalid values keep the existingbundleSize*4default. -
The
Blocksrequest handler now logs an"incoming firehose Blocks request"line as soon as a request starts, carryingtrace_id,organization_id,api_key_id,real_ip,start_block,stop_block,final_blocks_onlyandcursor. The existing"firehose process completed"line gainsduration(total request time),time_to_first_dataandfirst_sent_block(zero-valued if no block was ever sent). Both lines are now emitted for every request outcome, including early rejections (session denied, rate limited, unimplemented transforms) and client disconnects, so every request can be paired up downstream bytrace_id. A client-initiated cancellation is logged witherror: "context canceled"; a server-initiated one (e.g. a revoked session) logs its real cause instead of collapsing into the same bucket. -
The merger now records what a merged-blocks file holds on the object itself, as three custom metadata entries written as it uploads each bundle:
datasize, the file's size once decompressed,itemcount, the number of blocks it holds, andtimestamp, the time of its first block written as2025-10-12 10:23:12in UTC. A listing then tells what a file holds without reading it.Google Cloud Storage only: no other backend keeps custom object metadata a listing reads back, so elsewhere nothing is written and nothing changes. The size is counted as the bundle streams to the store, and the first block's time is read from the head of the first one-block file, far enough to reach the timestamp field and no further, so a chain with large blocks costs no more than one with small blocks. An annotation that cannot be written never fails the merge: the bundle is written and correct, only its description is missing, and
firecore tools annotate-merged-blocksfills that back in. -
Added
firecore tools annotate-merged-blocks <gs-store-url>, which writes those same three entries on merged-blocks files written before the merger did it. Google Cloud Storage only, for the same reason.Each file is streamed through a zstd decoder and dropped as it decodes, one block at a time into a scratch buffer reused from block to block and from file to file, so memory stays flat whatever the block size; of the first block only the head is looked at, far enough to reach its timestamp. Only the block's metadata fields are ever decoded, its chain-specific payload being skipped, so no chain block type is needed.
--parallelism(32 by default) sets how many files are read at once.Files a previous run already annotated with all three entries are skipped straight from the listing, which carries their metadata, so a rerun over a mostly-done store costs one listing and nothing else;
--overwriterecomputes them.--start-blockand--stop-blockbound the listing service-side,--dry-runreports what would be annotated without reading anything.Reads and metadata writes are both pinned to the generation and metageneration the listing returned, so a file the merger rewrites mid-run is reported as failed rather than annotated with values from a version that is gone.
--grpcswitches to the Cloud Storage gRPC API, which on GKE takes the DirectPath route straight to the storage backend, spread over--grpc-connection-poolconnections (one per 32 of--parallelismby default).The store URL takes the same two query parameters
dstorereads off ags://URL:?project=bills the reads to that project on a requester-pays bucket, and?client_protocol=grpcselects the same transport as--grpc. Setting a project turns DirectPath off, since DirectPath does not carry thex-goog-user-projectheader requester-pays billing needs. -
Added
firecore tools stats-merged-blocks <gs-store-url>, which reports a merged-blocks store's total compressed and uncompressed size, block count, compression ratio and bytes per block, broken down by month and totalled over--start-blockto--stop-block.Nothing is downloaded: every number comes from the three metadata entries above, which come back with the listing, so the whole report costs one listing however large the range is. Files missing any of the three are counted and reported, and contribute to nothing. Google Cloud Storage only, where the annotation lives.
The report is labelled with a chain name, which defaults to the second-to-last part of the store path, the bucket excluded (
eth-mainnetfor bothgs://mybucket/something/eth-mainnet/v1andgs://mybucket/eth-mainnet);--chain-nameoverrides it.Pass
--jsonto get the same report as a single JSON object on stdout instead of the table: the chain name, the requested range, the first and last block seen, one entry per month, the total, and the count of files carrying no complete annotation. -
firecore tools substreams purge,prune-statesandprune-outputsall skip any module folder carrying aDO_NOT_PRUNEfile at its root, next to thelast_used*.zstmarkers, and report how many folders that spared. -
Added
firecore tools last-oneblock <oneblocks-store>which prints the highest block number found among the store's one-block files, as a bare number on stdout, exiting non-zero when the store cannot be listed, holds no one-block file, or a filename does not parse as one. -
Added
firecore tools substreams purge <state-url>which deletes substreams module caches that have not been used recently, reading thelast_used*.zstmarkers thatsubstreams-tier1refreshes on every request it serves. The required state URL matches direct tier1 layouts (<state-url>/<hash>and<state-url>/<tag>/<hash>) and shared network roots (<state-url>/<network>/substreams-states/<tag>/<hash>);--networkis optional for restricting shared network roots. No database involved, the store is the only source of truth.Retention can be per billing plan (
--retention default=30d,pro=30d,scaling=14d,free=3d), a module folder being kept as soon as one of its markers is within its own plan's retention, so a folder last used 15 days ago by aprorequest survives a 3 daysfreeretention. Plans with no explicit retention fall back to thedefaultone, and a folder carrying no marker at all is never touched unless--purge-without-last-usedis set.A condemned folder is emptied completely,
substreams.spkg.zstandsubstreams.partial.spkg.zstincluded, unless--keepnames glob patterns to spare. The scan itself downloads nothing:substreams-tier1overwrites a marker on every request it serves, so the object's last-write time is the usage date and the listing already carries it (--read-marker-contentsdownloads each marker and reads the date tier1 stored inside it instead, which is what you want on any backend once the objects have been copied, migrated or rsynced, since that resets the last-write time and would otherwise make every module look freshly used). Finding the folders costs one small request per module folder, all independent, so it scales with--scan-workers(256 by default, worth raising on a large network); the state and output files of a folder are only enumerated once it is condemned.--scan-onlyreports what is past retention without listing or deleting anything.Storage goes entirely through
dstore, so every store it supports works (gs://,s3://,az://,file://, a bare local path), with no backend-specific code: names, sizes and modification times all come out of the listings it was already making. The command accepts direct tier1 paths ([tag/]module-hash) and shared roots (network/substreams-states/[tag/]module-hash). A folder listing is paged one round trip at a time however few folders come back, so on the stores that bound a listing service-side (gs://,s3://) each level is cut into 17 slices listed concurrently, which takes the module-hash level of a network holding 39k folders from 8.2s to 1.6s.Scopes are processed one at a time and fully isolated: a shared network or direct state-store scope that cannot be listed, or whose files refuse to be deleted, is reported and the run moves on to the next one, the command exiting non-zero with a per-scope summary. Use
--dry-run(-n) to see what would go,--force(-f) to skip the confirmation prompt,--networkto restrict shared-root networks (direct tier1 layouts always scan all cache tags), and--daemon --interval 12hto keep purging unattended (a failed pass is retried at the next interval instead of killing the daemon). -
Added
firecore tools substreams prune-states <store-url> --keep-every <blocks> --truncate-below-block <block>to delete intermediate store snapshots (<end>-<start>.kvfiles) and reclaim disk space on long-lived Substreams. The first snapshot of each--keep-every-aligned window is kept, windows starting on multiples of--keep-everywhatever the module's initial block is (the first window is simply shorter). Only snapshots whose end block is at or below--truncate-below-blockare thinned — everything above stays untouched, so requests in flight and the live head keep their nearby resume points — and the most recent snapshot of each module is always kept.--minimum-age, if set, additionally spares any snapshot last modified more recently than that (the modification time comes straight out of the listing).substreams-tier1rebuilds stores from the last remaining snapshot before the requested block, so pruning trades disk space for reprocessing time. Deletion asks for confirmation unless--forceis given; supports--dry-run. Module folders are discovered the same waypurgedoes — direct tier1 layouts, cache tags and sharednetwork/substreams-states/roots — and only each module'sstates/prefix is then listed; a URL pointing inside a single module folder falls back to walking the whole tree. -
Added
firecore tools substreams prune-outputs <store-url> --truncate-below-block <block> --minimum-age <age>to delete execution output files (<start>-<end>.outputunder a module'soutputs/folder) that are BOTH fully below--truncate-below-blockAND last modified longer than--minimum-ageago (the modification time comes straight out of the listing, nothing is downloaded). The URL may point at a network folder, at itssubstreams-statesfolder, at a cache tag under it, or directly at a single module folder; cache tags are discovered the same waypurgedoes, and nothing outsideoutputs/folders is ever touched. Folders carrying an spkg (substreams.spkg.zstorsubstreams.partial.spkg.zst), which marks a module requested directly rather than an intermediate dependency, are kept untouched by default;--output-module-minimum-age, if set, prunes them too using that (typically longer) age. Supports--dry-runand--force. -
New
--substreams-tier1-cpu-eviction-*flags lettingsubstreams-tier1shed requests when its own cgroup reports the CPU saturated.--substreams-tier1-cpu-eviction-mode(defaultoff, so nothing changes until you set it) selects what it does:observelogs the requests it would cancel without cancelling any,dev-onlycancels development-mode requests,fullcancels production ones as well. See thesubstreamsbump below for what a round of eviction does.The rest are tunables, each defaulting to the value the eviction was designed around:
Flag Default Meaning --substreams-tier1-cpu-eviction-threshold0.90CPU usage above which the instance counts as overloaded --substreams-tier1-cpu-eviction-sustain15show long that must hold before it acts --substreams-tier1-cpu-eviction-recover-threshold0.75CPU usage under which it stops counting as overloaded --substreams-tier1-cpu-eviction-recover-sustain30show long that must hold before it advertises itself as ready again --substreams-tier1-cpu-eviction-target-ratio0.75CPU usage a round of eviction cuts down to, which sizes the round --substreams-tier1-cpu-eviction-interval5show often it looks at the CPU --substreams-tier1-cpu-eviction-cooldown15sminimum delay between two rounds --substreams-tier1-cpu-eviction-drain-delay8swait between going unready and the first cancellation, covering the load balancer's routing lag --substreams-tier1-cpu-eviction-min-age90srequests younger than this are never cancelled, a request being at its most expensive while it loads its stores --substreams-tier1-cpu-eviction-min-burn-cores0.05requests burning less than this are never cancelled, since cancelling them frees nothing and costs a reconnect --substreams-tier1-cpu-eviction-quota-cores-override0CPU budget to measure usage against instead of the cgroup's cpu.max--substreams-tier1-cpu-eviction-nominal-capacity0requests a full instance carries; scales the new substreams_tier1_effective_active_requestsmetric and nothing else.0means default to--substreams-tier1-active-requests-soft-limitThresholds and ratios are fractions of the CPU budget. That budget is the CPU limit the cgroup carries, so an instance running under cgroup v2 with no limit set —
cpu.maxreadingmax— has nothing to compare usage to and logs a warning at startup, leaving the eviction off;--substreams-tier1-cpu-eviction-quota-cores-overridenames the budget yourself in that case, and should stay at or under whatever limit the kernel does enforce, since above it the instance is throttled before the eviction ever fires. Reading the cgroup CPU files failing outright, cgroup v1 included, also logs a warning and leaves the eviction off. Every one of these warnings is only emitted when the mode is notoff.
Changed
-
Regenerated well-known protobuf descriptors from the Buf Schema Registry. The Cosmos block model (
sf.cosmos.type.v2.Block) now includes CometBFT v1 consensus params (abci,synchrony,feature), abls12381public-key variant, and a localInt64Valuefor feature heights, which were previously unknown fields. -
Bumped
bstreamto enable parallel one-blocks downloading upon bootstrap or reconnect (very useful on fast chains) -
Bumped substreams:
substreams-tier1squashes store partials in runs of up to 1000 segments or 30 s of work per
command instead of one segment per command, which capped squashing at about 15 segments per minute on busy
backprocessing requests. Segments whose partials are all empty get a copy of the previous full store instead of a
merge, server-side on object stores that support it. -
Bumped substreams:
substreams-tier1asks the relayer for every block from its own LIB when it connects or
reconnects, instead of the last 2 blocks, so a gap left by a disconnect is filled from the relayer's memory rather
than from the one-block store. -
Bumped
dstore: S3CopyObjectis done server-side (multipart above 5 GiB) instead of downloading and uploading
the object back, falling back to the old behaviour on backends answeringNotImplementedorMethodNotAllowed. -
Block poller per-block lines (
about to fetch block,requesting block,optimistically fetching block,block was optimistically polled,fetching block with hash,processing block,saved cursor) are now logged atDebug; they fired several times per block atInfo. -
Bumped
golang.org/x/cryptotov0.56.0, clearing CVE-2026-78662 and CVE-2026-56855 (both HIGH), which the Docker Scout scan of the published image fails on. -
firecore tools substreams prune-statesandprune-outputsdelete much faster: deletions now run on their own--delete-parallelism(250 by default) instead of sharing the listing's--parallelism(16 and 64), each attempt is bounded at 5s instead of 30s, and a failed deletion is retried once after 50ms instead of four times over 7.5s. A deletion that still fails is reported as before and picked up by the next run. -
Updated the embedded Solana block protobuf definitions to the latest ones published on the Buf registry:
Message.version,Message.transaction_config(with the newTransactionConfigmessage holdingpriority_fee,compute_unit_limit,loaded_accounts_data_size_limitandheap_size) and theDeactivatedStakereward type are now decoded byfirecore tools printand friends. -
Bumped
substreamsto v1.22.1-0.20260903162505-4035f21109ec:-
Server:
substreams-tier1scheduling no longer slows down as a large backprocessing range progresses. Picking the next tier2 job walked every segment between the squasher and the job frontier on every call, re-checking dependencies that could not have changed, so a run over N segments cost O(N²) in scheduling. The scheduler now keeps, per stage, the lowest segment that may still be pending and the highest segment completed so far, and only looks at the handful of segments those point at. On a 3-stage graph with 4 workers and a squasher three times slower than the jobs, scheduling 8000 segments went from 1.18s to 2.7ms, and now grows linearly with the range. Job order is unchanged. -
Server:
substreams-tier1now downloads the next cached execution output files while it streams the current one to a production-mode client. Before, each segment was opened, decompressed and sent before the next one was even requested from the object store, so every segment paid a full store round trip on the critical path. Prefetching is bounded per request, with no flag to set: at most 4 segments ahead, holding at most 64 MiB of decompressed data. No size is ever asked of the store, the decompressed size of the last downloaded segment being the estimate for the next ones, and a file bigger than the whole budget turns prefetching off for the rest of the request. Missing files are left to the existing retry loop. -
Server:
substreams-tier1now sends each batch of cached execution output on its own goroutine, so decoding the next batch overlaps with compressing and writing the current one. At most one batch is being built while one is sent, and a segment is only reported done once every batch is out, so message order is unchanged. Decoding a batch also copies one less time, the item now aliasing the buffer it was read into. -
Server:
substreams-tier1writes thesubstreams.spkgandlast_usedcache markers in the background instead of before the pipeline starts, so those object store round trips no longer delay the first block sent. They run detached from the request with a 30 second timeout: the request neither starts nor exits waiting for them, and a client that disconnects early still leaves its usage marker behind. -
Server: store snapshots (fullKV files) can now be pruned to save disk space — which is what
firecore tools substreams prune-statesdoes:substreams-tier1no longer assumes that a fullKV at blockximplies that every earlier fullKV still exists. At request start it walks backwards from the first segment needing work, in growing listing windows, until it finds the last block where every store module still has a snapshot, and rebuilds the stores from there. Only snapshots actually seen are reused, and a job is only scheduled once the previous segment of every lower stage is done, so a pruned file is never read. -
Server: a store snapshot deleted while a request that still needs it is running now fails that request with
FailedPrecondition: ... does not exist (it may have been pruned); restart the requestinstead of retrying the tier2 job forever. Likewise, a mapper output file deleted after its job completed is re-produced after 30 seconds instead of being waited on forever, and a scheduler deadlock on partials left by an interrupted run (a unit shadowed under one merging a partial from disk) is fixed. -
Server: the initial store lookup no longer falls back to listing a store's whole history when its last snapshot is far behind: it lists at most a handful of bounded windows.
-
Server: progress messages are sent far less often, the cadence widening with the age of the request — every second for the first minute, every 10 seconds up to 5 minutes, every 30 seconds up to 10 minutes, then every minute — including in the linear phase, which previously sent one every 200ms. Progress messages count as egress like any other response, so a long-running or live request paid for a steady stream of them.
Request.progress_messages_interval_msis now honoured (it was validated and then ignored): setting it pins the cadence for the whole request, the 500ms minimum unchanged. -
Server:
substreams-tier1now names the usage marker it writes in every module cache folder after the request's plan tier:last_used_<plan>(lowercase, e.g.last_used_pro), still plainlast_usedwhen unauthenticated.firecore tools substreams purgereads the plan back from that name to apply a retention per plan. -
Server:
substreams-tier1can now evict requests when its CPU is saturated, which the new--substreams-tier1-cpu-eviction-*flags above turn on. When its own cgroup reports CPU usage above 90% of quota for 15 seconds, the instance advertises itself as unready to the load balancer, refuses new requests, waits 8 seconds for the balancer to stop routing to it, then cancels enough of the heaviest requests withUnavailableto bring usage back under 75% of quota, so their clients reconnect elsewhere. Development-mode requests are cancelled first, then production requests on live blocks, then production requests still catching up from files — a live request burning two cores keeps burning them for as long as it stays connected, while a catching-up one is spending them on work that ends, and cancelling it throws away the segment progress it would have to redo. Newsubstreams_tier1_cpu_*gauges and asubstreams_tier1_evicted_requests_counterreport what it sees and does, inobservemode included. It needs cgroup v2 CPU accounting, and a CPU budget to measure usage against — the cgroup's own limit, or--substreams-tier1-cpu-eviction-quota-cores-overridewhere the cgroup carries none. -
Server: new
substreams_tier1_effective_active_requestsgauge, meant to replacesubstreams_active_requestsas the horizontal autoscaler input on tier1: the higher of the plain active-request count and the number of requests the CPU budget is being spent at (nominal_capacity * cpu_usage_ratio / cpu_eviction_target_ratio). An instance full of expensive requests, or one holding its CPU down by cancelling requests, then reports itself at capacity and the autoscaler adds instances instead of leaving the count looking healthy. -
Server: the jobs of a tier1 request now reach the tier2 fleet in the order the client will read their output, instead of racing each other for whatever instance has room. A job asks a per-request launch queue for its turn before every request it sends to a tier2, first attempt and retries alike, and leaves the queue as soon as a tier2 takes it. The queue is ordered by lowest segment first and, within a segment, by highest stage, and only its first 20% may dial at all — at least two jobs, at most 15 — the ones behind sending nothing until a job ahead gets in and moves the window up. A job with nothing queued ahead of it dials immediately, so a fleet with room is paced no differently than before. Without this, the segment the client reads first was no more likely to land than one it would only read minutes later, and a whole request could idle behind a single unlucky low segment. Tunable with
SUBSTREAMS_WORKER_LAUNCH_WINDOW_PERCENT(default20) andSUBSTREAMS_WORKER_LAUNCH_WINDOW_MAX(default15). -
Server: a tier2 job that fails is no longer retried on a growing 1s-to-5s backoff of its own; every reason to dial again goes through that same queue, so retries keep the request's reading order. A job that could not get in at all (
ResourceExhausted: service currently overloaded, a refused connection,Unavailable: no healthy upstream) waits 100ms, and a job that got in and then failed waits 5 seconds once — if a tier2 then turns it away for capacity, it is back on the 100ms delay with its failure already counted. The counters that end a hopeless request are unchanged: five failures, or two execution timeouts, and neither is charged for a job that never got in. Tunable withSUBSTREAMS_WORKER_OVERLOADED_RETRY_DELAY(default100ms, was300ms),SUBSTREAMS_WORKER_FAILED_RETRY_DELAY(default5s) andSUBSTREAMS_WORKER_OVERLOADED_RETRY_JITTER(default100ms, added to a wait so jobs turned away at the same instant do not redial in lockstep). -
Server: jobs are now scheduled up to twice the request's worker count ahead of the blocks the client is reading, instead of 1.5 times, so a client that reads slowly still keeps the workers busy.
-
Server:
substreams-tier2logs far less per segment. A large backfill fans out into tens of thousands ofProcessRangecalls, each of which was writing about tenInfolines with no steady-state diagnostic value, enough to raise an instance's log volume by an order of magnitude. The duplicate auth-info log in the response handler is gone (the incoming request already logs it once per segment), the store-size and exec-output-file-open logs are nowDebug, and so are the four shutdown-lifecycle logs of the per-requestdmeteringevent emitter, which is opened and torn down on everyProcessRangecall. The benignhttp2: server: error reading preface ...: connection reset by peerlogged whenever a client drops a connection mid-handshake against the plaintext/h2c tier2 port is suppressed, like the existing TLS-handshake ones.
-
Removed
- Removed the
--reader-node-firehose-compressionflag. It has never had any effect: the connection to the upstream endpoint always uses zstd. Operators setting it must drop it, as an unknown flag stops the process from starting.
Fixed
-
The merger no longer spins when a walk over one-block files keeps hitting the unlinkable-blocks limit: that path skipped the
merger-time-between-store-lookupsdelay, so a merger stuck behind a gap in one-block files re-walked and loggedtoo many unlinkable blocks, continuing to next loopabout ten times per second. It now waits like any other iteration, the line is logged atWarninstead ofInfo, and the wait is interrupted by shutdown. -
Bumped
google.golang.org/grpcto v1.83.2, which fixes CVE-2026-84445. -
reader-node-firehoseno longer acts on undo signals from its upstream endpoint. It treated aSTEP_UNDOresponse like a new block and re-emitted it, now it just logs and skips them. (a reader does not take decisions on reorgs) -
The block poller no longer warns
no clients have been working for over 1 minute, still retryingon slower chains like Bitcoin/Litecoin with block rate exceeding 1 minute. Instead it only warns if fetches have been actually failing for over a minute. -
Bumped substreams:
substreams_tier1_effective_active_requestscould read belowsubstreams_active_requests, the
metric it is meant to replace as the horizontal autoscaler input. Requests still setting up were counted by one and
not the other, so a tier1 pod with requests queued in setup looked emptier to the autoscaler than it was. -
Bumped substreams: fixed handling of partial-blocks (flashblocks) streams on a tier1 that is shutting down. The
stream now ends withUnavailablelike a full-block stream does, so the client reconnects elsewhere. It used to
stay open but silent, then send an undo signal at each block boundary naming a block the client had never
received. An undo signal is also no longer sent for partial-block state whose outputs were never sent. -
Bumped substreams:
substreams-tier1no longer closes the squasher's cached stores while a squash is still
running. When the scheduler stopped early (a tier2 job failed, or the pod was shutting down), the in-flight merge
could panic the process or write an empty full store to storage. -
proto/generator: FileDescriptorSet JSON from Buf is unmarshalled with unknown fields discarded, sogo generateinproto/works against current BSR descriptor extensions.