Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ Client implementation and command-line tool for the Linera blockchain
Default value: `3600000`
* `--wait-for-outgoing-messages` — Whether to wait until a quorum of validators has confirmed that all sent cross-chain messages have been delivered
* `--allow-fast-blocks` — Whether to allow creating blocks in the fast round. Fast blocks have lower latency but must be used carefully so that there are never any conflicting fast block proposals
* `--disable-multi-leader-jitter` — Disable the multi-leader jitter delay. By default, when proposing in a multi-leader round with index `>= 1`, the client waits a deterministic delay derived from the owner and round before re-proposing. This spreads out concurrent proposals from honest clients; the owner with the lowest `hash(owner, round)` still proposes immediately
* `--long-lived-services` — (EXPERIMENTAL) Whether application services can persist in some cases between queries
* `--blanket-message-policy <BLANKET_MESSAGE_POLICY>` — The policy for handling incoming messages

Expand Down
142 changes: 9 additions & 133 deletions linera-base/src/ownership.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::{
crypto::{BcsHashable, CryptoHash},
data_types::{Round, TimeDelta},
doc_scalar,
identifiers::AccountOwner,
Expand All @@ -40,10 +39,9 @@ pub struct TimeoutConfig {
/// The duration of the fast round.
#[debug(skip_if = Option::is_none)]
pub fast_round_duration: Option<TimeDelta>,
/// The duration of the first multi-leader and single-leader rounds.
/// The duration of the first single-leader and all multi-leader rounds.
pub base_timeout: TimeDelta,
/// The duration by which the timeout increases after each multi-leader or
/// single-leader round.
/// The duration by which the timeout increases after each single-leader round.
pub timeout_increment: TimeDelta,
/// The age of an incoming tracked or protected message after which the validators start
/// transitioning the chain to fallback mode.
Expand Down Expand Up @@ -175,7 +173,11 @@ impl ChainOwnership {
}
match round {
Round::Fast => tc.fast_round_duration,
Round::MultiLeader(r) | Round::SingleLeader(r) | Round::Validator(r) => {
Round::MultiLeader(r) if r.saturating_add(1) == self.multi_leader_rounds => {
Some(tc.base_timeout)
}
Round::MultiLeader(_) => None,
Round::SingleLeader(r) | Round::Validator(r) => {
let increment = tc.timeout_increment.saturating_mul(u64::from(r));
Some(tc.base_timeout.saturating_add(increment))
}
Expand Down Expand Up @@ -227,73 +229,8 @@ impl ChainOwnership {
pub fn is_super_owner_no_regular_owners(&self, owner: &AccountOwner) -> bool {
self.owners.is_empty() && self.super_owners.contains(owner)
}

/// Returns whether `owner` has the lowest `hash(owner, round)` among the eligible
/// multi-leader proposers, and should therefore propose immediately rather than wait
/// out a jitter delay. Returns `false` for `open_multi_leader_rounds`, where the set
/// of proposers is unbounded.
///
/// This is a gentle-clients convention; it is not enforced by the protocol.
fn is_preferred_multi_leader_proposer(&self, owner: &AccountOwner, round_index: u32) -> bool {
if self.open_multi_leader_rounds || !self.can_propose_in_multi_leader_round(owner) {
return false;
}
let our_priority = multi_leader_priority(owner, round_index);
self.all_owners().all(|other| {
other == owner || multi_leader_priority(other, round_index) >= our_priority
})
}

/// Returns the deterministic delay this owner should wait before proposing in `round`,
/// to spread out concurrent proposals from honest clients. The preferred owner returns
/// `TimeDelta::ZERO`; others return `hash(owner, round) mod round_duration`. Returns
/// `None` outside of multi-leader rounds, in the first multi-leader round (where
/// honest clients all attempt to propose immediately), and `Some(ZERO)` if the round
/// has no configured timeout.
pub fn multi_leader_proposal_delay(
&self,
owner: &AccountOwner,
round: Round,
) -> Option<TimeDelta> {
let Round::MultiLeader(round_index) = round else {
return None;
};
if round_index == 0 {
return None;
}
let round_duration = self.round_timeout(round).unwrap_or(TimeDelta::ZERO);
if round_duration == TimeDelta::ZERO
|| self.is_preferred_multi_leader_proposer(owner, round_index)
{
return Some(TimeDelta::ZERO);
}
let priority = multi_leader_priority(owner, round_index);
let prefix = <[u8; 8]>::try_from(&priority.as_bytes().as_slice()[..8])
.expect("hash is at least 8 bytes long");
let hash_u64 = u64::from_le_bytes(prefix);
Some(TimeDelta::from_micros(
hash_u64 % round_duration.as_micros(),
))
}
}

/// Returns the deterministic priority of `owner` in the multi-leader round with the
/// given index. The owner with the lowest priority is preferred to propose first.
fn multi_leader_priority(owner: &AccountOwner, round_index: u32) -> CryptoHash {
CryptoHash::new(&MultiLeaderPriorityInput {
round: round_index,
owner: *owner,
})
}

#[derive(Serialize, Deserialize)]
struct MultiLeaderPriorityInput {
round: u32,
owner: AccountOwner,
}

impl BcsHashable<'_> for MultiLeaderPriorityInput {}

/// Errors that can happen when attempting to manage a chain (close it, change ownership, or
/// change application permissions).
#[derive(Clone, Copy, Debug, Error, WitStore, WitType)]
Expand Down Expand Up @@ -342,14 +279,11 @@ mod tests {
ownership.round_timeout(Round::Fast),
Some(TimeDelta::from_secs(5))
);
assert_eq!(ownership.round_timeout(Round::MultiLeader(8)), None);
assert_eq!(
ownership.round_timeout(Round::MultiLeader(0)),
ownership.round_timeout(Round::MultiLeader(9)),
Some(TimeDelta::from_secs(10))
);
assert_eq!(
ownership.round_timeout(Round::MultiLeader(8)),
Some(TimeDelta::from_secs(18))
);
assert_eq!(
ownership.round_timeout(Round::SingleLeader(0)),
Some(TimeDelta::from_secs(10))
Expand All @@ -363,64 +297,6 @@ mod tests {
Some(TimeDelta::from_secs(18))
);
}

#[test]
fn test_multi_leader_proposal_delay() {
let owner_a = AccountOwner::from(Ed25519SecretKey::generate().public());
let owner_b = AccountOwner::from(Ed25519SecretKey::generate().public());
let owner_c = AccountOwner::from(Ed25519SecretKey::generate().public());
let mut ownership = ChainOwnership::multiple(
[(owner_a, 100), (owner_b, 100), (owner_c, 100)],
10,
TimeoutConfig {
fast_round_duration: None,
base_timeout: TimeDelta::from_secs(10),
timeout_increment: TimeDelta::ZERO,
fallback_duration: TimeDelta::MAX,
},
);

// No jitter in MultiLeader(0): all clients race; lowest-hash recovery kicks in
// only from MultiLeader(1) onwards.
for owner in [owner_a, owner_b, owner_c] {
assert_eq!(
ownership.multi_leader_proposal_delay(&owner, Round::MultiLeader(0)),
None
);
}

// Outside multi-leader rounds, no delay is computed.
assert_eq!(
ownership.multi_leader_proposal_delay(&owner_a, Round::SingleLeader(1)),
None
);

// In MultiLeader(1) exactly one owner is preferred (delay = 0); the others
// get a deterministic, bounded delay.
let delays = [owner_a, owner_b, owner_c].map(|owner| {
ownership
.multi_leader_proposal_delay(&owner, Round::MultiLeader(1))
.expect("delay should be defined in a multi-leader round")
});
let zero_count = delays.iter().filter(|d| **d == TimeDelta::ZERO).count();
assert_eq!(
zero_count, 1,
"exactly one owner should be the preferred proposer"
);
for delay in delays {
assert!(delay < TimeDelta::from_secs(10));
}

// Open multi-leader rounds have no fixed proposer set; nobody is preferred,
// so every owner waits its own deterministic jitter.
ownership.open_multi_leader_rounds = true;
for owner in [owner_a, owner_b, owner_c] {
let delay = ownership
.multi_leader_proposal_delay(&owner, Round::MultiLeader(1))
.expect("delay should be defined in a multi-leader round");
assert!(delay > TimeDelta::ZERO && delay < TimeDelta::from_secs(10));
}
}
}

doc_scalar!(ChainOwnership, "Represents the owner(s) of a chain");
9 changes: 0 additions & 9 deletions linera-client/src/client_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,6 @@ pub struct Options {
#[arg(long)]
pub allow_fast_blocks: bool,

/// Disable the multi-leader jitter delay. By default, when proposing in a multi-leader
/// round with index `>= 1`, the client waits a deterministic delay derived from the
/// owner and round before re-proposing. This spreads out concurrent proposals from
/// honest clients; the owner with the lowest `hash(owner, round)` still proposes
/// immediately.
#[arg(long)]
pub disable_multi_leader_jitter: bool,

/// (EXPERIMENTAL) Whether application services can persist in some cases between queries.
#[arg(long)]
pub long_lived_services: bool,
Expand Down Expand Up @@ -377,7 +369,6 @@ impl Options {
max_concurrent_batch_downloads: self.max_concurrent_batch_downloads,
max_joined_tasks: self.max_joined_tasks,
allow_fast_blocks: self.allow_fast_blocks,
multi_leader_jitter: !self.disable_multi_leader_jitter,
notification_circuit_breaker_initial_probe_interval: self
.notification_circuit_breaker_initial_probe_interval,
notification_circuit_breaker_max_probe_interval: self
Expand Down
50 changes: 1 addition & 49 deletions linera-core/src/client/chain_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use linera_base::{
crypto::{signer, CryptoHash, Signer, ValidatorPublicKey},
data_types::{
Amount, ApplicationPermissions, ArithmeticError, Blob, BlobContent, BlockHeight,
ChainDescription, Epoch, MessagePolicy, Round, TimeDelta, Timestamp,
ChainDescription, Epoch, MessagePolicy, Round, Timestamp,
},
ensure,
identifiers::{
Expand Down Expand Up @@ -120,11 +120,6 @@ pub struct Options {
/// Whether to allow creating blocks in the fast round. Fast blocks have lower latency but
/// must be used carefully so that there are never any conflicting fast block proposals.
pub allow_fast_blocks: bool,
/// Whether to apply the multi-leader jitter delay before proposing in a multi-leader
/// round with index `>= 1`, to spread out concurrent proposals across honest clients.
/// The owner with the lowest `hash(owner, round)` still proposes immediately. The
/// jitter only takes effect when the round has a configured timeout.
pub multi_leader_jitter: bool,
/// Initial probe interval for the notification circuit breaker. When a validator's
/// notification stream exhausts retries, the circuit breaker waits this long before
/// probing again. Doubles on each failed probe.
Expand Down Expand Up @@ -178,7 +173,6 @@ impl Options {
max_concurrent_batch_downloads: DEFAULT_MAX_CONCURRENT_BATCH_DOWNLOADS,
max_joined_tasks: 100,
allow_fast_blocks: false,
multi_leader_jitter: false,
notification_circuit_breaker_initial_probe_interval: Duration::from_secs(300),
notification_circuit_breaker_max_probe_interval: Duration::from_secs(3600),
max_event_stream_queries: DEFAULT_MAX_EVENT_STREAM_QUERIES,
Expand Down Expand Up @@ -2393,13 +2387,6 @@ impl<Env: Environment> ChainClient<Env> {
.map(|v| (AccountOwner::from(v.account_public_key), v.votes))
.collect();
if manager.should_propose(identity, round, seed, &current_committee) {
if let Some(wait_until) = self.multi_leader_jitter_target(info, identity, round) {
return Ok(Either::Right(RoundTimeout {
timestamp: wait_until,
current_round: round,
next_block_height: info.next_block_height,
}));
}
return Ok(Either::Left(round));
}
if let Some(timeout) = info.round_timeout() {
Expand All @@ -2410,41 +2397,6 @@ impl<Env: Environment> ChainClient<Env> {
))
}

/// Returns the timestamp at which `owner` should propose in `round`, to spread out
/// concurrent proposals from honest clients in a multi-leader round. Returns `None` if
/// the owner should propose immediately (either because the round is not a multi-leader
/// round, the owner is the preferred proposer, or the jitter target is already in the past).
///
/// The delay is deterministic per `(owner, round)` and is anchored at the round's start
/// time when known, so that retrying after an interrupting notification does not extend
/// the wait further.
fn multi_leader_jitter_target(
&self,
info: &ChainInfo,
owner: &AccountOwner,
round: Round,
) -> Option<Timestamp> {
if !self.options.multi_leader_jitter {
return None;
}
let ownership = &info.manager.ownership;
let delay = ownership.multi_leader_proposal_delay(owner, round)?;
if delay == TimeDelta::ZERO {
return None;
}
let now = self.storage_client().clock().current_time();
let round_start = if round == info.manager.current_round {
match (info.manager.round_timeout, ownership.round_timeout(round)) {
(Some(end), Some(duration)) => end.saturating_sub(duration),
_ => now,
}
} else {
now
};
let propose_at = round_start.saturating_add(delay);
(propose_at > now).then_some(propose_at)
}

/// Discards the pending block proposal, if any, so that a fresh block can be
/// proposed instead of retrying the queued one.
///
Expand Down
11 changes: 10 additions & 1 deletion linera-core/src/unit_tests/client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2623,7 +2623,16 @@ where
let signer = InMemorySigner::new(None);
let clock = storage_builder.clock().clone();
let mut builder = TestBuilder::new(storage_builder, 4, 0, signer).await?;
let client = builder.add_root_chain(1, Amount::from_tokens(10)).await?;
// Give the chain a single multi-leader round at genesis so that its initial round
// (`MultiLeader(0)`) times out: without multi-leader jitter, earlier multi-leader rounds
// do not time out. The chain still has no blocks of its own, which is the case this
// regression test exercises: requesting a timeout certificate then only fetches the
// chain's genesis description blob.
let client = builder
.add_root_chain_with_ownership(1, Amount::from_tokens(10), |owner| {
ChainOwnership::multiple([(owner, 100)], 1, TimeoutConfig::default())
})
.await?;

// Advance the clock past the (default 10s) round timeout.
clock.set(Timestamp::from(20_000_000));
Expand Down
17 changes: 16 additions & 1 deletion linera-core/src/unit_tests/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,21 @@ where
&mut self,
index: u32,
balance: Amount,
) -> anyhow::Result<ChainClient<B::Storage>> {
self.add_root_chain_with_ownership(index, balance, ChainOwnership::single)
.await
}

/// Creates the root chain with the given `index` and a genesis ownership built from its
/// freshly generated owner key, and returns a client for it.
///
/// Root chain 0 is the admin chain and needs to be initialized first, otherwise its balance
/// is automatically set to zero.
pub async fn add_root_chain_with_ownership(
&mut self,
index: u32,
balance: Amount,
make_ownership: impl FnOnce(AccountOwner) -> ChainOwnership,
) -> anyhow::Result<ChainClient<B::Storage>> {
// Make sure the admin chain is initialized.
if self.admin_description.is_none() && index != 0 {
Expand All @@ -1021,7 +1036,7 @@ where
let origin = ChainOrigin::Root(index);
let public_key = self.signer.generate_new();
let open_chain_config = InitialChainConfig {
ownership: ChainOwnership::single(public_key.into()),
ownership: make_ownership(public_key.into()),
epoch: Epoch(0),
balance,
application_permissions: ApplicationPermissions::default(),
Expand Down
5 changes: 1 addition & 4 deletions web/@linera/client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,7 @@ impl Client {
const BLOCK_CACHE_SIZE: usize = 5000;
const EXECUTION_STATE_CACHE_SIZE: usize = 10000;

let mut options = options.unwrap_or_default();
if crate::multi_leader_jitter_disabled() {
options.disable_multi_leader_jitter = true;
}
let options = options.unwrap_or_default();

wallet.lock().await?;
let mut storage = storage::get_storage(&wallet.name()).await?;
Expand Down
7 changes: 2 additions & 5 deletions web/@linera/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@ function isBrokenSafari(): boolean {

export async function initialize(options?: wasm.InitializeOptions) {
if (window.location) {
// Allow overriding the application's log filters using the `LINERA_LOG`,
// `LINERA_PROFILING`, and `LINERA_DISABLE_MULTI_LEADER_JITTER` search params,
// for debugging.
// Allow overriding the application's log filters using the `LINERA_LOG` and
// `LINERA_PROFILING` search params, for debugging.
const params = new URL(window.location.href).searchParams;
const log = params.get('LINERA_LOG');

if (!options) options = {};
if (params.get('LINERA_PROFILING')) options.profiling = true;
if (params.get('LINERA_DISABLE_MULTI_LEADER_JITTER'))
options.disableMultiLeaderJitter = true;
if (log) options.log = log;
}

Expand Down
Loading
Loading