Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions crates/rpc/src/server/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use miden_node_utils::limiter::{
QueryParamNoteIdLimit,
QueryParamNoteTagLimit,
QueryParamNullifierLimit,
QueryParamNullifierPrefixLimit,
QueryParamStorageMapKeyTotalLimit,
};
use miden_protocol::batch::{ProposedBatch, ProvenBatch};
Expand Down Expand Up @@ -216,7 +217,7 @@ impl api_server::Api for RpcService {
) -> Result<Response<proto::rpc::SyncNullifiersResponse>, Status> {
debug!(target: COMPONENT, request = ?request.get_ref());

check::<QueryParamNullifierLimit>(request.get_ref().nullifiers.len())?;
check::<QueryParamNullifierPrefixLimit>(request.get_ref().nullifiers.len())?;

self.store.clone().sync_nullifiers(request).await
}
Expand Down Expand Up @@ -652,6 +653,7 @@ static RPC_LIMITS: LazyLock<proto::rpc::RpcLimits> = 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 {
Expand All @@ -662,7 +664,7 @@ static RPC_LIMITS: LazyLock<proto::rpc::RpcLimits> = LazyLock::new(|| {
),
(
"SyncNullifiers".into(),
endpoint_limits(&[(Nullifier::PARAM_NAME, Nullifier::LIMIT)]),
endpoint_limits(&[(NullifierPrefix::PARAM_NAME, NullifierPrefix::LIMIT)]),
),
(
"SyncTransactions".into(),
Expand Down
23 changes: 23 additions & 0 deletions crates/rpc/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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!(
Expand Down
4 changes: 4 additions & 0 deletions crates/store/src/server/rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use miden_node_utils::limiter::{
QueryParamNoteIdLimit,
QueryParamNoteTagLimit,
QueryParamNullifierLimit,
QueryParamNullifierPrefixLimit,
};
use miden_protocol::Word;
use miden_protocol::account::AccountId;
Expand Down Expand Up @@ -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::<QueryParamNullifierPrefixLimit>(request.nullifiers.len())?;

let chain_tip = self.state.chain_tip(Finality::Committed).await;
let block_range =
read_block_range::<SyncNullifiersError>(request.block_range, "SyncNullifiersRequest")?
Expand Down
5 changes: 2 additions & 3 deletions crates/utils/src/limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down
19 changes: 18 additions & 1 deletion docs/internal/src/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -57,4 +75,3 @@ enum SubmitProvenTransactionGrpcError {
```

Error codes are embedded as single bytes in `Status.details`

Loading