Add voluntary exit via validator manager (#6612)

* #4303
* #4804


  -Add voluntary exit feature to the validator manager
-Add delete all validators by using the keyword "all"
This commit is contained in:
chonghe
2025-07-01 11:07:49 +08:00
committed by GitHub
parent c1f94d9b7b
commit 257d270718
14 changed files with 983 additions and 23 deletions

3
Cargo.lock generated
View File

@@ -9933,6 +9933,7 @@ name = "validator_manager"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"account_utils", "account_utils",
"beacon_chain",
"clap", "clap",
"clap_utils", "clap_utils",
"derivative", "derivative",
@@ -9942,9 +9943,11 @@ dependencies = [
"eth2_wallet", "eth2_wallet",
"ethereum_serde_utils", "ethereum_serde_utils",
"hex", "hex",
"http_api",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"slot_clock",
"tempfile", "tempfile",
"tokio", "tokio",
"tree_hash", "tree_hash",

View File

@@ -28,6 +28,10 @@ Commands:
delete delete
Deletes one or more validators from a validator client using the HTTP Deletes one or more validators from a validator client using the HTTP
API. API.
exit
Exits one or more validators using the HTTP API. It can also be used
to generate a presigned voluntary exit message for a particular future
epoch.
help help
Print this message or the help of the given subcommand(s) Print this message or the help of the given subcommand(s)

View File

@@ -32,4 +32,4 @@ The `validator-manager` boasts the following features:
- [Creating and importing validators using the `create` and `import` commands.](./validator_manager_create.md) - [Creating and importing validators using the `create` and `import` commands.](./validator_manager_create.md)
- [Moving validators between two VCs using the `move` command.](./validator_manager_move.md) - [Moving validators between two VCs using the `move` command.](./validator_manager_move.md)
- [Managing validators such as delete, import and list validators.](./validator_manager_api.md) - [Managing validators such as exit, delete, import and list validators.](./validator_manager_api.md)

View File

@@ -2,6 +2,54 @@
The `lighthouse validator-manager` uses the [Keymanager API](https://ethereum.github.io/keymanager-APIs/#/) to list, import and delete keystores via the HTTP API. This requires the validator client running with the flag `--http`. By default, the validator client HTTP address is `http://localhost:5062`. If a different IP address or port is used, add the flag `--vc-url http://IP:port_number` to the command below. The `lighthouse validator-manager` uses the [Keymanager API](https://ethereum.github.io/keymanager-APIs/#/) to list, import and delete keystores via the HTTP API. This requires the validator client running with the flag `--http`. By default, the validator client HTTP address is `http://localhost:5062`. If a different IP address or port is used, add the flag `--vc-url http://IP:port_number` to the command below.
## Exit
The `exit` command exits one or more validators from the validator client. To `exit`:
> **Important note: Once the --beacon-node flag is used, it will publish the voluntary exit to the network. This action is irreversible.**
```bash
lighthouse vm exit --vc-token <API-TOKEN-PATH> --validators pubkey1,pubkey2 --beacon-node http://beacon-node-url:5052
```
Example:
```bash
lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4,0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f --beacon-node http://localhost:5052
```
If successful, the following log will be returned:
```text
Successfully validated and published voluntary exit for validator 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4
Successfully validated and published voluntary exit for validator
0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f
```
To exit all validators on the validator client, use the keyword `all`:
```bash
lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --beacon-node http://localhost:5052
```
To check the voluntary exit status, refer to [the list command](./validator_manager_api.md#list).
The following command will only generate a presigned voluntary exit message and save it to a file named `{validator_pubkey}.json`. It **will not** publish the voluntary exit to the network.
To generate a presigned exit message and save it to a file, use the flag `--presign`:
```bash
lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --presign
```
To generate a presigned exit message for a particular (future) epoch, use the flag `--exit-epoch`:
```bash
lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --presign --exit-epoch 1234567
```
The generated presigned exit message will only be valid at or after the specified exit-epoch, in this case, epoch 1234567.
## Delete ## Delete
The `delete` command deletes one or more validators from the validator client. It will also modify the `validator_definitions.yml` file automatically so there is no manual action required from the user after the delete. To `delete`: The `delete` command deletes one or more validators from the validator client. It will also modify the `validator_definitions.yml` file automatically so there is no manual action required from the user after the delete. To `delete`:
@@ -16,6 +64,12 @@ Example:
lighthouse vm delete --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4,0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f lighthouse vm delete --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4,0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f
``` ```
To delete all validators on the validator client, use the keyword `all`:
```bash
lighthouse vm delete --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all
```
## Import ## Import
The `import` command imports validator keystores generated by the `ethstaker-deposit-cli`. To import a validator keystore: The `import` command imports validator keystores generated by the `ethstaker-deposit-cli`. To import a validator keystore:
@@ -37,3 +91,26 @@ To list the validators running on the validator client:
```bash ```bash
lighthouse vm list --vc-token ~/.lighthouse/mainnet/validators/api-token.txt lighthouse vm list --vc-token ~/.lighthouse/mainnet/validators/api-token.txt
``` ```
The `list` command can also be used to check the voluntary exit status of validators. To do so, use both `--beacon-node` and `--validators` flags. The `--validators` flag accepts a comma-separated list of validator public keys, or the keyword `all` to check the voluntary exit status of all validators attached to the validator client.
```bash
lighthouse vm list --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 --beacon-node http://localhost:5052
```
If the validator voluntary exit has been accepted by the chain, the following log will be returned:
```text
Voluntary exit for validator 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 has been accepted into the beacon chain, but not yet finalized. Finalization may take several minutes or longer. Before finalization there is a low probability that the exit may be reverted.
Current epoch: 2, Exit epoch: 7, Withdrawable epoch: 263
Please keep your validator running till exit epoch
Exit epoch in approximately 480 secs
```
When the exit epoch is reached, querying the status will return:
```text
Validator 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 has exited at epoch: 7
```
You can safely shut down the validator client at this point.

View File

@@ -10,6 +10,8 @@ A validator can initiate a voluntary exit provided that the validator is current
It takes at a minimum 5 epochs (32 minutes) for a validator to exit after initiating a voluntary exit. It takes at a minimum 5 epochs (32 minutes) for a validator to exit after initiating a voluntary exit.
This number can be much higher depending on how many other validators are queued to exit. This number can be much higher depending on how many other validators are queued to exit.
You can also perform voluntary exit for one or more validators using the validator manager, see [Managing Validators](./validator_manager_api.md#exit) for more details.
## Initiating a voluntary exit ## Initiating a voluntary exit
In order to initiate an exit, users can use the `lighthouse account validator exit` command. In order to initiate an exit, users can use the `lighthouse account validator exit` command.

View File

@@ -10,6 +10,7 @@ use types::*;
use validator_manager::{ use validator_manager::{
create_validators::CreateConfig, create_validators::CreateConfig,
delete_validators::DeleteConfig, delete_validators::DeleteConfig,
exit_validators::ExitConfig,
import_validators::ImportConfig, import_validators::ImportConfig,
list_validators::ListConfig, list_validators::ListConfig,
move_validators::{MoveConfig, PasswordSource, Validators}, move_validators::{MoveConfig, PasswordSource, Validators},
@@ -119,6 +120,12 @@ impl CommandLineTest<DeleteConfig> {
} }
} }
impl CommandLineTest<ExitConfig> {
fn validators_exit() -> Self {
Self::default().flag("exit", None)
}
}
#[test] #[test]
pub fn validator_create_without_output_path() { pub fn validator_create_without_output_path() {
CommandLineTest::validators_create().assert_failed(); CommandLineTest::validators_create().assert_failed();
@@ -443,6 +450,8 @@ pub fn validator_list_defaults() {
let expected = ListConfig { let expected = ListConfig {
vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(),
vc_token_path: PathBuf::from("./token.json"), vc_token_path: PathBuf::from("./token.json"),
beacon_url: None,
validators_to_display: vec![],
}; };
assert_eq!(expected, config); assert_eq!(expected, config);
}); });
@@ -468,3 +477,106 @@ pub fn validator_delete_defaults() {
assert_eq!(expected, config); assert_eq!(expected, config);
}); });
} }
#[test]
pub fn validator_delete_missing_validator_flag() {
CommandLineTest::validators_delete()
.flag("--vc-token", Some("./token.json"))
.assert_failed();
}
#[test]
pub fn validator_exit_defaults() {
CommandLineTest::validators_exit()
.flag(
"--validators",
Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)),
)
.flag("--vc-token", Some("./token.json"))
.flag("--beacon-node", Some("http://localhost:5052"))
.assert_success(|config| {
let expected = ExitConfig {
vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(),
vc_token_path: PathBuf::from("./token.json"),
validators_to_exit: vec![
PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(),
PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(),
],
beacon_url: Some(SensitiveUrl::parse("http://localhost:5052").unwrap()),
exit_epoch: None,
presign: false,
};
assert_eq!(expected, config);
});
}
#[test]
pub fn validator_exit_exit_epoch_and_presign_flags() {
CommandLineTest::validators_exit()
.flag(
"--validators",
Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)),
)
.flag("--vc-token", Some("./token.json"))
.flag("--exit-epoch", Some("1234567"))
.flag("--presign", None)
.assert_success(|config| {
let expected = ExitConfig {
vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(),
vc_token_path: PathBuf::from("./token.json"),
validators_to_exit: vec![
PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(),
PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(),
],
beacon_url: None,
exit_epoch: Some(Epoch::new(1234567)),
presign: true,
};
assert_eq!(expected, config);
});
}
#[test]
pub fn validator_exit_missing_validator_flag() {
CommandLineTest::validators_exit()
.flag("--vc-token", Some("./token.json"))
.assert_failed();
}
#[test]
pub fn validator_exit_using_beacon_and_presign_flags() {
CommandLineTest::validators_exit()
.flag("--vc-token", Some("./token.json"))
.flag(
"--validators",
Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)),
)
.flag("--beacon-node", Some("http://localhost:1001"))
.flag("--presign", None)
.assert_failed();
}
#[test]
pub fn validator_exit_using_beacon_and_exit_epoch_flags() {
CommandLineTest::validators_exit()
.flag("--vc-token", Some("./token.json"))
.flag(
"--validators",
Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)),
)
.flag("--beacon-node", Some("http://localhost:1001"))
.flag("--exit-epoch", Some("1234567"))
.assert_failed();
}
#[test]
pub fn validator_exit_exit_epoch_flag_without_presign_flag() {
CommandLineTest::validators_exit()
.flag("--vc-token", Some("./token.json"))
.flag(
"--validators",
Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)),
)
.flag("--exit-epoch", Some("1234567"))
.assert_failed();
}

View File

@@ -26,6 +26,7 @@ use std::time::Duration;
use task_executor::test_utils::TestRuntime; use task_executor::test_utils::TestRuntime;
use tempfile::{tempdir, TempDir}; use tempfile::{tempdir, TempDir};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use types::ChainSpec;
use validator_services::block_service::BlockService; use validator_services::block_service::BlockService;
use zeroize::Zeroizing; use zeroize::Zeroizing;
@@ -61,6 +62,7 @@ pub struct ApiTester {
pub _server_shutdown: oneshot::Sender<()>, pub _server_shutdown: oneshot::Sender<()>,
pub validator_dir: TempDir, pub validator_dir: TempDir,
pub secrets_dir: TempDir, pub secrets_dir: TempDir,
pub spec: Arc<ChainSpec>,
} }
impl ApiTester { impl ApiTester {
@@ -69,6 +71,19 @@ impl ApiTester {
} }
pub async fn new_with_http_config(http_config: HttpConfig) -> Self { pub async fn new_with_http_config(http_config: HttpConfig) -> Self {
let slot_clock =
TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1));
let genesis_validators_root = Hash256::repeat_byte(42);
let spec = Arc::new(E::default_spec());
Self::new_with_options(http_config, slot_clock, genesis_validators_root, spec).await
}
pub async fn new_with_options(
http_config: HttpConfig,
slot_clock: TestingSlotClock,
genesis_validators_root: Hash256,
spec: Arc<ChainSpec>,
) -> Self {
let validator_dir = tempdir().unwrap(); let validator_dir = tempdir().unwrap();
let secrets_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap();
let token_path = tempdir().unwrap().path().join(PK_FILENAME); let token_path = tempdir().unwrap().path().join(PK_FILENAME);
@@ -91,20 +106,15 @@ impl ApiTester {
..Default::default() ..Default::default()
}; };
let spec = Arc::new(E::default_spec());
let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME);
let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap();
let slot_clock =
TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1));
let test_runtime = TestRuntime::default(); let test_runtime = TestRuntime::default();
let validator_store = Arc::new(LighthouseValidatorStore::new( let validator_store = Arc::new(LighthouseValidatorStore::new(
initialized_validators, initialized_validators,
slashing_protection, slashing_protection,
Hash256::repeat_byte(42), genesis_validators_root,
spec.clone(), spec.clone(),
Some(Arc::new(DoppelgangerService::default())), Some(Arc::new(DoppelgangerService::default())),
slot_clock.clone(), slot_clock.clone(),
@@ -127,7 +137,7 @@ impl ApiTester {
validator_store: Some(validator_store.clone()), validator_store: Some(validator_store.clone()),
graffiti_file: None, graffiti_file: None,
graffiti_flag: Some(Graffiti::default()), graffiti_flag: Some(Graffiti::default()),
spec, spec: spec.clone(),
config: http_config, config: http_config,
sse_logging_components: None, sse_logging_components: None,
slot_clock, slot_clock,
@@ -161,6 +171,7 @@ impl ApiTester {
_server_shutdown: shutdown_tx, _server_shutdown: shutdown_tx,
validator_dir, validator_dir,
secrets_dir, secrets_dir,
spec,
} }
} }

View File

@@ -17,12 +17,15 @@ ethereum_serde_utils = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
slot_clock = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tree_hash = { workspace = true } tree_hash = { workspace = true }
types = { workspace = true } types = { workspace = true }
zeroize = { workspace = true } zeroize = { workspace = true }
[dev-dependencies] [dev-dependencies]
beacon_chain = { workspace = true }
http_api = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
validator_http_api = { workspace = true } validator_http_api = { workspace = true }

View File

@@ -45,7 +45,10 @@ pub fn cli_app() -> Command {
Arg::new(VALIDATOR_FLAG) Arg::new(VALIDATOR_FLAG)
.long(VALIDATOR_FLAG) .long(VALIDATOR_FLAG)
.value_name("STRING") .value_name("STRING")
.help("Comma-separated list of validators (pubkey) that will be deleted.") .help(
"Comma-separated list of validators (pubkey) that will be deleted. \
To delete all validators, use the keyword \"all\".",
)
.action(ArgAction::Set) .action(ArgAction::Set)
.required(true) .required(true)
.display_order(0), .display_order(0),
@@ -64,10 +67,14 @@ impl DeleteConfig {
let validators_to_delete_str = let validators_to_delete_str =
clap_utils::parse_required::<String>(matches, VALIDATOR_FLAG)?; clap_utils::parse_required::<String>(matches, VALIDATOR_FLAG)?;
let validators_to_delete = validators_to_delete_str let validators_to_delete = if validators_to_delete_str.trim() == "all" {
.split(',') Vec::new()
.map(|s| s.trim().parse()) } else {
.collect::<Result<Vec<PublicKeyBytes>, _>>()?; validators_to_delete_str
.split(',')
.map(|s| s.trim().parse())
.collect::<Result<Vec<PublicKeyBytes>, _>>()?
};
Ok(Self { Ok(Self {
vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?,
@@ -90,11 +97,16 @@ async fn run(config: DeleteConfig) -> Result<(), String> {
let DeleteConfig { let DeleteConfig {
vc_url, vc_url,
vc_token_path, vc_token_path,
validators_to_delete, mut validators_to_delete,
} = config; } = config;
let (http_client, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; let (http_client, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?;
// Delete all validators on the VC
if validators_to_delete.is_empty() {
validators_to_delete = validators.iter().map(|v| v.validating_pubkey).collect();
}
for validator_to_delete in &validators_to_delete { for validator_to_delete in &validators_to_delete {
if !validators if !validators
.iter() .iter()

View File

@@ -0,0 +1,585 @@
use crate::{common::vc_http_client, DumpConfig};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap_utils::FLAG_HEADER;
use eth2::types::{ConfigAndPreset, Epoch, StateId, ValidatorId, ValidatorStatus};
use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts};
use serde::{Deserialize, Serialize};
use serde_json;
use slot_clock::{SlotClock, SystemTimeSlotClock};
use std::fs::write;
use std::path::PathBuf;
use std::time::Duration;
use types::{ChainSpec, EthSpec, PublicKeyBytes};
pub const CMD: &str = "exit";
pub const BEACON_URL_FLAG: &str = "beacon-node";
pub const VC_URL_FLAG: &str = "vc-url";
pub const VC_TOKEN_FLAG: &str = "vc-token";
pub const VALIDATOR_FLAG: &str = "validators";
pub const EXIT_EPOCH_FLAG: &str = "exit-epoch";
pub const PRESIGN_FLAG: &str = "presign";
pub fn cli_app() -> Command {
Command::new(CMD)
.about(
"Exits one or more validators using the HTTP API. It can \
also be used to generate a presigned voluntary exit message for a particular future epoch.",
)
.arg(
Arg::new(BEACON_URL_FLAG)
.long(BEACON_URL_FLAG)
.value_name("NETWORK_ADDRESS")
.help("Address to a beacon node HTTP API")
.action(ArgAction::Set)
.display_order(0)
.conflicts_with(PRESIGN_FLAG),
)
.arg(
Arg::new(VC_URL_FLAG)
.long(VC_URL_FLAG)
.value_name("HTTP_ADDRESS")
.help("A HTTP(S) address of a validator client using the keymanager-API.")
.default_value("http://localhost:5062")
.requires(VC_TOKEN_FLAG)
.action(ArgAction::Set)
.display_order(0),
)
.arg(
Arg::new(VC_TOKEN_FLAG)
.long(VC_TOKEN_FLAG)
.value_name("PATH")
.help("The file containing a token required by the validator client.")
.action(ArgAction::Set)
.display_order(0),
)
.arg(
Arg::new(VALIDATOR_FLAG)
.long(VALIDATOR_FLAG)
.value_name("STRING")
.help(
"Comma-separated list of validators (pubkey) to exit. \
To exit all validators, use the keyword \"all\".",
)
.action(ArgAction::Set)
.required(true)
.display_order(0),
)
.arg(
Arg::new(EXIT_EPOCH_FLAG)
.long(EXIT_EPOCH_FLAG)
.value_name("EPOCH")
.help(
"Provide the minimum epoch for processing voluntary exit. \
This flag is required to be used in combination with `--presign` to \
save the voluntary exit presign to a file for future use.",
)
.action(ArgAction::Set)
.display_order(0)
.requires(PRESIGN_FLAG)
.conflicts_with(BEACON_URL_FLAG),
)
.arg(
Arg::new(PRESIGN_FLAG)
.long(PRESIGN_FLAG)
.help(
"Generate the voluntary exit presign and save it to a file \
named {validator_pubkey}.json. Note: Using this without the \
`--beacon-node` flag will not publish the voluntary exit to the network.",
)
.help_heading(FLAG_HEADER)
.action(ArgAction::SetTrue)
.display_order(0)
.conflicts_with(BEACON_URL_FLAG),
)
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ExitConfig {
pub vc_url: SensitiveUrl,
pub vc_token_path: PathBuf,
pub validators_to_exit: Vec<PublicKeyBytes>,
pub beacon_url: Option<SensitiveUrl>,
pub exit_epoch: Option<Epoch>,
pub presign: bool,
}
impl ExitConfig {
fn from_cli(matches: &ArgMatches) -> Result<Self, String> {
let validators_to_exit_str = clap_utils::parse_required::<String>(matches, VALIDATOR_FLAG)?;
// Keyword "all" to exit all validators, vector to be created later
let validators_to_exit = if validators_to_exit_str.trim() == "all" {
Vec::new()
} else {
validators_to_exit_str
.split(',')
.map(|s| s.trim().parse())
.collect::<Result<Vec<PublicKeyBytes>, _>>()?
};
Ok(Self {
vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?,
vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?,
validators_to_exit,
beacon_url: clap_utils::parse_optional(matches, BEACON_URL_FLAG)?,
exit_epoch: clap_utils::parse_optional(matches, EXIT_EPOCH_FLAG)?,
presign: matches.get_flag(PRESIGN_FLAG),
})
}
}
pub async fn cli_run<E: EthSpec>(
matches: &ArgMatches,
dump_config: DumpConfig,
) -> Result<(), String> {
let config = ExitConfig::from_cli(matches)?;
if dump_config.should_exit_early(&config)? {
Ok(())
} else {
run::<E>(config).await
}
}
async fn run<E: EthSpec>(config: ExitConfig) -> Result<(), String> {
let ExitConfig {
vc_url,
vc_token_path,
mut validators_to_exit,
beacon_url,
exit_epoch,
presign,
} = config;
let (http_client, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?;
if validators_to_exit.is_empty() {
validators_to_exit = validators.iter().map(|v| v.validating_pubkey).collect();
}
for validator_to_exit in validators_to_exit {
// Check that the validators_to_exit is in the validator client
if !validators
.iter()
.any(|validator| validator.validating_pubkey == validator_to_exit)
{
return Err(format!("Validator {} doesn't exist", validator_to_exit));
}
let exit_message = http_client
.post_validator_voluntary_exit(&validator_to_exit, exit_epoch)
.await
.map_err(|e| format!("Failed to generate voluntary exit message: {}", e))?;
if presign {
let exit_message_json = serde_json::to_string(&exit_message.data);
match exit_message_json {
Ok(json) => {
// Save the exit message to JSON file(s)
let file_path = format!("{}.json", validator_to_exit);
write(&file_path, json).map_err(|e| {
format!("Failed to write voluntary exit message to file: {}", e)
})?;
println!("Voluntary exit message saved to {}", file_path);
}
Err(e) => eprintln!("Failed to serialize voluntary exit message: {}", e),
}
}
// Only publish the voluntary exit if the --beacon-node flag is present
if let Some(ref beacon_url) = beacon_url {
let beacon_node = BeaconNodeHttpClient::new(
SensitiveUrl::parse(beacon_url.as_ref())
.map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?,
Timeouts::set_all(Duration::from_secs(12)),
);
if beacon_node
.get_node_syncing()
.await
.map_err(|e| format!("Failed to get beacon node sync status: {:?}", e))?
.data
.is_syncing
{
return Err(
"Beacon node is syncing, submit the voluntary exit later when beacon node is synced"
.to_string(),
);
}
let genesis_data = beacon_node
.get_beacon_genesis()
.await
.map_err(|e| format!("Failed to get genesis data: {}", e))?
.data;
let config_and_preset = beacon_node
.get_config_spec::<ConfigAndPreset>()
.await
.map_err(|e| format!("Failed to get config spec: {}", e))?
.data;
let spec = ChainSpec::from_config::<E>(config_and_preset.config())
.ok_or("Failed to create chain spec")?;
let validator_data = beacon_node
.get_beacon_states_validator_id(
StateId::Head,
&ValidatorId::PublicKey(validator_to_exit),
)
.await
.map_err(|e| format!("Failed to get validator details: {:?}", e))?
.ok_or_else(|| {
format!(
"Validator {} is not present in the beacon state. \
Please ensure that your beacon node is synced \
and the validator has been deposited.",
validator_to_exit
)
})?
.data;
let activation_epoch = validator_data.validator.activation_epoch;
let current_epoch = get_current_epoch::<E>(genesis_data.genesis_time, &spec)
.ok_or("Failed to get current epoch. Please check your system time")?;
// Check if validator is eligible for exit
if validator_data.status == ValidatorStatus::ActiveOngoing
&& current_epoch < activation_epoch + spec.shard_committee_period
{
eprintln!(
"Validator {} is not eligible for exit. It will become eligible at epoch {}",
validator_to_exit,
activation_epoch + spec.shard_committee_period
)
} else if validator_data.status != ValidatorStatus::ActiveOngoing {
eprintln!(
"Validator {} is not eligible for exit. Validator status is: {:?}",
validator_to_exit, validator_data.status
)
} else {
// Only publish voluntary exit if validator status is ActiveOngoing
beacon_node
.post_beacon_pool_voluntary_exits(&exit_message.data)
.await
.map_err(|e| format!("Failed to publish voluntary exit: {}", e))?;
eprintln!(
"Successfully validated and published voluntary exit for validator {}",
validator_to_exit
);
}
}
}
Ok(())
}
pub fn get_current_epoch<E: EthSpec>(genesis_time: u64, spec: &ChainSpec) -> Option<Epoch> {
let slot_clock = SystemTimeSlotClock::new(
spec.genesis_slot,
Duration::from_secs(genesis_time),
Duration::from_secs(spec.seconds_per_slot),
);
slot_clock.now().map(|s| s.epoch(E::slots_per_epoch()))
}
#[cfg(not(debug_assertions))]
#[cfg(test)]
mod test {
use super::*;
use crate::{
common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder,
};
use account_utils::eth2_keystore::KeystoreBuilder;
use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy};
use eth2::lighthouse_vc::types::KeystoreJsonStr;
use http_api::test_utils::InteractiveTester;
use std::{
fs::{self, File},
io::Write,
sync::Arc,
};
use types::{ChainSpec, MainnetEthSpec};
use validator_http_api::{test_utils::ApiTester, Config as HttpConfig};
use zeroize::Zeroizing;
type E = MainnetEthSpec;
struct TestBuilder {
exit_config: Option<ExitConfig>,
src_import_builder: Option<ImportTestBuilder>,
http_config: HttpConfig,
vc_token: Option<String>,
validators: Vec<ValidatorSpecification>,
beacon_node: InteractiveTester<E>,
index_of_validators_to_exit: Vec<usize>,
spec: Arc<ChainSpec>,
}
impl TestBuilder {
async fn new() -> Self {
let mut spec = ChainSpec::mainnet();
spec.shard_committee_period = 1;
spec.altair_fork_epoch = Some(Epoch::new(0));
spec.bellatrix_fork_epoch = Some(Epoch::new(1));
spec.capella_fork_epoch = Some(Epoch::new(2));
spec.deneb_fork_epoch = Some(Epoch::new(3));
let beacon_node = InteractiveTester::new(Some(spec.clone()), 64).await;
let harness = &beacon_node.harness;
let mock_el = harness.mock_execution_layer.as_ref().unwrap();
let execution_ctx = mock_el.server.ctx.clone();
// Move to terminal block.
mock_el.server.all_payloads_valid();
execution_ctx
.execution_block_generator
.write()
.move_to_terminal_block()
.unwrap();
Self {
exit_config: None,
src_import_builder: None,
http_config: ApiTester::default_http_config(),
vc_token: None,
validators: vec![],
beacon_node,
index_of_validators_to_exit: vec![],
spec: spec.into(),
}
}
async fn with_validators(mut self, index_of_validators_to_exit: Vec<usize>) -> Self {
// Ensure genesis validators root matches the beacon node.
let genesis_validators_root = self
.beacon_node
.harness
.get_current_state()
.genesis_validators_root();
// And use a single slot clock and same spec for BN and VC to keep things simple.
let slot_clock = self.beacon_node.harness.chain.slot_clock.clone();
let vc = ApiTester::new_with_options(
self.http_config.clone(),
slot_clock,
genesis_validators_root,
self.spec.clone(),
)
.await;
let mut builder = ImportTestBuilder::new_with_vc(vc).await;
self.vc_token =
Some(fs::read_to_string(builder.get_import_config().vc_token_path).unwrap());
let local_validators: Vec<ValidatorSpecification> = index_of_validators_to_exit
.iter()
.map(|&index| {
let keystore = KeystoreBuilder::new(
&self.beacon_node.harness.validator_keypairs[index],
"password".as_bytes(),
"".into(),
)
.unwrap()
.build()
.unwrap();
ValidatorSpecification {
voting_keystore: KeystoreJsonStr(keystore),
voting_keystore_password: Zeroizing::new("password".into()),
slashing_protection: None,
fee_recipient: None,
gas_limit: None,
builder_proposals: None,
builder_boost_factor: None,
prefer_builder_proposals: None,
enabled: Some(true),
}
})
.collect();
let beacon_url = SensitiveUrl::parse(self.beacon_node.client.as_ref()).unwrap();
let validators_to_exit = index_of_validators_to_exit
.iter()
.map(|&index| {
self.beacon_node.harness.validator_keypairs[index]
.pk
.clone()
.into()
})
.collect();
let import_config = builder.get_import_config();
let validators_dir = import_config.vc_token_path.parent().unwrap();
let validators_file = validators_dir.join("validators.json");
builder = builder.mutate_import_config(|config| {
config.validators_file_path = Some(validators_file.clone());
});
fs::write(
&validators_file,
serde_json::to_string(&local_validators).unwrap(),
)
.unwrap();
self.exit_config = Some(ExitConfig {
vc_url: import_config.vc_url,
vc_token_path: import_config.vc_token_path,
validators_to_exit,
beacon_url: Some(beacon_url),
exit_epoch: None,
presign: false,
});
self.validators = local_validators.clone();
self.src_import_builder = Some(builder);
self.index_of_validators_to_exit = index_of_validators_to_exit;
self
}
pub async fn run_test(self) -> TestResult {
let import_builder = self.src_import_builder.unwrap();
let initialized_validators = import_builder.vc.initialized_validators.clone();
let import_test_result = import_builder.run_test().await;
assert!(import_test_result.result.is_ok());
// only assign the validator index after validator is imported to the VC
for &index in &self.index_of_validators_to_exit {
initialized_validators.write().set_index(
&self.beacon_node.harness.validator_keypairs[index]
.pk
.compress(),
index as u64,
);
}
let path = self.exit_config.clone().unwrap().vc_token_path;
let parent = path.parent().unwrap();
fs::create_dir_all(parent).expect("Was not able to create parent directory");
File::options()
.write(true)
.read(true)
.create(true)
.truncate(true)
.open(path.clone())
.unwrap()
.write_all(self.vc_token.clone().unwrap().as_bytes())
.unwrap();
// Advance beacon chain
self.beacon_node.harness.advance_slot();
self.beacon_node
.harness
.extend_chain(
100,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
let result = run::<E>(self.exit_config.clone().unwrap()).await;
self.beacon_node.harness.advance_slot();
self.beacon_node
.harness
.extend_chain(
1,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
let validator_data = self
.index_of_validators_to_exit
.iter()
.map(|&index| {
self.beacon_node
.harness
.get_current_state()
.get_validator(index)
.unwrap()
.clone()
})
.collect::<Vec<_>>();
let validator_exit_epoch = validator_data
.iter()
.map(|validator| validator.exit_epoch)
.collect::<Vec<_>>();
let validator_withdrawable_epoch = validator_data
.iter()
.map(|validator| validator.withdrawable_epoch)
.collect::<Vec<_>>();
let current_epoch = self.beacon_node.harness.get_current_state().current_epoch();
let max_seed_lookahead = self.beacon_node.harness.spec.max_seed_lookahead;
let min_withdrawability_delay = self
.beacon_node
.harness
.spec
.min_validator_withdrawability_delay;
// As per the spec:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_activation_exit_epoch
let beacon_exit_epoch = current_epoch + 1 + max_seed_lookahead;
let beacon_withdrawable_epoch = beacon_exit_epoch + min_withdrawability_delay;
assert!(validator_exit_epoch
.iter()
.all(|&epoch| epoch == beacon_exit_epoch));
assert!(validator_withdrawable_epoch
.iter()
.all(|&epoch| epoch == beacon_withdrawable_epoch));
if result.is_ok() {
return TestResult { result: Ok(()) };
}
TestResult {
result: Err(result.unwrap_err()),
}
}
}
#[must_use]
struct TestResult {
result: Result<(), String>,
}
impl TestResult {
fn assert_ok(self) {
assert_eq!(self.result, Ok(()))
}
}
#[tokio::test]
async fn exit_single_validator() {
TestBuilder::new()
.await
.with_validators(vec![0])
.await
.run_test()
.await
.assert_ok();
}
#[tokio::test]
async fn exit_multiple_validators() {
TestBuilder::new()
.await
.with_validators(vec![10, 20, 30])
.await
.run_test()
.await
.assert_ok();
}
}

View File

@@ -404,8 +404,12 @@ pub mod tests {
} }
pub async fn new_with_http_config(http_config: HttpConfig) -> Self { pub async fn new_with_http_config(http_config: HttpConfig) -> Self {
let dir = tempdir().unwrap();
let vc = ApiTester::new_with_http_config(http_config).await; let vc = ApiTester::new_with_http_config(http_config).await;
Self::new_with_vc(vc).await
}
pub async fn new_with_vc(vc: ApiTester) -> Self {
let dir = tempdir().unwrap();
let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME);
fs::write(&vc_token_path, &vc.api_token).unwrap(); fs::write(&vc_token_path, &vc.api_token).unwrap();

View File

@@ -9,6 +9,7 @@ use types::EthSpec;
pub mod common; pub mod common;
pub mod create_validators; pub mod create_validators;
pub mod delete_validators; pub mod delete_validators;
pub mod exit_validators;
pub mod import_validators; pub mod import_validators;
pub mod list_validators; pub mod list_validators;
pub mod move_validators; pub mod move_validators;
@@ -51,6 +52,7 @@ pub fn cli_app() -> Command {
.subcommand(move_validators::cli_app()) .subcommand(move_validators::cli_app())
.subcommand(list_validators::cli_app()) .subcommand(list_validators::cli_app())
.subcommand(delete_validators::cli_app()) .subcommand(delete_validators::cli_app())
.subcommand(exit_validators::cli_app())
} }
/// Run the account manager, returning an error if the operation did not succeed. /// Run the account manager, returning an error if the operation did not succeed.
@@ -79,11 +81,14 @@ pub fn run<E: EthSpec>(matches: &ArgMatches, env: Environment<E>) -> Result<(),
move_validators::cli_run(matches, dump_config).await move_validators::cli_run(matches, dump_config).await
} }
Some((list_validators::CMD, matches)) => { Some((list_validators::CMD, matches)) => {
list_validators::cli_run(matches, dump_config).await list_validators::cli_run::<E>(matches, dump_config).await
} }
Some((delete_validators::CMD, matches)) => { Some((delete_validators::CMD, matches)) => {
delete_validators::cli_run(matches, dump_config).await delete_validators::cli_run(matches, dump_config).await
} }
Some((exit_validators::CMD, matches)) => {
exit_validators::cli_run::<E>(matches, dump_config).await
}
Some(("", _)) => Err("No command supplied. See --help.".to_string()), Some(("", _)) => Err("No command supplied. See --help.".to_string()),
Some((unknown, _)) => Err(format!( Some((unknown, _)) => Err(format!(
"{} is not a valid {} command. See --help.", "{} is not a valid {} command. See --help.",

View File

@@ -1,14 +1,20 @@
use clap::{Arg, ArgAction, ArgMatches, Command}; use clap::{Arg, ArgAction, ArgMatches, Command};
use eth2::lighthouse_vc::types::SingleKeystoreResponse; use eth2::lighthouse_vc::types::SingleKeystoreResponse;
use eth2::SensitiveUrl; use eth2::types::{ConfigAndPreset, StateId, ValidatorId, ValidatorStatus};
use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration;
use types::{ChainSpec, EthSpec, PublicKeyBytes};
use crate::exit_validators::get_current_epoch;
use crate::{common::vc_http_client, DumpConfig}; use crate::{common::vc_http_client, DumpConfig};
pub const CMD: &str = "list"; pub const CMD: &str = "list";
pub const VC_URL_FLAG: &str = "vc-url"; pub const VC_URL_FLAG: &str = "vc-url";
pub const VC_TOKEN_FLAG: &str = "vc-token"; pub const VC_TOKEN_FLAG: &str = "vc-token";
pub const BEACON_URL_FLAG: &str = "beacon-node";
pub const VALIDATOR_FLAG: &str = "validators";
pub fn cli_app() -> Command { pub fn cli_app() -> Command {
Command::new(CMD) Command::new(CMD)
@@ -31,47 +37,177 @@ pub fn cli_app() -> Command {
.action(ArgAction::Set) .action(ArgAction::Set)
.display_order(0), .display_order(0),
) )
.arg(
Arg::new(BEACON_URL_FLAG)
.long(BEACON_URL_FLAG)
.value_name("NETWORK_ADDRESS")
.help(
"Address to a beacon node HTTP API. When supplied, \
the status of validators (with regard to voluntary exit) \
will be displayed. This flag is to be used together with \
the --validators flag.",
)
.action(ArgAction::Set)
.display_order(0)
.requires(VALIDATOR_FLAG),
)
.arg(
Arg::new(VALIDATOR_FLAG)
.long(VALIDATOR_FLAG)
.value_name("STRING")
.help(
"Comma-separated list of validators (pubkey) to display status for. \
To display the status for all validators, use the keyword \"all\". \
This flag is to be used together with the --beacon-node flag.",
)
.action(ArgAction::Set)
.display_order(0)
.requires(BEACON_URL_FLAG),
)
} }
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ListConfig { pub struct ListConfig {
pub vc_url: SensitiveUrl, pub vc_url: SensitiveUrl,
pub vc_token_path: PathBuf, pub vc_token_path: PathBuf,
pub beacon_url: Option<SensitiveUrl>,
pub validators_to_display: Vec<PublicKeyBytes>,
} }
impl ListConfig { impl ListConfig {
fn from_cli(matches: &ArgMatches) -> Result<Self, String> { fn from_cli(matches: &ArgMatches) -> Result<Self, String> {
let validators_to_display_str =
clap_utils::parse_optional::<String>(matches, VALIDATOR_FLAG)?;
// Keyword "all" to list all validators, vector to be created later
let validators_to_display = match validators_to_display_str {
Some(str) => {
if str.trim() == "all" {
Vec::new()
} else {
str.split(',')
.map(|s| s.trim().parse())
.collect::<Result<Vec<PublicKeyBytes>, _>>()?
}
}
None => Vec::new(),
};
Ok(Self { Ok(Self {
vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?,
vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?,
beacon_url: clap_utils::parse_optional(matches, BEACON_URL_FLAG)?,
validators_to_display,
}) })
} }
} }
pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<(), String> { pub async fn cli_run<E: EthSpec>(
matches: &ArgMatches,
dump_config: DumpConfig,
) -> Result<(), String> {
let config = ListConfig::from_cli(matches)?; let config = ListConfig::from_cli(matches)?;
if dump_config.should_exit_early(&config)? { if dump_config.should_exit_early(&config)? {
Ok(()) Ok(())
} else { } else {
run(config).await?; run::<E>(config).await?;
Ok(()) Ok(())
} }
} }
async fn run(config: ListConfig) -> Result<Vec<SingleKeystoreResponse>, String> { async fn run<E: EthSpec>(config: ListConfig) -> Result<Vec<SingleKeystoreResponse>, String> {
let ListConfig { let ListConfig {
vc_url, vc_url,
vc_token_path, vc_token_path,
beacon_url,
mut validators_to_display,
} = config; } = config;
let (_, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; let (_, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?;
println!("List of validators ({}):", validators.len()); println!("List of validators ({}):", validators.len());
for validator in &validators { if validators_to_display.is_empty() {
println!("{}", validator.validating_pubkey); validators_to_display = validators.iter().map(|v| v.validating_pubkey).collect();
} }
if let Some(ref beacon_url) = beacon_url {
for validator in &validators_to_display {
let beacon_node = BeaconNodeHttpClient::new(
SensitiveUrl::parse(beacon_url.as_ref())
.map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?,
Timeouts::set_all(Duration::from_secs(12)),
);
let validator_data = beacon_node
.get_beacon_states_validator_id(StateId::Head, &ValidatorId::PublicKey(*validator))
.await
.map_err(|e| format!("Failed to get updated validator details: {:?}", e))?
.ok_or_else(|| {
format!("Validator {} is not present in the beacon state", validator)
})?
.data;
match validator_data.status {
ValidatorStatus::ActiveExiting => {
let exit_epoch = validator_data.validator.exit_epoch;
let withdrawal_epoch = validator_data.validator.withdrawable_epoch;
let genesis_data = beacon_node
.get_beacon_genesis()
.await
.map_err(|e| format!("Failed to get genesis data: {}", e))?
.data;
let config_and_preset = beacon_node
.get_config_spec::<ConfigAndPreset>()
.await
.map_err(|e| format!("Failed to get config spec: {}", e))?
.data;
let spec = ChainSpec::from_config::<E>(config_and_preset.config())
.ok_or("Failed to create chain spec")?;
let current_epoch = get_current_epoch::<E>(genesis_data.genesis_time, &spec)
.ok_or("Failed to get current epoch. Please check your system time")?;
eprintln!(
"Voluntary exit for validator {} has been accepted into the beacon chain. \
Note that the voluntary exit is subject chain finalization. \
Before the chain has finalized, there is a low \
probability that the exit may be reverted.",
validator
);
eprintln!(
"Current epoch: {}, Exit epoch: {}, Withdrawable epoch: {}",
current_epoch, exit_epoch, withdrawal_epoch
);
eprintln!("Please keep your validator running till exit epoch");
eprintln!(
"Exit epoch in approximately {} secs",
(exit_epoch - current_epoch) * spec.seconds_per_slot * E::slots_per_epoch()
);
}
ValidatorStatus::ExitedSlashed | ValidatorStatus::ExitedUnslashed => {
eprintln!(
"Validator {} has exited at epoch: {}",
validator, validator_data.validator.exit_epoch
);
}
_ => {
eprintln!(
"Validator {} has not initiated voluntary exit or the voluntary exit \
is yet to be accepted into the beacon chain. Validator status is: {}",
validator, validator_data.status
)
}
}
}
} else {
for validator in &validators {
println!("{}", validator.validating_pubkey);
}
}
Ok(validators) Ok(validators)
} }
@@ -87,7 +223,9 @@ mod test {
use crate::{ use crate::{
common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder,
}; };
use types::MainnetEthSpec;
use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; use validator_http_api::{test_utils::ApiTester, Config as HttpConfig};
type E = MainnetEthSpec;
struct TestBuilder { struct TestBuilder {
list_config: Option<ListConfig>, list_config: Option<ListConfig>,
@@ -116,6 +254,8 @@ mod test {
self.list_config = Some(ListConfig { self.list_config = Some(ListConfig {
vc_url: builder.get_import_config().vc_url, vc_url: builder.get_import_config().vc_url,
vc_token_path: builder.get_import_config().vc_token_path, vc_token_path: builder.get_import_config().vc_token_path,
beacon_url: None,
validators_to_display: vec![],
}); });
self.vc_token = self.vc_token =
@@ -152,7 +292,7 @@ mod test {
.write_all(self.vc_token.clone().unwrap().as_bytes()) .write_all(self.vc_token.clone().unwrap().as_bytes())
.unwrap(); .unwrap();
let result = run(self.list_config.clone().unwrap()).await; let result = run::<E>(self.list_config.clone().unwrap()).await;
if result.is_ok() { if result.is_ok() {
let result_ref = result.as_ref().unwrap(); let result_ref = result.as_ref().unwrap();

View File

@@ -196,6 +196,8 @@ pem
performant performant
pid pid
pre pre
presign
presigned
pubkey pubkey
pubkeys pubkeys
rc rc