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    /// Use the experimental `sui-rpc-store` backend instead of the built-in
23    /// `rpc-index`.
24    ///
25    /// When set, the node builds the embedded `sui-rpc-store` indexer (the
26    /// derived-index and ledger-history column families, indexed independently
27    /// of the authority store) and serves the index read paths through it
28    /// rather than building the legacy `rpc-index`. The two are mutually
29    /// exclusive; raw chain data is still served from the perpetual store
30    /// either way.
31    ///
32    /// Experimental.
33    ///
34    /// Defaults to `false`.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub use_experimental_rpc_store: Option<bool>,
37
38    /// Configure the address to listen on for https
39    ///
40    /// Defaults to `0.0.0.0:9443` if not specified.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub https_address: Option<SocketAddr>,
43
44    /// TLS configuration to use for https.
45    ///
46    /// If not provided then the node will not create an https service.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub tls: Option<RpcTlsConfig>,
49
50    /// Maxumum budget for rendering a Move value into JSON.
51    ///
52    /// This sets the numbers of bytes that we are willing to spend on rendering field names and
53    /// values when rendering a Move value into a JSON value.
54    ///
55    /// Defaults to `1MiB` if not specified.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub max_json_move_value_size: Option<usize>,
58
59    /// Aggregate budget for Move-value JSON rendering across a single response.
60    ///
61    /// Endpoints that render many Move values in one response (e.g. `GetCheckpoint`
62    /// with a `read_mask` that selects every event's `json` field) share this
63    /// budget across all per-item renders, so the response cannot multiply one
64    /// request into hundreds of MiB of materialized `prost_types::Value`.
65    ///
66    /// Defaults to `16 MiB` if not specified.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub max_json_move_value_response_size: Option<usize>,
69
70    /// Configuration for RPC index initialization and bulk loading
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub index_initialization: Option<RpcIndexInitConfig>,
73
74    /// Enable historical checkpoint/transaction indexes for RPC queries.
75    ///
76    /// This flag is persisted in the `rpc-index` DB's own `settings` column
77    /// family. Enabling it triggers a full rebuild to backfill the historical
78    /// rows; disabling it drops the now-unused history column families in place
79    /// (no rebuild). While it stays put, forward indexing and pruning maintain
80    /// these indexes normally.
81    ///
82    /// Defaults to `false`.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub ledger_history_indexing: Option<bool>,
85
86    /// Tunables for the v2alpha ledger-history list APIs (`list_transactions`,
87    /// `list_events`, `list_checkpoints`). These scan the historical inverted
88    /// indexes, unlike the live object-set listings (`list_owned_objects`,
89    /// `list_dynamic_fields`), so they carry their own time and scan-cost bounds.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub ledger_history: Option<LedgerHistoryConfig>,
92
93    /// Configuration for rendering Objects based on the Display standard
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub display: Option<DisplayConfig>,
96}
97
98impl RpcConfig {
99    pub fn enable_indexing(&self) -> bool {
100        self.enable_indexing.unwrap_or(false)
101    }
102
103    pub fn use_experimental_rpc_store(&self) -> bool {
104        self.use_experimental_rpc_store.unwrap_or(false)
105    }
106
107    pub fn https_address(&self) -> SocketAddr {
108        self.https_address
109            .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 9443)))
110    }
111
112    pub fn tls_config(&self) -> Option<&RpcTlsConfig> {
113        self.tls.as_ref()
114    }
115
116    pub fn max_json_move_value_size(&self) -> usize {
117        self.max_json_move_value_size.unwrap_or(1024 * 1024)
118    }
119
120    pub fn max_json_move_value_response_size(&self) -> usize {
121        self.max_json_move_value_response_size
122            .unwrap_or(16 * 1024 * 1024)
123    }
124
125    pub fn index_initialization_config(&self) -> Option<&RpcIndexInitConfig> {
126        self.index_initialization.as_ref()
127    }
128
129    pub fn ledger_history_indexing(&self) -> bool {
130        self.ledger_history_indexing.unwrap_or(false)
131    }
132
133    pub fn ledger_history(&self) -> &LedgerHistoryConfig {
134        const DEFAULT_LEDGER_HISTORY_CONFIG: LedgerHistoryConfig = LedgerHistoryConfig {
135            list_transactions: None,
136            list_events: None,
137            list_checkpoints: None,
138            bitmap_bucket_scan_budget: None,
139            chunk_bucket_scan_budget: None,
140            max_bitmap_filter_literals: None,
141        };
142
143        self.ledger_history
144            .as_ref()
145            .unwrap_or(&DEFAULT_LEDGER_HISTORY_CONFIG)
146    }
147
148    /// Validate cross-field invariants. Call once at startup to fail fast on a
149    /// misconfiguration rather than surfacing it per-request.
150    pub fn validate(&self) -> anyhow::Result<()> {
151        self.ledger_history().validate()
152    }
153
154    pub fn display(&self) -> &DisplayConfig {
155        const DEFAULT_DISPLAY_CONFIG: DisplayConfig = DisplayConfig {
156            max_field_depth: None,
157            max_format_nodes: None,
158            max_object_loads: None,
159            max_move_value_depth: None,
160            max_output_size: None,
161        };
162
163        self.display.as_ref().unwrap_or(&DEFAULT_DISPLAY_CONFIG)
164    }
165}
166
167#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
168#[serde(rename_all = "kebab-case")]
169pub struct RpcTlsConfig {
170    /// File path to a PEM formatted TLS certificate chain
171    cert: String,
172    /// File path to a PEM formatted TLS private key
173    key: String,
174}
175
176impl RpcTlsConfig {
177    pub fn cert(&self) -> &str {
178        &self.cert
179    }
180
181    pub fn key(&self) -> &str {
182        &self.key
183    }
184}
185
186/// Configuration for RPC index initialization and bulk loading
187#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
188#[serde(rename_all = "kebab-case")]
189pub struct RpcIndexInitConfig {
190    /// Override for RocksDB's set_db_write_buffer_size during bulk indexing.
191    /// This is the total memory budget for all column families' memtables.
192    ///
193    /// Defaults to 90% of system RAM if not specified.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub db_write_buffer_size: Option<usize>,
196
197    /// Override for each column family's write buffer size during bulk indexing.
198    ///
199    /// Defaults to 25% of system RAM divided by max_write_buffer_number if not specified.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub cf_write_buffer_size: Option<usize>,
202
203    /// Override for the maximum number of write buffers per column family during bulk indexing.
204    /// This value is capped at 32 as an upper bound.
205    ///
206    /// Defaults to a dynamic value based on system RAM if not specified.
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub cf_max_write_buffer_number: Option<i32>,
209
210    /// Override for the number of background jobs during bulk indexing.
211    ///
212    /// Defaults to the number of CPU cores if not specified.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub max_background_jobs: Option<i32>,
215
216    /// Override for the batch size limit during bulk indexing.
217    /// This controls how much data is accumulated in memory before flushing to disk.
218    ///
219    /// Defaults to half the write buffer size or 128MB, whichever is smaller.
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub batch_size_limit: Option<usize>,
222}
223
224#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
225#[serde(rename_all = "kebab-case")]
226pub struct DisplayConfig {
227    /// Maximum number of times the parser can recurse into nested structures. Depth does not
228    /// account for all nodes, only nodes that can be contained within themselves.
229    ///
230    /// Defaults to `32` if not specified.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    max_field_depth: Option<usize>,
233
234    /// Maximum number of AST nodes that can be allocated during parsing. This counts all values
235    /// that are instances of AST types (but not, for example, `Vec<T>`).
236    ///
237    /// Defaults to `32768` if not specified.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    max_format_nodes: Option<usize>,
240
241    /// Maximum number of objects that can be loaded during formatting.
242    ///
243    /// Defaults to `8` if not specified.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    max_object_loads: Option<usize>,
246
247    /// Maximum depth to use when converting a rendered Display value to JSON.
248    ///
249    /// Defaults to `32` if not specified.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    max_move_value_depth: Option<usize>,
252
253    /// Maxumum budget for rendering an object based on its Display template.
254    ///
255    /// This sets the numbers of bytes that we are willing to spend on rendering field names and
256    /// values when rendering an object based on its Display template.
257    ///
258    /// Defaults to `1MiB` if not specified.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    max_output_size: Option<usize>,
261}
262
263impl DisplayConfig {
264    pub fn max_field_depth(&self) -> usize {
265        self.max_field_depth.unwrap_or(32)
266    }
267
268    pub fn max_format_nodes(&self) -> usize {
269        self.max_format_nodes.unwrap_or(32768)
270    }
271
272    pub fn max_object_loads(&self) -> usize {
273        self.max_object_loads.unwrap_or(8)
274    }
275
276    pub fn max_move_value_depth(&self) -> usize {
277        self.max_move_value_depth.unwrap_or(32)
278    }
279
280    pub fn max_output_size(&self) -> usize {
281        self.max_output_size.unwrap_or(1024 * 1024)
282    }
283}
284
285const DEFAULT_LEDGER_HISTORY_METHOD_TIMEOUT_MS: u64 = 5_000;
286const DEFAULT_BITMAP_BUCKET_SCAN_BUDGET: usize = 1_024;
287const DEFAULT_CHUNK_BUCKET_SCAN_BUDGET: usize = 256;
288const DEFAULT_MAX_BITMAP_FILTER_LITERALS: usize = 10;
289// A chunk never evaluates more buckets than the whole request is allowed, so the
290// per-chunk cap must not exceed the per-request budget. Enforced for the
291// defaults here; the accessors clamp configured values the same way.
292const _: () = assert!(DEFAULT_CHUNK_BUCKET_SCAN_BUDGET <= DEFAULT_BITMAP_BUCKET_SCAN_BUDGET);
293
294/// Built-in per-endpoint defaults. These differ per endpoint (e.g. checkpoints
295/// page smaller than transactions, and scan a narrower chunk).
296struct LedgerHistoryMethodDefaults {
297    default_limit_items: u32,
298    max_limit_items: u32,
299    chunk_max: usize,
300}
301
302const LIST_TRANSACTIONS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
303    default_limit_items: 50,
304    max_limit_items: 500,
305    chunk_max: 32,
306};
307const LIST_EVENTS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
308    default_limit_items: 50,
309    max_limit_items: 1_000,
310    chunk_max: 32,
311};
312const LIST_CHECKPOINTS_DEFAULTS: LedgerHistoryMethodDefaults = LedgerHistoryMethodDefaults {
313    default_limit_items: 10,
314    max_limit_items: 100,
315    chunk_max: 16,
316};
317
318/// Per-endpoint tunables for one ledger-history list API. Every field is optional
319/// and falls back to a built-in default; see [`ResolvedLedgerHistoryMethodConfig`].
320#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
321#[serde(rename_all = "kebab-case")]
322pub struct LedgerHistoryMethodConfig {
323    /// Per-request wall-clock timeout, in milliseconds. Defaults to `5000`.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub timeout_ms: Option<u64>,
326
327    /// Page size used when a request omits `limit_items`.
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub default_limit_items: Option<u32>,
330
331    /// Upper bound a request's `limit_items` is clamped to.
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub max_limit_items: Option<u32>,
334
335    /// Maximum items materialized per internal scan chunk.
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub chunk_max: Option<usize>,
338}
339
340/// A [`LedgerHistoryMethodConfig`] with all defaults applied.
341#[derive(Clone, Copy, Debug)]
342pub struct ResolvedLedgerHistoryMethodConfig {
343    pub timeout: Duration,
344    pub default_limit_items: u32,
345    pub max_limit_items: u32,
346    pub chunk_max: usize,
347}
348
349impl LedgerHistoryMethodConfig {
350    fn resolve(
351        this: Option<&LedgerHistoryMethodConfig>,
352        defaults: LedgerHistoryMethodDefaults,
353    ) -> ResolvedLedgerHistoryMethodConfig {
354        ResolvedLedgerHistoryMethodConfig {
355            timeout: Duration::from_millis(
356                this.and_then(|c| c.timeout_ms)
357                    .unwrap_or(DEFAULT_LEDGER_HISTORY_METHOD_TIMEOUT_MS),
358            ),
359            default_limit_items: this
360                .and_then(|c| c.default_limit_items)
361                .unwrap_or(defaults.default_limit_items),
362            max_limit_items: this
363                .and_then(|c| c.max_limit_items)
364                .unwrap_or(defaults.max_limit_items),
365            chunk_max: this.and_then(|c| c.chunk_max).unwrap_or(defaults.chunk_max),
366        }
367    }
368}
369
370/// Tunables for the v2alpha ledger-history list APIs. Per-endpoint knobs live in
371/// the three [`LedgerHistoryMethodConfig`] fields; the remaining knobs are global across
372/// all three. Every field is optional and falls back to a built-in default.
373#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
374#[serde(rename_all = "kebab-case")]
375pub struct LedgerHistoryConfig {
376    /// Per-endpoint tunables for `list_transactions`.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub list_transactions: Option<LedgerHistoryMethodConfig>,
379
380    /// Per-endpoint tunables for `list_events`.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub list_events: Option<LedgerHistoryMethodConfig>,
383
384    /// Per-endpoint tunables for `list_checkpoints`.
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub list_checkpoints: Option<LedgerHistoryMethodConfig>,
387
388    /// Total evaluated-bucket budget for one filtered request, shared by all
389    /// three list APIs. Exhausting it ends the query with `SCAN_LIMIT` and a
390    /// resume cursor, bounding the worst-case scan cost of a sparse filter.
391    ///
392    /// Defaults to `1024` if not specified.
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub bitmap_bucket_scan_budget: Option<usize>,
395
396    /// Per-chunk evaluated-bucket cap. A chunk that hits this while the request
397    /// budget remains emits a progress watermark and resumes in the next chunk,
398    /// so a long sparse scan reports incremental progress. Clamped to
399    /// `bitmap_bucket_scan_budget`.
400    ///
401    /// Defaults to `256` if not specified.
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub chunk_bucket_scan_budget: Option<usize>,
404
405    /// Maximum total filter literals (bitmap dimensions) accepted in one filtered
406    /// request, across all DNF terms. Each literal becomes one bitmap leaf, so
407    /// this bounds a single filter's scan fanout. Must not exceed
408    /// `bitmap_bucket_scan_budget` (see [`LedgerHistoryConfig::validate`]).
409    ///
410    /// Defaults to `10` if not specified.
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub max_bitmap_filter_literals: Option<usize>,
413}
414
415impl LedgerHistoryConfig {
416    pub fn list_transactions(&self) -> ResolvedLedgerHistoryMethodConfig {
417        LedgerHistoryMethodConfig::resolve(
418            self.list_transactions.as_ref(),
419            LIST_TRANSACTIONS_DEFAULTS,
420        )
421    }
422
423    pub fn list_events(&self) -> ResolvedLedgerHistoryMethodConfig {
424        LedgerHistoryMethodConfig::resolve(self.list_events.as_ref(), LIST_EVENTS_DEFAULTS)
425    }
426
427    pub fn list_checkpoints(&self) -> ResolvedLedgerHistoryMethodConfig {
428        LedgerHistoryMethodConfig::resolve(
429            self.list_checkpoints.as_ref(),
430            LIST_CHECKPOINTS_DEFAULTS,
431        )
432    }
433
434    pub fn bitmap_bucket_scan_budget(&self) -> usize {
435        self.bitmap_bucket_scan_budget
436            .unwrap_or(DEFAULT_BITMAP_BUCKET_SCAN_BUDGET)
437    }
438
439    pub fn chunk_bucket_scan_budget(&self) -> usize {
440        self.chunk_bucket_scan_budget
441            .unwrap_or(DEFAULT_CHUNK_BUCKET_SCAN_BUDGET)
442            .min(self.bitmap_bucket_scan_budget())
443    }
444
445    pub fn max_bitmap_filter_literals(&self) -> usize {
446        self.max_bitmap_filter_literals
447            .unwrap_or(DEFAULT_MAX_BITMAP_FILTER_LITERALS)
448    }
449
450    /// Reject configurations that cannot make forward progress. Each filter
451    /// literal becomes one bitmap leaf that must fetch at least one bucket to
452    /// emit its first watermark; if the per-request budget is below the literal
453    /// cap a `SCAN_LIMIT` can fire before any merged watermark reaches the wire,
454    /// leaving the client a cursorless `QueryEnd` it cannot resume from. Mirrors
455    /// the archival/BigTable side's `LedgerHistoryConfig::validate`.
456    pub fn validate(&self) -> anyhow::Result<()> {
457        anyhow::ensure!(
458            self.bitmap_bucket_scan_budget() >= self.max_bitmap_filter_literals(),
459            "ledger_history.bitmap_bucket_scan_budget ({}) must be >= \
460             max_bitmap_filter_literals ({}) so every filter leaf gets at least one \
461             bucket before SCAN_LIMIT",
462            self.bitmap_bucket_scan_budget(),
463            self.max_bitmap_filter_literals(),
464        );
465        Ok(())
466    }
467}