sui_config/rpc_config.rs
1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::net::SocketAddr;
5use std::time::Duration;
6
7#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
8#[serde(rename_all = "kebab-case")]
9pub struct RpcConfig {
10 /// Enable indexing of transactions and objects
11 ///
12 /// This enables indexing of transactions and objects which allows for a slightly richer rpc
13 /// api. There are some APIs which will be disabled/enabled based on this config while others
14 /// (eg GetTransaction) will still be enabled regardless of this config but may return slight
15 /// less data (eg GetTransaction won't return the checkpoint that includes the requested
16 /// transaction).
17 ///
18 /// Defaults to `false`, with indexing and APIs which require indexes being disabled
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub enable_indexing: Option<bool>,
21
22 /// Configure the address to listen on for https
23 ///
24 /// Defaults to `0.0.0.0:9443` if not specified.
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub https_address: Option<SocketAddr>,
27
28 /// TLS configuration to use for https.
29 ///
30 /// If not provided then the node will not create an https service.
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub tls: Option<RpcTlsConfig>,
33
34 /// Maxumum budget for rendering a Move value into JSON.
35 ///
36 /// This sets the numbers of bytes that we are willing to spend on rendering field names and
37 /// values when rendering a Move value into a JSON value.
38 ///
39 /// Defaults to `1MiB` if not specified.
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub max_json_move_value_size: Option<usize>,
42
43 /// Aggregate budget for Move-value JSON rendering across a single response.
44 ///
45 /// Endpoints that render many Move values in one response share this budget
46 /// across all per-item renders, so the response cannot multiply one request
47 /// into hundreds of MiB of materialized `prost_types::Value`. The budget
48 /// covers one checkpoint in `GetCheckpoint`, `ListCheckpoints`, and
49 /// `SubscribeCheckpoints`, and one transaction frame in
50 /// `SubscribeTransactions`. Values rendered after the budget runs out have
51 /// no `json`.
52 ///
53 /// `GetTransaction`, `ListTransactions`, `ListEvents`, and `SubscribeEvents`
54 /// instead render each value with its own `max_json_move_value_size` budget
55 /// and no aggregate cap, so one transaction's event JSON can differ between
56 /// `SubscribeTransactions` and `GetTransaction`.
57 ///
58 /// Defaults to `16 MiB` if not specified.
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub max_json_move_value_response_size: Option<usize>,
61
62 /// Configuration for RPC index initialization and bulk loading
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub index_initialization: Option<RpcIndexInitConfig>,
65
66 /// Tunables for the ledger-history list APIs (`list_transactions`,
67 /// `list_events`, `list_checkpoints`). These scan the historical inverted
68 /// indexes, unlike the live object-set listings (`list_owned_objects`,
69 /// `list_dynamic_fields`), so they carry their own time and scan-cost bounds.
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub ledger_history: Option<LedgerHistoryConfig>,
72
73 /// Number of consecutive checkpoints a filtered subscription may go without
74 /// producing an item before the server emits a progress-only frame,
75 /// so sparse subscribers always learn their resume point. Defaults to 25
76 /// (~5 seconds at mainnet checkpoint cadence).
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub subscription_watermark_interval: Option<u32>,
79
80 /// Maximum number of concurrent RPC subscriptions. Defaults to 1024.
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub subscription_max_subscribers: Option<usize>,
83
84 /// Number of parallel shard tasks that evaluate subscription filters and
85 /// deliver updates. Each subscriber lives on one shard; per-checkpoint
86 /// filter evaluation parallelizes across shards. Defaults to the host's
87 /// available parallelism.
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub subscription_shards: Option<u32>,
90
91 /// Configuration for rendering Objects based on the Display standard
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub display: Option<DisplayConfig>,
94
95 /// Maximum age of an RPC connection, in seconds. When a connection
96 /// reaches this age the server sends GOAWAY and stops accepting new
97 /// streams on it; in-flight requests are allowed to complete within
98 /// `max-connection-age-grace-secs`. Bounding connection lifetime is the
99 /// server-side backstop that reclaims streams wedged behind HTTP/2
100 /// flow-control windows that a stalled peer never reopens.
101 ///
102 /// Defaults to 4 hours. Set to `0` to disable (connections then live
103 /// forever).
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub max_connection_age_secs: Option<u64>,
106
107 /// Grace period, in seconds, that in-flight requests are given to
108 /// complete after a connection begins shutting down (its
109 /// `max-connection-age-secs` expired, or the node is stopping). When
110 /// the grace period expires the connection is closed even if streams
111 /// are still open. Set to `0` to close immediately at shutdown.
112 ///
113 /// Defaults to 10 minutes. Has no effect while
114 /// `max-connection-age-secs` is disabled and the node is running.
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub max_connection_age_grace_secs: Option<u64>,
117
118 /// Server-side timeout for gRPC requests, in milliseconds. Requests
119 /// that carry a `grpc-timeout` header are bounded by the smaller of the
120 /// two values. The timeout covers a unary request's full execution and
121 /// a streaming request's time to first response; it does not bound the
122 /// lifetime of an established stream.
123 ///
124 /// Defaults to 60 seconds. Set to `0` to disable.
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub grpc_timeout_ms: Option<u64>,
127}
128
129const DEFAULT_MAX_CONNECTION_AGE: Duration = Duration::from_secs(4 * 60 * 60);
130const DEFAULT_MAX_CONNECTION_AGE_GRACE: Duration = Duration::from_secs(10 * 60);
131const DEFAULT_GRPC_TIMEOUT: Duration = Duration::from_secs(60);
132
133impl RpcConfig {
134 pub fn enable_indexing(&self) -> bool {
135 self.enable_indexing.unwrap_or(false)
136 }
137
138 pub fn https_address(&self) -> SocketAddr {
139 self.https_address
140 .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 9443)))
141 }
142
143 pub fn tls_config(&self) -> Option<&RpcTlsConfig> {
144 self.tls.as_ref()
145 }
146
147 pub fn max_json_move_value_size(&self) -> usize {
148 self.max_json_move_value_size.unwrap_or(1024 * 1024)
149 }
150
151 pub fn max_json_move_value_response_size(&self) -> usize {
152 self.max_json_move_value_response_size
153 .unwrap_or(16 * 1024 * 1024)
154 }
155
156 pub fn index_initialization_config(&self) -> Option<&RpcIndexInitConfig> {
157 self.index_initialization.as_ref()
158 }
159
160 pub fn ledger_history(&self) -> &LedgerHistoryConfig {
161 const DEFAULT_LEDGER_HISTORY_CONFIG: LedgerHistoryConfig = LedgerHistoryConfig {
162 list_transactions: None,
163 list_events: None,
164 list_checkpoints: None,
165 bitmap_bucket_scan_budget: None,
166 chunk_bucket_scan_budget: None,
167 max_bitmap_filter_literals: None,
168 };
169
170 self.ledger_history
171 .as_ref()
172 .unwrap_or(&DEFAULT_LEDGER_HISTORY_CONFIG)
173 }
174
175 /// Maximum age of an RPC connection; `None` means unlimited.
176 pub fn max_connection_age(&self) -> Option<Duration> {
177 match self.max_connection_age_secs {
178 Some(0) => None,
179 Some(secs) => Some(Duration::from_secs(secs)),
180 None => Some(DEFAULT_MAX_CONNECTION_AGE),
181 }
182 }
183
184 /// Grace period for in-flight requests once a connection begins
185 /// shutting down; `Duration::ZERO` closes immediately.
186 pub fn max_connection_age_grace(&self) -> Duration {
187 self.max_connection_age_grace_secs
188 .map(Duration::from_secs)
189 .unwrap_or(DEFAULT_MAX_CONNECTION_AGE_GRACE)
190 }
191
192 /// Server-side default deadline for gRPC requests; `None` means no
193 /// server-imposed deadline (client `grpc-timeout` headers still apply).
194 pub fn grpc_timeout(&self) -> Option<Duration> {
195 match self.grpc_timeout_ms {
196 Some(0) => None,
197 Some(ms) => Some(Duration::from_millis(ms)),
198 None => Some(DEFAULT_GRPC_TIMEOUT),
199 }
200 }
201
202 /// Validate cross-field invariants. Call once at startup to fail fast on a
203 /// misconfiguration rather than surfacing it per-request.
204 pub fn validate(&self) -> anyhow::Result<()> {
205 self.ledger_history().validate()
206 }
207
208 pub fn display(&self) -> &DisplayConfig {
209 const DEFAULT_DISPLAY_CONFIG: DisplayConfig = DisplayConfig {
210 max_field_depth: None,
211 max_format_nodes: None,
212 max_object_loads: None,
213 max_move_value_depth: None,
214 max_output_size: None,
215 };
216
217 self.display.as_ref().unwrap_or(&DEFAULT_DISPLAY_CONFIG)
218 }
219}
220
221#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
222#[serde(rename_all = "kebab-case")]
223pub struct RpcTlsConfig {
224 /// File path to a PEM formatted TLS certificate chain
225 cert: String,
226 /// File path to a PEM formatted TLS private key
227 key: String,
228}
229
230impl RpcTlsConfig {
231 pub fn cert(&self) -> &str {
232 &self.cert
233 }
234
235 pub fn key(&self) -> &str {
236 &self.key
237 }
238}
239
240/// Configuration for RPC index initialization and bulk loading
241#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
242#[serde(rename_all = "kebab-case")]
243pub struct RpcIndexInitConfig {
244 /// Override for RocksDB's set_db_write_buffer_size during bulk indexing.
245 /// This is the total memory budget for all column families' memtables.
246 ///
247 /// Defaults to 90% of system RAM if not specified.
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub db_write_buffer_size: Option<usize>,
250
251 /// Override for each column family's write buffer size during bulk indexing.
252 ///
253 /// Defaults to 25% of system RAM divided by max_write_buffer_number if not specified.
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub cf_write_buffer_size: Option<usize>,
256
257 /// Override for the maximum number of write buffers per column family during bulk indexing.
258 /// This value is capped at 32 as an upper bound.
259 ///
260 /// Defaults to a dynamic value based on system RAM if not specified.
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub cf_max_write_buffer_number: Option<i32>,
263
264 /// Override for the number of background jobs during bulk indexing.
265 ///
266 /// Defaults to the number of CPU cores if not specified.
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub max_background_jobs: Option<i32>,
269
270 /// Override for the batch size limit during bulk indexing.
271 /// This controls how much data is accumulated in memory before flushing to disk.
272 ///
273 /// Defaults to half the write buffer size or 128MB, whichever is smaller.
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub batch_size_limit: Option<usize>,
276}
277
278#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
279#[serde(rename_all = "kebab-case")]
280pub struct DisplayConfig {
281 /// Maximum number of times the parser can recurse into nested structures. Depth does not
282 /// account for all nodes, only nodes that can be contained within themselves.
283 ///
284 /// Defaults to `32` if not specified.
285 #[serde(skip_serializing_if = "Option::is_none")]
286 max_field_depth: Option<usize>,
287
288 /// Maximum number of AST nodes that can be allocated during parsing. This counts all values
289 /// that are instances of AST types (but not, for example, `Vec<T>`).
290 ///
291 /// Defaults to `32768` if not specified.
292 #[serde(skip_serializing_if = "Option::is_none")]
293 max_format_nodes: Option<usize>,
294
295 /// Maximum number of objects that can be loaded during formatting.
296 ///
297 /// Defaults to `8` if not specified.
298 #[serde(skip_serializing_if = "Option::is_none")]
299 max_object_loads: Option<usize>,
300
301 /// Maximum depth to use when converting a rendered Display value to JSON.
302 ///
303 /// Defaults to `32` if not specified.
304 #[serde(skip_serializing_if = "Option::is_none")]
305 max_move_value_depth: Option<usize>,
306
307 /// Maxumum budget for rendering an object based on its Display template.
308 ///
309 /// This sets the numbers of bytes that we are willing to spend on rendering field names and
310 /// values when rendering an object based on its Display template.
311 ///
312 /// Defaults to `1MiB` if not specified.
313 #[serde(skip_serializing_if = "Option::is_none")]
314 max_output_size: Option<usize>,
315}
316
317impl DisplayConfig {
318 pub fn max_field_depth(&self) -> usize {
319 self.max_field_depth.unwrap_or(32)
320 }
321
322 pub fn max_format_nodes(&self) -> usize {
323 self.max_format_nodes.unwrap_or(32768)
324 }
325
326 pub fn max_object_loads(&self) -> usize {
327 self.max_object_loads.unwrap_or(8)
328 }
329
330 pub fn max_move_value_depth(&self) -> usize {
331 self.max_move_value_depth.unwrap_or(32)
332 }
333
334 pub fn max_output_size(&self) -> usize {
335 self.max_output_size.unwrap_or(1024 * 1024)
336 }
337}
338
339#[cfg(test)]
340mod connection_lifecycle_tests {
341 use super::*;
342
343 /// These defaults are availability-relevant: connection age plus grace
344 /// is the server-side backstop that reclaims streams wedged behind
345 /// HTTP/2 flow-control windows, and the gRPC deadline bounds requests
346 /// from clients that set none. Pin them so they cannot silently regress
347 /// to disabled.
348 #[test]
349 fn connection_lifecycle_defaults_are_enabled() {
350 let config = RpcConfig::default();
351 assert_eq!(
352 config.max_connection_age(),
353 Some(Duration::from_secs(4 * 60 * 60))
354 );
355 assert_eq!(
356 config.max_connection_age_grace(),
357 Duration::from_secs(10 * 60)
358 );
359 assert_eq!(config.grpc_timeout(), Some(Duration::from_secs(60)));
360 }
361
362 #[test]
363 fn zero_disables_age_and_timeout() {
364 let config = RpcConfig {
365 max_connection_age_secs: Some(0),
366 max_connection_age_grace_secs: Some(0),
367 grpc_timeout_ms: Some(0),
368 ..Default::default()
369 };
370 assert_eq!(config.max_connection_age(), None);
371 // A zero grace is a valid setting: close immediately at shutdown.
372 assert_eq!(config.max_connection_age_grace(), Duration::ZERO);
373 assert_eq!(config.grpc_timeout(), None);
374 }
375
376 #[test]
377 fn explicit_values_are_used() {
378 let config = RpcConfig {
379 max_connection_age_secs: Some(60),
380 max_connection_age_grace_secs: Some(5),
381 grpc_timeout_ms: Some(1_500),
382 ..Default::default()
383 };
384 assert_eq!(config.max_connection_age(), Some(Duration::from_secs(60)));
385 assert_eq!(config.max_connection_age_grace(), Duration::from_secs(5));
386 assert_eq!(config.grpc_timeout(), Some(Duration::from_millis(1_500)));
387 }
388}
389
390const DEFAULT_LEDGER_HISTORY_METHOD_TIMEOUT_MS: u64 = 5_000;
391const DEFAULT_BITMAP_BUCKET_SCAN_BUDGET: usize = 1_024;
392const DEFAULT_CHUNK_BUCKET_SCAN_BUDGET: usize = 256;
393const DEFAULT_MAX_BITMAP_FILTER_LITERALS: usize = 10;
394// A chunk never evaluates more buckets than the whole request is allowed, so the
395// per-chunk cap must not exceed the per-request budget. Enforced for the
396// defaults here; the accessors clamp configured values the same way.
397const _: () = assert!(DEFAULT_CHUNK_BUCKET_SCAN_BUDGET <= DEFAULT_BITMAP_BUCKET_SCAN_BUDGET);
398
399/// Built-in per-endpoint defaults. These differ per endpoint (e.g. checkpoints
400/// page smaller than transactions, and scan a narrower chunk).
401struct LedgerHistoryMethodDefaults {
402 default_limit_items: u32,
403 max_limit_items: u32,
404 chunk_max: usize,
405}
406
407const LIST_TRANSACTIONS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
408 default_limit_items: 50,
409 max_limit_items: 500,
410 chunk_max: 32,
411};
412const LIST_EVENTS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
413 default_limit_items: 50,
414 max_limit_items: 1_000,
415 chunk_max: 32,
416};
417const LIST_CHECKPOINTS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
418 default_limit_items: 10,
419 max_limit_items: 50,
420 chunk_max: 16,
421};
422
423/// Per-endpoint tunables for one ledger-history list API. Every field is optional
424/// and falls back to a built-in default; see [`ResolvedLedgerHistoryMethodConfig`].
425#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
426#[serde(rename_all = "kebab-case")]
427pub struct LedgerHistoryMethodConfig {
428 /// Per-request wall-clock timeout, in milliseconds. Defaults to `5000`.
429 #[serde(skip_serializing_if = "Option::is_none")]
430 pub timeout_ms: Option<u64>,
431
432 /// Page size used when a request omits `limit_items`.
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub default_limit_items: Option<u32>,
435
436 /// Upper bound a request's `limit_items` is clamped to.
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub max_limit_items: Option<u32>,
439
440 /// Maximum items materialized per internal scan chunk.
441 #[serde(skip_serializing_if = "Option::is_none")]
442 pub chunk_max: Option<usize>,
443}
444
445/// A [`LedgerHistoryMethodConfig`] with all defaults applied.
446#[derive(Clone, Copy, Debug)]
447pub struct ResolvedLedgerHistoryMethodConfig {
448 pub timeout: Duration,
449 pub default_limit_items: u32,
450 pub max_limit_items: u32,
451 pub chunk_max: usize,
452}
453
454impl LedgerHistoryMethodConfig {
455 fn resolve(
456 this: Option<&LedgerHistoryMethodConfig>,
457 defaults: LedgerHistoryMethodDefaults,
458 ) -> ResolvedLedgerHistoryMethodConfig {
459 ResolvedLedgerHistoryMethodConfig {
460 timeout: Duration::from_millis(
461 this.and_then(|c| c.timeout_ms)
462 .unwrap_or(DEFAULT_LEDGER_HISTORY_METHOD_TIMEOUT_MS),
463 ),
464 default_limit_items: this
465 .and_then(|c| c.default_limit_items)
466 .unwrap_or(defaults.default_limit_items),
467 max_limit_items: this
468 .and_then(|c| c.max_limit_items)
469 .unwrap_or(defaults.max_limit_items),
470 chunk_max: this.and_then(|c| c.chunk_max).unwrap_or(defaults.chunk_max),
471 }
472 }
473}
474
475/// Tunables for the ledger-history list APIs. Per-endpoint knobs live in
476/// the three [`LedgerHistoryMethodConfig`] fields; the remaining knobs are global across
477/// all three. Every field is optional and falls back to a built-in default.
478#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
479#[serde(rename_all = "kebab-case")]
480pub struct LedgerHistoryConfig {
481 /// Per-endpoint tunables for `list_transactions`.
482 #[serde(skip_serializing_if = "Option::is_none")]
483 pub list_transactions: Option<LedgerHistoryMethodConfig>,
484
485 /// Per-endpoint tunables for `list_events`.
486 #[serde(skip_serializing_if = "Option::is_none")]
487 pub list_events: Option<LedgerHistoryMethodConfig>,
488
489 /// Per-endpoint tunables for `list_checkpoints`.
490 #[serde(skip_serializing_if = "Option::is_none")]
491 pub list_checkpoints: Option<LedgerHistoryMethodConfig>,
492
493 /// Total evaluated-bucket budget for one filtered request, shared by all
494 /// three list APIs. Exhausting it ends the query with `SCAN_LIMIT` and a
495 /// resume cursor, bounding the worst-case scan cost of a sparse filter.
496 ///
497 /// Defaults to `1024` if not specified.
498 #[serde(skip_serializing_if = "Option::is_none")]
499 pub bitmap_bucket_scan_budget: Option<usize>,
500
501 /// Per-chunk evaluated-bucket cap. A chunk that hits this while the request
502 /// budget remains emits a progress watermark and resumes in the next chunk,
503 /// so a long sparse scan reports incremental progress. Clamped to
504 /// `bitmap_bucket_scan_budget`.
505 ///
506 /// Defaults to `256` if not specified.
507 #[serde(skip_serializing_if = "Option::is_none")]
508 pub chunk_bucket_scan_budget: Option<usize>,
509
510 /// Maximum total filter literals (bitmap dimensions) accepted in one filtered
511 /// request, across all DNF terms. Each literal becomes one bitmap leaf, so
512 /// this bounds a single filter's scan fanout. Must not exceed
513 /// `bitmap_bucket_scan_budget` (see [`LedgerHistoryConfig::validate`]).
514 ///
515 /// Defaults to `10` if not specified.
516 #[serde(skip_serializing_if = "Option::is_none")]
517 pub max_bitmap_filter_literals: Option<usize>,
518}
519
520impl LedgerHistoryConfig {
521 pub fn list_transactions(&self) -> ResolvedLedgerHistoryMethodConfig {
522 LedgerHistoryMethodConfig::resolve(
523 self.list_transactions.as_ref(),
524 LIST_TRANSACTIONS_DEFAULTS,
525 )
526 }
527
528 pub fn list_events(&self) -> ResolvedLedgerHistoryMethodConfig {
529 LedgerHistoryMethodConfig::resolve(self.list_events.as_ref(), LIST_EVENTS_DEFAULTS)
530 }
531
532 pub fn list_checkpoints(&self) -> ResolvedLedgerHistoryMethodConfig {
533 LedgerHistoryMethodConfig::resolve(
534 self.list_checkpoints.as_ref(),
535 LIST_CHECKPOINTS_DEFAULTS,
536 )
537 }
538
539 pub fn bitmap_bucket_scan_budget(&self) -> usize {
540 self.bitmap_bucket_scan_budget
541 .unwrap_or(DEFAULT_BITMAP_BUCKET_SCAN_BUDGET)
542 }
543
544 pub fn chunk_bucket_scan_budget(&self) -> usize {
545 self.chunk_bucket_scan_budget
546 .unwrap_or(DEFAULT_CHUNK_BUCKET_SCAN_BUDGET)
547 .min(self.bitmap_bucket_scan_budget())
548 }
549
550 pub fn max_bitmap_filter_literals(&self) -> usize {
551 self.max_bitmap_filter_literals
552 .unwrap_or(DEFAULT_MAX_BITMAP_FILTER_LITERALS)
553 }
554
555 /// Reject configurations that cannot make forward progress. Each filter
556 /// literal becomes one bitmap leaf that must fetch at least one bucket to
557 /// emit its first watermark; if the per-request budget is below the literal
558 /// cap a `SCAN_LIMIT` can fire before any merged watermark reaches the wire,
559 /// leaving the client a cursorless `QueryEnd` it cannot resume from. Mirrors
560 /// the archival/BigTable side's `LedgerHistoryConfig::validate`.
561 pub fn validate(&self) -> anyhow::Result<()> {
562 anyhow::ensure!(
563 self.bitmap_bucket_scan_budget() >= self.max_bitmap_filter_literals(),
564 "ledger_history.bitmap_bucket_scan_budget ({}) must be >= \
565 max_bitmap_filter_literals ({}) so every filter leaf gets at least one \
566 bucket before SCAN_LIMIT",
567 self.bitmap_bucket_scan_budget(),
568 self.max_bitmap_filter_literals(),
569 );
570 Ok(())
571 }
572}