IntersectMBO / IntersectMBO/cardano-api
gRPC: honour `FieldMask` in UTxO RPC requests in cardano-rpc
- Dominant language
- Haskell
- Stars
- 40
- Forks
- 30
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 30
Description
## Context
Every UTxO RPC service includes a `google.protobuf.FieldMask` in its request messages, letting clients select which response fields the server should materialise.
cardano-rpc currently ignores it everywhere; the only trace is a TODO in `readParamsMethod` (`Cardano/Rpc/Server/Internal/UtxoRpc/Query.hs`), which notes that masks need to be normalised before use, following the semantics of protobuf's [`FieldMaskTree`](https://github.com/protocolbuffers/protobuf/blob/main/java/util/src/main/java/com/google/protobuf/util/FieldMaskTree.java#L76).
Request messages carrying a `field_mask`:
| Service | Message | Method implemented today |
| --- | --- | --- |
| QueryService | `ReadParamsRequest` | yes |
| QueryService | `ReadUtxosRequest` | yes |
| QueryService | `SearchUtxosRequest` | yes |
| QueryService | `ReadGenesisRequest` | yes |
| QueryService | `ReadEraSummaryRequest` | no |
| QueryService | `ReadDataRequest` | no |
| QueryService | `ReadTxRequest` | no |
| SyncService | `FetchBlockRequest` | yes |
| SyncService | `FollowTipRequest` | yes (#1268) |
| SyncService | `DumpHistoryRequest` | no |
| SubmitService | `WatchMempoolRequest` | no |
## Motivation
Two distinct wins:
1. **Bandwidth**: responses like `AnyChainBlock` carry both raw CBOR (`native_bytes`) and the fully parsed `cardano` block.
A client interested only in headers, or only in raw bytes, currently receives everything.
#1219 already flags this ("`FieldMask` becomes valuable for letting clients request only headers, or only transaction hashes").
2. **Server-side cost**: raised in review of #1268 (https://github.com/IntersectMBO/cardano-api/pull/1268#discussion_r3676653326) - `FetchBlock` and every `FollowTip` stream fetch two block components per block, `GetRawBlock` **and** `GetBlock`, and the `GetBlock` decode is paid unconditionally.
A mask excluding the parsed `cardano` fields would let the server select the `BlockComponent` per request and skip block deserialisation entirely.
## Proposed behaviour
- An absent or empty mask means "return all fields" (standard `FieldMask` read semantics); current behaviour is therefore already correct for clients that send no mask.
- A non-empty mask is normalised first (deduplicate, drop paths covered by an ancestor path), following the semantics of protobuf's `FieldMaskTree` linked above.
- The mask drives response **construction**, not post-hoc pruning: handlers consult the mask before doing the work behind each field, so data that is masked out is never fetched, decoded, or converted in the first place.
Building the full response and then stripping fields would deliver the bandwidth win but none of the server-side cost win, which is the harder and more valuable half.
- Invalid paths: follow the protobuf recommendation for read masks - ignore unknown paths rather than failing the request (to be confirmed against what other UTxO RPC servers, e.g. Dolos, do).
## Implementation sketch
1. A shared mask-query helper in cardano-rpc: normalise a `FieldMask` into a path tree and expose "is this path (or any child of it) requested?" for handlers to branch on.
2. Wire it into the implemented handlers (`ReadParams`, `ReadUtxos`, `SearchUtxos`, `ReadGenesis`, `FetchBlock`, `FollowTip`): each expensive field (protocol parameter conversion, UTxO parsing, tx conversion) is computed only when its path is requested.
3. `FollowTip`/`FetchBlock` fetch-avoidance: when the mask excludes all parsed `cardano.*` paths, fetch only `GetRawBlock` and skip `GetBlock`; when it excludes `native_bytes`, fetch only `GetBlock`.
This changes the `BlockComponent` selected in `NodeKernelAccess`, so the follower/fetch component becomes a function of the request.
4. Unmentioned future handlers (`ReadEraSummary`, `ReadData`, `ReadTx`, `DumpHistory`, `WatchMempool`) apply the same helper when they land.
## Acceptance criteria
- [ ] Empty/absent mask returns full responses (no behaviour change for existing clients).
- [ ] Masked unary responses contain only the requested paths, with mask normalisation applied.
- [ ] `FollowTip` and `FetchBlock` skip the block decode when the mask excludes all parsed block fields, and skip raw-bytes fetch when the mask excludes `native_bytes`.
- [ ] Unknown mask paths are handled per the documented policy (ignored or rejected - decided and documented).
- [ ] Unit tests cover mask normalisation and masked response construction; conformance checked against another UTxO RPC server (e.g. Dolos) via the existing comparison tooling.
- [ ] README coverage table updated to note `FieldMask` support.
## Out of scope
- Field masks on methods not yet implemented (they inherit the helper when implemented).
- Write masks / update semantics (`FieldMask` is used purely as a read mask in UTxO RPC).
Contributor guide
Assessment
This issue has not been assessed yet.