Skip to content

Commit 91a7f51

Browse files
committed
Post merge local testnets (#3807)
## Issue Addressed N/A ## Proposed Changes Modifies the local testnet scripts to start a network with genesis validators embedded into the genesis state. This allows us to start a local testnet without the need for deploying a deposit contract or depositing validators pre-genesis. This also enables us to start a local test network at any fork we want without going through fork transitions. Also adds scripts to start multiple geth clients and peer them with each other and peer the geth clients with beacon nodes to start a post merge local testnet. ## Additional info Adds a new lcli command `mnemonics-validators` that generates validator directories derived from a given mnemonic. Adds a new `derived-genesis-state` option to the `lcli new-testnet` command to generate a genesis state populated with validators derived from a mnemonic.
1 parent b29bb2e commit 91a7f51

20 files changed

+2345
-146
lines changed

.github/workflows/local-testnet.yml

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,23 @@ jobs:
2525
uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612
2626
with:
2727
repo-token: ${{ secrets.GITHUB_TOKEN }}
28-
- name: Install anvil
29-
run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil
30-
28+
- name: Install geth (ubuntu)
29+
if: matrix.os == 'ubuntu-22.04'
30+
run: |
31+
sudo add-apt-repository -y ppa:ethereum/ethereum
32+
sudo apt-get update
33+
sudo apt-get install ethereum
34+
- name: Install geth (mac)
35+
if: matrix.os == 'macos-12'
36+
run: |
37+
brew tap ethereum/ethereum
38+
brew install ethereum
39+
- name: Install GNU sed & GNU grep
40+
if: matrix.os == 'macos-12'
41+
run: |
42+
brew install gnu-sed grep
43+
echo "$(brew --prefix)/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
44+
echo "$(brew --prefix)/opt/grep/libexec/gnubin" >> $GITHUB_PATH
3145
# https://github.com/actions/cache/blob/main/examples.md#rust---cargo
3246
- uses: actions/cache@v3
3347
id: cache-cargo
@@ -44,7 +58,7 @@ jobs:
4458
run: make && make install-lcli
4559

4660
- name: Start local testnet
47-
run: ./start_local_testnet.sh && sleep 60
61+
run: ./start_local_testnet.sh genesis.json && sleep 60
4862
working-directory: scripts/local_testnet
4963

5064
- name: Print logs
@@ -60,7 +74,7 @@ jobs:
6074
working-directory: scripts/local_testnet
6175

6276
- name: Start local testnet with blinded block production
63-
run: ./start_local_testnet.sh -p && sleep 60
77+
run: ./start_local_testnet.sh -p genesis.json && sleep 60
6478
working-directory: scripts/local_testnet
6579

6680
- name: Print logs for blinded block testnet
@@ -69,4 +83,4 @@ jobs:
6983

7084
- name: Stop local testnet with blinded block production
7185
run: ./stop_local_testnet.sh
72-
working-directory: scripts/local_testnet
86+
working-directory: scripts/local_testnet

.github/workflows/test-suite.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,6 @@ jobs:
228228
uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612
229229
with:
230230
repo-token: ${{ secrets.GITHUB_TOKEN }}
231-
- name: Install anvil
232-
run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil
233231
- name: Run the beacon chain sim without an eth1 connection
234232
run: cargo run --release --bin simulator no-eth1-sim
235233
syncing-simulator-ubuntu:
@@ -260,20 +258,23 @@ jobs:
260258
uses: arduino/setup-protoc@e52d9eb8f7b63115df1ac544a1376fdbf5a39612
261259
with:
262260
repo-token: ${{ secrets.GITHUB_TOKEN }}
263-
- name: Install anvil
264-
run: cargo install --git https://github.com/foundry-rs/foundry --locked anvil
261+
- name: Install geth
262+
run: |
263+
sudo add-apt-repository -y ppa:ethereum/ethereum
264+
sudo apt-get update
265+
sudo apt-get install ethereum
265266
- name: Install lighthouse and lcli
266267
run: |
267268
make
268269
make install-lcli
269-
- name: Run the doppelganger protection success test script
270+
- name: Run the doppelganger protection failure test script
270271
run: |
271272
cd scripts/tests
272-
./doppelganger_protection.sh success
273-
- name: Run the doppelganger protection failure test script
273+
./doppelganger_protection.sh failure genesis.json
274+
- name: Run the doppelganger protection success test script
274275
run: |
275276
cd scripts/tests
276-
./doppelganger_protection.sh failure
277+
./doppelganger_protection.sh success genesis.json
277278
execution-engine-integration-ubuntu:
278279
name: execution-engine-integration-ubuntu
279280
runs-on: ubuntu-latest

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lcli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ env_logger = "0.9.0"
2121
types = { path = "../consensus/types" }
2222
state_processing = { path = "../consensus/state_processing" }
2323
int_to_bytes = { path = "../consensus/int_to_bytes" }
24+
ethereum_hashing = "1.0.0-beta.2"
2425
ethereum_ssz = "0.5.0"
2526
environment = { path = "../lighthouse/environment" }
2627
eth2_network_config = { path = "../common/eth2_network_config" }
@@ -41,6 +42,7 @@ snap = "1.0.1"
4142
beacon_chain = { path = "../beacon_node/beacon_chain" }
4243
store = { path = "../beacon_node/store" }
4344
malloc_utils = { path = "../common/malloc_utils" }
45+
rayon = "1.7.0"
4446

4547
[package.metadata.cargo-udeps.ignore]
4648
normal = ["malloc_utils"]

lcli/src/main.rs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod generate_bootnode_enr;
1010
mod indexed_attestations;
1111
mod insecure_validators;
1212
mod interop_genesis;
13+
mod mnemonic_validators;
1314
mod new_testnet;
1415
mod parse_ssz;
1516
mod replace_state_pubkeys;
@@ -449,6 +450,22 @@ fn main() {
449450
"If present, a interop-style genesis.ssz file will be generated.",
450451
),
451452
)
453+
.arg(
454+
Arg::with_name("derived-genesis-state")
455+
.long("derived-genesis-state")
456+
.takes_value(false)
457+
.help(
458+
"If present, a genesis.ssz file will be generated with keys generated from a given mnemonic.",
459+
),
460+
)
461+
.arg(
462+
Arg::with_name("mnemonic-phrase")
463+
.long("mnemonic-phrase")
464+
.value_name("MNEMONIC_PHRASE")
465+
.takes_value(true)
466+
.requires("derived-genesis-state")
467+
.help("The mnemonic with which we generate the validator keys for a derived genesis state"),
468+
)
452469
.arg(
453470
Arg::with_name("min-genesis-time")
454471
.long("min-genesis-time")
@@ -568,14 +585,32 @@ fn main() {
568585
),
569586
)
570587
.arg(
571-
Arg::with_name("merge-fork-epoch")
572-
.long("merge-fork-epoch")
588+
Arg::with_name("bellatrix-fork-epoch")
589+
.long("bellatrix-fork-epoch")
573590
.value_name("EPOCH")
574591
.takes_value(true)
575592
.help(
576593
"The epoch at which to enable the Merge hard fork",
577594
),
578595
)
596+
.arg(
597+
Arg::with_name("capella-fork-epoch")
598+
.long("capella-fork-epoch")
599+
.value_name("EPOCH")
600+
.takes_value(true)
601+
.help(
602+
"The epoch at which to enable the Capella hard fork",
603+
),
604+
)
605+
.arg(
606+
Arg::with_name("ttd")
607+
.long("ttd")
608+
.value_name("TTD")
609+
.takes_value(true)
610+
.help(
611+
"The terminal total difficulty",
612+
),
613+
)
579614
.arg(
580615
Arg::with_name("eth1-block-hash")
581616
.long("eth1-block-hash")
@@ -695,13 +730,44 @@ fn main() {
695730
.long("count")
696731
.value_name("COUNT")
697732
.takes_value(true)
733+
.required(true)
734+
.help("Produces validators in the range of 0..count."),
735+
)
736+
.arg(
737+
Arg::with_name("base-dir")
738+
.long("base-dir")
739+
.value_name("BASE_DIR")
740+
.takes_value(true)
741+
.required(true)
742+
.help("The base directory where validator keypairs and secrets are stored"),
743+
)
744+
.arg(
745+
Arg::with_name("node-count")
746+
.long("node-count")
747+
.value_name("NODE_COUNT")
748+
.takes_value(true)
749+
.help("The number of nodes to divide the validator keys to"),
750+
)
751+
)
752+
.subcommand(
753+
SubCommand::with_name("mnemonic-validators")
754+
.about("Produces validator directories by deriving the keys from \
755+
a mnemonic. For testing purposes only, DO NOT USE IN \
756+
PRODUCTION!")
757+
.arg(
758+
Arg::with_name("count")
759+
.long("count")
760+
.value_name("COUNT")
761+
.takes_value(true)
762+
.required(true)
698763
.help("Produces validators in the range of 0..count."),
699764
)
700765
.arg(
701766
Arg::with_name("base-dir")
702767
.long("base-dir")
703768
.value_name("BASE_DIR")
704769
.takes_value(true)
770+
.required(true)
705771
.help("The base directory where validator keypairs and secrets are stored"),
706772
)
707773
.arg(
@@ -711,6 +777,14 @@ fn main() {
711777
.takes_value(true)
712778
.help("The number of nodes to divide the validator keys to"),
713779
)
780+
.arg(
781+
Arg::with_name("mnemonic-phrase")
782+
.long("mnemonic-phrase")
783+
.value_name("MNEMONIC_PHRASE")
784+
.takes_value(true)
785+
.required(true)
786+
.help("The mnemonic with which we generate the validator keys"),
787+
)
714788
)
715789
.subcommand(
716790
SubCommand::with_name("indexed-attestations")
@@ -853,6 +927,8 @@ fn run<T: EthSpec>(
853927
.map_err(|e| format!("Failed to run generate-bootnode-enr command: {}", e)),
854928
("insecure-validators", Some(matches)) => insecure_validators::run(matches)
855929
.map_err(|e| format!("Failed to run insecure-validators command: {}", e)),
930+
("mnemonic-validators", Some(matches)) => mnemonic_validators::run(matches)
931+
.map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)),
856932
("indexed-attestations", Some(matches)) => indexed_attestations::run::<T>(matches)
857933
.map_err(|e| format!("Failed to run indexed-attestations command: {}", e)),
858934
("block-root", Some(matches)) => block_root::run::<T>(env, matches)

lcli/src/mnemonic_validators.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder};
2+
use account_utils::random_password;
3+
use clap::ArgMatches;
4+
use eth2_wallet::bip39::Seed;
5+
use eth2_wallet::bip39::{Language, Mnemonic};
6+
use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType};
7+
use rayon::prelude::*;
8+
use std::fs;
9+
use std::path::PathBuf;
10+
use validator_dir::Builder as ValidatorBuilder;
11+
12+
/// Generates validator directories with keys derived from the given mnemonic.
13+
pub fn generate_validator_dirs(
14+
indices: &[usize],
15+
mnemonic_phrase: &str,
16+
validators_dir: PathBuf,
17+
secrets_dir: PathBuf,
18+
) -> Result<(), String> {
19+
if !validators_dir.exists() {
20+
fs::create_dir_all(&validators_dir)
21+
.map_err(|e| format!("Unable to create validators dir: {:?}", e))?;
22+
}
23+
24+
if !secrets_dir.exists() {
25+
fs::create_dir_all(&secrets_dir)
26+
.map_err(|e| format!("Unable to create secrets dir: {:?}", e))?;
27+
}
28+
let mnemonic = Mnemonic::from_phrase(mnemonic_phrase, Language::English).map_err(|e| {
29+
format!(
30+
"Unable to derive mnemonic from string {:?}: {:?}",
31+
mnemonic_phrase, e
32+
)
33+
})?;
34+
35+
let seed = Seed::new(&mnemonic, "");
36+
37+
let _: Vec<_> = indices
38+
.par_iter()
39+
.map(|index| {
40+
let voting_password = random_password();
41+
42+
let derive = |key_type: KeyType, password: &[u8]| -> Result<Keystore, String> {
43+
let (secret, path) = recover_validator_secret_from_mnemonic(
44+
seed.as_bytes(),
45+
*index as u32,
46+
key_type,
47+
)
48+
.map_err(|e| format!("Unable to recover validator keys: {:?}", e))?;
49+
50+
let keypair = keypair_from_secret(secret.as_bytes())
51+
.map_err(|e| format!("Unable build keystore: {:?}", e))?;
52+
53+
KeystoreBuilder::new(&keypair, password, format!("{}", path))
54+
.map_err(|e| format!("Unable build keystore: {:?}", e))?
55+
.build()
56+
.map_err(|e| format!("Unable build keystore: {:?}", e))
57+
};
58+
59+
let voting_keystore = derive(KeyType::Voting, voting_password.as_bytes()).unwrap();
60+
61+
println!("Validator {}", index + 1);
62+
63+
ValidatorBuilder::new(validators_dir.clone())
64+
.password_dir(secrets_dir.clone())
65+
.store_withdrawal_keystore(false)
66+
.voting_keystore(voting_keystore, voting_password.as_bytes())
67+
.build()
68+
.map_err(|e| format!("Unable to build validator: {:?}", e))
69+
.unwrap()
70+
})
71+
.collect();
72+
73+
Ok(())
74+
}
75+
76+
pub fn run(matches: &ArgMatches) -> Result<(), String> {
77+
let validator_count: usize = clap_utils::parse_required(matches, "count")?;
78+
let base_dir: PathBuf = clap_utils::parse_required(matches, "base-dir")?;
79+
let node_count: Option<usize> = clap_utils::parse_optional(matches, "node-count")?;
80+
let mnemonic_phrase: String = clap_utils::parse_required(matches, "mnemonic-phrase")?;
81+
if let Some(node_count) = node_count {
82+
let validators_per_node = validator_count / node_count;
83+
let validator_range = (0..validator_count).collect::<Vec<_>>();
84+
let indices_range = validator_range
85+
.chunks(validators_per_node)
86+
.collect::<Vec<_>>();
87+
88+
for (i, indices) in indices_range.iter().enumerate() {
89+
let validators_dir = base_dir.join(format!("node_{}", i + 1)).join("validators");
90+
let secrets_dir = base_dir.join(format!("node_{}", i + 1)).join("secrets");
91+
generate_validator_dirs(indices, &mnemonic_phrase, validators_dir, secrets_dir)?;
92+
}
93+
} else {
94+
let validators_dir = base_dir.join("validators");
95+
let secrets_dir = base_dir.join("secrets");
96+
generate_validator_dirs(
97+
(0..validator_count).collect::<Vec<_>>().as_slice(),
98+
&mnemonic_phrase,
99+
validators_dir,
100+
secrets_dir,
101+
)?;
102+
}
103+
Ok(())
104+
}

0 commit comments

Comments
 (0)