The Blockfrost chain layer calls queryTimeHandle from four places, all via the single
getTimeHandle defined in Hydra/Chain/Blockfrost.hs:
chainSyncHandler.onRollForward - once per confirmed block
chainSyncHandler.onRollBackward - per rollback (does not fire on Blockfrost)
mkChain.postTx - per posted L1 transaction (rare)
mkChain.draftDepositTx - per deposit draft (rare)
Each queryTimeHandle costs 3 Blockfrost requests: /blocks/latest (tip),
/genesis (system start), /network/eras (era history).
The per-block call in onRollForward dominates: at a 20s block time this is
~13,000 requests/day just to rebuild time handles, versus ~4,300/day for the
actual block polling. Roughly 75% of steady-state Blockfrost usage is time-handle churn.
Why it is unnecessary
systemStart is immutable for the lifetime of the network; it can be queried once at startup.
eraHistory only changes at hard forks, and the interpreter stays valid until its
safe-zone horizon (hours past the tip). It can be cached and refreshed on a TTL or
when a conversion fails with a past-horizon error.
- The tip query is entirely unused on the sync path:
onRollForward/onRollBackward
only use slotToUTCTime, never currentPointInTime. Only postTx and
draftDepositTx need a current point in time, and those are rare user actions.
Proposed fix
Cache systemStart at chain-component startup and eraHistory in a TVar
(refresh on TTL or past-horizon failure), then build the TimeHandle locally via
mkTimeHandle. Drop the tip query from the sync path (the block being processed
provides the current slot); keep a fresh tip query only for postTx/draftDepositTx.
This reduces steady-state usage from ~4 requests per block to ~1, with no behavioral
change to the handlers. Caution: era history must not be cached unconditionally
forever, or slotToUTCTime throws TimeConversionException once the tip outruns
the horizon (see #2798 for the analogous L2 issue).
The Blockfrost chain layer calls
queryTimeHandlefrom four places, all via the singlegetTimeHandledefined inHydra/Chain/Blockfrost.hs:chainSyncHandler.onRollForward- once per confirmed blockchainSyncHandler.onRollBackward- per rollback (does not fire on Blockfrost)mkChain.postTx- per posted L1 transaction (rare)mkChain.draftDepositTx- per deposit draft (rare)Each
queryTimeHandlecosts 3 Blockfrost requests:/blocks/latest(tip),/genesis(system start),/network/eras(era history).The per-block call in
onRollForwarddominates: at a 20s block time this is~13,000 requests/day just to rebuild time handles, versus ~4,300/day for the
actual block polling. Roughly 75% of steady-state Blockfrost usage is time-handle churn.
Why it is unnecessary
systemStartis immutable for the lifetime of the network; it can be queried once at startup.eraHistoryonly changes at hard forks, and the interpreter stays valid until itssafe-zone horizon (hours past the tip). It can be cached and refreshed on a TTL or
when a conversion fails with a past-horizon error.
onRollForward/onRollBackwardonly use
slotToUTCTime, nevercurrentPointInTime. OnlypostTxanddraftDepositTxneed a current point in time, and those are rare user actions.Proposed fix
Cache
systemStartat chain-component startup anderaHistoryin aTVar(refresh on TTL or past-horizon failure), then build the
TimeHandlelocally viamkTimeHandle. Drop the tip query from the sync path (the block being processedprovides the current slot); keep a fresh tip query only for
postTx/draftDepositTx.This reduces steady-state usage from ~4 requests per block to ~1, with no behavioral
change to the handlers. Caution: era history must not be cached unconditionally
forever, or
slotToUTCTimethrowsTimeConversionExceptiononce the tip outrunsthe horizon (see #2798 for the analogous L2 issue).