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