Skip to content

Extract a shared Blockfrost follower core, used by both hydra-node and hydra-chain-observer #2822

Description

@v0d1ch

Context

hydra-chain-observer got its Blockfrost mode in October 2024 (#1631), months before hydra-node's Blockfrost chain backend existed (April 2025). When the node backend was added, the reuse direction was inverted: the existing code lived in the downstream package, so it was copied into hydra-node instead of extracted, and the two copies have drifted ever since, accumulating the same classes of bugs independently.

#2753 made the cost concrete: the node's copy was overhauled (batched block fetching via getNextBlocks', tx_count-based skip of empty-block fetches, sleep only at tip, structured 429 handling with capped backoff, immediate submission error reporting), while the observer still runs the original design: chasing _blockNextBlock one getBlock at a time, with NotEnoughBlockConfirmations/MissingNextBlockHash exception control flow that the node has since deleted. Only the rate-limit pieces are shared today (the observer imports rateLimitBackoff/maxRateLimitRetries from Hydra.Chain.Blockfrost.Client); its runBlockfrostM, error type, isRetryable, toChainPoint, and follower remain private near-twins in hydra-chain-observer/src/Hydra/Blockfrost/ChainObserver.hs.

What is duplicated

┌─────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────┐
│ hydra-node │ hydra-chain-observer │
├─────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ Hydra.Chain.Blockfrost.Client.APIBlockfrostError + isRetryable │ private APIBlockfrostError + isRetryable (still carries the │
│ │ constructors the node removed) │
├─────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ runBlockfrostM with 429 backoff │ near-identical private copy │
├─────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ batched follower (pollForNewBlocks + processBlock in │ old walk-the-next-pointer rollForward loop │
│ Hydra.Chain.Blockfrost) │ │
├─────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────┤
│ toChainPoint, CBOR-to-Tx decoding │ private copies │
└─────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────┘

Proposal

  • Extract a shared follower from Hydra.Chain.Blockfrost (the pollForNewBlocks/processBlock pair), parameterized by a per-block callback with a state parameter: the node's callback is stateless (wallet update + ChainSyncHandler), but the observer threads state through the loop (its UTxO set and known script versions), so the callback shape is roughly s -> BlockHeader -> [Tx] -> m s.
  • Port hydra-chain-observer onto it, deleting its private runBlockfrostM, error type, isRetryable, toChainPoint, and rollForward. The observer inherits bounded catch-up, the empty-block skip, and steady ~1-block-from-tip behavior.
  • Unify on hydra-node's APIBlockfrostError; the dependency direction (observer already depends on hydra-node) makes this the legal sharing direction.
  • Add at least one Blockfrost-mode test for the observer while touching it; today its only e2e runs against a direct devnet, which is how the drift went unnoticed.

Non-goals

  • Rollback handling: still unhandled on the Blockfrost path in both consumers (the follower wedges retrying an orphaned hash); tracked separately.
  • Removing the observer's dependency on hydra-node (e.g. a standalone hydra-blockfrost package); worth considering only if that dependency is being cut anyway.

Follow-up to #2753.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage 🏥

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions