mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-15 10:52:43 +00:00
## Issue Addressed
NA
## Proposed Changes
Add the Holesky network config as per 36e4ff2d51/custom_config_data.
Since the genesis state is ~190MB, I've opted to *not* include it in the binary and instead download it at runtime (see #4564 for context). To download this file we have:
- A hard-coded URL for a SigP-hosted S3 bucket with the Holesky genesis state. Assuming this download works correctly, users will be none the wiser that the state wasn't included in the binary (apart from some additional logs)
- If the user provides a `--checkpoint-sync-url` flag, then LH will download the genesis state from that server rather than our S3 bucket.
- If the user provides a `--genesis-state-url` flag, then LH will download the genesis state from that server regardless of the S3 bucket or `--checkpoint-sync-url` flag.
- Whenever a genesis state is downloaded it is checked against a checksum baked into the binary.
- A genesis state will never be downloaded if it's already included in the binary.
- There is a `--genesis-state-url-timeout` flag to tweak the timeout for downloading the genesis state file.
## Log Output
Example of log output when a state is downloaded:
```bash
Aug 23 05:40:13.424 INFO Logging to file path: "/Users/paul/.lighthouse/holesky/beacon/logs/beacon.log"
Aug 23 05:40:13.425 INFO Lighthouse started version: Lighthouse/v4.3.0-bd9931f+
Aug 23 05:40:13.425 INFO Configured for network name: holesky
Aug 23 05:40:13.426 INFO Data directory initialised datadir: /Users/paul/.lighthouse/holesky
Aug 23 05:40:13.427 INFO Deposit contract address: 0x4242424242424242424242424242424242424242, deploy_block: 0
Aug 23 05:40:13.427 INFO Downloading genesis state info: this may take some time on testnets with large validator counts, timeout: 60s, server: https://sigp-public-genesis-states.s3.ap-southeast-2.amazonaws.com/
Aug 23 05:40:29.895 INFO Starting from known genesis state service: beacon
```
Example of log output when there are no URLs specified:
```
Aug 23 06:29:51.645 INFO Logging to file path: "/Users/paul/.lighthouse/goerli/beacon/logs/beacon.log"
Aug 23 06:29:51.646 INFO Lighthouse started version: Lighthouse/v4.3.0-666a39c+
Aug 23 06:29:51.646 INFO Configured for network name: goerli
Aug 23 06:29:51.647 INFO Data directory initialised datadir: /Users/paul/.lighthouse/goerli
Aug 23 06:29:51.647 INFO Deposit contract address: 0xff50ed3d0ec03ac01d4c79aad74928bff48a7b2b, deploy_block: 4367322
The genesis state is not present in the binary and there are no known download URLs. Please use --checkpoint-sync-url or --genesis-state-url.
```
## Additional Info
I tested the `--genesis-state-url` flag with all 9 Goerli checkpoint sync servers on https://eth-clients.github.io/checkpoint-sync-endpoints/ and they all worked 🎉
My IDE eagerly formatted some `Cargo.toml`. I've disabled it but I don't see the value in spending time reverting the changes that are already there.
I also added the `GenesisStateBytes` enum to avoid an unnecessary clone on the genesis state bytes baked into the binary. This is not a huge deal on Mainnet, but will become more relevant when testing with big genesis states.
When we do a fresh checkpoint sync we're downloading the genesis state to check the `genesis_validators_root` against the finalised state we receive. This is not *entirely* pointless, since we verify the checksum when we download the genesis state so we are actually guaranteeing that the finalised state is on the same network. There might be a smarter/less-download-y way to go about this, but I've run out of cycles to figure that out. Perhaps we can grab it in the next release?
314 lines
13 KiB
Rust
314 lines
13 KiB
Rust
use account_utils::eth2_keystore::keypair_from_secret;
|
|
use clap::ArgMatches;
|
|
use clap_utils::{parse_optional, parse_required, parse_ssz_optional};
|
|
use eth2_network_config::{Eth2NetworkConfig, GenesisStateSource};
|
|
use eth2_wallet::bip39::Seed;
|
|
use eth2_wallet::bip39::{Language, Mnemonic};
|
|
use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType};
|
|
use ethereum_hashing::hash;
|
|
use ssz::Decode;
|
|
use ssz::Encode;
|
|
use state_processing::process_activations;
|
|
use state_processing::upgrade::{upgrade_to_altair, upgrade_to_bellatrix};
|
|
use std::fs::File;
|
|
use std::io::Read;
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use types::ExecutionBlockHash;
|
|
use types::{
|
|
test_utils::generate_deterministic_keypairs, Address, BeaconState, ChainSpec, Config, Epoch,
|
|
Eth1Data, EthSpec, ExecutionPayloadHeader, ExecutionPayloadHeaderCapella,
|
|
ExecutionPayloadHeaderMerge, ExecutionPayloadHeaderRefMut, ForkName, Hash256, Keypair,
|
|
PublicKey, Validator,
|
|
};
|
|
|
|
pub fn run<T: EthSpec>(testnet_dir_path: PathBuf, matches: &ArgMatches) -> Result<(), String> {
|
|
let deposit_contract_address: Address = parse_required(matches, "deposit-contract-address")?;
|
|
let deposit_contract_deploy_block = parse_required(matches, "deposit-contract-deploy-block")?;
|
|
|
|
let overwrite_files = matches.is_present("force");
|
|
|
|
if testnet_dir_path.exists() && !overwrite_files {
|
|
return Err(format!(
|
|
"{:?} already exists, will not overwrite. Use --force to overwrite",
|
|
testnet_dir_path
|
|
));
|
|
}
|
|
|
|
let mut spec = T::default_spec();
|
|
|
|
// Update the spec value if the flag was defined. Otherwise, leave it as the default.
|
|
macro_rules! maybe_update {
|
|
($flag: tt, $var: ident) => {
|
|
if let Some(val) = parse_optional(matches, $flag)? {
|
|
spec.$var = val
|
|
}
|
|
};
|
|
}
|
|
|
|
spec.deposit_contract_address = deposit_contract_address;
|
|
|
|
maybe_update!("min-genesis-time", min_genesis_time);
|
|
maybe_update!("min-deposit-amount", min_deposit_amount);
|
|
maybe_update!(
|
|
"min-genesis-active-validator-count",
|
|
min_genesis_active_validator_count
|
|
);
|
|
maybe_update!("max-effective-balance", max_effective_balance);
|
|
maybe_update!("effective-balance-increment", effective_balance_increment);
|
|
maybe_update!("ejection-balance", ejection_balance);
|
|
maybe_update!("eth1-follow-distance", eth1_follow_distance);
|
|
maybe_update!("genesis-delay", genesis_delay);
|
|
maybe_update!("eth1-id", deposit_chain_id);
|
|
maybe_update!("eth1-id", deposit_network_id);
|
|
maybe_update!("seconds-per-slot", seconds_per_slot);
|
|
maybe_update!("seconds-per-eth1-block", seconds_per_eth1_block);
|
|
|
|
if let Some(v) = parse_ssz_optional(matches, "genesis-fork-version")? {
|
|
spec.genesis_fork_version = v;
|
|
}
|
|
|
|
if let Some(proposer_score_boost) = parse_optional(matches, "proposer-score-boost")? {
|
|
spec.proposer_score_boost = Some(proposer_score_boost);
|
|
}
|
|
|
|
if let Some(fork_epoch) = parse_optional(matches, "altair-fork-epoch")? {
|
|
spec.altair_fork_epoch = Some(fork_epoch);
|
|
}
|
|
|
|
if let Some(fork_epoch) = parse_optional(matches, "bellatrix-fork-epoch")? {
|
|
spec.bellatrix_fork_epoch = Some(fork_epoch);
|
|
}
|
|
|
|
if let Some(fork_epoch) = parse_optional(matches, "capella-fork-epoch")? {
|
|
spec.capella_fork_epoch = Some(fork_epoch);
|
|
}
|
|
|
|
if let Some(ttd) = parse_optional(matches, "ttd")? {
|
|
spec.terminal_total_difficulty = ttd;
|
|
}
|
|
|
|
let validator_count = parse_required(matches, "validator-count")?;
|
|
let execution_payload_header: Option<ExecutionPayloadHeader<T>> =
|
|
parse_optional(matches, "execution-payload-header")?
|
|
.map(|filename: String| {
|
|
let mut bytes = vec![];
|
|
let mut file = File::open(filename.as_str())
|
|
.map_err(|e| format!("Unable to open {}: {}", filename, e))?;
|
|
file.read_to_end(&mut bytes)
|
|
.map_err(|e| format!("Unable to read {}: {}", filename, e))?;
|
|
let fork_name = spec.fork_name_at_epoch(Epoch::new(0));
|
|
match fork_name {
|
|
ForkName::Base | ForkName::Altair => Err(ssz::DecodeError::BytesInvalid(
|
|
"genesis fork must be post-merge".to_string(),
|
|
)),
|
|
ForkName::Merge => {
|
|
ExecutionPayloadHeaderMerge::<T>::from_ssz_bytes(bytes.as_slice())
|
|
.map(ExecutionPayloadHeader::Merge)
|
|
}
|
|
ForkName::Capella => {
|
|
ExecutionPayloadHeaderCapella::<T>::from_ssz_bytes(bytes.as_slice())
|
|
.map(ExecutionPayloadHeader::Capella)
|
|
}
|
|
}
|
|
.map_err(|e| format!("SSZ decode failed: {:?}", e))
|
|
})
|
|
.transpose()?;
|
|
|
|
let (eth1_block_hash, genesis_time) = if let Some(payload) = execution_payload_header.as_ref() {
|
|
let eth1_block_hash =
|
|
parse_optional(matches, "eth1-block-hash")?.unwrap_or_else(|| payload.block_hash());
|
|
let genesis_time =
|
|
parse_optional(matches, "genesis-time")?.unwrap_or_else(|| payload.timestamp());
|
|
(eth1_block_hash, genesis_time)
|
|
} else {
|
|
let eth1_block_hash = parse_required(matches, "eth1-block-hash").map_err(|_| {
|
|
"One of `--execution-payload-header` or `--eth1-block-hash` must be set".to_string()
|
|
})?;
|
|
let genesis_time = parse_optional(matches, "genesis-time")?.unwrap_or(
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map_err(|e| format!("Unable to get time: {:?}", e))?
|
|
.as_secs(),
|
|
);
|
|
(eth1_block_hash, genesis_time)
|
|
};
|
|
|
|
let genesis_state_bytes = if matches.is_present("interop-genesis-state") {
|
|
let keypairs = generate_deterministic_keypairs(validator_count);
|
|
let keypairs: Vec<_> = keypairs.into_iter().map(|kp| (kp.clone(), kp)).collect();
|
|
|
|
let genesis_state = initialize_state_with_validators::<T>(
|
|
&keypairs,
|
|
genesis_time,
|
|
eth1_block_hash.into_root(),
|
|
execution_payload_header,
|
|
&spec,
|
|
)?;
|
|
|
|
Some(genesis_state.as_ssz_bytes())
|
|
} else if matches.is_present("derived-genesis-state") {
|
|
let mnemonic_phrase: String = clap_utils::parse_required(matches, "mnemonic-phrase")?;
|
|
let mnemonic = Mnemonic::from_phrase(&mnemonic_phrase, Language::English).map_err(|e| {
|
|
format!(
|
|
"Unable to derive mnemonic from string {:?}: {:?}",
|
|
mnemonic_phrase, e
|
|
)
|
|
})?;
|
|
let seed = Seed::new(&mnemonic, "");
|
|
let keypairs = (0..validator_count as u32)
|
|
.map(|index| {
|
|
let (secret, _) =
|
|
recover_validator_secret_from_mnemonic(seed.as_bytes(), index, KeyType::Voting)
|
|
.unwrap();
|
|
|
|
let voting_keypair = keypair_from_secret(secret.as_bytes()).unwrap();
|
|
|
|
let (secret, _) = recover_validator_secret_from_mnemonic(
|
|
seed.as_bytes(),
|
|
index,
|
|
KeyType::Withdrawal,
|
|
)
|
|
.unwrap();
|
|
let withdrawal_keypair = keypair_from_secret(secret.as_bytes()).unwrap();
|
|
(voting_keypair, withdrawal_keypair)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let genesis_state = initialize_state_with_validators::<T>(
|
|
&keypairs,
|
|
genesis_time,
|
|
eth1_block_hash.into_root(),
|
|
execution_payload_header,
|
|
&spec,
|
|
)?;
|
|
Some(genesis_state.as_ssz_bytes())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let testnet = Eth2NetworkConfig {
|
|
deposit_contract_deploy_block,
|
|
boot_enr: Some(vec![]),
|
|
genesis_state_bytes: genesis_state_bytes.map(Into::into),
|
|
genesis_state_source: GenesisStateSource::IncludedBytes,
|
|
config: Config::from_chain_spec::<T>(&spec),
|
|
};
|
|
|
|
testnet.write_to_file(testnet_dir_path, overwrite_files)
|
|
}
|
|
|
|
/// Returns a `BeaconState` with the given validator keypairs embedded into the
|
|
/// genesis state. This allows us to start testnets without having to deposit validators
|
|
/// manually.
|
|
///
|
|
/// The optional `execution_payload_header` allows us to start a network from the bellatrix
|
|
/// fork without the need to transition to altair and bellatrix.
|
|
///
|
|
/// We need to ensure that `eth1_block_hash` is equal to the genesis block hash that is
|
|
/// generated from the execution side `genesis.json`.
|
|
fn initialize_state_with_validators<T: EthSpec>(
|
|
keypairs: &[(Keypair, Keypair)], // Voting and Withdrawal keypairs
|
|
genesis_time: u64,
|
|
eth1_block_hash: Hash256,
|
|
execution_payload_header: Option<ExecutionPayloadHeader<T>>,
|
|
spec: &ChainSpec,
|
|
) -> Result<BeaconState<T>, String> {
|
|
// If no header is provided, then start from a Bellatrix state by default
|
|
let default_header: ExecutionPayloadHeader<T> =
|
|
ExecutionPayloadHeader::Merge(ExecutionPayloadHeaderMerge {
|
|
block_hash: ExecutionBlockHash::from_root(eth1_block_hash),
|
|
parent_hash: ExecutionBlockHash::zero(),
|
|
..ExecutionPayloadHeaderMerge::default()
|
|
});
|
|
let execution_payload_header = execution_payload_header.unwrap_or(default_header);
|
|
// Empty eth1 data
|
|
let eth1_data = Eth1Data {
|
|
block_hash: eth1_block_hash,
|
|
deposit_count: 0,
|
|
deposit_root: Hash256::from_str(
|
|
"0xd70a234731285c6804c2a4f56711ddb8c82c99740f207854891028af34e27e5e",
|
|
)
|
|
.unwrap(), // empty deposit tree root
|
|
};
|
|
let mut state = BeaconState::new(genesis_time, eth1_data, spec);
|
|
|
|
// Seed RANDAO with Eth1 entropy
|
|
state.fill_randao_mixes_with(eth1_block_hash);
|
|
|
|
for keypair in keypairs.iter() {
|
|
let withdrawal_credentials = |pubkey: &PublicKey| {
|
|
let mut credentials = hash(&pubkey.as_ssz_bytes());
|
|
credentials[0] = spec.bls_withdrawal_prefix_byte;
|
|
Hash256::from_slice(&credentials)
|
|
};
|
|
let amount = spec.max_effective_balance;
|
|
// Create a new validator.
|
|
let validator = Validator {
|
|
pubkey: keypair.0.pk.clone().into(),
|
|
withdrawal_credentials: withdrawal_credentials(&keypair.1.pk),
|
|
activation_eligibility_epoch: spec.far_future_epoch,
|
|
activation_epoch: spec.far_future_epoch,
|
|
exit_epoch: spec.far_future_epoch,
|
|
withdrawable_epoch: spec.far_future_epoch,
|
|
effective_balance: std::cmp::min(
|
|
amount - amount % (spec.effective_balance_increment),
|
|
spec.max_effective_balance,
|
|
),
|
|
slashed: false,
|
|
};
|
|
state.validators_mut().push(validator).unwrap();
|
|
state.balances_mut().push(amount).unwrap();
|
|
}
|
|
|
|
process_activations(&mut state, spec).unwrap();
|
|
|
|
if spec
|
|
.altair_fork_epoch
|
|
.map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch())
|
|
{
|
|
upgrade_to_altair(&mut state, spec).unwrap();
|
|
|
|
state.fork_mut().previous_version = spec.altair_fork_version;
|
|
}
|
|
|
|
// Similarly, perform an upgrade to the merge if configured from genesis.
|
|
if spec
|
|
.bellatrix_fork_epoch
|
|
.map_or(false, |fork_epoch| fork_epoch == T::genesis_epoch())
|
|
{
|
|
upgrade_to_bellatrix(&mut state, spec).unwrap();
|
|
|
|
// Remove intermediate Altair fork from `state.fork`.
|
|
state.fork_mut().previous_version = spec.bellatrix_fork_version;
|
|
|
|
// Override latest execution payload header.
|
|
// See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/merge/beacon-chain.md#testing
|
|
|
|
// Currently, we only support starting from a bellatrix state
|
|
match state
|
|
.latest_execution_payload_header_mut()
|
|
.map_err(|e| format!("Failed to get execution payload header: {:?}", e))?
|
|
{
|
|
ExecutionPayloadHeaderRefMut::Merge(header_mut) => {
|
|
if let ExecutionPayloadHeader::Merge(eph) = execution_payload_header {
|
|
*header_mut = eph;
|
|
} else {
|
|
return Err("Execution payload header must be a bellatrix header".to_string());
|
|
}
|
|
}
|
|
ExecutionPayloadHeaderRefMut::Capella(_) => {
|
|
return Err("Cannot start genesis from a capella state".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now that we have our validators, initialize the caches (including the committees)
|
|
state.build_caches(spec).unwrap();
|
|
|
|
// Set genesis validators root for domain separation and chain versioning
|
|
*state.genesis_validators_root_mut() = state.update_validators_tree_hash_cache().unwrap();
|
|
|
|
Ok(state)
|
|
}
|