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