Skip to main content

miden_client/rpc/domain/
limits.rs

1// RPC LIMITS
2// ================================================================================================
3
4use alloc::format;
5use core::convert::TryFrom;
6
7use miden_tx::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14
15use crate::rpc::RpcEndpoint;
16use crate::rpc::errors::RpcConversionError;
17use crate::rpc::generated::rpc as proto;
18
19/// Key used to store RPC limits in the settings table.
20pub(crate) const RPC_LIMITS_STORE_SETTING: &str = "rpc_limits";
21
22const DEFAULT_NOTE_IDS_LIMIT: u32 = 100;
23const DEFAULT_NULLIFIERS_LIMIT: u32 = 1000;
24const DEFAULT_ACCOUNT_IDS_LIMIT: u32 = 1000;
25const DEFAULT_NOTE_TAGS_LIMIT: u32 = 1000;
26
27/// Domain type representing RPC endpoint limits.
28///
29/// These limits define the maximum number of items that can be sent in a single RPC request.
30/// Exceeding these limits will result in the request being rejected by the node.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct RpcLimits {
33    /// Maximum number of note IDs that can be sent in a single `GetNotesById` request.
34    pub note_ids_limit: u32,
35    /// Maximum number of nullifier prefixes that can be sent in a single `SyncNullifiers` request.
36    pub nullifiers_limit: u32,
37    /// Maximum number of account IDs that can be sent in a single `SyncTransactions` request.
38    pub account_ids_limit: u32,
39    /// Maximum number of note tags that can be sent in a single `SyncNotes` request.
40    pub note_tags_limit: u32,
41}
42
43impl Default for RpcLimits {
44    fn default() -> Self {
45        Self {
46            note_ids_limit: DEFAULT_NOTE_IDS_LIMIT,
47            nullifiers_limit: DEFAULT_NULLIFIERS_LIMIT,
48            account_ids_limit: DEFAULT_ACCOUNT_IDS_LIMIT,
49            note_tags_limit: DEFAULT_NOTE_TAGS_LIMIT,
50        }
51    }
52}
53
54impl Serializable for RpcLimits {
55    fn write_into<W: ByteWriter>(&self, target: &mut W) {
56        self.note_ids_limit.write_into(target);
57        self.nullifiers_limit.write_into(target);
58        self.account_ids_limit.write_into(target);
59        self.note_tags_limit.write_into(target);
60    }
61}
62
63impl Deserializable for RpcLimits {
64    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
65        Ok(Self {
66            note_ids_limit: u32::read_from(source)?,
67            nullifiers_limit: u32::read_from(source)?,
68            account_ids_limit: u32::read_from(source)?,
69            note_tags_limit: u32::read_from(source)?,
70        })
71    }
72}
73
74/// Extracts a parameter limit from the proto response for a given endpoint and parameter name.
75fn get_param(
76    proto: &proto::RpcLimits,
77    endpoint: RpcEndpoint,
78    param: &'static str,
79) -> Result<u32, RpcConversionError> {
80    let ep = proto.endpoints.get(endpoint.proto_name()).ok_or(
81        RpcConversionError::MissingFieldInProtobufRepresentation {
82            entity: "RpcLimits",
83            field_name: param,
84        },
85    )?;
86    let limit = ep.parameters.get(param).ok_or(
87        RpcConversionError::MissingFieldInProtobufRepresentation {
88            entity: "RpcLimits",
89            field_name: param,
90        },
91    )?;
92    if *limit == 0 {
93        return Err(RpcConversionError::InvalidField(format!(
94            "{}.{} must be greater than zero",
95            endpoint.proto_name(),
96            param
97        )));
98    }
99
100    Ok(*limit)
101}
102
103impl TryFrom<proto::RpcLimits> for RpcLimits {
104    type Error = RpcConversionError;
105
106    fn try_from(proto: proto::RpcLimits) -> Result<Self, Self::Error> {
107        Ok(Self {
108            note_ids_limit: get_param(&proto, RpcEndpoint::GetNotesById, "note_id")?,
109            nullifiers_limit: get_param(&proto, RpcEndpoint::SyncNullifiers, "nullifier_prefix")?,
110            account_ids_limit: get_param(&proto, RpcEndpoint::SyncTransactions, "account_id")?,
111            note_tags_limit: get_param(&proto, RpcEndpoint::SyncNotes, "note_tag")?,
112        })
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use alloc::string::String;
119
120    use super::*;
121
122    #[test]
123    fn rpc_limits_serialization_roundtrip() {
124        let original = RpcLimits {
125            note_ids_limit: 100,
126            nullifiers_limit: 1000,
127            account_ids_limit: 1000,
128            note_tags_limit: 1000,
129        };
130
131        let bytes = original.to_bytes();
132        let deserialized = RpcLimits::read_from_bytes(&bytes).expect("deserialization failed");
133
134        assert_eq!(original, deserialized);
135    }
136
137    #[test]
138    fn rejects_zero_limits_from_rpc_response() {
139        let mut proto = proto::RpcLimits::default();
140
141        proto.endpoints.insert(
142            RpcEndpoint::GetNotesById.proto_name().into(),
143            proto::EndpointLimits {
144                parameters: [(String::from("note_id"), 0)].into(),
145            },
146        );
147        proto.endpoints.insert(
148            RpcEndpoint::SyncNullifiers.proto_name().into(),
149            proto::EndpointLimits {
150                parameters: [(String::from("nullifier_prefix"), 1000)].into(),
151            },
152        );
153        proto.endpoints.insert(
154            RpcEndpoint::SyncTransactions.proto_name().into(),
155            proto::EndpointLimits {
156                parameters: [(String::from("account_id"), 1000)].into(),
157            },
158        );
159        proto.endpoints.insert(
160            RpcEndpoint::SyncNotes.proto_name().into(),
161            proto::EndpointLimits {
162                parameters: [(String::from("note_tag"), 1000)].into(),
163            },
164        );
165
166        let err = RpcLimits::try_from(proto).expect_err("zero limit should be rejected");
167
168        assert!(matches!(err, RpcConversionError::InvalidField(_)), "got {err:?}");
169    }
170}