1use 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#[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 #[debug(skip_if = Option::is_none)]
42 pub fast_round_duration: Option<TimeDelta>,
43 pub base_timeout: TimeDelta,
45 pub timeout_increment: TimeDelta,
48 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 fallback_duration: TimeDelta::MAX,
62 }
63 }
64}
65
66#[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 #[debug(skip_if = BTreeSet::is_empty)]
84 pub super_owners: BTreeSet<AccountOwner>,
85 #[debug(skip_if = BTreeMap::is_empty)]
87 pub owners: BTreeMap<AccountOwner, u64>,
88 pub first_leader: Option<AccountOwner>,
90 pub multi_leader_rounds: u32,
92 pub open_multi_leader_rounds: bool,
96 pub timeout_config: TimeoutConfig,
98}
99
100impl ChainOwnership {
101 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 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 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 #[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 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 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 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 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; }
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 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 pub fn all_owners(&self) -> impl Iterator<Item = &AccountOwner> {
200 self.super_owners.iter().chain(self.owners.keys())
201 }
202
203 pub fn has_fallback(&self) -> bool {
206 self.timeout_config.fallback_duration < TimeDelta::MAX
207 }
208
209 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 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 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 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
280fn 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#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
300pub enum ManageChainError {
301 #[error("Unauthorized chain management operation")]
303 NotPermitted,
304}
305
306#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
309pub enum AccountPermissionError {
310 #[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 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 assert_eq!(
394 ownership.multi_leader_proposal_delay(&owner_a, Round::SingleLeader(1)),
395 None
396 );
397
398 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 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");