linera_base/
ownership.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Structures defining the set of owners and super owners, as well as the consensus
5//! round types and timeouts for chains.
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    iter,
10};
11
12use allocative::Allocative;
13use custom_debug_derive::Debug;
14use linera_witty::{WitLoad, WitStore, WitType};
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18use crate::{
19    data_types::{Round, TimeDelta},
20    doc_scalar,
21    identifiers::AccountOwner,
22};
23
24/// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
25#[derive(
26    PartialEq,
27    Eq,
28    Clone,
29    Hash,
30    Debug,
31    Serialize,
32    Deserialize,
33    WitLoad,
34    WitStore,
35    WitType,
36    Allocative,
37)]
38pub struct TimeoutConfig {
39    /// The duration of the fast round.
40    #[debug(skip_if = Option::is_none)]
41    pub fast_round_duration: Option<TimeDelta>,
42    /// The duration of the first single-leader and all multi-leader rounds.
43    pub base_timeout: TimeDelta,
44    /// The duration by which the timeout increases after each single-leader round.
45    pub timeout_increment: TimeDelta,
46    /// The age of an incoming tracked or protected message after which the validators start
47    /// transitioning the chain to fallback mode.
48    pub fallback_duration: TimeDelta,
49}
50
51impl Default for TimeoutConfig {
52    fn default() -> Self {
53        Self {
54            fast_round_duration: None,
55            base_timeout: TimeDelta::from_secs(10),
56            timeout_increment: TimeDelta::from_secs(1),
57            // This is `MAX` because the validators are not currently expected to start clients for
58            // every chain with an old tracked message in the inbox.
59            fallback_duration: TimeDelta::MAX,
60        }
61    }
62}
63
64/// Represents the owner(s) of a chain.
65#[derive(
66    PartialEq,
67    Eq,
68    Clone,
69    Hash,
70    Debug,
71    Default,
72    Serialize,
73    Deserialize,
74    WitLoad,
75    WitStore,
76    WitType,
77    Allocative,
78)]
79pub struct ChainOwnership {
80    /// Super owners can propose fast blocks in the first round, and regular blocks in any round.
81    #[debug(skip_if = BTreeSet::is_empty)]
82    pub super_owners: BTreeSet<AccountOwner>,
83    /// The regular owners, with their weights that determine how often they are round leader.
84    #[debug(skip_if = BTreeMap::is_empty)]
85    pub owners: BTreeMap<AccountOwner, u64>,
86    /// The leader of the first single-leader round. If not set, this is random like other rounds.
87    pub first_leader: Option<AccountOwner>,
88    /// The number of rounds in which all owners are allowed to propose blocks.
89    pub multi_leader_rounds: u32,
90    /// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
91    /// This should only be `true` on chains with restrictive application permissions and an
92    /// application-based mechanism to select block proposers.
93    pub open_multi_leader_rounds: bool,
94    /// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
95    pub timeout_config: TimeoutConfig,
96}
97
98impl ChainOwnership {
99    /// Creates a `ChainOwnership` with a single super owner.
100    pub fn single_super(owner: AccountOwner) -> Self {
101        ChainOwnership {
102            super_owners: iter::once(owner).collect(),
103            owners: BTreeMap::new(),
104            first_leader: None,
105            multi_leader_rounds: 2,
106            open_multi_leader_rounds: false,
107            timeout_config: TimeoutConfig::default(),
108        }
109    }
110
111    /// Creates a `ChainOwnership` with a single regular owner.
112    pub fn single(owner: AccountOwner) -> Self {
113        ChainOwnership {
114            super_owners: BTreeSet::new(),
115            owners: iter::once((owner, 100)).collect(),
116            first_leader: None,
117            multi_leader_rounds: 2,
118            open_multi_leader_rounds: false,
119            timeout_config: TimeoutConfig::default(),
120        }
121    }
122
123    /// Creates a `ChainOwnership` with the specified regular owners.
124    pub fn multiple(
125        owners_and_weights: impl IntoIterator<Item = (AccountOwner, u64)>,
126        multi_leader_rounds: u32,
127        timeout_config: TimeoutConfig,
128    ) -> Self {
129        ChainOwnership {
130            super_owners: BTreeSet::new(),
131            owners: owners_and_weights.into_iter().collect(),
132            first_leader: None,
133            multi_leader_rounds,
134            open_multi_leader_rounds: false,
135            timeout_config,
136        }
137    }
138
139    /// Adds a regular owner.
140    pub fn with_regular_owner(mut self, owner: AccountOwner, weight: u64) -> Self {
141        self.owners.insert(owner, weight);
142        self
143    }
144
145    /// Fixes the given owner as the leader of the first single-leader round on all heights.
146    pub fn with_first_leader(mut self, owner: AccountOwner) -> Self {
147        self.first_leader = Some(owner);
148        self
149    }
150
151    /// Returns whether there are any owners or super owners or it is a public chain.
152    pub fn is_active(&self) -> bool {
153        !self.super_owners.is_empty()
154            || !self.owners.is_empty()
155            || self.timeout_config.fallback_duration == TimeDelta::ZERO
156    }
157
158    /// Returns `true` if this is an owner or super owner.
159    pub fn is_owner(&self, owner: &AccountOwner) -> bool {
160        self.super_owners.contains(owner)
161            || self.owners.contains_key(owner)
162            || self.first_leader.as_ref().is_some_and(|fl| fl == owner)
163    }
164
165    /// Returns `true` if this is an owner or if `open_multi_leader_rounds`.
166    pub fn is_multi_leader_owner(&self, owner: &AccountOwner) -> bool {
167        self.open_multi_leader_rounds
168            || self.owners.contains_key(owner)
169            || self.super_owners.contains(owner)
170    }
171
172    /// Returns the duration of the given round.
173    pub fn round_timeout(&self, round: Round) -> Option<TimeDelta> {
174        let tc = &self.timeout_config;
175        if round.is_fast() && self.owners.is_empty() {
176            return None; // Fast round only times out if there are regular owners.
177        }
178        match round {
179            Round::Fast => tc.fast_round_duration,
180            Round::MultiLeader(r) if r.saturating_add(1) == self.multi_leader_rounds => {
181                Some(tc.base_timeout)
182            }
183            Round::MultiLeader(_) => None,
184            Round::SingleLeader(r) | Round::Validator(r) => {
185                let increment = tc.timeout_increment.saturating_mul(u64::from(r));
186                Some(tc.base_timeout.saturating_add(increment))
187            }
188        }
189    }
190
191    /// Returns the first consensus round for this configuration.
192    pub fn first_round(&self) -> Round {
193        if !self.super_owners.is_empty() {
194            Round::Fast
195        } else if self.owners.is_empty() {
196            Round::Validator(0)
197        } else if self.multi_leader_rounds > 0 {
198            Round::MultiLeader(0)
199        } else {
200            Round::SingleLeader(0)
201        }
202    }
203
204    /// Returns an iterator over all super owners, followed by all owners.
205    pub fn all_owners(&self) -> impl Iterator<Item = &AccountOwner> {
206        self.super_owners.iter().chain(self.owners.keys())
207    }
208
209    /// Returns whether fallback mode is enabled on this chain, i.e. the fallback duration
210    /// is less than `TimeDelta::MAX`.
211    pub fn has_fallback(&self) -> bool {
212        self.timeout_config.fallback_duration < TimeDelta::MAX
213    }
214
215    /// Returns the round following the specified one, if any.
216    pub fn next_round(&self, round: Round) -> Option<Round> {
217        let next_round = match round {
218            Round::Fast if self.multi_leader_rounds == 0 => Round::SingleLeader(0),
219            Round::Fast => Round::MultiLeader(0),
220            Round::MultiLeader(r) => r
221                .checked_add(1)
222                .filter(|r| *r < self.multi_leader_rounds)
223                .map_or(Round::SingleLeader(0), Round::MultiLeader),
224            Round::SingleLeader(r) => r
225                .checked_add(1)
226                .map_or(Round::Validator(0), Round::SingleLeader),
227            Round::Validator(r) => Round::Validator(r.checked_add(1)?),
228        };
229        Some(next_round)
230    }
231
232    /// Returns whether the given owner a super owner and there are no regular owners.
233    pub fn is_super_owner_no_regular_owners(&self, owner: &AccountOwner) -> bool {
234        self.owners.is_empty() && self.super_owners.contains(owner)
235    }
236}
237
238/// Errors that can happen when attempting to close a chain.
239#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
240pub enum CloseChainError {
241    /// The application wasn't allowed to close the chain.
242    #[error("Unauthorized attempt to close the chain")]
243    NotPermitted,
244}
245
246/// Errors that can happen when attempting to change the application permissions.
247#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
248pub enum ChangeApplicationPermissionsError {
249    /// The application wasn't allowed to change the application permissions.
250    #[error("Unauthorized attempt to change the application permissions")]
251    NotPermitted,
252}
253
254/// Errors that can happen when verifying the authentication of an operation over an
255/// account.
256#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
257pub enum AccountPermissionError {
258    /// Operations on this account are not permitted in the current execution context.
259    #[error("Unauthorized attempt to access account owned by {0}")]
260    NotPermitted(AccountOwner),
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::crypto::{Ed25519SecretKey, Secp256k1SecretKey};
267
268    #[test]
269    fn test_ownership_round_timeouts() {
270        let super_pub_key = Ed25519SecretKey::generate().public();
271        let super_owner = AccountOwner::from(super_pub_key);
272        let pub_key = Secp256k1SecretKey::generate().public();
273        let owner = AccountOwner::from(pub_key);
274
275        let ownership = ChainOwnership {
276            super_owners: BTreeSet::from_iter([super_owner]),
277            owners: BTreeMap::from_iter([(owner, 100)]),
278            first_leader: Some(owner),
279            multi_leader_rounds: 10,
280            open_multi_leader_rounds: false,
281            timeout_config: TimeoutConfig {
282                fast_round_duration: Some(TimeDelta::from_secs(5)),
283                base_timeout: TimeDelta::from_secs(10),
284                timeout_increment: TimeDelta::from_secs(1),
285                fallback_duration: TimeDelta::from_secs(60 * 60),
286            },
287        };
288
289        assert_eq!(
290            ownership.round_timeout(Round::Fast),
291            Some(TimeDelta::from_secs(5))
292        );
293        assert_eq!(ownership.round_timeout(Round::MultiLeader(8)), None);
294        assert_eq!(
295            ownership.round_timeout(Round::MultiLeader(9)),
296            Some(TimeDelta::from_secs(10))
297        );
298        assert_eq!(
299            ownership.round_timeout(Round::SingleLeader(0)),
300            Some(TimeDelta::from_secs(10))
301        );
302        assert_eq!(
303            ownership.round_timeout(Round::SingleLeader(1)),
304            Some(TimeDelta::from_secs(11))
305        );
306        assert_eq!(
307            ownership.round_timeout(Round::SingleLeader(8)),
308            Some(TimeDelta::from_secs(18))
309        );
310    }
311}
312
313doc_scalar!(ChainOwnership, "Represents the owner(s) of a chain");