linera_client/
client_options.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{collections::HashSet, fmt, iter};
5
6use linera_base::{
7    data_types::{ApplicationPermissions, TimeDelta},
8    identifiers::{AccountOwner, ApplicationId, ChainId, GenericApplicationId},
9    ownership::ChainOwnership,
10    time::Duration,
11};
12use linera_core::{
13    client::{
14        BlanketMessagePolicy, ChainClientOptions, MessagePolicy,
15        DEFAULT_CERTIFICATE_DOWNLOAD_BATCH_SIZE, DEFAULT_SENDER_CERTIFICATE_DOWNLOAD_BATCH_SIZE,
16    },
17    node::CrossChainMessageDelivery,
18    DEFAULT_QUORUM_GRACE_PERIOD,
19};
20use linera_execution::ResourceControlPolicy;
21
22#[cfg(not(web))]
23use crate::client_metrics::TimingConfig;
24use crate::util;
25
26#[derive(Debug, thiserror::Error)]
27pub enum Error {
28    #[error("I/O error: {0}")]
29    IoError(#[from] std::io::Error),
30    #[error("there are {public_keys} public keys but {weights} weights")]
31    MisalignedWeights { public_keys: usize, weights: usize },
32    #[error("config error: {0}")]
33    Config(#[from] crate::config::Error),
34}
35
36util::impl_from_infallible!(Error);
37
38#[derive(Clone, clap::Parser, serde::Deserialize, tsify::Tsify)]
39#[tsify(from_wasm_abi)]
40#[group(skip)]
41#[serde(default, rename_all = "camelCase")]
42pub struct Options {
43    /// Timeout for sending queries (milliseconds)
44    #[arg(long = "send-timeout-ms", default_value = "4000", value_parser = util::parse_millis)]
45    pub send_timeout: Duration,
46
47    /// Timeout for receiving responses (milliseconds)
48    #[arg(long = "recv-timeout-ms", default_value = "4000", value_parser = util::parse_millis)]
49    pub recv_timeout: Duration,
50
51    /// The maximum number of incoming message bundles to include in a block proposal.
52    #[arg(long, default_value = "10")]
53    pub max_pending_message_bundles: usize,
54
55    /// The duration in milliseconds after which an idle chain worker will free its memory.
56    #[arg(
57        long = "chain-worker-ttl-ms",
58        default_value = "30000",
59        env = "LINERA_CHAIN_WORKER_TTL_MS",
60        value_parser = util::parse_millis,
61    )]
62    pub chain_worker_ttl: Duration,
63
64    /// The duration, in milliseconds, after which an idle sender chain worker will
65    /// free its memory.
66    #[arg(
67        long = "sender-chain-worker-ttl-ms",
68        default_value = "1000",
69        env = "LINERA_SENDER_CHAIN_WORKER_TTL_MS",
70        value_parser = util::parse_millis
71    )]
72    pub sender_chain_worker_ttl: Duration,
73
74    /// Delay increment for retrying to connect to a validator.
75    #[arg(
76        long = "retry-delay-ms",
77        default_value = "1000",
78        value_parser = util::parse_millis
79    )]
80    pub retry_delay: Duration,
81
82    /// Number of times to retry connecting to a validator.
83    #[arg(long, default_value = "10")]
84    pub max_retries: u32,
85
86    /// Whether to wait until a quorum of validators has confirmed that all sent cross-chain
87    /// messages have been delivered.
88    #[arg(long)]
89    pub wait_for_outgoing_messages: bool,
90
91    /// (EXPERIMENTAL) Whether application services can persist in some cases between queries.
92    #[arg(long)]
93    pub long_lived_services: bool,
94
95    /// The policy for handling incoming messages.
96    #[arg(long, default_value_t, value_enum)]
97    pub blanket_message_policy: BlanketMessagePolicy,
98
99    /// A set of chains to restrict incoming messages from. By default, messages
100    /// from all chains are accepted. To reject messages from all chains, specify
101    /// an empty string.
102    #[arg(long, value_parser = util::parse_chain_set)]
103    pub restrict_chain_ids_to: Option<HashSet<ChainId>>,
104
105    /// A set of application IDs. If specified, only bundles with at least one message from one of
106    /// these applications will be accepted.
107    #[arg(long, value_parser = util::parse_app_set)]
108    pub reject_message_bundles_without_application_ids: Option<HashSet<GenericApplicationId>>,
109
110    /// A set of application IDs. If specified, only bundles where all messages are from one of
111    /// these applications will be accepted.
112    #[arg(long, value_parser = util::parse_app_set)]
113    pub reject_message_bundles_with_other_application_ids: Option<HashSet<GenericApplicationId>>,
114
115    /// Enable timing reports during operations
116    #[cfg(not(web))]
117    #[arg(long)]
118    pub timings: bool,
119
120    /// Interval in seconds between timing reports (defaults to 5)
121    #[cfg(not(web))]
122    #[arg(long, default_value = "5")]
123    pub timing_interval: u64,
124
125    /// An additional delay, after reaching a quorum, to wait for additional validator signatures,
126    /// as a fraction of time taken to reach quorum.
127    #[arg(long, default_value_t = DEFAULT_QUORUM_GRACE_PERIOD)]
128    pub quorum_grace_period: f64,
129
130    /// The delay when downloading a blob, after which we try a second validator, in milliseconds.
131    #[arg(
132        long = "blob-download-timeout-ms",
133        default_value = "1000",
134        value_parser = util::parse_millis,
135    )]
136    pub blob_download_timeout: Duration,
137
138    /// The delay when downloading a batch of certificates, after which we try a second validator,
139    /// in milliseconds.
140    #[arg(
141        long = "cert-batch-download-timeout-ms",
142        default_value = "1000",
143        value_parser = util::parse_millis
144    )]
145    pub certificate_batch_download_timeout: Duration,
146
147    /// Maximum number of certificates that we download at a time from one validator when
148    /// synchronizing one of our chains.
149    #[arg(
150        long,
151        default_value_t = DEFAULT_CERTIFICATE_DOWNLOAD_BATCH_SIZE,
152    )]
153    pub certificate_download_batch_size: u64,
154
155    /// Maximum number of sender certificates we try to download and receive in one go
156    /// when syncing sender chains.
157    #[arg(
158        long,
159        default_value_t = DEFAULT_SENDER_CERTIFICATE_DOWNLOAD_BATCH_SIZE,
160    )]
161    pub sender_certificate_download_batch_size: usize,
162
163    /// Maximum number of tasks that can are joined concurrently in the client.
164    #[arg(long, default_value = "100")]
165    pub max_joined_tasks: usize,
166
167    /// Maximum expected latency in milliseconds for score normalization.
168    #[arg(
169        long,
170        default_value_t = linera_core::client::requests_scheduler::MAX_ACCEPTED_LATENCY_MS,
171        env = "LINERA_REQUESTS_SCHEDULER_MAX_ACCEPTED_LATENCY_MS"
172    )]
173    pub max_accepted_latency_ms: f64,
174
175    /// Time-to-live for cached responses in milliseconds.
176    #[arg(
177        long,
178        default_value_t = linera_core::client::requests_scheduler::CACHE_TTL_MS,
179        env = "LINERA_REQUESTS_SCHEDULER_CACHE_TTL_MS"
180    )]
181    pub cache_ttl_ms: u64,
182
183    /// Maximum number of entries in the cache.
184    #[arg(
185        long,
186        default_value_t = linera_core::client::requests_scheduler::CACHE_MAX_SIZE,
187        env = "LINERA_REQUESTS_SCHEDULER_CACHE_MAX_SIZE"
188    )]
189    pub cache_max_size: usize,
190
191    /// Maximum latency for an in-flight request before we stop deduplicating it (in milliseconds).
192    #[arg(
193        long,
194        default_value_t = linera_core::client::requests_scheduler::MAX_REQUEST_TTL_MS,
195        env = "LINERA_REQUESTS_SCHEDULER_MAX_REQUEST_TTL_MS"
196    )]
197    pub max_request_ttl_ms: u64,
198
199    /// Smoothing factor for Exponential Moving Averages (0 < alpha < 1).
200    /// Higher values give more weight to recent observations.
201    /// Typical values are between 0.01 and 0.5.
202    /// A value of 0.1 means that 10% of the new observation is considered
203    /// and 90% of the previous average is retained.
204    #[arg(
205        long,
206        default_value_t = linera_core::client::requests_scheduler::ALPHA_SMOOTHING_FACTOR,
207        env = "LINERA_REQUESTS_SCHEDULER_ALPHA"
208    )]
209    pub alpha: f64,
210
211    /// Delay in milliseconds between starting requests to different peers.
212    /// This helps to stagger requests and avoid overwhelming the network.
213    #[arg(
214        long,
215        default_value_t = linera_core::client::requests_scheduler::STAGGERED_DELAY_MS,
216        env = "LINERA_REQUESTS_SCHEDULER_ALTERNATIVE_PEERS_RETRY_DELAY_MS"
217    )]
218    pub alternative_peers_retry_delay_ms: u64,
219
220    #[serde(flatten)]
221    #[clap(flatten)]
222    pub chain_listener_config: crate::chain_listener::ChainListenerConfig,
223}
224
225impl Default for Options {
226    fn default() -> Self {
227        use clap::Parser;
228
229        #[derive(Parser)]
230        struct OptionsParser {
231            #[clap(flatten)]
232            options: Options,
233        }
234
235        OptionsParser::try_parse_from(std::iter::empty::<std::ffi::OsString>())
236            .expect("Options has no required arguments")
237            .options
238    }
239}
240
241impl Options {
242    /// Creates [`ChainClientOptions`] with the corresponding values.
243    pub(crate) fn to_chain_client_options(&self) -> ChainClientOptions {
244        let message_policy = MessagePolicy::new(
245            self.blanket_message_policy,
246            self.restrict_chain_ids_to.clone(),
247            self.reject_message_bundles_without_application_ids.clone(),
248            self.reject_message_bundles_with_other_application_ids
249                .clone(),
250        );
251        let cross_chain_message_delivery =
252            CrossChainMessageDelivery::new(self.wait_for_outgoing_messages);
253        ChainClientOptions {
254            max_pending_message_bundles: self.max_pending_message_bundles,
255            message_policy,
256            cross_chain_message_delivery,
257            quorum_grace_period: self.quorum_grace_period,
258            blob_download_timeout: self.blob_download_timeout,
259            certificate_batch_download_timeout: self.certificate_batch_download_timeout,
260            certificate_download_batch_size: self.certificate_download_batch_size,
261            sender_certificate_download_batch_size: self.sender_certificate_download_batch_size,
262            max_joined_tasks: self.max_joined_tasks,
263        }
264    }
265
266    /// Creates [`TimingConfig`] with the corresponding values.
267    #[cfg(not(web))]
268    pub(crate) fn to_timing_config(&self) -> TimingConfig {
269        TimingConfig {
270            enabled: self.timings,
271            report_interval_secs: self.timing_interval,
272        }
273    }
274
275    /// Creates [`RequestsSchedulerConfig`] with the corresponding values.
276    pub(crate) fn to_requests_scheduler_config(
277        &self,
278    ) -> linera_core::client::RequestsSchedulerConfig {
279        linera_core::client::RequestsSchedulerConfig {
280            max_accepted_latency_ms: self.max_accepted_latency_ms,
281            cache_ttl_ms: self.cache_ttl_ms,
282            cache_max_size: self.cache_max_size,
283            max_request_ttl_ms: self.max_request_ttl_ms,
284            alpha: self.alpha,
285            retry_delay_ms: self.alternative_peers_retry_delay_ms,
286        }
287    }
288}
289
290#[derive(Debug, Clone, clap::Args)]
291pub struct ChainOwnershipConfig {
292    /// A JSON list of the new super owners. Absence of the argument leaves the current
293    /// set of super owners unchanged.
294    // NOTE (applies to all fields): we need the std::option:: and std::vec:: qualifiers in order
295    // to throw off the #[derive(Args)] macro's automatic inference of the type it should expect
296    // from the parser. Without it, it infers the inner type (so either ApplicationId or
297    // Vec<ApplicationId>), which is not what we want here - we want the parsers to return the full
298    // expected types.
299    #[arg(long, value_parser = util::parse_json::<Vec<AccountOwner>>)]
300    pub super_owners: Option<std::vec::Vec<AccountOwner>>,
301
302    /// A JSON list of the new owners. Absence of the argument leaves the current list of
303    /// owners unchanged.
304    #[arg(long, value_parser = util::parse_json::<Vec<AccountOwner>>)]
305    pub owners: Option<std::vec::Vec<AccountOwner>>,
306
307    /// A JSON list of weights for the new owners.
308    ///
309    /// If they are specified there must be exactly one weight for each owner.
310    ///
311    /// Absence of the argument gives each owner a weight of 100 if --owners is specified,
312    /// or leaves the owners unchanged if it is not specified.
313    ///
314    /// Note: if --owner is not specified, but this argument is, the weights will be
315    /// assigned to the existing owners in lexicographical order.
316    #[arg(long, value_parser = util::parse_json::<Vec<u64>>)]
317    pub owner_weights: Option<std::vec::Vec<u64>>,
318
319    /// The number of rounds in which every owner can propose blocks, i.e. the first round
320    /// number in which only a single designated leader is allowed to propose blocks. "null" is
321    /// equivalent to 2^32 - 1. Absence of the argument leaves the current setting unchanged.
322    #[arg(long, value_parser = util::parse_json::<Option<u32>>)]
323    pub multi_leader_rounds: Option<std::option::Option<u32>>,
324
325    /// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
326    /// This should only be `true` on chains with restrictive application permissions and an
327    /// application-based mechanism to select block proposers.
328    #[arg(long)]
329    pub open_multi_leader_rounds: bool,
330
331    /// The duration of the fast round, in milliseconds. "null" means the fast round will
332    /// not time out. Absence of the argument leaves the current setting unchanged.
333    #[arg(long = "fast-round-ms", value_parser = util::parse_json_optional_millis_delta)]
334    pub fast_round_duration: Option<std::option::Option<TimeDelta>>,
335
336    /// The duration of the first single-leader and all multi-leader rounds. Absence of
337    /// the argument leaves the current setting unchanged.
338    #[arg(
339        long = "base-timeout-ms",
340        value_parser = util::parse_millis_delta
341    )]
342    pub base_timeout: Option<TimeDelta>,
343
344    /// The number of milliseconds by which the timeout increases after each
345    /// single-leader round. Absence of the argument leaves the current setting unchanged.
346    #[arg(
347        long = "timeout-increment-ms",
348        value_parser = util::parse_millis_delta
349    )]
350    pub timeout_increment: Option<TimeDelta>,
351
352    /// The age of an incoming tracked or protected message after which the validators start
353    /// transitioning the chain to fallback mode, in milliseconds. Absence of the argument
354    /// leaves the current setting unchanged.
355    #[arg(
356        long = "fallback-duration-ms",
357        value_parser = util::parse_millis_delta
358    )]
359    pub fallback_duration: Option<TimeDelta>,
360}
361
362impl ChainOwnershipConfig {
363    pub fn update(self, chain_ownership: &mut ChainOwnership) -> Result<(), Error> {
364        let ChainOwnershipConfig {
365            super_owners,
366            owners,
367            owner_weights,
368            multi_leader_rounds,
369            fast_round_duration,
370            open_multi_leader_rounds,
371            base_timeout,
372            timeout_increment,
373            fallback_duration,
374        } = self;
375
376        if let Some(owner_weights) = owner_weights {
377            let owners = owners
378                .unwrap_or_else(|| chain_ownership.owners.keys().cloned().collect::<Vec<_>>());
379            if owner_weights.len() != owners.len() {
380                return Err(Error::MisalignedWeights {
381                    public_keys: owners.len(),
382                    weights: owner_weights.len(),
383                });
384            }
385            chain_ownership.owners = owners.into_iter().zip(owner_weights).collect();
386        } else if let Some(owners) = owners {
387            chain_ownership.owners = owners.into_iter().zip(iter::repeat(100)).collect();
388        }
389
390        if let Some(super_owners) = super_owners {
391            chain_ownership.super_owners = super_owners.into_iter().collect();
392        }
393
394        if let Some(multi_leader_rounds) = multi_leader_rounds {
395            chain_ownership.multi_leader_rounds = multi_leader_rounds.unwrap_or(u32::MAX);
396        }
397
398        chain_ownership.open_multi_leader_rounds = open_multi_leader_rounds;
399
400        if let Some(fast_round_duration) = fast_round_duration {
401            chain_ownership.timeout_config.fast_round_duration = fast_round_duration;
402        }
403        if let Some(base_timeout) = base_timeout {
404            chain_ownership.timeout_config.base_timeout = base_timeout;
405        }
406        if let Some(timeout_increment) = timeout_increment {
407            chain_ownership.timeout_config.timeout_increment = timeout_increment;
408        }
409        if let Some(fallback_duration) = fallback_duration {
410            chain_ownership.timeout_config.fallback_duration = fallback_duration;
411        }
412
413        Ok(())
414    }
415}
416
417impl TryFrom<ChainOwnershipConfig> for ChainOwnership {
418    type Error = Error;
419
420    fn try_from(config: ChainOwnershipConfig) -> Result<ChainOwnership, Error> {
421        let mut chain_ownership = ChainOwnership::default();
422        config.update(&mut chain_ownership)?;
423        Ok(chain_ownership)
424    }
425}
426
427#[derive(Debug, Clone, clap::Args)]
428pub struct ApplicationPermissionsConfig {
429    /// A JSON list of applications allowed to execute operations on this chain. If set to null, all
430    /// operations will be allowed. Otherwise, only operations from the specified applications are
431    /// allowed, and no system operations. Absence of the argument leaves current permissions
432    /// unchanged.
433    // NOTE (applies to all fields): we need the std::option:: and std::vec:: qualifiers in order
434    // to throw off the #[derive(Args)] macro's automatic inference of the type it should expect
435    // from the parser. Without it, it infers the inner type (so either ApplicationId or
436    // Vec<ApplicationId>), which is not what we want here - we want the parsers to return the full
437    // expected types.
438    #[arg(long, value_parser = util::parse_json::<Option<Vec<ApplicationId>>>)]
439    pub execute_operations: Option<std::option::Option<Vec<ApplicationId>>>,
440    /// A JSON list of applications, such that at least one operation or incoming message from each
441    /// of these applications must occur in every block. Absence of the argument leaves
442    /// current mandatory applications unchanged.
443    #[arg(long, value_parser = util::parse_json::<Vec<ApplicationId>>)]
444    pub mandatory_applications: Option<std::vec::Vec<ApplicationId>>,
445    /// A JSON list of applications allowed to close the chain. Absence of the argument leaves
446    /// the current list unchanged.
447    #[arg(long, value_parser = util::parse_json::<Vec<ApplicationId>>)]
448    pub close_chain: Option<std::vec::Vec<ApplicationId>>,
449    /// A JSON list of applications allowed to change the application permissions on the current
450    /// chain using the system API. Absence of the argument leaves the current list unchanged.
451    #[arg(long, value_parser = util::parse_json::<Vec<ApplicationId>>)]
452    pub change_application_permissions: Option<std::vec::Vec<ApplicationId>>,
453    /// A JSON list of applications that are allowed to call services as oracles on the current
454    /// chain using the system API. If set to null, all applications will be able to do
455    /// so. Absence of the argument leaves the current value of the setting unchanged.
456    #[arg(long, value_parser = util::parse_json::<Option<Vec<ApplicationId>>>)]
457    pub call_service_as_oracle: Option<std::option::Option<Vec<ApplicationId>>>,
458    /// A JSON list of applications that are allowed to make HTTP requests on the current chain
459    /// using the system API. If set to null, all applications will be able to do so.
460    /// Absence of the argument leaves the current value of the setting unchanged.
461    #[arg(long, value_parser = util::parse_json::<Option<Vec<ApplicationId>>>)]
462    pub make_http_requests: Option<std::option::Option<Vec<ApplicationId>>>,
463}
464
465impl ApplicationPermissionsConfig {
466    pub fn update(self, application_permissions: &mut ApplicationPermissions) {
467        if let Some(execute_operations) = self.execute_operations {
468            application_permissions.execute_operations = execute_operations;
469        }
470        if let Some(mandatory_applications) = self.mandatory_applications {
471            application_permissions.mandatory_applications = mandatory_applications;
472        }
473        if let Some(close_chain) = self.close_chain {
474            application_permissions.close_chain = close_chain;
475        }
476        if let Some(change_application_permissions) = self.change_application_permissions {
477            application_permissions.change_application_permissions = change_application_permissions;
478        }
479        if let Some(call_service_as_oracle) = self.call_service_as_oracle {
480            application_permissions.call_service_as_oracle = call_service_as_oracle;
481        }
482        if let Some(make_http_requests) = self.make_http_requests {
483            application_permissions.make_http_requests = make_http_requests;
484        }
485    }
486}
487
488#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
489pub enum ResourceControlPolicyConfig {
490    NoFees,
491    Testnet,
492    #[cfg(with_testing)]
493    OnlyFuel,
494    #[cfg(with_testing)]
495    AllCategories,
496}
497
498impl ResourceControlPolicyConfig {
499    pub fn into_policy(self) -> ResourceControlPolicy {
500        match self {
501            ResourceControlPolicyConfig::NoFees => ResourceControlPolicy::no_fees(),
502            ResourceControlPolicyConfig::Testnet => ResourceControlPolicy::testnet(),
503            #[cfg(with_testing)]
504            ResourceControlPolicyConfig::OnlyFuel => ResourceControlPolicy::only_fuel(),
505            #[cfg(with_testing)]
506            ResourceControlPolicyConfig::AllCategories => ResourceControlPolicy::all_categories(),
507        }
508    }
509}
510
511impl std::str::FromStr for ResourceControlPolicyConfig {
512    type Err = String;
513
514    fn from_str(s: &str) -> Result<Self, Self::Err> {
515        clap::ValueEnum::from_str(s, true)
516    }
517}
518
519impl fmt::Display for ResourceControlPolicyConfig {
520    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
521        write!(f, "{:?}", self)
522    }
523}