linera_client/
client_options.rs

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