From bbf9afe3ef368c7e6181c36d9580fb171480e2e3 Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Tue, 5 May 2026 16:55:04 +0200 Subject: [PATCH 1/4] refactor(rpc): remove `CheckNullifiers` gRPC method --- crates/rpc/README.md | 19 -------- crates/rpc/src/server/api.rs | 25 +---------- crates/rpc/src/tests.rs | 13 ------ crates/store/README.md | 17 -------- crates/store/src/errors.rs | 12 ----- crates/store/src/server/rpc_api.rs | 29 +------------ crates/store/src/state/mod.rs | 16 +------ crates/utils/src/limiter.rs | 3 +- docs/external/src/rpc.md | 70 ------------------------------ docs/internal/src/rpc.md | 1 - proto/proto/internal/store.proto | 13 ------ proto/proto/rpc.proto | 30 +------------ 12 files changed, 5 insertions(+), 243 deletions(-) diff --git a/crates/rpc/README.md b/crates/rpc/README.md index 6a6c4d58e..486f3d90f 100644 --- a/crates/rpc/README.md +++ b/crates/rpc/README.md @@ -13,7 +13,6 @@ The full gRPC method definitions can be found in the [proto](../proto/README.md) -- [CheckNullifiers](#checknullifiers) - [SyncNullifiers](#syncnullifiers) - [GetAccount](#getaccount) - [GetBlockByNumber](#getblockbynumber) @@ -32,24 +31,6 @@ The full gRPC method definitions can be found in the [proto](../proto/README.md) --- -### CheckNullifiers - -Returns a nullifier proof for each of the requested nullifiers. - -**Limits:** `nullifier` (1000) - -#### Error Handling - -When nullifier checking fails, detailed error information is provided through gRPC status details. The following error codes may be returned: - -| Error Code | Value | gRPC Status | Description | -|---------------------------|-------|--------------------|---------------------------------------| -| `INTERNAL_ERROR` | 0 | `INTERNAL` | Internal server error occurred | -| `DESERIALIZATION_FAILED` | 1 | `INVALID_ARGUMENT` | Malformed nullifier format | -| `TOO_MANY_NULLIFIERS` | 2 | `INVALID_ARGUMENT` | Too many nullifiers in request | - ---- - ### GetAccount Returns an account witness (Merkle proof of inclusion in the account tree) and optionally account details. diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index e34d81498..a38c31a7a 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -23,7 +23,6 @@ use miden_node_utils::limiter::{ QueryParamLimiter, QueryParamNoteIdLimit, QueryParamNoteTagLimit, - QueryParamNullifierLimit, QueryParamNullifierPrefixLimit, QueryParamStorageMapKeyTotalLimit, }; @@ -247,24 +246,6 @@ impl RpcService { impl api_server::Api for RpcService { // -- Nullifier endpoints ----------------------------------------------------------------- - async fn check_nullifiers( - &self, - request: Request, - ) -> Result, Status> { - debug!(target: COMPONENT, request = ?request.get_ref()); - - check::(request.get_ref().nullifiers.len())?; - - // validate all the nullifiers from the user request - for nullifier in &request.get_ref().nullifiers { - let _: Word = nullifier - .try_into() - .or(Err(Status::invalid_argument("Word field is not in the modulus range")))?; - } - - self.store.clone().check_nullifiers(request).await - } - async fn sync_nullifiers( &self, request: Request, @@ -760,16 +741,12 @@ static RPC_LIMITS: LazyLock = LazyLock::new(|| { use QueryParamAccountIdLimit as AccountId; use QueryParamNoteIdLimit as NoteId; use QueryParamNoteTagLimit as NoteTag; - use QueryParamNullifierLimit as Nullifier; + use QueryParamNullifierPrefixLimit as NullifierPrefix; use QueryParamStorageMapKeyTotalLimit as StorageMapKeyTotal; proto::rpc::RpcLimits { endpoints: std::collections::HashMap::from([ - ( - "CheckNullifiers".into(), - endpoint_limits(&[(Nullifier::PARAM_NAME, Nullifier::LIMIT)]), - ), ( "SyncNullifiers".into(), endpoint_limits(&[(NullifierPrefix::PARAM_NAME, NullifierPrefix::LIMIT)]), diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 1eee0555b..058495b53 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -16,7 +16,6 @@ use miden_node_utils::limiter::{ QueryParamLimiter, QueryParamNoteIdLimit, QueryParamNoteTagLimit, - QueryParamNullifierLimit, QueryParamNullifierPrefixLimit, }; use miden_protocol::Word; @@ -628,18 +627,6 @@ async fn get_limits_endpoint() { // Verify the response contains expected endpoints and limits assert!(!limits.endpoints.is_empty(), "endpoints should not be empty"); - // Verify CheckNullifiers endpoint - let check_nullifiers = - limits.endpoints.get("CheckNullifiers").expect("CheckNullifiers should exist"); - - assert_eq!( - check_nullifiers.parameters.get(QueryParamNullifierLimit::PARAM_NAME), - Some(&(QueryParamNullifierLimit::LIMIT as u32)), - "CheckNullifiers {} limit should be {}", - QueryParamNullifierLimit::PARAM_NAME, - QueryParamNullifierLimit::LIMIT - ); - let sync_transactions = limits.endpoints.get("SyncTransactions").expect("SyncTransactions should exist"); assert_eq!( diff --git a/crates/store/README.md b/crates/store/README.md index ca368b297..5331a0df6 100644 --- a/crates/store/README.md +++ b/crates/store/README.md @@ -42,7 +42,6 @@ The full gRPC API can be found [here](../../proto/proto/store.proto). - [ApplyBlock](#applyblock) -- [CheckNullifiers](#checknullifiers) - [GetAccount](#getaccount) - [GetBlockByNumber](#getblockbynumber) - [GetBlockHeaderByNumber](#getblockheaderbynumber) @@ -67,22 +66,6 @@ Applies changes of a new block to the DB and in-memory data structures. Raw bloc --- -### CheckNullifiers - -Returns a nullifier proof for each of the requested nullifiers. - -#### Error Handling - -When nullifier checking fails, detailed error information is provided through gRPC status details. The following error codes may be returned: - -| Error Code | Value | gRPC Status | Description | -|---------------------------|-------|--------------------|---------------------------------------| -| `INTERNAL_ERROR` | 0 | `INTERNAL` | Internal server error occurred | -| `DESERIALIZATION_FAILED` | 1 | `INVALID_ARGUMENT` | Malformed nullifier format | -| `TOO_MANY_NULLIFIERS` | 2 | `INVALID_ARGUMENT` | Too many nullifiers in request | - ---- - ### GetAccount Returns an account witness (Merkle proof of inclusion in the account tree) and optionally account details. diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index 1e008b593..12df2d5be 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -460,18 +460,6 @@ pub enum GetNoteScriptByRootError { ScriptNotFound, } -// CHECK NULLIFIERS ERRORS -// ================================================================================================ - -#[derive(Debug, Error, GrpcError)] -pub enum CheckNullifiersError { - #[error("database error")] - #[grpc(internal)] - DatabaseError(#[from] DatabaseError), - #[error("malformed nullifier")] - DeserializationFailed(#[from] ConversionError), -} - // SYNC TRANSACTIONS ERRORS // ================================================================================================ diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index 90a82ad6f..2f7b5a00a 100644 --- a/crates/store/src/server/rpc_api.rs +++ b/crates/store/src/server/rpc_api.rs @@ -1,4 +1,3 @@ -use miden_node_proto::convert; use miden_node_proto::decode::{ convert_digests_to_words, read_account_id, @@ -14,7 +13,6 @@ use miden_node_utils::limiter::{ QueryParamLimiter, QueryParamNoteIdLimit, QueryParamNoteTagLimit, - QueryParamNullifierLimit, QueryParamNullifierPrefixLimit, }; use miden_protocol::Word; @@ -26,7 +24,6 @@ use tracing::{debug, info}; use crate::COMPONENT; use crate::errors::{ - CheckNullifiersError, GetAccountError, GetBlockByNumberError, GetNoteScriptByRootError, @@ -38,7 +35,7 @@ use crate::errors::{ SyncNullifiersError, SyncTransactionsError, }; -use crate::server::api::{StoreApi, internal_error, validate_nullifiers}; +use crate::server::api::{StoreApi, internal_error}; use crate::state::Finality; // CLIENT ENDPOINTS @@ -56,30 +53,6 @@ impl rpc_server::Rpc for StoreApi { self.get_block_header_by_number_inner(request).await } - /// Returns info on whether the specified nullifiers have been consumed. - /// - /// This endpoint also returns Merkle authentication path for each requested nullifier which can - /// be verified against the latest root of the nullifier database. - async fn check_nullifiers( - &self, - request: Request, - ) -> Result, Status> { - // Validate the nullifiers and convert them to Word values. Stop on first error. - let request = request.into_inner(); - - // Validate nullifiers count - check::(request.nullifiers.len())?; - - let nullifiers = validate_nullifiers::(&request.nullifiers)?; - - // Query the state for the request's nullifiers - let proofs = self.state.check_nullifiers(&nullifiers).await; - - Ok(Response::new(proto::rpc::CheckNullifiersResponse { - proofs: convert(proofs).collect(), - })) - } - /// Returns nullifiers that match the specified prefixes and have been consumed. /// /// Currently the only supported prefix length is 16 bits. diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index acb909e38..c52cccfe2 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -32,7 +32,7 @@ use miden_protocol::block::account_tree::AccountWitness; use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness}; use miden_protocol::block::{BlockHeader, BlockInputs, BlockNumber, Blockchain}; use miden_protocol::crypto::merkle::mmr::{MmrPeaks, MmrProof, PartialMmr}; -use miden_protocol::crypto::merkle::smt::{LargeSmt, SmtProof, SmtStorage}; +use miden_protocol::crypto::merkle::smt::{LargeSmt, SmtStorage}; use miden_protocol::note::{NoteId, NoteScript, Nullifier}; use miden_protocol::transaction::PartialBlockchain; use tokio::sync::{Mutex, RwLock, watch}; @@ -300,20 +300,6 @@ impl State { } } - /// Generates membership proofs for each one of the `nullifiers` against the latest nullifier - /// tree. - /// - /// Note: these proofs are invalidated once the nullifier tree is modified, i.e. on a new block. - #[instrument(level = "debug", target = COMPONENT, skip_all, ret)] - pub async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Vec { - let inner = self.inner.read().await; - nullifiers - .iter() - .map(|n| inner.nullifier_tree.open(n)) - .map(NullifierWitness::into_proof) - .collect() - } - /// Queries a list of notes from the database. /// /// If the provided list of [`NoteId`] given is empty or no note matches the provided diff --git a/crates/utils/src/limiter.rs b/crates/utils/src/limiter.rs index 3d713e064..cffe7d80b 100644 --- a/crates/utils/src/limiter.rs +++ b/crates/utils/src/limiter.rs @@ -68,8 +68,7 @@ impl QueryParamLimiter for QueryParamNullifierPrefixLimit { const LIMIT: usize = GENERAL_REQUEST_LIMIT; } -/// Used for the following RPC endpoints: -/// * `check_nullifiers` +/// Only used internally, not exposed via public RPC. /// /// Capped at 1000 nullifiers to bound `IN` clauses and keep response sizes under the 4 MB budget. pub struct QueryParamNullifierLimit; diff --git a/docs/external/src/rpc.md b/docs/external/src/rpc.md index 585388aab..ad390b51c 100644 --- a/docs/external/src/rpc.md +++ b/docs/external/src/rpc.md @@ -11,7 +11,6 @@ The gRPC service definition can be found in the Miden node's `proto` [directory] -- [CheckNullifiers](#checknullifiers) - [GetAccount](#getaccount) - [GetBlockByNumber](#getblockbynumber) - [GetBlockHeaderByNumber](#getblockheaderbynumber) @@ -32,74 +31,6 @@ The gRPC service definition can be found in the Miden node's `proto` [directory] ## API Endpoints -### CheckNullifiers - -Request Sparse Merkle Tree opening proofs to verify whether nullifiers have been consumed. - -#### Request - -```protobuf -message NullifierList { - repeated Digest nullifiers = 1; // List of nullifiers to check -} -``` - -#### Response - -```protobuf -message CheckNullifiersResponse { - repeated SmtOpening proofs = 1; // One proof per requested nullifier -} - -message SmtOpening { - SparseMerklePath path = 1; // Merkle authentication path - SmtLeaf leaf = 2; // Leaf at this position -} - -message SmtLeaf { - oneof leaf { - uint64 empty_leaf_index = 1; - SmtLeafEntry single = 2; - SmtLeafEntryList multiple = 3; - } -} -``` - -#### Understanding Proofs - -**Non-Inclusion (Nullifier NOT consumed):** -- `leaf` contains `empty_leaf_index` -- Note can still be consumed - -**Inclusion (Nullifier IS consumed):** -- `leaf` contains `single` or `multiple` with key-value pairs, including the `nullifier` key -- Note has been spent - -#### Verification - -```rust -use miden_crypto::merkle::{SmtProof, SmtProofError}; - -let block_header = get_latest_block_header(); -let nullifier_tree_root = block_header.state_commitment().nullifier_root(); - -let proof: SmtProof = smt_opening.try_into()?; - -match proof.verify_unset(&nullifier, &nullifier_tree_root) { - Ok(()) => { - // Nullifier is NOT in the tree - note can be consumed - } - Err(SmtProofError::ValueMismatch { .. }) => { - // Proof is valid, but nullifier has a value (not empty) - note already consumed - } - Err(_) => { - // Proof is invalid (wrong root, wrong key, etc.) - } -} -``` - -**Limits:** `nullifier` (1000) - ### GetAccount Request an account witness (Merkle proof of inclusion in the account tree) and optionally account details. @@ -140,7 +71,6 @@ This endpoint allows clients to discover the maximum number of items that can be ```json { "endpoints": { - "CheckNullifiers": { "parameters": { "nullifier": 1000 } }, "SyncNullifiers": { "parameters": { "nullifier": 1000 } }, "SyncTransactions": { "parameters": { "account_id": 1000 } }, "SyncAccountVault": { "parameters": { "account_id": 1000 } }, diff --git a/docs/internal/src/rpc.md b/docs/internal/src/rpc.md index b00f36605..7b13595c5 100644 --- a/docs/internal/src/rpc.md +++ b/docs/internal/src/rpc.md @@ -31,7 +31,6 @@ the store) to keep database queries bounded and to keep response payloads within | 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 | diff --git a/proto/proto/internal/store.proto b/proto/proto/internal/store.proto index 1c8ffd295..04fb41bb2 100644 --- a/proto/proto/internal/store.proto +++ b/proto/proto/internal/store.proto @@ -18,19 +18,6 @@ service Rpc { // Returns the status info. rpc Status(google.protobuf.Empty) returns (rpc.StoreStatus) {} - // Returns a Sparse Merkle Tree opening proof for each requested nullifier - // - // Each proof demonstrates either: - // - **Inclusion**: Nullifier exists in the tree (note was consumed) - // - **Non-inclusion**: Nullifier does not exist (note was not consumed) - // - // The `leaf` field indicates the status: - // - `empty_leaf_index`: Non-inclusion proof - // - `single` or `multiple`: Inclusion proof if the nullifier key is present - // - // Verify proofs against the nullifier tree root in the latest block header. - rpc CheckNullifiers(rpc.NullifierList) returns (rpc.CheckNullifiersResponse) {} - // Returns the latest details the specified account. rpc GetAccount(rpc.AccountRequest) returns (rpc.AccountResponse) {} diff --git a/proto/proto/rpc.proto b/proto/proto/rpc.proto index ede29f151..64527b5bb 100644 --- a/proto/proto/rpc.proto +++ b/proto/proto/rpc.proto @@ -24,19 +24,6 @@ service Api { // multiple smaller requests. rpc GetLimits(google.protobuf.Empty) returns (RpcLimits) {} - // Returns a Sparse Merkle Tree opening proof for each requested nullifier - // - // Each proof demonstrates either: - // - **Inclusion**: Nullifier exists in the tree (note was consumed) - // - **Non-inclusion**: Nullifier does not exist (note was not consumed) - // - // The `leaf` field indicates the status: - // * `empty_leaf_index`: Non-inclusion proof (nullifier not in tree) - // * `single` or `multiple`: Inclusion proof only if the requested nullifier appears as a key. - // - // Verify proofs against the nullifier tree root in the latest block header. - rpc CheckNullifiers(NullifierList) returns (CheckNullifiersResponse) {} - // Returns the latest details of the specified account. rpc GetAccount(AccountRequest) returns (AccountResponse) {} @@ -358,21 +345,6 @@ message AccountStorageDetails { repeated AccountStorageMapDetails map_details = 2; } -// CHECK NULLIFIERS -// ================================================================================================ - -// List of nullifiers to return proofs for. -message NullifierList { - // List of nullifiers to return proofs for. - repeated primitives.Digest nullifiers = 1; -} - -// Represents the result of checking nullifiers. -message CheckNullifiersResponse { - // Each requested nullifier has its corresponding nullifier proof at the same position. - repeated primitives.SmtOpening proofs = 1; -} - // SYNC NULLIFIERS // ================================================================================================ @@ -685,7 +657,7 @@ message GetNetworkNoteStatusResponse { // Represents the query parameter limits for RPC endpoints. message RpcLimits { // Maps RPC endpoint names to their parameter limits. - // Key: endpoint name (e.g., "CheckNullifiers") + // Key: endpoint name (e.g., "SyncNullifiers") // Value: map of parameter names to their limit values map endpoints = 1; } From 78a3d996735ac8c8fddb999a262d1f1607d03bbf Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Tue, 5 May 2026 17:08:54 +0200 Subject: [PATCH 2/4] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f75985f5..a0c95207b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Added a `replica` mode to the store, which streams blocks from an upstream master store ([#1987](https://github.com/0xMiden/node/pull/1987)). - Added `StoreReplica` gRPC service with endpoints for streaming blocks and proofs ([#1987](https://github.com/0xMiden/node/pull/1987)). - Replaced the network monitor's JavaScript dashboard with a server-rendered Maud + HTMX frontend ([#2024](https://github.com/0xMiden/node/pull/2024)). +- [BREAKING] Removed `CheckNullifiers` endpoint ([#2049](https://github.com/0xMiden/node/pull/2049)). ## v0.14.10 (2026-05-29) From e4f4c4520e952a4d49c2dbf566a1c119f433b11c Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Tue, 5 May 2026 17:09:19 +0200 Subject: [PATCH 3/4] fixup! refactor(rpc): remove `CheckNullifiers` gRPC method --- crates/rpc/src/server/api.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index a38c31a7a..c194ac6f4 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -741,7 +741,6 @@ static RPC_LIMITS: LazyLock = LazyLock::new(|| { use QueryParamAccountIdLimit as AccountId; use QueryParamNoteIdLimit as NoteId; use QueryParamNoteTagLimit as NoteTag; - use QueryParamNullifierPrefixLimit as NullifierPrefix; use QueryParamStorageMapKeyTotalLimit as StorageMapKeyTotal; From 79bed41010b123f062f0ca32601687bd2af427b8 Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Wed, 6 May 2026 10:42:27 +0200 Subject: [PATCH 4/4] fixup! refactor(rpc): remove `CheckNullifiers` gRPC method --- crates/store/src/db/models/queries/nullifiers.rs | 2 -- crates/utils/src/limiter.rs | 9 --------- 2 files changed, 11 deletions(-) diff --git a/crates/store/src/db/models/queries/nullifiers.rs b/crates/store/src/db/models/queries/nullifiers.rs index 89f62b4ab..552110ca7 100644 --- a/crates/store/src/db/models/queries/nullifiers.rs +++ b/crates/store/src/db/models/queries/nullifiers.rs @@ -15,7 +15,6 @@ use diesel::{ use miden_node_utils::limiter::{ MAX_RESPONSE_PAYLOAD_BYTES, QueryParamLimiter, - QueryParamNullifierLimit, QueryParamNullifierPrefixLimit, }; use miden_protocol::block::BlockNumber; @@ -236,7 +235,6 @@ pub(crate) fn insert_nullifiers_for_block( nullifiers: &[Nullifier], block_num: BlockNumber, ) -> Result { - QueryParamNullifierLimit::check(nullifiers.len())?; let serialized_nullifiers = Vec::>::from_iter(nullifiers.iter().map(Nullifier::to_bytes)); diff --git a/crates/utils/src/limiter.rs b/crates/utils/src/limiter.rs index cffe7d80b..98d28d713 100644 --- a/crates/utils/src/limiter.rs +++ b/crates/utils/src/limiter.rs @@ -68,15 +68,6 @@ impl QueryParamLimiter for QueryParamNullifierPrefixLimit { const LIMIT: usize = GENERAL_REQUEST_LIMIT; } -/// Only used internally, not exposed via public RPC. -/// -/// Capped at 1000 nullifiers to bound `IN` clauses and keep response sizes under the 4 MB budget. -pub struct QueryParamNullifierLimit; -impl QueryParamLimiter for QueryParamNullifierLimit { - const PARAM_NAME: &str = "nullifier"; - const LIMIT: usize = GENERAL_REQUEST_LIMIT; -} - /// Used for the following RPC endpoints /// * `get_note_sync_multi` ///