Skip to content

[gRPC] read_mask is not applied below TransactionKind.data, and no error is returned #27548

Description

@0u-Y

Filing this as a question rather than a defect report. I'm new to this codebase and may be misreading the intent. Everything below is measured against a public fullnode, and I'm happy to be told this is working as designed.

Summary: a read_mask path that descends into the TransactionKind.data oneof is neither honored nor rejected. The server returns the entire oneof payload. This appears to diverge from the FieldMask specification in two separate ways.

1. The spec treats oneof members as ordinary maskable fields

google/protobuf/field_mask.proto, section Field Masks and Oneof Fields:

Field masks treat fields in oneofs just as regular fields.

The section includes a worked example showing mask { paths: "sub_message" } is valid for a oneof member. The only stated restriction is:

Note that oneof type names ("test_oneof" in this case) cannot be used in paths.

transactions.transaction.kind.programmable_transaction.inputs names the oneof member (programmable_transaction), not the oneof group (data), so it looks well-formed under the spec.

Same file, section Field Mask Verification:

The implementation of any API method which has a FieldMask type field in the request should verify the included field paths, and return an INVALID_ARGUMENT error if any path is unmappable.

So even if sub-oneof masking is deliberately unsupported here, silently widening doesn't look like the spec-conformant response. Erroring would be.

2. Sui's docs describe nested paths as supported, and document exceptions where they exist

From docs/content/develop/accessing-data/grpc/using-grpc.mdx:

Each field path in the mask must match the field structure of the response proto message. Nested fields are supported using dot notation.

The same list documents the exceptions that do apply:

In batch APIs, only the top-level read_mask is respected. The API ignores any masks inside sub-requests.

In some cases, non-terminal repeated fields might be supported in the mask, even if this is atypical per standard FieldMask behavior.

I don't think the batch case is precedent for this one: a batch request carries two masks (top-level and per-sub-request) and the docs resolve that conflict by declaring which wins. Here there is a single mask and no conflict. The path simply isn't applied to completion. The repeated-fields bullet does suggest the convention is to document deviations from standard FieldMask behavior; I couldn't find a corresponding note for oneof members.

Steps to reproduce

Three GetCheckpoint calls on the same checkpoint, differing only in read_mask:

grpcurl -import-path sui-apis/proto -proto sui/rpc/v2/ledger_service.proto \
  -d '{"sequence_number":"304700000","read_mask":{"paths":["PATH"]}}' \
  fullnode.mainnet.sui.io:443 sui.rpc.v2.LedgerService/GetCheckpoint
PATH response size sha256 (first 16)
transactions.transaction.kind 130,914 B 73be917e2625f339
transactions.transaction.kind.programmable_transaction.inputs 130,914 B 73be917e2625f339
transactions.transaction.kind.consensus_commit_prologue 130,914 B 73be917e2625f339

Expected result

Either the deeper paths return only the requested sub-field, or the request fails with INVALID_ARGUMENT.

Actual result

All three responses are byte-identical. The two deeper paths add nothing over kind.

The response to the …programmable_transaction.inputs mask contains commands for all 19 transactions carrying a programmable_transaction payload (12 PROGRAMMABLE_TRANSACTION + 7 PROGRAMMABLE_SYSTEM_TRANSACTION), plus fully populated consensusCommitPrologue messages for the 3 prologue transactions. None of that was requested, and no error was returned.

Masking behaves correctly at every level above this: transactions.digest, transactions.effects.status and transactions.transaction.kind all narrow the response as expected.

Possible mechanism

I may be misreading this, but in crates/sui-types/src/rpc_proto_conversions.rs at tag testnet-v1.76.1, line 2269:

if mask.contains(Transaction::KIND_FIELD.name) {
    message.kind = Some(source.kind.clone().into());
}

Two things seem to combine:

  1. FieldMaskTree::contains is a prefix match. Its doctest in crates/sui-rpc/src/field/field_mask_tree.rs states "A path is considered a match and contained by this tree if it is a prefix for any contained paths", so kind.consensus_commit_prologue satisfies contains("kind").
  2. .into() dispatches to impl From<crate::transaction::TransactionKind> for TransactionKind (same file, line 2399), which takes no FieldMaskTree parameter. mask.subtree(KIND_FIELD) is never called, so the submask is discarded and the whole oneof is rendered.

Sibling fields in the same file do thread a subtree through: mask.subtree(Checkpoint::TRANSACTIONS_FIELD) (line 67), ExecutedTransaction::TRANSACTION_FIELD (94), SIGNATURES_FIELD (131), EFFECTS_FIELD (139), EVENTS_FIELD (153). That asymmetry is what leaves me unsure whether kind is an intentional exception.

Cost

The two fields needed to distinguish consensus-path from fastpath-eligible transactions both live under kind: consensus_commit_prologue.{round, commit_timestamp} and programmable_transaction.inputs[].kind. Requesting either pulls in every PTB's full commands array and every input payload.

Measured over 2,000 checkpoints (304700000-304701999), comparing the mask I have to use against a synthesized response containing only the fields consumed, i.e. what a working sub-oneof mask would return:

raw B/cp zstd-12 B/cp
what the server returns 20,441 1,810
what is actually needed 7,137 1,008
overhead 2.86x 1.79x

Responses carry grpc-encoding: zstd, so 1.79x is the bandwidth-relevant figure and 2.86x is the decode/parse figure.

Since JSON-RPC was disabled on public fullnodes the week of 2026-07-27, gRPC is the only bulk path to checkpoint data. Any external integrator doing sustained checkpoint collection pays this multiplier, and cannot opt out of it, because the finer mask paths are accepted without effect.

Questions

  1. Is masking below TransactionKind.data intended to be unsupported?
  2. If so, should an unapplied path return INVALID_ARGUMENT per the spec's Field Mask Verification section, rather than being silently widened? Silent widening is the hard case for a caller, because the response looks correct.
  3. And should the limitation be documented alongside the batch-API and repeated-field exceptions already listed in using-grpc.mdx?

Environment

  • endpoint fullnode.mainnet.sui.io:443, node sui-node/1.76.1-433212f8f276
  • mainnet, chain id 4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S
  • grpcurl v1.9.1, protos from MystenLabs/sui-apis
  • observed 2026-08-02

For context, not as prior art: read_mask granularity came up once during the gRPC beta feedback round in #23151. @FrankC01, 2025-08-25 noted "Extending read_masks throughout would be a good add", without a specific case attached.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions