Skip to content

feat(serve): full ogmios v7 interop for the ouroboros N2C endpoint#1120

Draft
Javieracost wants to merge 4 commits into
txpipe:mainfrom
Javieracost:feat/o7s-txmonitor
Draft

feat(serve): full ogmios v7 interop for the ouroboros N2C endpoint#1120
Javieracost wants to merge 4 commits into
txpipe:mainfrom
Javieracost:feat/o7s-txmonitor

Conversation

@Javieracost

Copy link
Copy Markdown

Depends on txpipe/pallas#792 — the first commit bumps the pallas dependency to that revision so every commit in this PR builds standalone.

What / Why

Running ogmios v7 on top of dolos 1.5.0's ouroboros N2C endpoint fails at three independent layers today:

  1. Sessions are killed at open. Ogmios opens local-tx-monitor on every connection; the endpoint only implements chainsync and statequery, so the multiplexer tears down the whole session as soon as protocol 9 is opened.
  2. Statequery dies while the node syncs. Ogmios's health client acquires the volatile tip on connect and on every tip tick. Dolos validated the cursor block against the archive store — which lags behind the cursor during catch-up — and answered Failure(PointNotOnChain) to a request that cannot legitimately fail. Worse, the request loop entered the acquired state even after sending a failure, so the client's spec-correct follow-up acquire was rejected as invalid and the whole statequery protocol died.
  3. Historical chain-sync hangs silently. Any client intersecting below the WAL window (ogmios, kupo, any N2C syncer) gets findIntersection instantly and then waits forever for the first block. Four distinct defects: the initial rollback was sent to the first crawled block's point instead of the negotiated intersection (consuming that block — and archive-crawled points are slot-only, so the send failed outright); that failure was silently discarded, leaving the protocol dead while the client waited; crawling from origin skipped the first block of the chain; and a client awaiting at the tip died on the next re-org, because an undo tip event returned without sending the owed reply, leaving the server receiving without agency.

This PR fixes all of the above — one commit per concern, each self-contained with protocol-level integration tests (a real pallas NodeClient over a unix socket against the session handler):

  • feat(serve): add local-tx-monitor server to the ouroboros N2C endpoint — mempool-backed sessions: a snapshot is pending + inflight transactions (Confirmed excluded, as a node mempool would not hold them) tagged with the cursor slot; era-wrapped NextTx payloads (block era − 1, same convention as the tx-submission peer client); HasTx matches on hash alone so era-probing clients get correct answers; capacity mirrors the cardano-node default (2× max block body size); AwaitAcquire blocks on snapshot fingerprint change, waking on mempool events or tip changes.
  • fix(serve): stop failing statequery volatile-tip acquires during sync — volatile-tip acquires acquire the current cursor directly (the node's own tip needs no archive validation), and a failed acquire or re-acquire returns the loop to the idle state.
  • fix(serve): serve historical chain-sync from the archive — the opening rollback echoes the negotiated intersection without consuming a block; chain-sync session errors are logged and propagated like the other mini-protocols; origin batches start at the first block in the archive; the await loop skips undo events until the mark or apply that follows produces the owed reply (undos have no wire counterpart — the mark that follows becomes the rollback).

The three concerns are one story — making dolos a drop-in N2C backend for ogmios — but each commit stands alone; we can split the PR if you'd prefer smaller units.

Validation

Beyond the test suite, we validated end-to-end on mainnet: a full-history dolos + ogmios v7.0.0 replacing cardano-node + ogmios as the feed for a mainnet indexing pipeline.

  • Derived state came out byte-identical to the node-backed reference across two legs — genesis → epoch 300, and epochs 500 → 510.
  • The ogmios health endpoint holds connected / networkSynchronization 1.0 at tip; the undo-at-tip agency fix was caught live by a mainnet re-org during the soak.
  • Beyond correctness, the swap simply helped us: about 3× the historical sync throughput at roughly a quarter of the memory.

Out of scope (observed while validating, pre-existing)

  • Crawling past the archive end during catch-up can hit the no overlap between archive and wal panic when the crawl cursor is a slot-only point.
  • Byron EBBs are not present in the slot-keyed archive, so archive-sourced chain-sync doesn't stream them (they carry no transactions, so ledger derivations are unaffected, but noting it).

Happy to follow up on either separately.

Javieracost and others added 4 commits July 20, 2026 00:55
Points at the feat/txmonitor-server revision submitted upstream as
txpipe/pallas#792; to be repinned to the mainline merge commit once
that PR lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ouroboros serve endpoint implemented only chainsync and
statequery. Clients that open the local-tx-monitor protocol on the
same connection - ogmios does so eagerly on every session - had the
whole session killed by the multiplexer, since accepting the N2C
handshake claims support for the full protocol set.

Adds a txmonitor session handler backed by the domain mempool:

- a snapshot is pending + inflight transactions, excluding Confirmed
  (already on-chain, a node mempool would not hold them), tagged with
  the current cursor slot
- NextTx serves the era-wrapped payload; the wire era is the block
  era number minus one, same convention as the tx-submission peer
  client
- HasTx matches on the tx hash alone, ignoring the era wrapper, so
  clients probing the same hash under multiple eras get a correct
  answer
- GetSizes/GetMeasures report capacity as twice the max block body
  size from effective pparams, mirroring the cardano-node default
- AwaitAcquire blocks until the snapshot fingerprint changes, waking
  on mempool events or tip changes; a closed mempool stream degrades
  to tip-driven wake-ups instead of busy-looping

Includes two ToyDomain integration tests driving a real pallas
NodeClient over a unix socket: snapshot queries against a seeded
mempool, and await-acquire woken by a tip event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Acquiring the volatile tip validated the cursor block against the
archive store, which lags behind the cursor while the node is catching
up, so a fully-functional node answered Failure(PointNotOnChain) to a
request that cannot legitimately fail. On top of that, the request loop
entered the acquired state even after sending a failure, so the client's
spec-correct follow-up acquire was rejected as invalid for the current
state and the whole statequery protocol died. Ogmios v7's health client
hits both on every reconnect against a syncing mainnet node.

Volatile-tip acquires now acquire the current cursor directly, and a
failed acquire or re-acquire returns the loop to the idle state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four defects left archive-backed chain-sync unusable for any client
that intersects below the WAL window (ogmios, kupo, any N2C syncer):

- the initial rollback that opens every chain-sync exchange was sent to
  the first crawled block's point instead of the negotiated
  intersection, consuming that block in the process; archive-crawled
  points are slot-only and cannot encode into a wire point, so the send
  failed outright
- that failure was silently discarded, so the server just stopped
  serving the protocol while the client waited forever for a reply
- crawling from origin skipped the first block of the chain, since the
  archive batch loader always assumed the cursor block had already been
  delivered
- a client awaiting at the tip died on the next chain re-org: an undo
  tip event returned without sending anything, leaving the client owed
  a reply and the server attempting to receive without agency

The rollback now echoes the negotiated intersection point without
consuming a block, chain-sync session errors are logged and propagated
like the other mini-protocols, origin batches start at the first block
in the archive, and the await loop skips over undo events until the
mark or apply that follows them produces the owed reply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a53b6ed-cdc4-48dc-802e-5596c8872c33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant