iota_graphql_rpc/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{collections::BTreeSet, fmt::Display, time::Duration};
6
7use async_graphql::*;
8use fastcrypto_zkp::bn254::zk_login_api::ZkLoginEnv;
9use iota_graphql_config::GraphQLConfig;
10use iota_names::config::IotaNamesConfig;
11use serde::{Deserialize, Serialize};
12
13use crate::functional_group::FunctionalGroup;
14
15pub(crate) const RPC_TIMEOUT_ERR_SLEEP_RETRY_PERIOD: Duration = Duration::from_millis(10_000);
16pub(crate) const MAX_CONCURRENT_REQUESTS: usize = 1_000;
17
18/// The combination of all configurations for the GraphQL service.
19#[GraphQLConfig]
20#[derive(Default)]
21pub struct ServerConfig {
22    pub service: ServiceConfig,
23    pub connection: ConnectionConfig,
24    pub internal_features: InternalFeatureConfig,
25    pub tx_exec_full_node: TxExecFullNodeConfig,
26    pub ide: Ide,
27}
28
29/// Configuration for connections for the RPC, passed in as command-line
30/// arguments. This configures specific connections between this service and
31/// other services, and might differ from instance to instance of the GraphQL
32/// service.
33#[GraphQLConfig]
34#[derive(Clone, Eq, PartialEq)]
35pub struct ConnectionConfig {
36    /// Port to bind the server to
37    pub(crate) port: u16,
38    /// Host to bind the server to
39    pub(crate) host: String,
40    pub(crate) db_url: String,
41    pub(crate) db_pool_size: u32,
42    pub(crate) prom_url: String,
43    pub(crate) prom_port: u16,
44}
45
46/// Configuration on features supported by the GraphQL service, passed in a
47/// TOML-based file. These configurations are shared across fleets of the
48/// service, i.e. all testnet services will have the same `ServiceConfig`.
49#[GraphQLConfig]
50#[derive(Default)]
51pub struct ServiceConfig {
52    pub(crate) versions: Versions,
53    pub(crate) limits: Limits,
54    pub(crate) disabled_features: BTreeSet<FunctionalGroup>,
55    pub(crate) experiments: Experiments,
56    pub(crate) iota_names: IotaNamesConfig,
57    pub(crate) background_tasks: BackgroundTasksConfig,
58    pub(crate) zklogin: ZkLoginConfig,
59}
60
61#[GraphQLConfig]
62pub struct Versions {
63    versions: Vec<String>,
64}
65
66#[GraphQLConfig]
67pub struct Limits {
68    /// Maximum depth of nodes in the requests.
69    pub max_query_depth: u32,
70    /// Maximum number of nodes in the requests.
71    pub max_query_nodes: u32,
72    /// Maximum number of output nodes allowed in the response.
73    pub max_output_nodes: u32,
74    /// Maximum size (in bytes) of a GraphQL request.
75    pub max_query_payload_size: u32,
76    /// Queries whose EXPLAIN cost are more than this will be logged. Given in
77    /// the units used by the database (where 1.0 is roughly the cost of a
78    /// sequential page access).
79    pub max_db_query_cost: u32,
80    /// Paginated queries will return this many elements if a page size is not
81    /// provided.
82    pub default_page_size: u32,
83    /// Paginated queries can return at most this many elements.
84    pub max_page_size: u32,
85    /// Time (in milliseconds) to wait for a transaction to be executed and the
86    /// results returned from GraphQL. If the transaction takes longer than
87    /// this time to execute, the request will return a timeout error, but
88    /// the transaction may continue executing.
89    pub mutation_timeout_ms: u32,
90    /// Time (in milliseconds) to wait for a read request from the GraphQL
91    /// service. Requests that take longer than this time to return a result
92    /// will return a timeout error.
93    pub request_timeout_ms: u32,
94    /// Maximum amount of nesting among type arguments (type arguments nest when
95    /// a type argument is itself generic and has arguments).
96    pub max_type_argument_depth: u32,
97    /// Maximum number of type parameters a type can have.
98    pub max_type_argument_width: u32,
99    /// Maximum size of a fully qualified type.
100    pub max_type_nodes: u32,
101    /// Maximum deph of a move value.
102    pub max_move_value_depth: u32,
103    /// Maximum number of transaction ids that can be passed to a
104    /// `TransactionBlockFilter`.
105    pub max_transaction_ids: u32,
106    /// Maximum number of candidates to scan when gathering a page of results.
107    pub max_scan_limit: u32,
108}
109
110#[GraphQLConfig]
111#[derive(Copy)]
112pub struct BackgroundTasksConfig {
113    /// How often the watermark task checks the indexer database to update the
114    /// checkpoint and epoch watermarks.
115    pub watermark_update_ms: u64,
116}
117
118/// The Version of the service. `year.month` represents the major release.
119/// New `patch` versions represent backwards compatible fixes for their major
120/// release. The `full` version is `year.month.patch-sha`.
121#[derive(Copy, Clone, Debug)]
122pub struct Version {
123    /// The year of this release.
124    pub year: &'static str,
125    /// The month of this release.
126    pub month: &'static str,
127    /// The patch is a positive number incremented for every compatible release
128    /// on top of the major.month release.
129    pub patch: &'static str,
130    /// The commit sha for this release.
131    pub sha: &'static str,
132    /// The full version string.
133    /// Note that this extra field is used only for the uptime_metric function
134    /// which requires a &'static str.
135    pub full: &'static str,
136}
137
138impl Version {
139    /// Use for testing when you need the Version obj and a year.month &str
140    pub fn for_testing() -> Self {
141        Self {
142            year: env!("CARGO_PKG_VERSION_MAJOR"),
143            month: env!("CARGO_PKG_VERSION_MINOR"),
144            patch: env!("CARGO_PKG_VERSION_PATCH"),
145            sha: "testing-no-sha",
146            // note that this full field is needed for metrics but not for testing
147            full: const_str::concat!(
148                env!("CARGO_PKG_VERSION_MAJOR"),
149                ".",
150                env!("CARGO_PKG_VERSION_MINOR"),
151                ".",
152                env!("CARGO_PKG_VERSION_PATCH"),
153                "-testing-no-sha"
154            ),
155        }
156    }
157}
158
159#[GraphQLConfig]
160pub struct Ide {
161    pub(crate) ide_title: String,
162}
163
164#[GraphQLConfig]
165#[derive(Default)]
166pub struct Experiments {
167    // Add experimental flags here, to provide access to them through-out the GraphQL
168    // implementation.
169    #[cfg(test)]
170    test_flag: bool,
171}
172
173#[GraphQLConfig]
174pub struct InternalFeatureConfig {
175    pub(crate) query_limits_checker: bool,
176    pub(crate) directive_checker: bool,
177    pub(crate) feature_gate: bool,
178    pub(crate) logger: bool,
179    pub(crate) query_timeout: bool,
180    pub(crate) metrics: bool,
181    pub(crate) tracing: bool,
182    pub(crate) apollo_tracing: bool,
183    pub(crate) open_telemetry: bool,
184}
185
186#[GraphQLConfig]
187#[derive(Default)]
188pub struct TxExecFullNodeConfig {
189    pub(crate) node_rpc_url: Option<String>,
190}
191
192#[GraphQLConfig]
193#[derive(Default)]
194pub struct ZkLoginConfig {
195    pub env: ZkLoginEnv,
196}
197
198/// The enabled features and service limits configured by the server.
199#[Object]
200impl ServiceConfig {
201    /// Check whether `feature` is enabled on this GraphQL service.
202    async fn is_enabled(&self, feature: FunctionalGroup) -> bool {
203        !self.disabled_features.contains(&feature)
204    }
205
206    /// List the available versions for this GraphQL service.
207    async fn available_versions(&self) -> Vec<String> {
208        self.versions.versions.clone()
209    }
210
211    /// List of all features that are enabled on this GraphQL service.
212    async fn enabled_features(&self) -> Vec<FunctionalGroup> {
213        FunctionalGroup::all()
214            .iter()
215            .filter(|g| !self.disabled_features.contains(g))
216            .copied()
217            .collect()
218    }
219
220    /// The maximum depth a GraphQL query can be to be accepted by this service.
221    pub async fn max_query_depth(&self) -> u32 {
222        self.limits.max_query_depth
223    }
224
225    /// The maximum number of nodes (field names) the service will accept in a
226    /// single query.
227    pub async fn max_query_nodes(&self) -> u32 {
228        self.limits.max_query_nodes
229    }
230
231    /// The maximum number of output nodes in a GraphQL response.
232    ///
233    /// Non-connection nodes have a count of 1, while connection nodes are
234    /// counted as the specified 'first' or 'last' number of items, or the
235    /// default_page_size as set by the server if those arguments are not
236    /// set.
237    ///
238    /// Counts accumulate multiplicatively down the query tree. For example, if
239    /// a query starts with a connection of first: 10 and has a field to a
240    /// connection with last: 20, the count at the second level would be 200
241    /// nodes. This is then summed to the count of 10 nodes at the first
242    /// level, for a total of 210 nodes.
243    pub async fn max_output_nodes(&self) -> u32 {
244        self.limits.max_output_nodes
245    }
246
247    /// Maximum estimated cost of a database query used to serve a GraphQL
248    /// request.  This is measured in the same units that the database uses
249    /// in EXPLAIN queries.
250    async fn max_db_query_cost(&self) -> u32 {
251        self.limits.max_db_query_cost
252    }
253
254    /// Default number of elements allowed on a single page of a connection.
255    async fn default_page_size(&self) -> u32 {
256        self.limits.default_page_size
257    }
258
259    /// Maximum number of elements allowed on a single page of a connection.
260    async fn max_page_size(&self) -> u32 {
261        self.limits.max_page_size
262    }
263
264    /// Maximum time in milliseconds spent waiting for a response from fullnode
265    /// after issuing a transaction to execute. Note that the transaction
266    /// may still succeed even in the case of a timeout. Transactions are
267    /// idempotent, so a transaction that times out should be resubmitted
268    /// until the network returns a definite response (success or failure, not
269    /// timeout).
270    async fn mutation_timeout_ms(&self) -> u32 {
271        self.limits.mutation_timeout_ms
272    }
273
274    /// Maximum time in milliseconds that will be spent to serve one query
275    /// request.
276    async fn request_timeout_ms(&self) -> u32 {
277        self.limits.request_timeout_ms
278    }
279
280    /// Maximum length of a query payload string.
281    async fn max_query_payload_size(&self) -> u32 {
282        self.limits.max_query_payload_size
283    }
284
285    /// Maximum nesting allowed in type arguments in Move Types resolved by this
286    /// service.
287    async fn max_type_argument_depth(&self) -> u32 {
288        self.limits.max_type_argument_depth
289    }
290
291    /// Maximum number of type arguments passed into a generic instantiation of
292    /// a Move Type resolved by this service.
293    async fn max_type_argument_width(&self) -> u32 {
294        self.limits.max_type_argument_width
295    }
296
297    /// Maximum number of structs that need to be processed when calculating the
298    /// layout of a single Move Type.
299    async fn max_type_nodes(&self) -> u32 {
300        self.limits.max_type_nodes
301    }
302
303    /// Maximum nesting allowed in struct fields when calculating the layout of
304    /// a single Move Type.
305    async fn max_move_value_depth(&self) -> u32 {
306        self.limits.max_move_value_depth
307    }
308
309    /// Maximum number of transaction ids that can be passed to a
310    /// `TransactionBlockFilter`.
311    async fn max_transaction_ids(&self) -> u32 {
312        self.limits.max_transaction_ids
313    }
314
315    /// Maximum number of candidates to scan when gathering a page of results.
316    async fn max_scan_limit(&self) -> u32 {
317        self.limits.max_scan_limit
318    }
319}
320
321impl TxExecFullNodeConfig {
322    pub fn new(node_rpc_url: Option<String>) -> Self {
323        Self { node_rpc_url }
324    }
325}
326
327impl ConnectionConfig {
328    pub fn new(
329        port: Option<u16>,
330        host: Option<String>,
331        db_url: Option<String>,
332        db_pool_size: Option<u32>,
333        prom_url: Option<String>,
334        prom_port: Option<u16>,
335    ) -> Self {
336        let default = Self::default();
337        Self {
338            port: port.unwrap_or(default.port),
339            host: host.unwrap_or(default.host),
340            db_url: db_url.unwrap_or(default.db_url),
341            db_pool_size: db_pool_size.unwrap_or(default.db_pool_size),
342            prom_url: prom_url.unwrap_or(default.prom_url),
343            prom_port: prom_port.unwrap_or(default.prom_port),
344        }
345    }
346
347    pub fn ci_integration_test_cfg() -> Self {
348        Self {
349            db_url: "postgres://postgres:postgrespw@localhost:5432/iota_graphql_rpc_e2e_tests"
350                .to_string(),
351            ..Default::default()
352        }
353    }
354
355    pub fn ci_integration_test_cfg_with_db_name(
356        db_name: String,
357        port: u16,
358        prom_port: u16,
359    ) -> Self {
360        Self {
361            db_url: format!("postgres://postgres:postgrespw@localhost:5432/{}", db_name),
362            port,
363            prom_port,
364            ..Default::default()
365        }
366    }
367
368    pub fn db_name(&self) -> String {
369        self.db_url.split('/').next_back().unwrap().to_string()
370    }
371
372    pub fn db_url(&self) -> String {
373        self.db_url.clone()
374    }
375
376    pub fn db_pool_size(&self) -> u32 {
377        self.db_pool_size
378    }
379
380    pub fn server_address(&self) -> String {
381        format!("{}:{}", self.host, self.port)
382    }
383}
384
385impl ServiceConfig {
386    pub fn read(contents: &str) -> Result<Self, toml::de::Error> {
387        toml::de::from_str::<Self>(contents)
388    }
389
390    pub fn test_defaults() -> Self {
391        Self {
392            background_tasks: BackgroundTasksConfig::test_defaults(),
393            zklogin: ZkLoginConfig {
394                env: ZkLoginEnv::Test,
395            },
396            ..Default::default()
397        }
398    }
399}
400
401impl Limits {
402    /// Extract limits for the package resolver.
403    pub fn package_resolver_limits(&self) -> iota_package_resolver::Limits {
404        iota_package_resolver::Limits {
405            max_type_argument_depth: self.max_type_argument_depth as usize,
406            max_type_argument_width: self.max_type_argument_width as usize,
407            max_type_nodes: self.max_type_nodes as usize,
408            max_move_value_depth: self.max_move_value_depth as usize,
409        }
410    }
411}
412
413impl Ide {
414    pub fn new(ide_title: Option<String>) -> Self {
415        ide_title
416            .map(|ide_title| Ide { ide_title })
417            .unwrap_or_default()
418    }
419}
420
421impl BackgroundTasksConfig {
422    pub fn test_defaults() -> Self {
423        Self {
424            watermark_update_ms: 100, // Set to 100ms for testing
425        }
426    }
427}
428
429impl Default for Versions {
430    fn default() -> Self {
431        Self {
432            versions: vec![format!(
433                "{}.{}",
434                env!("CARGO_PKG_VERSION_MAJOR"),
435                env!("CARGO_PKG_VERSION_MINOR")
436            )],
437        }
438    }
439}
440
441impl Default for Ide {
442    fn default() -> Self {
443        Self {
444            ide_title: "IOTA GraphQL IDE".to_string(),
445        }
446    }
447}
448
449impl Default for ConnectionConfig {
450    fn default() -> Self {
451        Self {
452            port: 8000,
453            host: "127.0.0.1".to_string(),
454            db_url: "postgres://postgres:postgrespw@localhost:5432/iota_indexer".to_string(),
455            db_pool_size: 10,
456            prom_url: "0.0.0.0".to_string(),
457            prom_port: 9184,
458        }
459    }
460}
461
462impl Default for Limits {
463    fn default() -> Self {
464        // Picked so that TS SDK shim layer queries all pass limit.
465        // TODO: calculate proper cost limits
466        Self {
467            max_query_depth: 20,
468            max_query_nodes: 300,
469            max_output_nodes: 100_000,
470            max_query_payload_size: 5_000,
471            max_db_query_cost: 20_000,
472            default_page_size: 20,
473            max_page_size: 50,
474            // This default was picked as the sum of pre- and post- quorum timeouts from
475            // [`iota_core::authority_aggregator::TimeoutConfig`], with a 10% buffer.
476            //
477            // <https://github.com/iotaledger/iota/blob/eaf05fe5d293c06e3a2dfc22c87ba2aef419d8ea/crates/iota-core/src/authority_aggregator.rs#L84-L85>
478            mutation_timeout_ms: 74_000,
479            request_timeout_ms: 40_000,
480            // The following limits reflect the max values set in ProtocolConfig, at time of
481            // writing. <https://github.com/iotaledger/iota/blob/333f87061f0656607b1928aba423fa14ca16899e/crates/iota-protocol-config/src/lib.rs#L1580>
482            max_type_argument_depth: 16,
483            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1618>
484            max_type_argument_width: 32,
485            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1622>
486            max_type_nodes: 256,
487            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1988>
488            max_move_value_depth: 128,
489            // Filter-specific limits, such as the number of transaction ids that can be specified
490            // for the `TransactionBlockFilter`.
491            max_transaction_ids: 1000,
492            max_scan_limit: 100_000_000,
493        }
494    }
495}
496
497impl Default for InternalFeatureConfig {
498    fn default() -> Self {
499        Self {
500            query_limits_checker: true,
501            directive_checker: true,
502            feature_gate: true,
503            logger: true,
504            query_timeout: true,
505            metrics: true,
506            tracing: false,
507            apollo_tracing: false,
508            open_telemetry: false,
509        }
510    }
511}
512
513impl Default for BackgroundTasksConfig {
514    fn default() -> Self {
515        Self {
516            watermark_update_ms: 500,
517        }
518    }
519}
520
521impl Display for Version {
522    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523        write!(f, "{}", self.full)
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_read_empty_service_config() {
533        let actual = ServiceConfig::read("").unwrap();
534        let expect = ServiceConfig::default();
535        assert_eq!(actual, expect);
536    }
537
538    #[test]
539    fn test_read_limits_in_service_config() {
540        let actual = ServiceConfig::read(
541            r#" [limits]
542                max-query-depth = 100
543                max-query-nodes = 300
544                max-output-nodes = 200000
545                max-query-payload-size = 2000
546                max-db-query-cost = 50
547                default-page-size = 20
548                max-page-size = 50
549                mutation-timeout-ms = 74000
550                request-timeout-ms = 27000
551                max-type-argument-depth = 32
552                max-type-argument-width = 64
553                max-type-nodes = 128
554                max-move-value-depth = 256
555                max-transaction-ids = 11
556                max-scan-limit = 50
557            "#,
558        )
559        .unwrap();
560
561        let expect = ServiceConfig {
562            limits: Limits {
563                max_query_depth: 100,
564                max_query_nodes: 300,
565                max_output_nodes: 200000,
566                max_query_payload_size: 2000,
567                max_db_query_cost: 50,
568                default_page_size: 20,
569                max_page_size: 50,
570                mutation_timeout_ms: 74_000,
571                request_timeout_ms: 27_000,
572                max_type_argument_depth: 32,
573                max_type_argument_width: 64,
574                max_type_nodes: 128,
575                max_move_value_depth: 256,
576                max_transaction_ids: 11,
577                max_scan_limit: 50,
578            },
579            ..Default::default()
580        };
581
582        assert_eq!(actual, expect)
583    }
584
585    #[test]
586    fn test_read_enabled_features_in_service_config() {
587        let actual = ServiceConfig::read(
588            r#" disabled-features = [
589                  "coins",
590                ]
591            "#,
592        )
593        .unwrap();
594
595        use FunctionalGroup as G;
596        let expect = ServiceConfig {
597            disabled_features: BTreeSet::from([G::Coins]),
598            ..Default::default()
599        };
600
601        assert_eq!(actual, expect)
602    }
603
604    #[test]
605    fn test_read_experiments_in_service_config() {
606        let actual = ServiceConfig::read(
607            r#" [experiments]
608                test-flag = true
609            "#,
610        )
611        .unwrap();
612
613        let expect = ServiceConfig {
614            experiments: Experiments { test_flag: true },
615            ..Default::default()
616        };
617
618        assert_eq!(actual, expect)
619    }
620
621    #[test]
622    fn test_read_everything_in_service_config() {
623        let actual = ServiceConfig::read(
624            r#" disabled-features = ["analytics"]
625
626                [limits]
627                max-query-depth = 42
628                max-query-nodes = 320
629                max-output-nodes = 200000
630                max-query-payload-size = 200
631                max-db-query-cost = 20
632                default-page-size = 10
633                max-page-size = 20
634                mutation-timeout-ms = 74000
635                request-timeout-ms = 30000
636                max-type-argument-depth = 32
637                max-type-argument-width = 64
638                max-type-nodes = 128
639                max-move-value-depth = 256
640                max-transaction-ids = 42
641                max-scan-limit = 420
642
643                [experiments]
644                test-flag = true
645            "#,
646        )
647        .unwrap();
648
649        let expect = ServiceConfig {
650            limits: Limits {
651                max_query_depth: 42,
652                max_query_nodes: 320,
653                max_output_nodes: 200000,
654                max_query_payload_size: 200,
655                max_db_query_cost: 20,
656                default_page_size: 10,
657                max_page_size: 20,
658                mutation_timeout_ms: 74_000,
659                request_timeout_ms: 30_000,
660                max_type_argument_depth: 32,
661                max_type_argument_width: 64,
662                max_type_nodes: 128,
663                max_move_value_depth: 256,
664                max_transaction_ids: 42,
665                max_scan_limit: 420,
666            },
667            disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
668            experiments: Experiments { test_flag: true },
669            ..Default::default()
670        };
671
672        assert_eq!(actual, expect);
673    }
674
675    #[test]
676    fn test_read_partial_in_service_config() {
677        let actual = ServiceConfig::read(
678            r#" disabled-features = ["analytics"]
679
680                [limits]
681                max-query-depth = 42
682                max-query-nodes = 320
683            "#,
684        )
685        .unwrap();
686
687        // When reading partially, the other parts will come from the default
688        // implementation.
689        let expect = ServiceConfig {
690            limits: Limits {
691                max_query_depth: 42,
692                max_query_nodes: 320,
693                ..Default::default()
694            },
695            disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
696            ..Default::default()
697        };
698
699        assert_eq!(actual, expect);
700    }
701}