Skip to main content

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    crypto::{BcsHashable, CryptoHash},
20    data_types::{Round, TimeDelta},
21    doc_scalar,
22    identifiers::AccountOwner,
23};
24
25/// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
26#[derive(
27    PartialEq,
28    Eq,
29    Clone,
30    Hash,
31    Debug,
32    Serialize,
33    Deserialize,
34    WitLoad,
35    WitStore,
36    WitType,
37    Allocative,
38)]
39pub struct TimeoutConfig {
40    /// The duration of the fast round.
41    #[debug(skip_if = Option::is_none)]
42    pub fast_round_duration: Option<TimeDelta>,
43    /// The duration of the first multi-leader and single-leader rounds.
44    pub base_timeout: TimeDelta,
45    /// The duration by which the timeout increases after each multi-leader or
46    /// single-leader round.
47    pub timeout_increment: TimeDelta,
48    /// The age of an incoming tracked or protected message after which the validators start
49    /// transitioning the chain to fallback mode.
50    pub fallback_duration: TimeDelta,
51}
52
53impl Default for TimeoutConfig {
54    fn default() -> Self {
55        Self {
56            fast_round_duration: None,
57            base_timeout: TimeDelta::from_secs(10),
58            timeout_increment: TimeDelta::from_secs(1),
59            // This is `MAX` because the validators are not currently expected to start clients for
60            // every chain with an old tracked message in the inbox.
61            fallback_duration: TimeDelta::MAX,
62        }
63    }
64}
65
66/// Represents the owner(s) of a chain.
67#[derive(
68    PartialEq,
69    Eq,
70    Clone,
71    Hash,
72    Debug,
73    Default,
74    Serialize,
75    Deserialize,
76    WitLoad,
77    WitStore,
78    WitType,
79    Allocative,
80)]
81pub struct ChainOwnership {
82    /// Super owners can propose fast blocks in the first round, and regular blocks in any round.
83    #[debug(skip_if = BTreeSet::is_empty)]
84    pub super_owners: BTreeSet<AccountOwner>,
85    /// The regular owners, with their weights that determine how often they are round leader.
86    #[debug(skip_if = BTreeMap::is_empty)]
87    pub owners: BTreeMap<AccountOwner, u64>,
88    /// The leader of the first single-leader round. If not set, this is random like other rounds.
89    pub first_leader: Option<AccountOwner>,
90    /// The number of rounds in which all owners are allowed to propose blocks.
91    pub multi_leader_rounds: u32,
92    /// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
93    /// This should only be `true` on chains with restrictive application permissions and an
94    /// application-based mechanism to select block proposers.
95    pub open_multi_leader_rounds: bool,
96    /// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
97    pub timeout_config: TimeoutConfig,
98}
99
100impl ChainOwnership {
101    /// Creates a `ChainOwnership` with a single super owner.
102    pub fn single_super(owner: AccountOwner) -> Self {
103        ChainOwnership {
104            super_owners: iter::once(owner).collect(),
105            owners: BTreeMap::new(),
106            first_leader: None,
107            multi_leader_rounds: 5,
108            open_multi_leader_rounds: false,
109            timeout_config: TimeoutConfig::default(),
110        }
111    }
112
113    /// Creates a `ChainOwnership` with a single regular owner.
114    pub fn single(owner: AccountOwner) -> Self {
115        ChainOwnership {
116            super_owners: BTreeSet::new(),
117            owners: iter::once((owner, 100)).collect(),
118            first_leader: None,
119            multi_leader_rounds: 5,
120            open_multi_leader_rounds: false,
121            timeout_config: TimeoutConfig::default(),
122        }
123    }
124
125    /// Creates a `ChainOwnership` with the specified regular owners.
126    pub fn multiple(
127        owners_and_weights: impl IntoIterator<Item = (AccountOwner, u64)>,
128        multi_leader_rounds: u32,
129        timeout_config: TimeoutConfig,
130    ) -> Self {
131        ChainOwnership {
132            super_owners: BTreeSet::new(),
133            owners: owners_and_weights.into_iter().collect(),
134            first_leader: None,
135            multi_leader_rounds,
136            open_multi_leader_rounds: false,
137            timeout_config,
138        }
139    }
140
141    /// Adds a regular owner.
142    #[cfg(with_testing)]
143    pub fn with_regular_owner(mut self, owner: AccountOwner, weight: u64) -> Self {
144        self.owners.insert(owner, weight);
145        self
146    }
147
148    /// Returns whether there are any owners or super owners or it is a public chain.
149    pub fn is_active(&self) -> bool {
150        !self.super_owners.is_empty()
151            || !self.owners.is_empty()
152            || self.timeout_config.fallback_duration == TimeDelta::ZERO
153    }
154
155    /// Returns `true` if this is a regular owner or super owner or the designated first leader.
156    pub fn is_owner(&self, owner: &AccountOwner) -> bool {
157        self.super_owners.contains(owner)
158            || self.owners.contains_key(owner)
159            || self.first_leader.as_ref().is_some_and(|fl| fl == owner)
160    }
161
162    /// Returns `true` if this owner can participate in multi-leader rounds, i.e. it
163    /// is a regular owner or super owner or `open_multi_leader_rounds == true`.
164    pub fn can_propose_in_multi_leader_round(&self, owner: &AccountOwner) -> bool {
165        self.open_multi_leader_rounds
166            || self.owners.contains_key(owner)
167            || self.super_owners.contains(owner)
168    }
169
170    /// Returns the duration of the given round.
171    pub fn round_timeout(&self, round: Round) -> Option<TimeDelta> {
172        let tc = &self.timeout_config;
173        if round.is_fast() && self.owners.is_empty() {
174            return None; // Fast round only times out if there are regular owners.
175        }
176        match round {
177            Round::Fast => tc.fast_round_duration,
178            Round::MultiLeader(r) | Round::SingleLeader(r) | Round::Validator(r) => {
179                let increment = tc.timeout_increment.saturating_mul(u64::from(r));
180                Some(tc.base_timeout.saturating_add(increment))
181            }
182        }
183    }
184
185    /// Returns the first consensus round for this configuration.
186    pub fn first_round(&self) -> Round {
187        if !self.super_owners.is_empty() {
188            Round::Fast
189        } else if self.owners.is_empty() {
190            Round::Validator(0)
191        } else if self.multi_leader_rounds > 0 {
192            Round::MultiLeader(0)
193        } else {
194            Round::SingleLeader(0)
195        }
196    }
197
198    /// Returns an iterator over all super owners, followed by all owners.
199    pub fn all_owners(&self) -> impl Iterator<Item = &AccountOwner> {
200        self.super_owners.iter().chain(self.owners.keys())
201    }
202
203    /// Returns whether fallback mode is enabled on this chain, i.e. the fallback duration
204    /// is less than `TimeDelta::MAX`.
205    pub fn has_fallback(&self) -> bool {
206        self.timeout_config.fallback_duration < TimeDelta::MAX
207    }
208
209    /// Returns the round following the specified one, if any.
210    pub fn next_round(&self, round: Round) -> Option<Round> {
211        let next_round = match round {
212            Round::Fast if self.multi_leader_rounds == 0 => Round::SingleLeader(0),
213            Round::Fast => Round::MultiLeader(0),
214            Round::MultiLeader(r) => r
215                .checked_add(1)
216                .filter(|r| *r < self.multi_leader_rounds)
217                .map_or(Round::SingleLeader(0), Round::MultiLeader),
218            Round::SingleLeader(r) => r
219                .checked_add(1)
220                .map_or(Round::Validator(0), Round::SingleLeader),
221            Round::Validator(r) => Round::Validator(r.checked_add(1)?),
222        };
223        Some(next_round)
224    }
225
226    /// Returns whether the given owner a super owner and there are no regular owners.
227    pub fn is_super_owner_no_regular_owners(&self, owner: &AccountOwner) -> bool {
228        self.owners.is_empty() && self.super_owners.contains(owner)
229    }
230
231    /// Returns whether `owner` has the lowest `hash(owner, round)` among the eligible
232    /// multi-leader proposers, and should therefore propose immediately rather than wait
233    /// out a jitter delay. Returns `false` for `open_multi_leader_rounds`, where the set
234    /// of proposers is unbounded.
235    ///
236    /// This is a gentle-clients convention; it is not enforced by the protocol.
237    fn is_preferred_multi_leader_proposer(&self, owner: &AccountOwner, round_index: u32) -> bool {
238        if self.open_multi_leader_rounds || !self.can_propose_in_multi_leader_round(owner) {
239            return false;
240        }
241        let our_priority = multi_leader_priority(owner, round_index);
242        self.all_owners().all(|other| {
243            other == owner || multi_leader_priority(other, round_index) >= our_priority
244        })
245    }
246
247    /// Returns the deterministic delay this owner should wait before proposing in `round`,
248    /// to spread out concurrent proposals from honest clients. The preferred owner returns
249    /// `TimeDelta::ZERO`; others return `hash(owner, round) mod round_duration`. Returns
250    /// `None` outside of multi-leader rounds, in the first multi-leader round (where
251    /// honest clients all attempt to propose immediately), and `Some(ZERO)` if the round
252    /// has no configured timeout.
253    pub fn multi_leader_proposal_delay(
254        &self,
255        owner: &AccountOwner,
256        round: Round,
257    ) -> Option<TimeDelta> {
258        let Round::MultiLeader(round_index) = round else {
259            return None;
260        };
261        if round_index == 0 {
262            return None;
263        }
264        let round_duration = self.round_timeout(round).unwrap_or(TimeDelta::ZERO);
265        if round_duration == TimeDelta::ZERO
266            || self.is_preferred_multi_leader_proposer(owner, round_index)
267        {
268            return Some(TimeDelta::ZERO);
269        }
270        let priority = multi_leader_priority(owner, round_index);
271        let prefix = <[u8; 8]>::try_from(&priority.as_bytes().as_slice()[..8])
272            .expect("hash is at least 8 bytes long");
273        let hash_u64 = u64::from_le_bytes(prefix);
274        Some(TimeDelta::from_micros(
275            hash_u64 % round_duration.as_micros(),
276        ))
277    }
278}
279
280/// Returns the deterministic priority of `owner` in the multi-leader round with the
281/// given index. The owner with the lowest priority is preferred to propose first.
282fn multi_leader_priority(owner: &AccountOwner, round_index: u32) -> CryptoHash {
283    CryptoHash::new(&MultiLeaderPriorityInput {
284        round: round_index,
285        owner: *owner,
286    })
287}
288
289#[derive(Serialize, Deserialize)]
290struct MultiLeaderPriorityInput {
291    round: u32,
292    owner: AccountOwner,
293}
294
295impl BcsHashable<'_> for MultiLeaderPriorityInput {}
296
297/// Errors that can happen when attempting to manage a chain (close it, change ownership, or
298/// change application permissions).
299#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
300pub enum ManageChainError {
301    /// The application wasn't allowed to perform this chain management operation.
302    #[error("Unauthorized chain management operation")]
303    NotPermitted,
304}
305
306/// Errors that can happen when verifying the authentication of an operation over an
307/// account.
308#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
309pub enum AccountPermissionError {
310    /// Operations on this account are not permitted in the current execution context.
311    #[error("Unauthorized attempt to access account owned by {0}")]
312    NotPermitted(AccountOwner),
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::crypto::{Ed25519SecretKey, Secp256k1SecretKey};
319
320    #[test]
321    fn test_ownership_round_timeouts() {
322        let super_pub_key = Ed25519SecretKey::generate().public();
323        let super_owner = AccountOwner::from(super_pub_key);
324        let pub_key = Secp256k1SecretKey::generate().public();
325        let owner = AccountOwner::from(pub_key);
326
327        let ownership = ChainOwnership {
328            super_owners: BTreeSet::from_iter([super_owner]),
329            owners: BTreeMap::from_iter([(owner, 100)]),
330            first_leader: Some(owner),
331            multi_leader_rounds: 10,
332            open_multi_leader_rounds: false,
333            timeout_config: TimeoutConfig {
334                fast_round_duration: Some(TimeDelta::from_secs(5)),
335                base_timeout: TimeDelta::from_secs(10),
336                timeout_increment: TimeDelta::from_secs(1),
337                fallback_duration: TimeDelta::from_secs(60 * 60),
338            },
339        };
340
341        assert_eq!(
342            ownership.round_timeout(Round::Fast),
343            Some(TimeDelta::from_secs(5))
344        );
345        assert_eq!(
346            ownership.round_timeout(Round::MultiLeader(0)),
347            Some(TimeDelta::from_secs(10))
348        );
349        assert_eq!(
350            ownership.round_timeout(Round::MultiLeader(8)),
351            Some(TimeDelta::from_secs(18))
352        );
353        assert_eq!(
354            ownership.round_timeout(Round::SingleLeader(0)),
355            Some(TimeDelta::from_secs(10))
356        );
357        assert_eq!(
358            ownership.round_timeout(Round::SingleLeader(1)),
359            Some(TimeDelta::from_secs(11))
360        );
361        assert_eq!(
362            ownership.round_timeout(Round::SingleLeader(8)),
363            Some(TimeDelta::from_secs(18))
364        );
365    }
366
367    #[test]
368    fn test_multi_leader_proposal_delay() {
369        let owner_a = AccountOwner::from(Ed25519SecretKey::generate().public());
370        let owner_b = AccountOwner::from(Ed25519SecretKey::generate().public());
371        let owner_c = AccountOwner::from(Ed25519SecretKey::generate().public());
372        let mut ownership = ChainOwnership::multiple(
373            [(owner_a, 100), (owner_b, 100), (owner_c, 100)],
374            10,
375            TimeoutConfig {
376                fast_round_duration: None,
377                base_timeout: TimeDelta::from_secs(10),
378                timeout_increment: TimeDelta::ZERO,
379                fallback_duration: TimeDelta::MAX,
380            },
381        );
382
383        // No jitter in MultiLeader(0): all clients race; lowest-hash recovery kicks in
384        // only from MultiLeader(1) onwards.
385        for owner in [owner_a, owner_b, owner_c] {
386            assert_eq!(
387                ownership.multi_leader_proposal_delay(&owner, Round::MultiLeader(0)),
388                None
389            );
390        }
391
392        // Outside multi-leader rounds, no delay is computed.
393        assert_eq!(
394            ownership.multi_leader_proposal_delay(&owner_a, Round::SingleLeader(1)),
395            None
396        );
397
398        // In MultiLeader(1) exactly one owner is preferred (delay = 0); the others
399        // get a deterministic, bounded delay.
400        let delays = [owner_a, owner_b, owner_c].map(|owner| {
401            ownership
402                .multi_leader_proposal_delay(&owner, Round::MultiLeader(1))
403                .expect("delay should be defined in a multi-leader round")
404        });
405        let zero_count = delays.iter().filter(|d| **d == TimeDelta::ZERO).count();
406        assert_eq!(
407            zero_count, 1,
408            "exactly one owner should be the preferred proposer"
409        );
410        for delay in delays {
411            assert!(delay < TimeDelta::from_secs(10));
412        }
413
414        // Open multi-leader rounds have no fixed proposer set; nobody is preferred,
415        // so every owner waits its own deterministic jitter.
416        ownership.open_multi_leader_rounds = true;
417        for owner in [owner_a, owner_b, owner_c] {
418            let delay = ownership
419                .multi_leader_proposal_delay(&owner, Round::MultiLeader(1))
420                .expect("delay should be defined in a multi-leader round");
421            assert!(delay > TimeDelta::ZERO && delay < TimeDelta::from_secs(10));
422        }
423    }
424}
425
426doc_scalar!(ChainOwnership, "Represents the owner(s) of a chain");