diff --git a/CHANGELOG.md b/CHANGELOG.md index 85da940be..1c0577be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Updated the RocksDB SMT backend to use budgeted deserialization for bytes read from disk, ported from `0xMiden/crypto` PR [#846](https://github.com/0xMiden/crypto/pull/846) ([#1923](https://github.com/0xMiden/node/pull/1923)). - [BREAKING] Network monitor `/status` endpoint now emits a single `RemoteProverStatus` entry per remote prover that bundles status, workers, and test results, instead of separate entries ([#1980](https://github.com/0xMiden/node/pull/1980)). - Refactored the validator gRPC API implementation to use the new per-method trait implementations ([#1959](https://github.com/0xMiden/node/pull/1959)). +- Aligned `SyncNullifiers` list-limit validation in RPC and store with `nullifier_prefix` parameter semantics, extended `GetLimits` test coverage, and documented query parameter limits ([#1986](https://github.com/0xMiden/node/pull/1986)). ## v0.14.9 (2026-04-21) diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 699c813f7..4a11d17a5 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -21,6 +21,7 @@ use miden_node_utils::limiter::{ QueryParamNoteIdLimit, QueryParamNoteTagLimit, QueryParamNullifierLimit, + QueryParamNullifierPrefixLimit, QueryParamStorageMapKeyTotalLimit, }; use miden_protocol::batch::{ProposedBatch, ProvenBatch}; @@ -216,7 +217,7 @@ impl api_server::Api for RpcService { ) -> Result, Status> { debug!(target: COMPONENT, request = ?request.get_ref()); - check::(request.get_ref().nullifiers.len())?; + check::(request.get_ref().nullifiers.len())?; self.store.clone().sync_nullifiers(request).await } @@ -652,6 +653,7 @@ static RPC_LIMITS: LazyLock = LazyLock::new(|| { use QueryParamNoteIdLimit as NoteId; use QueryParamNoteTagLimit as NoteTag; use QueryParamNullifierLimit as Nullifier; + use QueryParamNullifierPrefixLimit as NullifierPrefix; use QueryParamStorageMapKeyTotalLimit as StorageMapKeyTotal; proto::rpc::RpcLimits { @@ -662,7 +664,7 @@ static RPC_LIMITS: LazyLock = LazyLock::new(|| { ), ( "SyncNullifiers".into(), - endpoint_limits(&[(Nullifier::PARAM_NAME, Nullifier::LIMIT)]), + endpoint_limits(&[(NullifierPrefix::PARAM_NAME, NullifierPrefix::LIMIT)]), ), ( "SyncTransactions".into(), diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index da19ab6d0..78e8becf9 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -15,7 +15,9 @@ use miden_node_utils::limiter::{ QueryParamAccountIdLimit, QueryParamLimiter, QueryParamNoteIdLimit, + QueryParamNoteTagLimit, QueryParamNullifierLimit, + QueryParamNullifierPrefixLimit, }; use miden_protocol::Word; use miden_protocol::account::delta::AccountUpdateDetails; @@ -576,6 +578,27 @@ async fn get_limits_endpoint() { QueryParamAccountIdLimit::LIMIT ); + // Verify SyncNullifiers endpoint + let sync_nullifiers = + limits.endpoints.get("SyncNullifiers").expect("SyncNullifiers should exist"); + assert_eq!( + sync_nullifiers.parameters.get(QueryParamNullifierPrefixLimit::PARAM_NAME), + Some(&(QueryParamNullifierPrefixLimit::LIMIT as u32)), + "SyncNullifiers {} limit should be {}", + QueryParamNullifierPrefixLimit::PARAM_NAME, + QueryParamNullifierPrefixLimit::LIMIT + ); + + // Verify SyncNotes endpoint + let sync_notes = limits.endpoints.get("SyncNotes").expect("SyncNotes should exist"); + assert_eq!( + sync_notes.parameters.get(QueryParamNoteTagLimit::PARAM_NAME), + Some(&(QueryParamNoteTagLimit::LIMIT as u32)), + "SyncNotes {} limit should be {}", + QueryParamNoteTagLimit::PARAM_NAME, + QueryParamNoteTagLimit::LIMIT + ); + // SyncAccountVault and SyncAccountStorageMaps accept a singular account_id, // not a repeated list, so they do not have list parameter limits. assert!( diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index 87055936f..7fa9de1e6 100644 --- a/crates/store/src/server/rpc_api.rs +++ b/crates/store/src/server/rpc_api.rs @@ -8,6 +8,7 @@ use miden_node_utils::limiter::{ QueryParamNoteIdLimit, QueryParamNoteTagLimit, QueryParamNullifierLimit, + QueryParamNullifierPrefixLimit, }; use miden_protocol::Word; use miden_protocol::account::AccountId; @@ -94,6 +95,9 @@ impl rpc_server::Rpc for StoreApi { return Err(SyncNullifiersError::InvalidPrefixLength(request.prefix_len).into()); } + // Validate nullifier prefix list size before querying state. + check::(request.nullifiers.len())?; + let chain_tip = self.state.chain_tip(Finality::Committed).await; let block_range = read_block_range::(request.block_range, "SyncNullifiersRequest")? diff --git a/crates/utils/src/limiter.rs b/crates/utils/src/limiter.rs index 993b3be68..b55c1e294 100644 --- a/crates/utils/src/limiter.rs +++ b/crates/utils/src/limiter.rs @@ -58,7 +58,7 @@ impl QueryParamLimiter for QueryParamAccountIdLimit { } /// Used for the following RPC endpoints: -/// * `select_nullifiers_by_prefix` +/// * `sync_nullifiers` /// /// Capped at 1000 prefixes to keep queries and responses comfortably within the 4 MB payload /// budget and to avoid unbounded prefix scans. @@ -69,8 +69,7 @@ impl QueryParamLimiter for QueryParamNullifierPrefixLimit { } /// Used for the following RPC endpoints: -/// * `select_nullifiers_by_prefix` -/// * `sync_nullifiers` +/// * `check_nullifiers` /// /// Capped at 1000 nullifiers to bound `IN` clauses and keep response sizes under the 4 MB budget. pub struct QueryParamNullifierLimit; diff --git a/docs/internal/src/rpc.md b/docs/internal/src/rpc.md index c477b940d..88bb8ea0f 100644 --- a/docs/internal/src/rpc.md +++ b/docs/internal/src/rpc.md @@ -27,6 +27,24 @@ multi-value parameters (e.g. number of nullifiers, note tags, note IDs, account These limits are defined centrally in `miden_node_utils::limiter` and are enforced at the RPC boundary (and also inside the store) to keep database queries bounded and to keep response payloads within the ~4 MB budget. +`GENERAL_REQUEST_LIMIT` is currently `1000`, and endpoint-specific limits are: + +| Endpoint | Parameter | Limit | Rationale | +| ------------------ | ------------------ | ------ | -------------------------------------------------------------------- | +| | `CheckNullifiers` | `nullifier` | `1000` | Bounds `IN`-style lookups and keeps responses under payload budget | +| `GetAccount` | `storage_map_key` | `64` | SMT proof generation for storage map keys is comparatively expensive | +| `GetNotesById` | `note_id` | `100` | Notes can be large (~32 KiB), so this is intentionally tighter | +| `SyncNotes` | `note_tag` | `1000` | Keeps note sync responses within payload budget | +| `SyncNullifiers` | `nullifier_prefix` | `1000` | Bounds prefix-based nullifier scans | +| `SyncTransactions` | `account_id` | `1000` | Bounds account filter fan-out and response size | + +Additional internal-only limits in `miden_node_utils::limiter` (not surfaced by `GetLimits`) include: + +| Parameter | Limit | Used by | +| ----------------- | ------ | -------------------------------------- | +| `note_commitment` | `1000` | Internal note proof lookups | +| `block_header` | `1000` | Internal batch/block header operations | + ## Error Handling The RPC component uses domain-specific error enums for structured error reporting instead of proto-generated error types. This provides better control over error codes and makes error handling more maintainable. @@ -57,4 +75,3 @@ enum SubmitProvenTransactionGrpcError { ``` Error codes are embedded as single bytes in `Status.details` -