mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-02 16:21:42 +00:00
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:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
585
validator_manager/src/exit_validators.rs
Normal file
585
validator_manager/src/exit_validators.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ pem
|
|||||||
performant
|
performant
|
||||||
pid
|
pid
|
||||||
pre
|
pre
|
||||||
|
presign
|
||||||
|
presigned
|
||||||
pubkey
|
pubkey
|
||||||
pubkeys
|
pubkeys
|
||||||
rc
|
rc
|
||||||
|
|||||||
Reference in New Issue
Block a user