Skip to content

Commit 9cf15b8

Browse files
authored
[testnet] change-validators command; sync first. (#4833, #4854) (#4853)
Backport of #4833 and #4854. ## Motivation `set-validator` and `remove-validator` each create a new epoch just to add or remove a single validator. We sometimes want to add or remove multiple validators, and creating lots of new epochs increases the protocol overhead when syncing the admin chain. ## Proposal Add `change-validators` to add and remove multiple validators in one epoch. Also, remove the outdated `process_inbox` call and log message. As a precaution, also backport #4854 and sync before any change to the committee. ## Test Plan The reconfiguration test was updated to use this command, too. ## Release Plan - These changes should be released in a new SDK. ## Links - PRs to main: #4833, #4854 - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 5cd5202 commit 9cf15b8

File tree

5 files changed

+284
-67
lines changed

5 files changed

+284
-67
lines changed

CLI.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This document contains the help content for the `linera` command-line program.
2424
* [`linera sync-validator`](#linera-sync-validator)
2525
* [`linera set-validator`](#linera-set-validator)
2626
* [`linera remove-validator`](#linera-remove-validator)
27+
* [`linera change-validators`](#linera-change-validators)
2728
* [`linera revoke-epochs`](#linera-revoke-epochs)
2829
* [`linera resource-control-policy`](#linera-resource-control-policy)
2930
* [`linera benchmark`](#linera-benchmark)
@@ -96,6 +97,7 @@ Client implementation and command-line tool for the Linera blockchain
9697
* `sync-validator` — Synchronizes a validator with the local state of chains
9798
* `set-validator` — Add or modify a validator (admin only)
9899
* `remove-validator` — Remove a validator (admin only)
100+
* `change-validators` — Add, modify, and/or remove multiple validators in a single epoch (admin only)
99101
* `revoke-epochs` — Deprecates all committees up to and including the specified one
100102
* `resource-control-policy` — View or update the resource control policy
101103
* `benchmark` — Run benchmarks to test network performance
@@ -512,6 +514,8 @@ Synchronizes a validator with the local state of chains
512514

513515
Add or modify a validator (admin only)
514516

517+
Deprecated: Use change-validators instead, which allows adding, changing and removing any number of validators in a single operation.
518+
515519
**Usage:** `linera set-validator [OPTIONS] --public-key <PUBLIC_KEY> --account-key <ACCOUNT_KEY> --address <ADDRESS>`
516520

517521
###### **Options:**
@@ -530,6 +534,8 @@ Add or modify a validator (admin only)
530534

531535
Remove a validator (admin only)
532536

537+
Deprecated: Use change-validators instead, which allows adding, changing and removing any number of validators in a single operation.
538+
533539
**Usage:** `linera remove-validator --public-key <PUBLIC_KEY>`
534540

535541
###### **Options:**
@@ -538,6 +544,23 @@ Remove a validator (admin only)
538544

539545

540546

547+
## `linera change-validators`
548+
549+
Add, modify, and/or remove multiple validators in a single epoch (admin only)
550+
551+
This command allows you to make multiple validator changes (additions, modifications, and removals) in a single new epoch, avoiding the creation of unnecessary short-lived epochs.
552+
553+
**Usage:** `linera change-validators [OPTIONS]`
554+
555+
###### **Options:**
556+
557+
* `--add <VALIDATOR_SPEC>` — Validators to add, specified as "public_key,account_key,address,votes". Fails if the validator already exists in the committee. Can be specified multiple times. Example: --add "public_key1,account_key1,address1,1"
558+
* `--modify <VALIDATOR_SPEC>` — Validators to modify, specified as "public_key,account_key,address,votes". Fails if the validator does not exist in the committee. Can be specified multiple times. Example: --modify "public_key1,account_key1,address1,2"
559+
* `--remove <REMOVE_VALIDATORS>` — Validators to remove, specified by their public key. Fails if the validator does not exist in the committee. Can be specified multiple times. Example: --remove public_key1 --remove public_key2
560+
* `--skip-online-check` — Skip the version and genesis config checks for added and modified validators
561+
562+
563+
541564
## `linera revoke-epochs`
542565

543566
Deprecates all committees up to and including the specified one

linera-service/src/cli/command.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,34 @@ const DEFAULT_WRAP_UP_MAX_IN_FLIGHT: usize = 5;
2626
const DEFAULT_NUM_CHAINS: usize = 10;
2727
const DEFAULT_BPS: usize = 10;
2828

29+
/// Specification for a validator to be added to the committee.
30+
#[derive(Clone, Debug)]
31+
pub struct ValidatorToAdd {
32+
pub public_key: ValidatorPublicKey,
33+
pub account_key: AccountPublicKey,
34+
pub address: String,
35+
pub votes: u64,
36+
}
37+
38+
impl std::str::FromStr for ValidatorToAdd {
39+
type Err = anyhow::Error;
40+
41+
fn from_str(s: &str) -> Result<Self, Self::Err> {
42+
let parts: Vec<&str> = s.split(',').collect();
43+
anyhow::ensure!(
44+
parts.len() == 4,
45+
"Validator spec must be in format: public_key,account_key,address,votes"
46+
);
47+
48+
Ok(ValidatorToAdd {
49+
public_key: parts[0].parse()?,
50+
account_key: parts[1].parse()?,
51+
address: parts[2].to_string(),
52+
votes: parts[3].parse()?,
53+
})
54+
}
55+
}
56+
2957
#[derive(Clone, clap::Args, serde::Serialize)]
3058
#[serde(rename_all = "kebab-case")]
3159
pub struct BenchmarkOptions {
@@ -359,6 +387,9 @@ pub enum ClientCommand {
359387
},
360388

361389
/// Add or modify a validator (admin only)
390+
///
391+
/// Deprecated: Use change-validators instead, which allows adding, changing and removing
392+
/// any number of validators in a single operation.
362393
SetValidator {
363394
/// The public key of the validator.
364395
#[arg(long)]
@@ -382,12 +413,46 @@ pub enum ClientCommand {
382413
},
383414

384415
/// Remove a validator (admin only)
416+
///
417+
/// Deprecated: Use change-validators instead, which allows adding, changing and removing
418+
/// any number of validators in a single operation.
385419
RemoveValidator {
386420
/// The public key of the validator.
387421
#[arg(long)]
388422
public_key: ValidatorPublicKey,
389423
},
390424

425+
/// Add, modify, and/or remove multiple validators in a single epoch (admin only)
426+
///
427+
/// This command allows you to make multiple validator changes (additions, modifications,
428+
/// and removals) in a single new epoch, avoiding the creation of unnecessary short-lived epochs.
429+
ChangeValidators {
430+
/// Validators to add, specified as "public_key,account_key,address,votes".
431+
/// Fails if the validator already exists in the committee.
432+
/// Can be specified multiple times.
433+
/// Example: --add "public_key1,account_key1,address1,1"
434+
#[arg(long = "add", value_name = "VALIDATOR_SPEC")]
435+
add_validators: Vec<ValidatorToAdd>,
436+
437+
/// Validators to modify, specified as "public_key,account_key,address,votes".
438+
/// Fails if the validator does not exist in the committee.
439+
/// Can be specified multiple times.
440+
/// Example: --modify "public_key1,account_key1,address1,2"
441+
#[arg(long = "modify", value_name = "VALIDATOR_SPEC")]
442+
modify_validators: Vec<ValidatorToAdd>,
443+
444+
/// Validators to remove, specified by their public key.
445+
/// Fails if the validator does not exist in the committee.
446+
/// Can be specified multiple times.
447+
/// Example: --remove public_key1 --remove public_key2
448+
#[arg(long = "remove")]
449+
remove_validators: Vec<ValidatorPublicKey>,
450+
451+
/// Skip the version and genesis config checks for added and modified validators.
452+
#[arg(long)]
453+
skip_online_check: bool,
454+
},
455+
391456
/// Deprecates all committees up to and including the specified one.
392457
RevokeEpochs { epoch: Epoch },
393458

@@ -988,6 +1053,7 @@ impl ClientCommand {
9881053
| ClientCommand::SyncValidator { .. }
9891054
| ClientCommand::SetValidator { .. }
9901055
| ClientCommand::RemoveValidator { .. }
1056+
| ClientCommand::ChangeValidators { .. }
9911057
| ClientCommand::ResourceControlPolicy { .. }
9921058
| ClientCommand::RevokeEpochs { .. }
9931059
| ClientCommand::CreateGenesisConfig { .. }

linera-service/src/cli/main.rs

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ impl Runnable for Job {
605605

606606
command @ (SetValidator { .. }
607607
| RemoveValidator { .. }
608+
| ChangeValidators { .. }
608609
| ResourceControlPolicy { .. }) => {
609610
info!("Starting operations to change validator set");
610611
let time_start = Instant::now();
@@ -617,31 +618,46 @@ impl Runnable for Job {
617618

618619
let context = Arc::new(Mutex::new(context));
619620
let mut context = context.lock().await;
620-
if let SetValidator {
621-
public_key: _,
622-
account_key: _,
623-
address,
624-
votes: _,
625-
skip_online_check: false,
626-
} = &command
627-
{
628-
let node = context.make_node_provider().make_node(address)?;
629-
context
630-
.check_compatible_version_info(address, &node)
631-
.await?;
632-
context
633-
.check_matching_network_description(address, &node)
634-
.await?;
621+
match &command {
622+
SetValidator {
623+
public_key: _,
624+
account_key: _,
625+
address,
626+
votes: _,
627+
skip_online_check: false,
628+
} => {
629+
let node = context.make_node_provider().make_node(address)?;
630+
context
631+
.check_compatible_version_info(address, &node)
632+
.await?;
633+
context
634+
.check_matching_network_description(address, &node)
635+
.await?;
636+
}
637+
ChangeValidators {
638+
add_validators,
639+
modify_validators,
640+
remove_validators: _,
641+
skip_online_check: false,
642+
} => {
643+
for validator in add_validators.iter().chain(modify_validators.iter()) {
644+
let node =
645+
context.make_node_provider().make_node(&validator.address)?;
646+
context
647+
.check_compatible_version_info(&validator.address, &node)
648+
.await?;
649+
context
650+
.check_matching_network_description(&validator.address, &node)
651+
.await?;
652+
}
653+
}
654+
_ => {}
635655
}
636-
let chain_client = context.make_chain_client(context.wallet.genesis_admin_chain());
637-
let n = context
638-
.process_inbox(&chain_client)
639-
.await
640-
.unwrap()
641-
.into_iter()
642-
.map(|c| c.block().messages().len())
643-
.sum::<usize>();
644-
info!("Subscribed {} chains to new committees", n);
656+
let admin_id = context.wallet.genesis_admin_chain();
657+
let chain_client = context.make_chain_client(admin_id);
658+
// Synchronize the chain state to make sure we're applying the changes to the
659+
// latest committee.
660+
chain_client.synchronize_chain_state(admin_id).await?;
645661
let maybe_certificate = context
646662
.apply_client_command(&chain_client, |chain_client| {
647663
let chain_client = chain_client.clone();
@@ -670,10 +686,87 @@ impl Runnable for Job {
670686
}
671687
RemoveValidator { public_key } => {
672688
if validators.remove(&public_key).is_none() {
673-
warn!("Skipping removal of nonexistent validator");
689+
error!("Validator {public_key} does not exist; aborting.");
674690
return Ok(ClientOutcome::Committed(None));
675691
}
676692
}
693+
ChangeValidators {
694+
add_validators,
695+
modify_validators,
696+
remove_validators,
697+
skip_online_check: _,
698+
} => {
699+
// Validate that all validators to add do not already exist.
700+
for validator in &add_validators {
701+
if validators.contains_key(&validator.public_key) {
702+
error!(
703+
"Cannot add existing validator: {}. Aborting operation.",
704+
validator.public_key
705+
);
706+
return Ok(ClientOutcome::Committed(None));
707+
}
708+
}
709+
// Validate that all validators to modify already exist and are actually modified.
710+
for validator in &modify_validators {
711+
match validators.get(&validator.public_key) {
712+
None => {
713+
error!(
714+
"Cannot modify nonexistent validator: {}. Aborting operation.",
715+
validator.public_key
716+
);
717+
return Ok(ClientOutcome::Committed(None));
718+
}
719+
Some(existing) => {
720+
// Check that at least one field is different.
721+
if existing.network_address == validator.address
722+
&& existing.account_public_key == validator.account_key
723+
&& existing.votes == validator.votes
724+
{
725+
error!(
726+
"Validator {} is not being modified. Aborting operation.",
727+
validator.public_key
728+
);
729+
return Ok(ClientOutcome::Committed(None));
730+
}
731+
}
732+
}
733+
}
734+
// Validate that all validators to remove exist.
735+
for public_key in &remove_validators {
736+
if !validators.contains_key(public_key) {
737+
error!(
738+
"Cannot remove nonexistent validator: {public_key}. Aborting operation."
739+
);
740+
return Ok(ClientOutcome::Committed(None));
741+
}
742+
}
743+
// Add validators
744+
for validator in add_validators {
745+
validators.insert(
746+
validator.public_key,
747+
ValidatorState {
748+
network_address: validator.address,
749+
votes: validator.votes,
750+
account_public_key: validator.account_key,
751+
},
752+
);
753+
}
754+
// Modify validators
755+
for validator in modify_validators {
756+
validators.insert(
757+
validator.public_key,
758+
ValidatorState {
759+
network_address: validator.address,
760+
votes: validator.votes,
761+
account_public_key: validator.account_key,
762+
},
763+
);
764+
}
765+
// Remove validators
766+
for public_key in remove_validators {
767+
validators.remove(&public_key);
768+
}
769+
}
677770
ResourceControlPolicy {
678771
wasm_fuel_unit,
679772
evm_fuel_unit,

linera-service/src/cli_wrappers/wallet.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,35 @@ impl ClientWrapper {
10391039
Ok(())
10401040
}
10411041

1042+
pub async fn change_validators(
1043+
&self,
1044+
add_validators: &[(String, String, usize, usize)], // (public_key, account_key, port, votes)
1045+
modify_validators: &[(String, String, usize, usize)], // (public_key, account_key, port, votes)
1046+
remove_validators: &[String],
1047+
) -> Result<()> {
1048+
let mut command = self.command().await?;
1049+
command.arg("change-validators");
1050+
1051+
for (public_key, account_key, port, votes) in add_validators {
1052+
let address = format!("{}:127.0.0.1:{}", self.network.short(), port);
1053+
let validator_spec = format!("{public_key},{account_key},{address},{votes}");
1054+
command.args(["--add", &validator_spec]);
1055+
}
1056+
1057+
for (public_key, account_key, port, votes) in modify_validators {
1058+
let address = format!("{}:127.0.0.1:{}", self.network.short(), port);
1059+
let validator_spec = format!("{public_key},{account_key},{address},{votes}");
1060+
command.args(["--modify", &validator_spec]);
1061+
}
1062+
1063+
for validator_key in remove_validators {
1064+
command.args(["--remove", validator_key]);
1065+
}
1066+
1067+
command.spawn_and_wait_for_stdout().await?;
1068+
Ok(())
1069+
}
1070+
10421071
pub async fn revoke_epochs(&self, epoch: Epoch) -> Result<()> {
10431072
self.command()
10441073
.await?

0 commit comments

Comments
 (0)