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