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:
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").
.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
- Is masking below
TransactionKind.data intended to be unsupported?
- 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.
- 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.
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_maskpath that descends into theTransactionKind.dataoneof is neither honored nor rejected. The server returns the entire oneof payload. This appears to diverge from theFieldMaskspecification 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:The section includes a worked example showing
mask { paths: "sub_message" }is valid for a oneof member. The only stated restriction is:transactions.transaction.kind.programmable_transaction.inputsnames the oneof member (programmable_transaction), not the oneof group (data), so it looks well-formed under the spec.Same file, section Field Mask Verification:
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:The same list documents the exceptions that do apply:
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
FieldMaskbehavior; I couldn't find a corresponding note for oneof members.Steps to reproduce
Three
GetCheckpointcalls on the same checkpoint, differing only inread_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/GetCheckpointPATHtransactions.transaction.kind73be917e2625f339transactions.transaction.kind.programmable_transaction.inputs73be917e2625f339transactions.transaction.kind.consensus_commit_prologue73be917e2625f339Expected 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.inputsmask containscommandsfor all 19 transactions carrying aprogrammable_transactionpayload (12PROGRAMMABLE_TRANSACTION+ 7PROGRAMMABLE_SYSTEM_TRANSACTION), plus fully populatedconsensusCommitProloguemessages 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.statusandtransactions.transaction.kindall narrow the response as expected.Possible mechanism
I may be misreading this, but in
crates/sui-types/src/rpc_proto_conversions.rsat tagtestnet-v1.76.1, line 2269:Two things seem to combine:
FieldMaskTree::containsis a prefix match. Its doctest incrates/sui-rpc/src/field/field_mask_tree.rsstates "A path is considered a match and contained by this tree if it is a prefix for any contained paths", sokind.consensus_commit_prologuesatisfiescontains("kind")..into()dispatches toimpl From<crate::transaction::TransactionKind> for TransactionKind(same file, line 2399), which takes noFieldMaskTreeparameter.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 whetherkindis 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}andprogrammable_transaction.inputs[].kind. Requesting either pulls in every PTB's fullcommandsarray 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:
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
TransactionKind.dataintended to be unsupported?INVALID_ARGUMENTper 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.using-grpc.mdx?Environment
fullnode.mainnet.sui.io:443, nodesui-node/1.76.1-433212f8f2764btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6SMystenLabs/sui-apisFor 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.