Skip to content

Commit 3674411

Browse files
authored
Add an option for open (permissionless) multi-leader rounds. (#3168)
## Motivation For #3162, some chains need to be "permissionless", i.e. admit any signer as a block proposer, as far as the chain manager is concerned. ## Proposal Add an `open_multi_leader_rounds` option. If it is `true`, the multi-leader rounds are not restricted to the chain owners anymore. (All other round types still are restricted as usual.) ## Test Plan A test was added. ## Release Plan - Nothing to do / These changes follow the usual release cycle. ## Links - Closes #3163. - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 2053b61 commit 3674411

File tree

17 files changed

+116
-8
lines changed

17 files changed

+116
-8
lines changed

CLI.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ Open (i.e. activate) a new multi-owner chain deriving the UID from an existing o
200200

201201
If they are specified there must be exactly one weight for each owner. If no weights are given, every owner will have weight 100.
202202
* `--multi-leader-rounds <MULTI_LEADER_ROUNDS>` — The number of rounds in which every owner can propose blocks, i.e. the first round number in which only a single designated leader is allowed to propose blocks
203+
* `--open-multi-leader-rounds` — Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners. This should only be `true` on chains with restrictive application permissions and an application-based mechanism to select block proposers
203204
* `--fast-round-ms <FAST_ROUND_DURATION>` — The duration of the fast round, in milliseconds
204205
* `--base-timeout-ms <BASE_TIMEOUT>` — The duration of the first single-leader and all multi-leader rounds
205206

@@ -236,6 +237,7 @@ Specify the complete set of new owners, by public key. Existing owners that are
236237

237238
If they are specified there must be exactly one weight for each owner. If no weights are given, every owner will have weight 100.
238239
* `--multi-leader-rounds <MULTI_LEADER_ROUNDS>` — The number of rounds in which every owner can propose blocks, i.e. the first round number in which only a single designated leader is allowed to propose blocks
240+
* `--open-multi-leader-rounds` — Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners. This should only be `true` on chains with restrictive application permissions and an application-based mechanism to select block proposers
239241
* `--fast-round-ms <FAST_ROUND_DURATION>` — The duration of the fast round, in milliseconds
240242
* `--base-timeout-ms <BASE_TIMEOUT>` — The duration of the first single-leader and all multi-leader rounds
241243

linera-base/src/ownership.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@ pub struct ChainOwnership {
5757
/// The regular owners, with their weights that determine how often they are round leader.
5858
#[debug(skip_if = BTreeMap::is_empty)]
5959
pub owners: BTreeMap<Owner, u64>,
60-
/// The number of initial rounds after 0 in which all owners are allowed to propose blocks.
60+
/// The number of rounds in which all owners are allowed to propose blocks.
6161
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,
6266
/// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
6367
pub timeout_config: TimeoutConfig,
6468
}
@@ -70,6 +74,7 @@ impl ChainOwnership {
7074
super_owners: iter::once(owner).collect(),
7175
owners: BTreeMap::new(),
7276
multi_leader_rounds: 2,
77+
open_multi_leader_rounds: false,
7378
timeout_config: TimeoutConfig::default(),
7479
}
7580
}
@@ -80,6 +85,7 @@ impl ChainOwnership {
8085
super_owners: BTreeSet::new(),
8186
owners: iter::once((owner, 100)).collect(),
8287
multi_leader_rounds: 2,
88+
open_multi_leader_rounds: false,
8389
timeout_config: TimeoutConfig::default(),
8490
}
8591
}
@@ -94,6 +100,7 @@ impl ChainOwnership {
94100
super_owners: BTreeSet::new(),
95101
owners: owners_and_weights.into_iter().collect(),
96102
multi_leader_rounds,
103+
open_multi_leader_rounds: false,
97104
timeout_config,
98105
}
99106
}
@@ -197,6 +204,7 @@ mod tests {
197204
super_owners: BTreeSet::from_iter([super_owner]),
198205
owners: BTreeMap::from_iter([(owner, 100)]),
199206
multi_leader_rounds: 10,
207+
open_multi_leader_rounds: false,
200208
timeout_config: TimeoutConfig {
201209
fast_round_duration: Some(TimeDelta::from_secs(5)),
202210
base_timeout: TimeDelta::from_secs(10),

linera-base/src/unit_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ fn chain_ownership_test_case() -> ChainOwnership {
147147
super_owners,
148148
owners,
149149
multi_leader_rounds: 5,
150+
open_multi_leader_rounds: false,
150151
timeout_config: TimeoutConfig {
151152
fast_round_duration: None,
152153
base_timeout: TimeDelta::ZERO,

linera-chain/src/manager.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,8 +579,9 @@ where
579579
false // Only super owners can propose in the first round.
580580
}
581581
Round::MultiLeader(_) => {
582+
let ownership = self.ownership.get();
582583
// Not in leader rotation mode; any owner is allowed to propose.
583-
self.ownership.get().owners.contains_key(owner)
584+
ownership.open_multi_leader_rounds || ownership.owners.contains_key(owner)
584585
}
585586
Round::SingleLeader(r) => {
586587
let Some(index) = self.round_leader_index(r) else {

linera-chain/src/unit_tests/chain_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ async fn test_block_size_limit() {
112112
let mut chain = ChainStateView::new(chain_id).await;
113113

114114
// The size of the executed valid block below.
115-
let maximum_executed_block_size = 675;
115+
let maximum_executed_block_size = 676;
116116

117117
// Initialize the chain.
118118
let mut config = make_open_chain_config();

linera-client/src/client_options.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,12 @@ pub struct ChainOwnershipConfig {
12361236
#[arg(long)]
12371237
multi_leader_rounds: Option<u32>,
12381238

1239+
/// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
1240+
/// This should only be `true` on chains with restrictive application permissions and an
1241+
/// application-based mechanism to select block proposers.
1242+
#[arg(long)]
1243+
open_multi_leader_rounds: bool,
1244+
12391245
/// The duration of the fast round, in milliseconds.
12401246
#[arg(long = "fast-round-ms", value_parser = util::parse_millis_delta)]
12411247
fast_round_duration: Option<TimeDelta>,
@@ -1277,6 +1283,7 @@ impl TryFrom<ChainOwnershipConfig> for ChainOwnership {
12771283
owner_weights,
12781284
multi_leader_rounds,
12791285
fast_round_duration,
1286+
open_multi_leader_rounds,
12801287
base_timeout,
12811288
timeout_increment,
12821289
fallback_duration,
@@ -1303,6 +1310,7 @@ impl TryFrom<ChainOwnershipConfig> for ChainOwnership {
13031310
super_owners,
13041311
owners,
13051312
multi_leader_rounds,
1313+
open_multi_leader_rounds,
13061314
timeout_config,
13071315
})
13081316
}

linera-core/src/client/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2646,6 +2646,7 @@ where
26462646
super_owners: vec![new_owner],
26472647
owners: Vec::new(),
26482648
multi_leader_rounds: 2,
2649+
open_multi_leader_rounds: false,
26492650
timeout_config: TimeoutConfig::default(),
26502651
}))
26512652
.await
@@ -2671,6 +2672,7 @@ where
26712672
super_owners: Vec::new(),
26722673
owners,
26732674
multi_leader_rounds: ownership.multi_leader_rounds,
2675+
open_multi_leader_rounds: ownership.open_multi_leader_rounds,
26742676
timeout_config: ownership.timeout_config,
26752677
})];
26762678
match self.execute_block(operations).await? {
@@ -2701,6 +2703,7 @@ where
27012703
super_owners: ownership.super_owners.into_iter().collect(),
27022704
owners: ownership.owners.into_iter().collect(),
27032705
multi_leader_rounds: ownership.multi_leader_rounds,
2706+
open_multi_leader_rounds: ownership.open_multi_leader_rounds,
27042707
timeout_config: ownership.timeout_config.clone(),
27052708
}))
27062709
.await

linera-core/src/unit_tests/client_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,7 @@ where
14481448
super_owners: Vec::new(),
14491449
owners: vec![(owner2_a, 50), (owner2_b, 50)],
14501450
multi_leader_rounds: 10,
1451+
open_multi_leader_rounds: false,
14511452
timeout_config: TimeoutConfig::default(),
14521453
});
14531454
client2_a
@@ -1579,6 +1580,7 @@ where
15791580
super_owners: Vec::new(),
15801581
owners: vec![(owner3_a, 50), (owner3_b, 50), (owner3_c, 50)],
15811582
multi_leader_rounds: 10,
1583+
open_multi_leader_rounds: false,
15821584
timeout_config: TimeoutConfig::default(),
15831585
});
15841586
client3_a

linera-core/src/unit_tests/worker_tests.rs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3230,6 +3230,7 @@ where
32303230
super_owners: Vec::new(),
32313231
owners: vec![(owner0, 100), (owner1, 100)],
32323232
multi_leader_rounds: 0,
3233+
open_multi_leader_rounds: false,
32333234
timeout_config: TimeoutConfig::default(),
32343235
})
32353236
.with_authenticated_signer(Some(owner0));
@@ -3433,6 +3434,7 @@ where
34333434
super_owners: vec![owner0],
34343435
owners: vec![(owner0, 100), (owner1, 100)],
34353436
multi_leader_rounds: 2,
3437+
open_multi_leader_rounds: false,
34363438
timeout_config: TimeoutConfig {
34373439
fast_round_duration: Some(TimeDelta::from_secs(5)),
34383440
..TimeoutConfig::default()
@@ -3450,12 +3452,11 @@ where
34503452
assert_eq!(response.info.manager.leader, None);
34513453

34523454
// So owner 1 cannot propose a block in this round. And the next round hasn't started yet.
3453-
let proposal =
3454-
make_child_block(&value0.clone()).into_proposal_with_round(&key_pairs[1], Round::Fast);
3455+
let proposal = make_child_block(&value0).into_proposal_with_round(&key_pairs[1], Round::Fast);
34553456
let result = worker.handle_block_proposal(proposal).await;
34563457
assert_matches!(result, Err(WorkerError::InvalidOwner));
3457-
let proposal = make_child_block(&value0.clone())
3458-
.into_proposal_with_round(&key_pairs[1], Round::MultiLeader(0));
3458+
let proposal =
3459+
make_child_block(&value0).into_proposal_with_round(&key_pairs[1], Round::MultiLeader(0));
34593460
let result = worker.handle_block_proposal(proposal).await;
34603461
assert_matches!(result, Err(WorkerError::ChainError(ref error))
34613462
if matches!(**error, ChainError::WrongRound(Round::Fast))
@@ -3499,6 +3500,69 @@ where
34993500
Ok(())
35003501
}
35013502

3503+
#[test_case(MemoryStorageBuilder::default(); "memory")]
3504+
#[cfg_attr(feature = "rocksdb", test_case(RocksDbStorageBuilder::new().await; "rocks_db"))]
3505+
#[cfg_attr(feature = "dynamodb", test_case(DynamoDbStorageBuilder::default(); "dynamo_db"))]
3506+
#[cfg_attr(feature = "scylladb", test_case(ScyllaDbStorageBuilder::default(); "scylla_db"))]
3507+
#[test_log::test(tokio::test)]
3508+
async fn test_open_multi_leader_rounds<B>(mut storage_builder: B) -> anyhow::Result<()>
3509+
where
3510+
B: StorageBuilder,
3511+
{
3512+
let storage = storage_builder.build().await?;
3513+
let chain_id = ChainId::root(0);
3514+
let key_pair = KeyPair::generate();
3515+
let owner = key_pair.public().into();
3516+
let description = ChainDescription::Root(0);
3517+
let (committee, worker) =
3518+
init_worker_with_chain(storage, description, owner, Amount::from_tokens(2)).await;
3519+
3520+
// Configure open multi-leader rounds.
3521+
let change_ownership_block =
3522+
make_first_block(chain_id).with_operation(SystemOperation::ChangeOwnership {
3523+
super_owners: vec![],
3524+
owners: vec![(owner, 100)],
3525+
multi_leader_rounds: 2,
3526+
open_multi_leader_rounds: true,
3527+
timeout_config: TimeoutConfig {
3528+
fast_round_duration: Some(TimeDelta::from_secs(5)),
3529+
..TimeoutConfig::default()
3530+
},
3531+
});
3532+
let (change_ownership_executed_block, _) =
3533+
worker.stage_block_execution(change_ownership_block).await?;
3534+
let change_ownership_value = Hashed::new(ConfirmedBlock::new(change_ownership_executed_block));
3535+
let change_ownership_certificate =
3536+
make_certificate(&committee, &worker, change_ownership_value.clone());
3537+
worker
3538+
.fully_handle_certificate_with_notifications(change_ownership_certificate, &())
3539+
.await?;
3540+
3541+
// The first round is the multi-leader round 0. Anyone is allowed to propose.
3542+
// But non-owners are not allowed to transfer the chain's funds.
3543+
let proposal = make_child_block(&change_ownership_value)
3544+
.with_transfer(None, Recipient::Burn, Amount::from_tokens(1))
3545+
.into_proposal_with_round(&KeyPair::generate(), Round::MultiLeader(0));
3546+
let result = worker.handle_block_proposal(proposal).await;
3547+
assert_matches!(result, Err(WorkerError::ChainError(error)) if matches!(&*error,
3548+
ChainError::ExecutionError(error, _) if matches!(&**error,
3549+
ExecutionError::SystemError(SystemExecutionError::UnauthenticatedTransferOwner
3550+
))));
3551+
3552+
// Without the transfer, a random key pair can propose a block.
3553+
let proposal = make_child_block(&change_ownership_value)
3554+
.into_proposal_with_round(&KeyPair::generate(), Round::MultiLeader(0));
3555+
let (executed_block, _) = worker
3556+
.stage_block_execution(proposal.content.proposal.clone())
3557+
.await?;
3558+
let value = Hashed::new(ConfirmedBlock::new(executed_block));
3559+
let (response, _) = worker.handle_block_proposal(proposal).await?;
3560+
let vote = response.info.manager.pending.unwrap();
3561+
assert_eq!(vote.round, Round::MultiLeader(0));
3562+
assert_eq!(vote.value.value_hash, value.hash());
3563+
Ok(())
3564+
}
3565+
35023566
#[test_case(MemoryStorageBuilder::default(); "memory")]
35033567
#[cfg_attr(feature = "rocksdb", test_case(RocksDbStorageBuilder::new().await; "rocks_db"))]
35043568
#[cfg_attr(feature = "dynamodb", test_case(DynamoDbStorageBuilder::default(); "dynamo_db"))]
@@ -3522,6 +3586,7 @@ where
35223586
super_owners: vec![owner0],
35233587
owners: vec![(owner0, 100), (owner1, 100)],
35243588
multi_leader_rounds: 3,
3589+
open_multi_leader_rounds: false,
35253590
timeout_config: TimeoutConfig {
35263591
fast_round_duration: Some(TimeDelta::from_millis(5)),
35273592
..TimeoutConfig::default()

linera-execution/src/system.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ pub enum SystemOperation {
146146
owners: Vec<(Owner, u64)>,
147147
/// The number of initial rounds after 0 in which all owners are allowed to propose blocks.
148148
multi_leader_rounds: u32,
149+
/// Whether the multi-leader rounds are unrestricted, i.e. not limited to chain owners.
150+
/// This should only be `true` on chains with restrictive application permissions and an
151+
/// application-based mechanism to select block proposers.
152+
open_multi_leader_rounds: bool,
149153
/// The timeout configuration: how long fast, multi-leader and single-leader rounds last.
150154
timeout_config: TimeoutConfig,
151155
},
@@ -472,12 +476,14 @@ where
472476
super_owners,
473477
owners,
474478
multi_leader_rounds,
479+
open_multi_leader_rounds,
475480
timeout_config,
476481
} => {
477482
self.ownership.set(ChainOwnership {
478483
super_owners: super_owners.into_iter().collect(),
479484
owners: owners.into_iter().collect(),
480485
multi_leader_rounds,
486+
open_multi_leader_rounds,
481487
timeout_config,
482488
});
483489
}

0 commit comments

Comments
 (0)