Summary
Implement the FollowTip RPC method from the UTxO RPC SyncService spec, providing a server-streaming gRPC endpoint that delivers blocks as they are applied or rolled back on the chain.
This is the gRPC equivalent of the Ouroboros ChainSync mini-protocol and the most architecturally significant method in the UTxO RPC spec.
Motivation
Replacing ChainSync for remote clients
Chain-following clients (Kupo, Scrolls, Oura, custom indexers) currently connect to the cardano-node Unix socket and run the ChainSync mini-protocol directly.
This requires:
- Local filesystem access to the node socket
- A Haskell (or compatible) implementation of the Ouroboros N2C handshake and ChainSync state machine
- Version negotiation with the node
FollowTip exposes the same block stream over gRPC, which:
- Works over TCP (no filesystem coupling)
- Uses standard gRPC client libraries available in every language
- Handles versioning at the proto/gRPC level rather than the Ouroboros protocol level
Kupo's primary data path
Kupo's entire indexing pipeline runs on a single ChainSync connection.
It uses MsgFindIntersect to establish a starting point, then streams blocks via MsgRequestNext with adaptive pipelining.
FollowTip maps directly onto this: intersect for the starting point, apply for roll-forward, undo for roll-backward.
Background
What the spec requires
FollowTipRequest takes:
repeated BlockRef intersect - candidate block references for finding a common point (analogous to Ouroboros MsgFindIntersect)
- Optional
FieldMask - to select which parsed fields to materialise in the response
FollowTipResponse is a server-streaming response where each message carries:
oneof action:
apply (AnyChainBlock) - a new block to apply (= Ouroboros RollForward)
undo (AnyChainBlock) - a block to undo (= Ouroboros RollBackward). Notably, the full block data is included, so the client does not need to maintain its own block cache for rollback processing
reset (BlockRef) - server-initiated reset to a point. No direct Ouroboros analogue; used when no intersect matched or on reinitialisation
tip (BlockRef) - current chain tip for lag measurement
Key differences from Ouroboros ChainSync
| Aspect |
ChainSync |
FollowTip |
| Transport |
Unix socket (N2C) |
TCP/gRPC |
| Rollback data |
Only the rollback point |
Full block included in undo |
| No-intersect |
MsgIntersectNotFound |
reset action |
| Pipelining |
Client-driven (MsgRequestNextPipelined) |
Server-driven (gRPC stream backpressure) |
| Block format |
CBOR |
Proto (AnyChainBlock with native_bytes and/or parsed cardano.Block) |
Why this depends on node kernel access
The existing N2C path is insufficient for a production-quality FollowTip:
- N2C ChainSync is per-connection: each gRPC client would require a separate N2C socket connection to the node, each running its own ChainSync follower. This does not scale.
- No shared follower: the node's ChainSync server creates a fresh follower per N2C connection. There is no way to multiplex multiple gRPC clients onto a single follower.
- In-process access: ADR-019's
NodeAccess record can expose a ChainDB.newFollower callback, letting cardano-rpc create followers directly against the consensus ChainDB. Multiple gRPC clients can each get their own follower without socket overhead.
The in-process path also enables zero-copy block delivery: ChainDB.getBlockComponent GetRawBlock returns the raw CBOR directly from the ImmutableDB/VolatileDB without serialisation.
Proposed approach
NodeAccess extension
Extend the NodeAccess record (ADR-019) with:
- A follower creation callback (wrapping
ChainDB.newFollower)
- Follower operations: read next (with rollback support), find intersection
The exact API depends on how ChainDB.Follower is surfaced through the consensus interface.
Follower lifecycle
Each FollowTip gRPC stream creates a new ChainDB follower.
On stream setup:
- Create a follower via
NodeAccess.
- Find intersection using the client's
repeated BlockRef intersect.
- If no intersection found, send a
reset action to genesis (or the oldest available point).
On each iteration:
- Read the next change from the follower (blocking if at tip).
- Translate to
apply, undo, or reset action.
- Send the
FollowTipResponse on the gRPC stream.
On stream teardown (client disconnect or error):
- Close the follower to free resources.
Block encoding
For the initial implementation, populate native_bytes (raw block CBOR) only.
Populating the parsed cardano.Block oneof requires mapping the full block structure to proto, which is a large effort and can follow separately.
Clients that need parsed data can deserialise the CBOR themselves.
Backpressure
gRPC server-streaming has built-in flow control via HTTP/2 window management.
The follower read loop should respect this: if the client is slow to consume, the server blocks on the gRPC send rather than buffering unboundedly.
Server wiring
Register FollowTip in the SyncService alongside FetchBlock and ReadTip.
Implementation notes
Scalability considerations
Each follower maintains a pointer into the ChainDB.
The node already supports multiple followers (one per N2C ChainSync connection), so this is not a new resource type.
However, gRPC makes it easier to open many connections from remote clients.
Consider:
- A configurable maximum number of concurrent followers
- Logging/metrics for active follower count
- Idle timeout for followers that stop consuming
Parsed block support (future)
The FollowTipResponse can carry a parsed cardano.Block in the AnyChainBlock.
This requires mapping the full Cardano block structure (header, body, witnesses, auxiliary data) to the proto model.
This is significant additional work and should be a follow-up issue, not a blocker for the initial FollowTip implementation.
FieldMask
FollowTipRequest includes a FieldMask.
As with other methods, this can be ignored initially (always return native_bytes).
Once parsed block support exists, FieldMask becomes valuable for letting clients request only headers, or only transaction hashes, reducing bandwidth.
Acceptance criteria
Out of scope
- Parsed
cardano.Block population (native_bytes only for initial implementation).
FieldMask support.
- Concurrent follower limits (can be added as a follow-up).
DumpHistory (separate issue).
Dependencies
References
Summary
Implement the
FollowTipRPC method from the UTxO RPCSyncServicespec, providing a server-streaming gRPC endpoint that delivers blocks as they are applied or rolled back on the chain.This is the gRPC equivalent of the Ouroboros ChainSync mini-protocol and the most architecturally significant method in the UTxO RPC spec.
Motivation
Replacing ChainSync for remote clients
Chain-following clients (Kupo, Scrolls, Oura, custom indexers) currently connect to the cardano-node Unix socket and run the ChainSync mini-protocol directly.
This requires:
FollowTipexposes the same block stream over gRPC, which:Kupo's primary data path
Kupo's entire indexing pipeline runs on a single ChainSync connection.
It uses
MsgFindIntersectto establish a starting point, then streams blocks viaMsgRequestNextwith adaptive pipelining.FollowTipmaps directly onto this:intersectfor the starting point,applyfor roll-forward,undofor roll-backward.Background
What the spec requires
FollowTipRequesttakes:repeated BlockRef intersect- candidate block references for finding a common point (analogous to OuroborosMsgFindIntersect)FieldMask- to select which parsed fields to materialise in the responseFollowTipResponseis a server-streaming response where each message carries:oneof action:apply(AnyChainBlock) - a new block to apply (= Ouroboros RollForward)undo(AnyChainBlock) - a block to undo (= Ouroboros RollBackward). Notably, the full block data is included, so the client does not need to maintain its own block cache for rollback processingreset(BlockRef) - server-initiated reset to a point. No direct Ouroboros analogue; used when no intersect matched or on reinitialisationtip(BlockRef) - current chain tip for lag measurementKey differences from Ouroboros ChainSync
undoMsgIntersectNotFoundresetactionMsgRequestNextPipelined)AnyChainBlockwithnative_bytesand/or parsedcardano.Block)Why this depends on node kernel access
The existing N2C path is insufficient for a production-quality
FollowTip:NodeAccessrecord can expose aChainDB.newFollowercallback, letting cardano-rpc create followers directly against the consensusChainDB. Multiple gRPC clients can each get their own follower without socket overhead.The in-process path also enables zero-copy block delivery:
ChainDB.getBlockComponent GetRawBlockreturns the raw CBOR directly from the ImmutableDB/VolatileDB without serialisation.Proposed approach
NodeAccess extension
Extend the
NodeAccessrecord (ADR-019) with:ChainDB.newFollower)The exact API depends on how
ChainDB.Followeris surfaced through the consensus interface.Follower lifecycle
Each
FollowTipgRPC stream creates a newChainDBfollower.On stream setup:
NodeAccess.repeated BlockRef intersect.resetaction to genesis (or the oldest available point).On each iteration:
apply,undo, orresetaction.FollowTipResponseon the gRPC stream.On stream teardown (client disconnect or error):
Block encoding
For the initial implementation, populate
native_bytes(raw block CBOR) only.Populating the parsed
cardano.Blockoneof requires mapping the full block structure to proto, which is a large effort and can follow separately.Clients that need parsed data can deserialise the CBOR themselves.
Backpressure
gRPC server-streaming has built-in flow control via HTTP/2 window management.
The follower read loop should respect this: if the client is slow to consume, the server blocks on the gRPC send rather than buffering unboundedly.
Server wiring
Register
FollowTipin theSyncServicealongsideFetchBlockandReadTip.Implementation notes
Scalability considerations
Each follower maintains a pointer into the
ChainDB.The node already supports multiple followers (one per N2C ChainSync connection), so this is not a new resource type.
However, gRPC makes it easier to open many connections from remote clients.
Consider:
Parsed block support (future)
The
FollowTipResponsecan carry a parsedcardano.Blockin theAnyChainBlock.This requires mapping the full Cardano block structure (header, body, witnesses, auxiliary data) to the proto model.
This is significant additional work and should be a follow-up issue, not a blocker for the initial
FollowTipimplementation.FieldMask
FollowTipRequestincludes aFieldMask.As with other methods, this can be ignored initially (always return
native_bytes).Once parsed block support exists,
FieldMaskbecomes valuable for letting clients request only headers, or only transaction hashes, reducing bandwidth.Acceptance criteria
NodeAccess(ADR-019) exposes follower creation and intersection-finding.followTipMethodhandler implemented as a gRPC server-streaming endpoint.BlockReflist, server finds most recent common point.apply(with raw block CBOR),undo(with raw block CBOR), andresetactions.tip.FollowTip, submit a transaction, verify the block containing it arrives as anapplyaction with valid CBOR.cardano-rpc/README.mdupdated to markFollowTipas supported in the coverage table.Out of scope
cardano.Blockpopulation (native_bytes only for initial implementation).FieldMasksupport.DumpHistory(separate issue).Dependencies
NodeAccessblock-lookup foundation.References
utxorpc/specrepo,proto/utxorpc/v1beta/sync/sync.proto