Skip to content

Commit 7cc6001

Browse files
authored
change-validators command. (#4833)
## 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. ## Test Plan The reconfiguration test was updated to use this command, too. ## Release Plan - These changes should be backported `testnet_conway`, then - be released in a new SDK. ## Links - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
1 parent 29783e6 commit 7cc6001

File tree

5 files changed

+279
-66
lines changed

5 files changed

+279
-66
lines changed

CLI.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ This document contains the help content for the `linera` command-line program.
2626
* [`linera sync-all-validators`](#linera-sync-all-validators)
2727
* [`linera set-validator`](#linera-set-validator)
2828
* [`linera remove-validator`](#linera-remove-validator)
29+
* [`linera change-validators`](#linera-change-validators)
2930
* [`linera revoke-epochs`](#linera-revoke-epochs)
3031
* [`linera resource-control-policy`](#linera-resource-control-policy)
3132
* [`linera benchmark`](#linera-benchmark)
@@ -100,6 +101,7 @@ Client implementation and command-line tool for the Linera blockchain
100101
* `sync-all-validators` — Synchronizes all validators with the local state of chains
101102
* `set-validator` — Add or modify a validator (admin only)
102103
* `remove-validator` — Remove a validator (admin only)
104+
* `change-validators` — Add, modify, and/or remove multiple validators in a single epoch (admin only)
103105
* `revoke-epochs` — Deprecates all committees up to and including the specified one
104106
* `resource-control-policy` — View or update the resource control policy
105107
* `benchmark` — Run benchmarks to test network performance
@@ -543,6 +545,8 @@ Synchronizes all validators with the local state of chains
543545

544546
Add or modify a validator (admin only)
545547

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

548552
###### **Options:**
@@ -561,6 +565,8 @@ Add or modify a validator (admin only)
561565

562566
Remove a validator (admin only)
563567

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

566572
###### **Options:**
@@ -569,6 +575,23 @@ Remove a validator (admin only)
569575

570576

571577

578+
## `linera change-validators`
579+
580+
Add, modify, and/or remove multiple validators in a single epoch (admin only)
581+
582+
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.
583+
584+
**Usage:** `linera change-validators [OPTIONS]`
585+
586+
###### **Options:**
587+
588+
* `--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"
589+
* `--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"
590+
* `--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
591+
* `--skip-online-check` — Skip the version and genesis config checks for added and modified validators
592+
593+
594+
572595
## `linera revoke-epochs`
573596

574597
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 {
@@ -379,6 +407,9 @@ pub enum ClientCommand {
379407
},
380408

381409
/// Add or modify a validator (admin only)
410+
///
411+
/// Deprecated: Use change-validators instead, which allows adding, changing and removing
412+
/// any number of validators in a single operation.
382413
SetValidator {
383414
/// The public key of the validator.
384415
#[arg(long)]
@@ -402,12 +433,46 @@ pub enum ClientCommand {
402433
},
403434

404435
/// Remove a validator (admin only)
436+
///
437+
/// Deprecated: Use change-validators instead, which allows adding, changing and removing
438+
/// any number of validators in a single operation.
405439
RemoveValidator {
406440
/// The public key of the validator.
407441
#[arg(long)]
408442
public_key: ValidatorPublicKey,
409443
},
410444

445+
/// Add, modify, and/or remove multiple validators in a single epoch (admin only)
446+
///
447+
/// This command allows you to make multiple validator changes (additions, modifications,
448+
/// and removals) in a single new epoch, avoiding the creation of unnecessary short-lived epochs.
449+
ChangeValidators {
450+
/// Validators to add, specified as "public_key,account_key,address,votes".
451+
/// Fails if the validator already exists in the committee.
452+
/// Can be specified multiple times.
453+
/// Example: --add "public_key1,account_key1,address1,1"
454+
#[arg(long = "add", value_name = "VALIDATOR_SPEC")]
455+
add_validators: Vec<ValidatorToAdd>,
456+
457+
/// Validators to modify, specified as "public_key,account_key,address,votes".
458+
/// Fails if the validator does not exist in the committee.
459+
/// Can be specified multiple times.
460+
/// Example: --modify "public_key1,account_key1,address1,2"
461+
#[arg(long = "modify", value_name = "VALIDATOR_SPEC")]
462+
modify_validators: Vec<ValidatorToAdd>,
463+
464+
/// Validators to remove, specified by their public key.
465+
/// Fails if the validator does not exist in the committee.
466+
/// Can be specified multiple times.
467+
/// Example: --remove public_key1 --remove public_key2
468+
#[arg(long = "remove")]
469+
remove_validators: Vec<ValidatorPublicKey>,
470+
471+
/// Skip the version and genesis config checks for added and modified validators.
472+
#[arg(long)]
473+
skip_online_check: bool,
474+
},
475+
411476
/// Deprecates all committees up to and including the specified one.
412477
RevokeEpochs { epoch: Epoch },
413478

@@ -1010,6 +1075,7 @@ impl ClientCommand {
10101075
| ClientCommand::SyncAllValidators { .. }
10111076
| ClientCommand::SetValidator { .. }
10121077
| ClientCommand::RemoveValidator { .. }
1078+
| ClientCommand::ChangeValidators { .. }
10131079
| ClientCommand::ResourceControlPolicy { .. }
10141080
| ClientCommand::RevokeEpochs { .. }
10151081
| ClientCommand::CreateGenesisConfig { .. }

linera-service/src/cli/main.rs

Lines changed: 113 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -624,38 +624,50 @@ impl Runnable for Job {
624624

625625
command @ (SetValidator { .. }
626626
| RemoveValidator { .. }
627+
| ChangeValidators { .. }
627628
| ResourceControlPolicy { .. }) => {
628629
info!("Starting operations to change validator set");
629630
let time_start = Instant::now();
630631
let context = options.create_client_context(storage, wallet, signer.into_value());
631632

632633
let context = Arc::new(Mutex::new(context));
633634
let mut context = context.lock().await;
634-
if let SetValidator {
635-
public_key: _,
636-
account_key: _,
637-
address,
638-
votes: _,
639-
skip_online_check: false,
640-
} = &command
641-
{
642-
let node = context.make_node_provider().make_node(address)?;
643-
context
644-
.check_compatible_version_info(address, &node)
645-
.await?;
646-
context
647-
.check_matching_network_description(address, &node)
648-
.await?;
635+
match &command {
636+
SetValidator {
637+
public_key: _,
638+
account_key: _,
639+
address,
640+
votes: _,
641+
skip_online_check: false,
642+
} => {
643+
let node = context.make_node_provider().make_node(address)?;
644+
context
645+
.check_compatible_version_info(address, &node)
646+
.await?;
647+
context
648+
.check_matching_network_description(address, &node)
649+
.await?;
650+
}
651+
ChangeValidators {
652+
add_validators,
653+
modify_validators,
654+
remove_validators: _,
655+
skip_online_check: false,
656+
} => {
657+
for validator in add_validators.iter().chain(modify_validators.iter()) {
658+
let node =
659+
context.make_node_provider().make_node(&validator.address)?;
660+
context
661+
.check_compatible_version_info(&validator.address, &node)
662+
.await?;
663+
context
664+
.check_matching_network_description(&validator.address, &node)
665+
.await?;
666+
}
667+
}
668+
_ => {}
649669
}
650670
let chain_client = context.make_chain_client(context.wallet.genesis_admin_chain());
651-
let n = context
652-
.process_inbox(&chain_client)
653-
.await
654-
.unwrap()
655-
.into_iter()
656-
.map(|c| c.block().messages().len())
657-
.sum::<usize>();
658-
info!("Subscribed {} chains to new committees", n);
659671
let maybe_certificate = context
660672
.apply_client_command(&chain_client, |chain_client| {
661673
let chain_client = chain_client.clone();
@@ -684,10 +696,87 @@ impl Runnable for Job {
684696
}
685697
RemoveValidator { public_key } => {
686698
if validators.remove(&public_key).is_none() {
687-
warn!("Skipping removal of nonexistent validator");
699+
error!("Validator {public_key} does not exist; aborting.");
688700
return Ok(ClientOutcome::Committed(None));
689701
}
690702
}
703+
ChangeValidators {
704+
add_validators,
705+
modify_validators,
706+
remove_validators,
707+
skip_online_check: _,
708+
} => {
709+
// Validate that all validators to add do not already exist.
710+
for validator in &add_validators {
711+
if validators.contains_key(&validator.public_key) {
712+
error!(
713+
"Cannot add existing validator: {}. Aborting operation.",
714+
validator.public_key
715+
);
716+
return Ok(ClientOutcome::Committed(None));
717+
}
718+
}
719+
// Validate that all validators to modify already exist and are actually modified.
720+
for validator in &modify_validators {
721+
match validators.get(&validator.public_key) {
722+
None => {
723+
error!(
724+
"Cannot modify nonexistent validator: {}. Aborting operation.",
725+
validator.public_key
726+
);
727+
return Ok(ClientOutcome::Committed(None));
728+
}
729+
Some(existing) => {
730+
// Check that at least one field is different.
731+
if existing.network_address == validator.address
732+
&& existing.account_public_key == validator.account_key
733+
&& existing.votes == validator.votes
734+
{
735+
error!(
736+
"Validator {} is not being modified. Aborting operation.",
737+
validator.public_key
738+
);
739+
return Ok(ClientOutcome::Committed(None));
740+
}
741+
}
742+
}
743+
}
744+
// Validate that all validators to remove exist.
745+
for public_key in &remove_validators {
746+
if !validators.contains_key(public_key) {
747+
error!(
748+
"Cannot remove nonexistent validator: {public_key}. Aborting operation."
749+
);
750+
return Ok(ClientOutcome::Committed(None));
751+
}
752+
}
753+
// Add validators
754+
for validator in add_validators {
755+
validators.insert(
756+
validator.public_key,
757+
ValidatorState {
758+
network_address: validator.address,
759+
votes: validator.votes,
760+
account_public_key: validator.account_key,
761+
},
762+
);
763+
}
764+
// Modify validators
765+
for validator in modify_validators {
766+
validators.insert(
767+
validator.public_key,
768+
ValidatorState {
769+
network_address: validator.address,
770+
votes: validator.votes,
771+
account_public_key: validator.account_key,
772+
},
773+
);
774+
}
775+
// Remove validators
776+
for public_key in remove_validators {
777+
validators.remove(&public_key);
778+
}
779+
}
691780
ResourceControlPolicy {
692781
wasm_fuel_unit,
693782
evm_fuel_unit,

linera-service/src/cli_wrappers/wallet.rs

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

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

0 commit comments

Comments
 (0)