Replace local testnet script with Kurtosis (#5865)

* Kurtosis local testnet.

* Remove unused `lcli` subcommands

* Migrate doppelganger_protection test to kurtosis and further cleanup.

* Fix lint

* Add missing download image step and remove unused `lcli` dependencies.

* doppelganger success case working

* Run tests on hosted runner and improve error handling.

* Start the dp vc only after epoch 1

* Add more logging to test results.

* Fix exit code and speed up docker build.

* Fix incorrect exit codes and split doppelganger tests on CI.

* Missing the escape for double quotes 😫

* Remove unnecessary vc params in kurtosis config.
This commit is contained in:
Jimmy Chen
2024-06-04 13:03:26 +10:00
committed by GitHub
parent 1b7c4a4523
commit 5fc01454dc
37 changed files with 333 additions and 3909 deletions

View File

@@ -13,87 +13,154 @@ concurrency:
cancel-in-progress: true
jobs:
run-local-testnet:
strategy:
matrix:
os:
- ubuntu-22.04
- macos-12
runs-on: ${{ matrix.os }}
env:
# Enable portable to prevent issues with caching `blst` for the wrong CPU type
FEATURES: portable,jemalloc
dockerfile-ubuntu:
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
run: rustup update stable
- name: Install geth (ubuntu)
if: matrix.os == 'ubuntu-22.04'
- name: Build Docker image
run: |
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum
- name: Install geth (mac)
if: matrix.os == 'macos-12'
run: |
brew tap ethereum/ethereum
brew install ethereum
- name: Install GNU sed & GNU grep
if: matrix.os == 'macos-12'
run: |
brew install gnu-sed grep
echo "$(brew --prefix)/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
echo "$(brew --prefix)/opt/grep/libexec/gnubin" >> $GITHUB_PATH
# https://github.com/actions/cache/blob/main/examples.md#rust---cargo
- uses: actions/cache@v4
id: cache-cargo
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
docker build --build-arg FEATURES=portable -t lighthouse:local .
docker save lighthouse:local -o lighthouse-docker.tar
- name: Install lighthouse
run: make && make install-lcli
- name: Upload Docker image artifact
uses: actions/upload-artifact@v4
with:
name: lighthouse-docker
path: lighthouse-docker.tar
retention-days: 3
run-local-testnet:
runs-on: ubuntu-22.04
needs: dockerfile-ubuntu
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo add-apt-repository ppa:rmescandon/yq
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli yq
kurtosis analytics disable
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
name: lighthouse-docker
path: .
- name: Load Docker image
run: docker load -i lighthouse-docker.tar
- name: Start local testnet
run: ./start_local_testnet.sh genesis.json && sleep 60
run: ./start_local_testnet.sh -e local -c -b false && sleep 60
working-directory: scripts/local_testnet
- name: Print logs
run: ./dump_logs.sh
working-directory: scripts/local_testnet
- name: Stop local testnet
run: ./stop_local_testnet.sh
working-directory: scripts/local_testnet
- name: Clean-up testnet
run: ./clean.sh
- name: Stop local testnet and dump logs
run: ./stop_local_testnet.sh local
working-directory: scripts/local_testnet
- name: Start local testnet with blinded block production
run: ./start_local_testnet.sh -p genesis.json && sleep 60
run: ./start_local_testnet.sh -e local-blinded -c -p -b false && sleep 60
working-directory: scripts/local_testnet
- name: Print logs for blinded block testnet
run: ./dump_logs.sh
- name: Stop local testnet and dump logs
run: ./stop_local_testnet.sh local-blinded
working-directory: scripts/local_testnet
- name: Stop local testnet with blinded block production
run: ./stop_local_testnet.sh
working-directory: scripts/local_testnet
- name: Upload logs artifact
uses: actions/upload-artifact@v4
with:
name: logs-local-testnet
path: |
scripts/local_testnet/logs
retention-days: 3
doppelganger-protection-success-test:
needs: dockerfile-ubuntu
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo add-apt-repository ppa:rmescandon/yq
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli yq
kurtosis analytics disable
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
name: lighthouse-docker
path: .
- name: Load Docker image
run: docker load -i lighthouse-docker.tar
- name: Run the doppelganger protection success test script
run: |
./doppelganger_protection.sh success
working-directory: scripts/tests
- name: Upload logs artifact
uses: actions/upload-artifact@v4
with:
name: logs-doppelganger-protection-success
path: |
scripts/local_testnet/logs
retention-days: 3
doppelganger-protection-failure-test:
needs: dockerfile-ubuntu
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo add-apt-repository ppa:rmescandon/yq
echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list
sudo apt update
sudo apt install -y kurtosis-cli yq
kurtosis analytics disable
- name: Download Docker image artifact
uses: actions/download-artifact@v4
with:
name: lighthouse-docker
path: .
- name: Load Docker image
run: docker load -i lighthouse-docker.tar
- name: Run the doppelganger protection failure test script
run: |
./doppelganger_protection.sh failure
working-directory: scripts/tests
- name: Upload logs artifact
uses: actions/upload-artifact@v4
with:
name: logs-doppelganger-protection-failure
path: |
scripts/local_testnet/logs
retention-days: 3
# This job succeeds ONLY IF all others succeed. It is used by the merge queue to determine whether
# a PR is safe to merge. New jobs should be added here.
local-testnet-success:
name: local-testnet-success
runs-on: ubuntu-latest
needs: ["run-local-testnet"]
needs: [
'dockerfile-ubuntu',
'run-local-testnet',
'doppelganger-protection-success-test',
'doppelganger-protection-failure-test',
]
steps:
- uses: actions/checkout@v4
- name: Check that success job is dependent on all others

View File

@@ -259,17 +259,6 @@ jobs:
- name: Show cache stats
if: env.SELF_HOSTED_RUNNERS == 'true'
run: sccache --show-stats
dockerfile-ubuntu:
name: dockerfile-ubuntu
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build the root Dockerfile
run: docker build --build-arg FEATURES=portable -t lighthouse:local .
- name: Test the built image
run: docker run -t lighthouse:local lighthouse --version
basic-simulator-ubuntu:
name: basic-simulator-ubuntu
needs: [check-labels]
@@ -298,41 +287,6 @@ jobs:
cache-target: release
- name: Run a beacon chain sim which tests VC fallback behaviour
run: cargo run --release --bin simulator fallback-sim
doppelganger-protection-test:
name: doppelganger-protection-test
needs: [check-labels]
if: needs.check-labels.outputs.skip_ci != 'true'
runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }}
env:
# Enable portable to prevent issues with caching `blst` for the wrong CPU type
FEATURES: jemalloc,portable
steps:
- uses: actions/checkout@v4
- name: Get latest version of stable Rust
if: env.SELF_HOSTED_RUNNERS == 'false'
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
- name: Install geth
if: env.SELF_HOSTED_RUNNERS == 'false'
run: |
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum
- name: Install lighthouse
run: |
make
- name: Install lcli
run: make install-lcli
- name: Run the doppelganger protection failure test script
run: |
cd scripts/tests
./doppelganger_protection.sh failure genesis.json
- name: Run the doppelganger protection success test script
run: |
cd scripts/tests
./doppelganger_protection.sh success genesis.json
execution-engine-integration-ubuntu:
name: execution-engine-integration-ubuntu
needs: [check-labels]
@@ -465,10 +419,8 @@ jobs:
'debug-tests-ubuntu',
'state-transition-vectors-ubuntu',
'ef-tests-ubuntu',
'dockerfile-ubuntu',
'basic-simulator-ubuntu',
'fallback-simulator-ubuntu',
'doppelganger-protection-test',
'execution-engine-integration-ubuntu',
'check-code',
'check-msrv',

3
Cargo.lock generated
View File

@@ -4327,21 +4327,18 @@ dependencies = [
"deposit_contract",
"env_logger 0.9.3",
"environment",
"eth1_test_rig",
"eth2",
"eth2_network_config",
"eth2_wallet",
"ethereum_hashing",
"ethereum_ssz",
"execution_layer",
"genesis",
"hex",
"lighthouse_network",
"lighthouse_version",
"log",
"malloc_utils",
"rayon",
"sensitive_url",
"serde",
"serde_json",
"serde_yaml",

View File

@@ -1,7 +1,3 @@
//! NOTE: These tests will not pass unless an anvil is running on `ENDPOINT` (see below).
//!
//! You can start a suitable instance using the `anvil_test_node.sh` script in the `scripts`
//! dir in the root of the `lighthouse` repo.
#![cfg(test)]
use environment::{Environment, EnvironmentBuilder};
use eth1::{Eth1Endpoint, DEFAULT_CHAIN_ID};

View File

@@ -24,17 +24,14 @@ ethereum_hashing = { workspace = true }
ethereum_ssz = { workspace = true }
environment = { workspace = true }
eth2_network_config = { workspace = true }
genesis = { workspace = true }
deposit_contract = { workspace = true }
tree_hash = { workspace = true }
clap_utils = { workspace = true }
lighthouse_network = { workspace = true }
validator_dir = { workspace = true, features = ["insecure_keys"] }
validator_dir = { workspace = true }
lighthouse_version = { workspace = true }
account_utils = { workspace = true }
eth2_wallet = { workspace = true }
eth1_test_rig = { workspace = true }
sensitive_url = { workspace = true }
eth2 = { workspace = true }
snap = { workspace = true }
beacon_chain = { workspace = true }

View File

@@ -1,45 +0,0 @@
use clap::ArgMatches;
use eth2_network_config::Eth2NetworkConfig;
use ssz::Encode;
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use types::{BeaconState, EthSpec};
pub fn run<E: EthSpec>(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> {
let path = matches
.get_one::<String>("ssz-state")
.ok_or("ssz-state not specified")?
.parse::<PathBuf>()
.map_err(|e| format!("Unable to parse ssz-state: {}", e))?;
let genesis_time = matches
.get_one::<String>("genesis-time")
.ok_or("genesis-time not specified")?
.parse::<u64>()
.map_err(|e| format!("Unable to parse genesis-time: {}", e))?;
let eth2_network_config = Eth2NetworkConfig::load(testnet_dir)?;
let spec = &eth2_network_config.chain_spec::<E>()?;
let mut state: BeaconState<E> = {
let mut file = File::open(&path).map_err(|e| format!("Unable to open file: {}", e))?;
let mut ssz = vec![];
file.read_to_end(&mut ssz)
.map_err(|e| format!("Unable to read file: {}", e))?;
BeaconState::from_ssz_bytes(&ssz, spec)
.map_err(|e| format!("Unable to decode SSZ: {:?}", e))?
};
*state.genesis_time_mut() = genesis_time;
let mut file = File::create(path).map_err(|e| format!("Unable to create file: {}", e))?;
file.write_all(&state.as_ssz_bytes())
.map_err(|e| format!("Unable to write to file: {}", e))?;
Ok(())
}

View File

@@ -1,69 +0,0 @@
use clap::ArgMatches;
use clap_utils::{parse_optional, parse_required};
use ssz::Encode;
use std::fs::File;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use types::{
EthSpec, ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix,
ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra,
ForkName,
};
pub fn run<E: EthSpec>(matches: &ArgMatches) -> Result<(), String> {
let eth1_block_hash = parse_required(matches, "execution-block-hash")?;
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(),
);
let base_fee_per_gas = parse_required(matches, "base-fee-per-gas")?;
let gas_limit = parse_required(matches, "gas-limit")?;
let file_name = matches
.get_one::<String>("file")
.ok_or("No file supplied")?;
let fork_name: ForkName = parse_optional(matches, "fork")?.unwrap_or(ForkName::Bellatrix);
let execution_payload_header: ExecutionPayloadHeader<E> = match fork_name {
ForkName::Base | ForkName::Altair => return Err("invalid fork name".to_string()),
ForkName::Bellatrix => ExecutionPayloadHeader::Bellatrix(ExecutionPayloadHeaderBellatrix {
gas_limit,
base_fee_per_gas,
timestamp: genesis_time,
block_hash: eth1_block_hash,
prev_randao: eth1_block_hash.into_root(),
..ExecutionPayloadHeaderBellatrix::default()
}),
ForkName::Capella => ExecutionPayloadHeader::Capella(ExecutionPayloadHeaderCapella {
gas_limit,
base_fee_per_gas,
timestamp: genesis_time,
block_hash: eth1_block_hash,
prev_randao: eth1_block_hash.into_root(),
..ExecutionPayloadHeaderCapella::default()
}),
ForkName::Deneb => ExecutionPayloadHeader::Deneb(ExecutionPayloadHeaderDeneb {
gas_limit,
base_fee_per_gas,
timestamp: genesis_time,
block_hash: eth1_block_hash,
prev_randao: eth1_block_hash.into_root(),
..ExecutionPayloadHeaderDeneb::default()
}),
ForkName::Electra => ExecutionPayloadHeader::Electra(ExecutionPayloadHeaderElectra {
gas_limit,
base_fee_per_gas,
timestamp: genesis_time,
block_hash: eth1_block_hash,
prev_randao: eth1_block_hash.into_root(),
..ExecutionPayloadHeaderElectra::default()
}),
};
let mut file = File::create(file_name).map_err(|_| "Unable to create file".to_string())?;
let bytes = execution_payload_header.as_ssz_bytes();
file.write_all(bytes.as_slice())
.map_err(|_| "Unable to write to file".to_string())?;
Ok(())
}

View File

@@ -1,32 +0,0 @@
use clap::ArgMatches;
use environment::Environment;
use types::EthSpec;
use eth1_test_rig::{Http, Provider};
pub fn run<E: EthSpec>(env: Environment<E>, matches: &ArgMatches) -> Result<(), String> {
let eth1_http: String = clap_utils::parse_required(matches, "eth1-http")?;
let confirmations: usize = clap_utils::parse_required(matches, "confirmations")?;
let validator_count: Option<usize> = clap_utils::parse_optional(matches, "validator-count")?;
let client = Provider::<Http>::try_from(&eth1_http)
.map_err(|e| format!("Unable to connect to eth1 HTTP: {:?}", e))?;
env.runtime().block_on(async {
let contract = eth1_test_rig::DepositContract::deploy(client, confirmations, None)
.await
.map_err(|e| format!("Failed to deploy deposit contract: {:?}", e))?;
println!("Deposit contract address: {:?}", contract.address());
// Deposit insecure validators to the deposit contract created
if let Some(validator_count) = validator_count {
let amount = env.eth2_config.spec.max_effective_balance;
for i in 0..validator_count {
println!("Submitting deposit for validator {}...", i);
contract.deposit_deterministic_async::<E>(i, amount).await?;
}
}
Ok(())
})
}

View File

@@ -1,66 +0,0 @@
use clap::ArgMatches;
use environment::Environment;
use eth2_network_config::Eth2NetworkConfig;
use genesis::{Eth1Config, Eth1Endpoint, Eth1GenesisService};
use sensitive_url::SensitiveUrl;
use ssz::Encode;
use std::cmp::max;
use std::path::PathBuf;
use std::time::Duration;
use types::EthSpec;
/// Interval between polling the eth1 node for genesis information.
pub const ETH1_GENESIS_UPDATE_INTERVAL: Duration = Duration::from_millis(7_000);
pub fn run<E: EthSpec>(
env: Environment<E>,
testnet_dir: PathBuf,
matches: &ArgMatches,
) -> Result<(), String> {
let endpoints = matches
.get_one::<String>("eth1-endpoint")
.map(|e| {
warn!("The --eth1-endpoint flag is deprecated. Please use --eth1-endpoints instead");
String::from(e)
})
.or_else(|| {
matches
.get_one::<String>("eth1-endpoints")
.map(String::from)
});
let mut eth2_network_config = Eth2NetworkConfig::load(testnet_dir.clone())?;
let spec = eth2_network_config.chain_spec::<E>()?;
let mut config = Eth1Config::default();
if let Some(v) = endpoints.clone() {
let endpoint = SensitiveUrl::parse(&v)
.map_err(|e| format!("Unable to parse eth1 endpoint URL: {:?}", e))?;
config.endpoint = Eth1Endpoint::NoAuth(endpoint);
}
config.deposit_contract_address = format!("{:?}", spec.deposit_contract_address);
config.deposit_contract_deploy_block = eth2_network_config.deposit_contract_deploy_block;
config.lowest_cached_block_number = eth2_network_config.deposit_contract_deploy_block;
config.follow_distance = spec.eth1_follow_distance / 2;
config.node_far_behind_seconds = max(5, config.follow_distance) * spec.seconds_per_eth1_block;
let genesis_service =
Eth1GenesisService::new(config, env.core_context().log().clone(), spec.clone())?;
env.runtime().block_on(async {
let _ = genesis_service
.wait_for_genesis_state::<E>(ETH1_GENESIS_UPDATE_INTERVAL, spec)
.await
.map(move |genesis_state| {
eth2_network_config.genesis_state_bytes = Some(genesis_state.as_ssz_bytes().into());
eth2_network_config.force_write_to_file(testnet_dir)
})
.map_err(|e| format!("Failed to find genesis: {}", e))?;
info!("Starting service to produce genesis BeaconState from eth1");
info!("Connecting to eth1 http endpoints: {:?}", endpoints);
Ok(())
})
}

View File

@@ -1,64 +0,0 @@
use clap::ArgMatches;
use std::fs;
use std::path::PathBuf;
use validator_dir::Builder as ValidatorBuilder;
/// Generates validator directories with INSECURE, deterministic keypairs given the range
/// of indices, validator and secret directories.
pub fn generate_validator_dirs(
indices: &[usize],
validators_dir: PathBuf,
secrets_dir: PathBuf,
) -> Result<(), String> {
if !validators_dir.exists() {
fs::create_dir_all(&validators_dir)
.map_err(|e| format!("Unable to create validators dir: {:?}", e))?;
}
if !secrets_dir.exists() {
fs::create_dir_all(&secrets_dir)
.map_err(|e| format!("Unable to create secrets dir: {:?}", e))?;
}
for i in indices {
println!("Validator {}", i + 1);
ValidatorBuilder::new(validators_dir.clone())
.password_dir(secrets_dir.clone())
.store_withdrawal_keystore(false)
.insecure_voting_keypair(*i)
.map_err(|e| format!("Unable to generate keys: {:?}", e))?
.build()
.map_err(|e| format!("Unable to build validator: {:?}", e))?;
}
Ok(())
}
pub fn run(matches: &ArgMatches) -> Result<(), String> {
let validator_count: usize = clap_utils::parse_required(matches, "count")?;
let base_dir: PathBuf = clap_utils::parse_required(matches, "base-dir")?;
let node_count: Option<usize> = clap_utils::parse_optional(matches, "node-count")?;
if let Some(node_count) = node_count {
let validators_per_node = validator_count / node_count;
let validator_range = (0..validator_count).collect::<Vec<_>>();
let indices_range = validator_range
.chunks(validators_per_node)
.collect::<Vec<_>>();
for (i, indices) in indices_range.iter().enumerate() {
let validators_dir = base_dir.join(format!("node_{}", i + 1)).join("validators");
let secrets_dir = base_dir.join(format!("node_{}", i + 1)).join("secrets");
generate_validator_dirs(indices, validators_dir, secrets_dir)?;
}
} else {
let validators_dir = base_dir.join("validators");
let secrets_dir = base_dir.join("secrets");
generate_validator_dirs(
(0..validator_count).collect::<Vec<_>>().as_slice(),
validators_dir,
secrets_dir,
)?;
}
Ok(())
}

View File

@@ -1,49 +0,0 @@
use clap::ArgMatches;
use clap_utils::parse_ssz_optional;
use eth2_network_config::Eth2NetworkConfig;
use genesis::{interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH};
use ssz::Encode;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use types::{test_utils::generate_deterministic_keypairs, EthSpec, Hash256};
pub fn run<E: EthSpec>(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> {
let validator_count = matches
.get_one::<String>("validator-count")
.ok_or("validator-count not specified")?
.parse::<usize>()
.map_err(|e| format!("Unable to parse validator-count: {}", e))?;
let genesis_time = if let Some(genesis_time) = matches.get_one::<String>("genesis-time") {
genesis_time
.parse::<u64>()
.map_err(|e| format!("Unable to parse genesis-time: {}", e))?
} else {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("Unable to get time: {:?}", e))?
.as_secs()
};
let mut eth2_network_config = Eth2NetworkConfig::load(testnet_dir.clone())?;
let mut spec = eth2_network_config.chain_spec::<E>()?;
if let Some(v) = parse_ssz_optional(matches, "genesis-fork-version")? {
spec.genesis_fork_version = v;
}
let keypairs = generate_deterministic_keypairs(validator_count);
let genesis_state = interop_genesis_state::<E>(
&keypairs,
genesis_time,
Hash256::from_slice(DEFAULT_ETH1_BLOCK_HASH),
None,
&spec,
)?;
eth2_network_config.genesis_state_bytes = Some(genesis_state.as_ssz_bytes().into());
eth2_network_config.force_write_to_file(testnet_dir)?;
Ok(())
}

View File

@@ -1,20 +1,12 @@
#[macro_use]
extern crate log;
mod block_root;
mod change_genesis_time;
mod check_deposit_data;
mod create_payload_header;
mod deploy_deposit_contract;
mod eth1_genesis;
mod generate_bootnode_enr;
mod indexed_attestations;
mod insecure_validators;
mod interop_genesis;
mod mnemonic_validators;
mod mock_el;
mod new_testnet;
mod parse_ssz;
mod replace_state_pubkeys;
mod skip_slots;
mod state_root;
mod transition_blocks;
@@ -272,489 +264,6 @@ fn main() {
.display_order(0)
)
)
.subcommand(
Command::new("deploy-deposit-contract")
.about(
"Deploy a testing eth1 deposit contract.",
)
.arg(
Arg::new("eth1-http")
.long("eth1-http")
.short('e')
.value_name("ETH1_HTTP_PATH")
.help("Path to an Eth1 JSON-RPC IPC endpoint")
.action(ArgAction::Set)
.required(true)
.display_order(0)
)
.arg(
Arg::new("confirmations")
.value_name("INTEGER")
.long("confirmations")
.action(ArgAction::Set)
.default_value("3")
.help("The number of block confirmations before declaring the contract deployed.")
.display_order(0)
)
.arg(
Arg::new("validator-count")
.value_name("VALIDATOR_COUNT")
.long("validator-count")
.action(ArgAction::Set)
.help("If present, makes `validator_count` number of INSECURE deterministic deposits after \
deploying the deposit contract."
)
.display_order(0)
)
)
.subcommand(
Command::new("eth1-genesis")
.about("Listens to the eth1 chain and finds the genesis beacon state")
.arg(
Arg::new("eth1-endpoint")
.short('e')
.long("eth1-endpoint")
.value_name("HTTP_SERVER")
.action(ArgAction::Set)
.help("Deprecated. Use --eth1-endpoints.")
.display_order(0)
)
.arg(
Arg::new("eth1-endpoints")
.long("eth1-endpoints")
.value_name("HTTP_SERVER_LIST")
.action(ArgAction::Set)
.conflicts_with("eth1-endpoint")
.help(
"One or more comma-delimited URLs to eth1 JSON-RPC http APIs. \
If multiple endpoints are given the endpoints are used as \
fallback in the given order.",
)
.display_order(0)
),
)
.subcommand(
Command::new("interop-genesis")
.about("Produces an interop-compatible genesis state using deterministic keypairs")
.arg(
Arg::new("validator-count")
.long("validator-count")
.index(1)
.value_name("INTEGER")
.action(ArgAction::Set)
.default_value("1024")
.help("The number of validators in the genesis state.")
.display_order(0)
)
.arg(
Arg::new("genesis-time")
.long("genesis-time")
.short('t')
.value_name("UNIX_EPOCH")
.action(ArgAction::Set)
.help("The value for state.genesis_time. Defaults to now.")
.display_order(0)
)
.arg(
Arg::new("genesis-fork-version")
.long("genesis-fork-version")
.value_name("HEX")
.action(ArgAction::Set)
.help(
"Used to avoid reply attacks between testnets. Recommended to set to
non-default.",
)
.display_order(0)
),
)
.subcommand(
Command::new("change-genesis-time")
.about(
"Loads a file with an SSZ-encoded BeaconState and modifies the genesis time.",
)
.arg(
Arg::new("ssz-state")
.index(1)
.value_name("PATH")
.action(ArgAction::Set)
.required(true)
.help("The path to the SSZ file")
.display_order(0)
)
.arg(
Arg::new("genesis-time")
.index(2)
.value_name("UNIX_EPOCH")
.action(ArgAction::Set)
.required(true)
.help("The value for state.genesis_time.")
.display_order(0)
),
)
.subcommand(
Command::new("replace-state-pubkeys")
.about(
"Loads a file with an SSZ-encoded BeaconState and replaces \
all the validator pubkeys with ones derived from the mnemonic \
such that validator indices correspond to EIP-2334 voting keypair \
derivation paths.",
)
.arg(
Arg::new("ssz-state")
.index(1)
.value_name("PATH")
.action(ArgAction::Set)
.required(true)
.help("The path to the SSZ file")
.display_order(0)
)
.arg(
Arg::new("mnemonic")
.index(2)
.value_name("BIP39_MNENMONIC")
.action(ArgAction::Set)
.default_value(
"replace nephew blur decorate waste convince soup column \
orient excite play baby",
)
.help("The mnemonic for key derivation.")
.display_order(0)
),
)
.subcommand(
Command::new("create-payload-header")
.about("Generates an SSZ file containing bytes for an `ExecutionPayloadHeader`. \
Useful as input for `lcli new-testnet --execution-payload-header FILE`. If `--fork` \
is not provided, a payload header for the `Bellatrix` fork will be created.")
.arg(
Arg::new("execution-block-hash")
.long("execution-block-hash")
.value_name("BLOCK_HASH")
.action(ArgAction::Set)
.help("The block hash used when generating an execution payload. This \
value is used for `execution_payload_header.block_hash` as well as \
`execution_payload_header.random`")
.default_value(
"0x0000000000000000000000000000000000000000000000000000000000000000",
)
.display_order(0)
)
.arg(
Arg::new("genesis-time")
.long("genesis-time")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("The genesis time when generating an execution payload.")
.display_order(0)
)
.arg(
Arg::new("base-fee-per-gas")
.long("base-fee-per-gas")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("The base fee per gas field in the execution payload generated.")
.default_value("1000000000")
.display_order(0)
)
.arg(
Arg::new("gas-limit")
.long("gas-limit")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("The gas limit field in the execution payload generated.")
.default_value("30000000")
.display_order(0)
)
.arg(
Arg::new("file")
.long("file")
.value_name("FILE")
.action(ArgAction::Set)
.required(true)
.help("Output file")
.display_order(0)
).arg(
Arg::new("fork")
.long("fork")
.value_name("FORK")
.action(ArgAction::Set)
.default_value("bellatrix")
.help("The fork for which the execution payload header should be created.")
.value_parser(["bellatrix", "capella", "deneb", "electra"])
.display_order(0)
)
)
.subcommand(
Command::new("new-testnet")
.about(
"Produce a new testnet directory. If any of the optional flags are not
supplied the values will remain the default for the --spec flag",
)
.arg(
Arg::new("force")
.long("force")
.short('f')
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help("Overwrites any previous testnet configurations")
.display_order(0)
)
.arg(
Arg::new("interop-genesis-state")
.long("interop-genesis-state")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help(
"If present, a interop-style genesis.ssz file will be generated.",
)
.display_order(0)
)
.arg(
Arg::new("derived-genesis-state")
.long("derived-genesis-state")
.action(ArgAction::SetTrue)
.help_heading(FLAG_HEADER)
.help(
"If present, a genesis.ssz file will be generated with keys generated from a given mnemonic.",
)
.display_order(0)
)
.arg(
Arg::new("mnemonic-phrase")
.long("mnemonic-phrase")
.value_name("MNEMONIC_PHRASE")
.action(ArgAction::Set)
.requires("derived-genesis-state")
.help("The mnemonic with which we generate the validator keys for a derived genesis state")
.display_order(0)
)
.arg(
Arg::new("min-genesis-time")
.long("min-genesis-time")
.value_name("UNIX_SECONDS")
.action(ArgAction::Set)
.help(
"The minimum permitted genesis time. For non-eth1 testnets will be
the genesis time. Defaults to now.",
)
.display_order(0)
)
.arg(
Arg::new("min-genesis-active-validator-count")
.long("min-genesis-active-validator-count")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("The number of validators required to trigger eth2 genesis.")
.display_order(0)
)
.arg(
Arg::new("genesis-delay")
.long("genesis-delay")
.value_name("SECONDS")
.action(ArgAction::Set)
.help("The delay between sufficient eth1 deposits and eth2 genesis.")
.display_order(0)
)
.arg(
Arg::new("min-deposit-amount")
.long("min-deposit-amount")
.value_name("GWEI")
.action(ArgAction::Set)
.help("The minimum permitted deposit amount.")
.display_order(0)
)
.arg(
Arg::new("max-effective-balance")
.long("max-effective-balance")
.value_name("GWEI")
.action(ArgAction::Set)
.help("The amount required to become a validator.")
.display_order(0)
)
.arg(
Arg::new("effective-balance-increment")
.long("effective-balance-increment")
.value_name("GWEI")
.action(ArgAction::Set)
.help("The steps in effective balance calculation.")
.display_order(0)
)
.arg(
Arg::new("ejection-balance")
.long("ejection-balance")
.value_name("GWEI")
.action(ArgAction::Set)
.help("The balance at which a validator gets ejected.")
.display_order(0)
)
.arg(
Arg::new("eth1-follow-distance")
.long("eth1-follow-distance")
.value_name("ETH1_BLOCKS")
.action(ArgAction::Set)
.help("The distance to follow behind the eth1 chain head.")
.display_order(0)
)
.arg(
Arg::new("genesis-fork-version")
.long("genesis-fork-version")
.value_name("HEX")
.action(ArgAction::Set)
.help(
"Used to avoid reply attacks between testnets. Recommended to set to
non-default.",
)
.display_order(0)
)
.arg(
Arg::new("seconds-per-slot")
.long("seconds-per-slot")
.value_name("SECONDS")
.action(ArgAction::Set)
.help("Eth2 slot time")
.display_order(0)
)
.arg(
Arg::new("seconds-per-eth1-block")
.long("seconds-per-eth1-block")
.value_name("SECONDS")
.action(ArgAction::Set)
.help("Eth1 block time")
.display_order(0)
)
.arg(
Arg::new("eth1-id")
.long("eth1-id")
.value_name("ETH1_ID")
.action(ArgAction::Set)
.help("The chain id and network id for the eth1 testnet.")
.display_order(0)
)
.arg(
Arg::new("deposit-contract-address")
.long("deposit-contract-address")
.value_name("ETH1_ADDRESS")
.action(ArgAction::Set)
.required(true)
.help("The address of the deposit contract.")
.display_order(0)
)
.arg(
Arg::new("deposit-contract-deploy-block")
.long("deposit-contract-deploy-block")
.value_name("ETH1_BLOCK_NUMBER")
.action(ArgAction::Set)
.default_value("0")
.help(
"The block the deposit contract was deployed. Setting this is a huge
optimization for nodes, please do it.",
)
.display_order(0)
)
.arg(
Arg::new("altair-fork-epoch")
.long("altair-fork-epoch")
.value_name("EPOCH")
.action(ArgAction::Set)
.help(
"The epoch at which to enable the Altair hard fork",
)
.display_order(0)
)
.arg(
Arg::new("bellatrix-fork-epoch")
.long("bellatrix-fork-epoch")
.value_name("EPOCH")
.action(ArgAction::Set)
.help(
"The epoch at which to enable the Bellatrix hard fork",
)
.display_order(0)
)
.arg(
Arg::new("capella-fork-epoch")
.long("capella-fork-epoch")
.value_name("EPOCH")
.action(ArgAction::Set)
.help(
"The epoch at which to enable the Capella hard fork",
)
.display_order(0)
)
.arg(
Arg::new("deneb-fork-epoch")
.long("deneb-fork-epoch")
.value_name("EPOCH")
.action(ArgAction::Set)
.help(
"The epoch at which to enable the Deneb hard fork",
)
.display_order(0)
)
.arg(
Arg::new("electra-fork-epoch")
.long("electra-fork-epoch")
.value_name("EPOCH")
.action(ArgAction::Set)
.help(
"The epoch at which to enable the Electra hard fork",
)
.display_order(0)
)
.arg(
Arg::new("ttd")
.long("ttd")
.value_name("TTD")
.action(ArgAction::Set)
.help(
"The terminal total difficulty",
)
.display_order(0)
)
.arg(
Arg::new("eth1-block-hash")
.long("eth1-block-hash")
.value_name("BLOCK_HASH")
.action(ArgAction::Set)
.help("The eth1 block hash used when generating a genesis state.")
.display_order(0)
)
.arg(
Arg::new("execution-payload-header")
.long("execution-payload-header")
.value_name("FILE")
.action(ArgAction::Set)
.required(false)
.help("Path to file containing `ExecutionPayloadHeader` SSZ bytes to be \
used in the genesis state.")
.display_order(0)
)
.arg(
Arg::new("validator-count")
.long("validator-count")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("The number of validators when generating a genesis state.")
.display_order(0)
)
.arg(
Arg::new("genesis-time")
.long("genesis-time")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("The genesis time when generating a genesis state.")
.display_order(0)
)
.arg(
Arg::new("proposer-score-boost")
.long("proposer-score-boost")
.value_name("INTEGER")
.action(ArgAction::Set)
.help("The proposer score boost to apply as a percentage, e.g. 70 = 70%")
.display_order(0)
)
)
.subcommand(
Command::new("check-deposit-data")
.about("Checks the integrity of some deposit data.")
@@ -834,36 +343,6 @@ fn main() {
.display_order(0)
),
)
.subcommand(
Command::new("insecure-validators")
.about("Produces validator directories with INSECURE, deterministic keypairs.")
.arg(
Arg::new("count")
.long("count")
.value_name("COUNT")
.action(ArgAction::Set)
.required(true)
.help("Produces validators in the range of 0..count.")
.display_order(0)
)
.arg(
Arg::new("base-dir")
.long("base-dir")
.value_name("BASE_DIR")
.action(ArgAction::Set)
.required(true)
.help("The base directory where validator keypairs and secrets are stored")
.display_order(0)
)
.arg(
Arg::new("node-count")
.long("node-count")
.value_name("NODE_COUNT")
.action(ArgAction::Set)
.help("The number of nodes to divide the validator keys to")
.display_order(0)
)
)
.subcommand(
Command::new("mnemonic-validators")
.about("Produces validator directories by deriving the keys from \
@@ -1128,9 +607,6 @@ fn run<E: EthSpec>(env_builder: EnvironmentBuilder<E>, matches: &ArgMatches) ->
(None, Some(network_name))
};
// Lazily load either the testnet dir or the network config, as required.
// Some subcommands like new-testnet need the testnet dir but not the network config.
let get_testnet_dir = || testnet_dir.clone().ok_or("testnet-dir is required");
let get_network_config = || {
if let Some(testnet_dir) = &testnet_dir {
Eth2NetworkConfig::load(testnet_dir.clone()).map_err(|e| {
@@ -1162,43 +638,10 @@ fn run<E: EthSpec>(env_builder: EnvironmentBuilder<E>, matches: &ArgMatches) ->
run_parse_ssz::<E>(network_config, matches)
.map_err(|e| format!("Failed to pretty print hex: {}", e))
}
Some(("deploy-deposit-contract", matches)) => {
deploy_deposit_contract::run::<E>(env, matches)
.map_err(|e| format!("Failed to run deploy-deposit-contract command: {}", e))
}
Some(("eth1-genesis", matches)) => {
let testnet_dir = get_testnet_dir()?;
eth1_genesis::run::<E>(env, testnet_dir, matches)
.map_err(|e| format!("Failed to run eth1-genesis command: {}", e))
}
Some(("interop-genesis", matches)) => {
let testnet_dir = get_testnet_dir()?;
interop_genesis::run::<E>(testnet_dir, matches)
.map_err(|e| format!("Failed to run interop-genesis command: {}", e))
}
Some(("change-genesis-time", matches)) => {
let testnet_dir = get_testnet_dir()?;
change_genesis_time::run::<E>(testnet_dir, matches)
.map_err(|e| format!("Failed to run change-genesis-time command: {}", e))
}
Some(("create-payload-header", matches)) => create_payload_header::run::<E>(matches)
.map_err(|e| format!("Failed to run create-payload-header command: {}", e)),
Some(("replace-state-pubkeys", matches)) => {
let testnet_dir = get_testnet_dir()?;
replace_state_pubkeys::run::<E>(testnet_dir, matches)
.map_err(|e| format!("Failed to run replace-state-pubkeys command: {}", e))
}
Some(("new-testnet", matches)) => {
let testnet_dir = get_testnet_dir()?;
new_testnet::run::<E>(testnet_dir, matches)
.map_err(|e| format!("Failed to run new_testnet command: {}", e))
}
Some(("check-deposit-data", matches)) => check_deposit_data::run(matches)
.map_err(|e| format!("Failed to run check-deposit-data command: {}", e)),
Some(("generate-bootnode-enr", matches)) => generate_bootnode_enr::run::<E>(matches)
.map_err(|e| format!("Failed to run generate-bootnode-enr command: {}", e)),
Some(("insecure-validators", matches)) => insecure_validators::run(matches)
.map_err(|e| format!("Failed to run insecure-validators command: {}", e)),
Some(("mnemonic-validators", matches)) => mnemonic_validators::run(matches)
.map_err(|e| format!("Failed to run mnemonic-validators command: {}", e)),
Some(("indexed-attestations", matches)) => indexed_attestations::run::<E>(matches)

View File

@@ -1,393 +0,0 @@
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, TRUSTED_SETUP_BYTES};
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, upgrade_to_capella, upgrade_to_deneb,
upgrade_to_electra,
};
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, ExecutionPayloadHeaderBellatrix,
ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra,
ForkName, Hash256, Keypair, PublicKey, Validator,
};
pub fn run<E: 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.get_flag("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 = E::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(fork_epoch) = parse_optional(matches, "deneb-fork-epoch")? {
spec.deneb_fork_epoch = Some(fork_epoch);
}
if let Some(fork_epoch) = parse_optional(matches, "electra-fork-epoch")? {
spec.electra_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<E>> =
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::Bellatrix => {
ExecutionPayloadHeaderBellatrix::<E>::from_ssz_bytes(bytes.as_slice())
.map(ExecutionPayloadHeader::Bellatrix)
}
ForkName::Capella => {
ExecutionPayloadHeaderCapella::<E>::from_ssz_bytes(bytes.as_slice())
.map(ExecutionPayloadHeader::Capella)
}
ForkName::Deneb => {
ExecutionPayloadHeaderDeneb::<E>::from_ssz_bytes(bytes.as_slice())
.map(ExecutionPayloadHeader::Deneb)
}
ForkName::Electra => {
ExecutionPayloadHeaderElectra::<E>::from_ssz_bytes(bytes.as_slice())
.map(ExecutionPayloadHeader::Electra)
}
}
.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.get_flag("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::<E>(
&keypairs,
genesis_time,
eth1_block_hash.into_root(),
execution_payload_header,
&spec,
)?;
Some(genesis_state.as_ssz_bytes())
} else if matches.get_flag("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::<E>(
&keypairs,
genesis_time,
eth1_block_hash.into_root(),
execution_payload_header,
&spec,
)?;
Some(genesis_state.as_ssz_bytes())
} else {
None
};
let kzg_trusted_setup = if let Some(epoch) = spec.deneb_fork_epoch {
// Only load the trusted setup if the deneb fork epoch is set
if epoch != Epoch::max_value() {
Some(TRUSTED_SETUP_BYTES.to_vec())
} else {
None
}
} 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::<E>(&spec),
kzg_trusted_setup,
};
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<E: EthSpec>(
keypairs: &[(Keypair, Keypair)], // Voting and Withdrawal keypairs
genesis_time: u64,
eth1_block_hash: Hash256,
execution_payload_header: Option<ExecutionPayloadHeader<E>>,
spec: &ChainSpec,
) -> Result<BeaconState<E>, String> {
// If no header is provided, then start from a Bellatrix state by default
let default_header: ExecutionPayloadHeader<E> =
ExecutionPayloadHeader::Bellatrix(ExecutionPayloadHeaderBellatrix {
block_hash: ExecutionBlockHash::from_root(eth1_block_hash),
parent_hash: ExecutionBlockHash::zero(),
..ExecutionPayloadHeaderBellatrix::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).unwrap();
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 == E::genesis_epoch())
{
upgrade_to_altair(&mut state, spec).unwrap();
state.fork_mut().previous_version = spec.altair_fork_version;
}
// Similarly, perform an upgrade to Bellatrix if configured from genesis.
if spec
.bellatrix_fork_epoch
.map_or(false, |fork_epoch| fork_epoch == E::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/bellatrix/beacon-chain.md#testing
if let ExecutionPayloadHeader::Bellatrix(ref header) = execution_payload_header {
*state
.latest_execution_payload_header_bellatrix_mut()
.or(Err("mismatched fork".to_string()))? = header.clone();
}
}
// Similarly, perform an upgrade to Capella if configured from genesis.
if spec
.capella_fork_epoch
.map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch())
{
upgrade_to_capella(&mut state, spec).unwrap();
// Remove intermediate Bellatrix fork from `state.fork`.
state.fork_mut().previous_version = spec.capella_fork_version;
// Override latest execution payload header.
// See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/bellatrix/beacon-chain.md#testing
if let ExecutionPayloadHeader::Capella(ref header) = execution_payload_header {
*state
.latest_execution_payload_header_capella_mut()
.or(Err("mismatched fork".to_string()))? = header.clone();
}
}
// Similarly, perform an upgrade to Deneb if configured from genesis.
if spec
.deneb_fork_epoch
.map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch())
{
upgrade_to_deneb(&mut state, spec).unwrap();
// Remove intermediate Capella fork from `state.fork`.
state.fork_mut().previous_version = spec.deneb_fork_version;
// Override latest execution payload header.
// See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/bellatrix/beacon-chain.md#testing
if let ExecutionPayloadHeader::Deneb(ref header) = execution_payload_header {
*state
.latest_execution_payload_header_deneb_mut()
.or(Err("mismatched fork".to_string()))? = header.clone();
}
}
// Similarly, perform an upgrade to Electra if configured from genesis.
if spec
.electra_fork_epoch
.map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch())
{
upgrade_to_electra(&mut state, spec).unwrap();
// Remove intermediate Deneb fork from `state.fork`.
state.fork_mut().previous_version = spec.electra_fork_version;
// Override latest execution payload header.
// See https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/bellatrix/beacon-chain.md#testing
if let ExecutionPayloadHeader::Electra(ref header) = execution_payload_header {
*state
.latest_execution_payload_header_electra_mut()
.or(Err("mismatched fork".to_string()))? = header.clone();
}
}
// 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();
// Sanity check for state fork matching config fork.
state
.fork_name(spec)
.map_err(|e| format!("state fork mismatch: {e:?}"))?;
Ok(state)
}

View File

@@ -1,86 +0,0 @@
use account_utils::{eth2_keystore::keypair_from_secret, mnemonic_from_phrase};
use clap::ArgMatches;
use eth2_network_config::Eth2NetworkConfig;
use eth2_wallet::bip39::Seed;
use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType};
use ssz::Encode;
use state_processing::common::DepositDataTree;
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use tree_hash::TreeHash;
use types::{BeaconState, DepositData, EthSpec, Hash256, SignatureBytes, DEPOSIT_TREE_DEPTH};
pub fn run<E: EthSpec>(testnet_dir: PathBuf, matches: &ArgMatches) -> Result<(), String> {
let path = matches
.get_one::<String>("ssz-state")
.ok_or("ssz-state not specified")?
.parse::<PathBuf>()
.map_err(|e| format!("Unable to parse ssz-state: {}", e))?;
let mnemonic_phrase = matches
.get_one::<String>("mnemonic")
.ok_or("mnemonic not specified")?;
let eth2_network_config = Eth2NetworkConfig::load(testnet_dir)?;
let spec = &eth2_network_config.chain_spec::<E>()?;
let mut state: BeaconState<E> = {
let mut file = File::open(&path).map_err(|e| format!("Unable to open file: {}", e))?;
let mut ssz = vec![];
file.read_to_end(&mut ssz)
.map_err(|e| format!("Unable to read file: {}", e))?;
BeaconState::from_ssz_bytes(&ssz, spec)
.map_err(|e| format!("Unable to decode SSZ: {:?}", e))?
};
let mnemonic = mnemonic_from_phrase(mnemonic_phrase)?;
let seed = Seed::new(&mnemonic, "");
let mut deposit_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH);
let mut deposit_root = Hash256::zero();
let validators = state.validators_mut();
for index in 0..validators.len() {
let (secret, _) =
recover_validator_secret_from_mnemonic(seed.as_bytes(), index as u32, KeyType::Voting)
.map_err(|e| format!("Unable to generate validator key: {:?}", e))?;
let keypair = keypair_from_secret(secret.as_bytes())
.map_err(|e| format!("Unable build keystore: {:?}", e))?;
eprintln!("{}: {}", index, keypair.pk);
validators.get_mut(index).unwrap().pubkey = keypair.pk.into();
// Update the deposit tree.
let mut deposit_data = DepositData {
pubkey: validators.get(index).unwrap().pubkey,
// Set this to a junk value since it's very time consuming to generate the withdrawal
// keys and it's not useful for the time being.
withdrawal_credentials: Hash256::zero(),
amount: spec.min_deposit_amount,
signature: SignatureBytes::empty(),
};
deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec);
deposit_tree
.push_leaf(deposit_data.tree_hash_root())
.map_err(|e| format!("failed to create deposit tree: {:?}", e))?;
deposit_root = deposit_tree.root();
}
// Update the genesis validators root since we changed the validators.
*state.genesis_validators_root_mut() = state.validators().tree_hash_root();
// Update the deposit root with our simulated deposits.
state.eth1_data_mut().deposit_root = deposit_root;
let mut file = File::create(path).map_err(|e| format!("Unable to create file: {}", e))?;
file.write_all(&state.as_ssz_bytes())
.map_err(|e| format!("Unable to write to file: {}", e))?;
Ok(())
}

1
scripts/local_testnet/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
logs

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source ./vars.env
exec anvil \
--balance 1000000000 \
--gas-limit 1000000000 \
--accounts 10 \
--mnemonic "$ETH1_NETWORK_MNEMONIC" \
--block-time $SECONDS_PER_ETH1_BLOCK \
--port 8545 \
--chain-id "$CHAIN_ID"

View File

@@ -1,70 +0,0 @@
#!/usr/bin/env bash
#
# Starts a beacon node based upon a genesis state created by `./setup.sh`.
#
set -Eeuo pipefail
source ./vars.env
SUBSCRIBE_ALL_SUBNETS=
DEBUG_LEVEL=${DEBUG_LEVEL:-info}
# Get options
while getopts "d:sh" flag; do
case "${flag}" in
d) DEBUG_LEVEL=${OPTARG};;
s) SUBSCRIBE_ALL_SUBNETS="--subscribe-all-subnets";;
h)
echo "Start a beacon node"
echo
echo "usage: $0 <Options> <DATADIR> <NETWORK-PORT> <HTTP-PORT>"
echo
echo "Options:"
echo " -s: pass --subscribe-all-subnets to 'lighthouse bn ...', default is not passed"
echo " -d: DEBUG_LEVEL, default info"
echo " -h: this help"
echo
echo "Positional arguments:"
echo " DATADIR Value for --datadir parameter"
echo " NETWORK-PORT Value for --enr-udp-port, --enr-tcp-port and --port"
echo " HTTP-PORT Value for --http-port"
echo " EXECUTION-ENDPOINT Value for --execution-endpoint"
echo " EXECUTION-JWT Value for --execution-jwt"
exit
;;
esac
done
# Get positional arguments
data_dir=${@:$OPTIND+0:1}
tcp_port=${@:$OPTIND+1:1}
quic_port=${@:$OPTIND+2:1}
http_port=${@:$OPTIND+3:1}
execution_endpoint=${@:$OPTIND+4:1}
execution_jwt=${@:$OPTIND+5:1}
lighthouse_binary=lighthouse
exec $lighthouse_binary \
--debug-level $DEBUG_LEVEL \
bn \
$SUBSCRIBE_ALL_SUBNETS \
--datadir $data_dir \
--testnet-dir $TESTNET_DIR \
--enable-private-discovery \
--disable-peer-scoring \
--staking \
--enr-address 127.0.0.1 \
--enr-udp-port $tcp_port \
--enr-tcp-port $tcp_port \
--enr-quic-port $quic_port \
--port $tcp_port \
--quic-port $quic_port \
--http-port $http_port \
--disable-packet-filter \
--target-peers $((BN_COUNT - 1)) \
--execution-endpoint $execution_endpoint \
--execution-jwt $execution_jwt \
$BN_ARGS

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env bash
#
# Generates a bootnode enr and saves it in $TESTNET/boot_enr.yaml
# Starts a bootnode from the generated enr.
#
set -Eeuo pipefail
source ./vars.env
echo "Generating bootnode enr"
lcli \
generate-bootnode-enr \
--ip 127.0.0.1 \
--udp-port $BOOTNODE_PORT \
--tcp-port $BOOTNODE_PORT \
--genesis-fork-version $GENESIS_FORK_VERSION \
--output-dir $DATADIR/bootnode
bootnode_enr=`cat $DATADIR/bootnode/enr.dat`
echo "- $bootnode_enr" > $TESTNET_DIR/boot_enr.yaml
echo "Generated bootnode enr and written to $TESTNET_DIR/boot_enr.yaml"
DEBUG_LEVEL=${1:-info}
echo "Starting bootnode"
exec lighthouse boot_node \
--testnet-dir $TESTNET_DIR \
--port $BOOTNODE_PORT \
--listen-address 127.0.0.1 \
--disable-packet-filter \
--network-dir $DATADIR/bootnode \

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
#
# Deletes all files associated with the local testnet.
#
set -Eeuo pipefail
source ./vars.env
if [ -d $DATADIR ]; then
rm -rf $DATADIR
fi

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
# Print all the logs output from local testnet
set -Eeuo pipefail
source ./vars.env
for f in "$TESTNET_DIR"/*.log
do
[[ -e "$f" ]] || break # handle the case of no *.log files
echo "============================================================================="
echo "$f"
echo "============================================================================="
cat "$f"
echo ""
done

View File

@@ -1,3 +0,0 @@
priv_key="02fd74636e96a8ffac8e7b01b0de8dea94d6bcf4989513b38cf59eb32163ff91"
source ./vars.env
exec $EL_BOOTNODE_BINARY --nodekeyhex $priv_key

File diff suppressed because one or more lines are too long

View File

@@ -1,53 +0,0 @@
set -Eeuo pipefail
source ./vars.env
# Get options
while getopts "d:sh" flag; do
case "${flag}" in
d) DEBUG_LEVEL=${OPTARG};;
s) SUBSCRIBE_ALL_SUBNETS="--subscribe-all-subnets";;
h)
echo "Start a geth node"
echo
echo "usage: $0 <Options> <DATADIR> <NETWORK-PORT> <HTTP-PORT>"
echo
echo "Options:"
echo " -h: this help"
echo
echo "Positional arguments:"
echo " DATADIR Value for --datadir parameter"
echo " NETWORK-PORT Value for --port"
echo " HTTP-PORT Value for --http.port"
echo " AUTH-PORT Value for --authrpc.port"
echo " GENESIS_FILE Value for geth init"
exit
;;
esac
done
# Get positional arguments
data_dir=${@:$OPTIND+0:1}
network_port=${@:$OPTIND+1:1}
http_port=${@:$OPTIND+2:1}
auth_port=${@:$OPTIND+3:1}
genesis_file=${@:$OPTIND+4:1}
# Init
$GETH_BINARY init \
--datadir $data_dir \
$genesis_file
echo "Completed init"
exec $GETH_BINARY \
--datadir $data_dir \
--ipcdisable \
--http \
--http.api="engine,eth,web3,net,debug" \
--networkid=$CHAIN_ID \
--syncmode=full \
--bootnodes $EL_BOOTNODE_ENODE \
--port $network_port \
--http.port $http_port \
--authrpc.port $auth_port

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# Kill processes
set -Euo pipefail
# First parameter is the file with
# one pid per line.
if [ -f "$1" ]; then
while read pid
do
# handle the case of blank lines
[[ -n "$pid" ]] || continue
echo killing $pid
kill $pid || true
done < $1
fi

View File

@@ -0,0 +1,14 @@
# Full configuration reference [here](https://github.com/kurtosis-tech/ethereum-package?tab=readme-ov-file#configuration).
participants:
- el_type: geth
el_image: ethereum/client-go:latest
cl_type: lighthouse
cl_image: lighthouse:local
cl_extra_params:
- --target-peers=3
count: 4
network_params:
deneb_fork_epoch: 0
seconds_per_slot: 3
global_log_level: debug
snooper_enabled: false

View File

@@ -1,18 +0,0 @@
#!/bin/bash
#
# Resets the beacon state genesis time to now.
#
set -Eeuo pipefail
source ./vars.env
NOW=$(date +%s)
lcli \
change-genesis-time \
$TESTNET_DIR/genesis.ssz \
$(date +%s)
echo "Reset genesis time to now ($NOW)"

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env bash
#
# Produces a testnet specification and a genesis state where the genesis time
# is now + $GENESIS_DELAY.
#
# Generates datadirs for multiple validator keys according to the
# $VALIDATOR_COUNT and $BN_COUNT variables.
#
set -o nounset -o errexit -o pipefail
source ./vars.env
NOW=`date +%s`
GENESIS_TIME=`expr $NOW + $GENESIS_DELAY`
lcli \
new-testnet \
--spec $SPEC_PRESET \
--deposit-contract-address $DEPOSIT_CONTRACT_ADDRESS \
--testnet-dir $TESTNET_DIR \
--min-genesis-active-validator-count $GENESIS_VALIDATOR_COUNT \
--min-genesis-time $GENESIS_TIME \
--genesis-delay $GENESIS_DELAY \
--genesis-fork-version $GENESIS_FORK_VERSION \
--altair-fork-epoch $ALTAIR_FORK_EPOCH \
--bellatrix-fork-epoch $BELLATRIX_FORK_EPOCH \
--capella-fork-epoch $CAPELLA_FORK_EPOCH \
--deneb-fork-epoch $DENEB_FORK_EPOCH \
--electra-fork-epoch $ELECTRA_FORK_EPOCH \
--ttd $TTD \
--eth1-block-hash $ETH1_BLOCK_HASH \
--eth1-id $CHAIN_ID \
--eth1-follow-distance 128 \
--seconds-per-slot $SECONDS_PER_SLOT \
--seconds-per-eth1-block $SECONDS_PER_ETH1_BLOCK \
--proposer-score-boost "$PROPOSER_SCORE_BOOST" \
--validator-count $GENESIS_VALIDATOR_COUNT \
--interop-genesis-state \
--force
echo Specification and genesis.ssz generated at $TESTNET_DIR.
echo "Generating $VALIDATOR_COUNT validators concurrently... (this may take a while)"
lcli \
insecure-validators \
--count $VALIDATOR_COUNT \
--base-dir $DATADIR \
--node-count $VC_COUNT
echo Validators generated with keystore passwords at $DATADIR.

View File

@@ -1,35 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
source ./vars.env
# Function to output SLOT_PER_EPOCH for mainnet or minimal
get_spec_preset_value() {
case "$SPEC_PRESET" in
mainnet) echo 32 ;;
minimal) echo 8 ;;
gnosis) echo 16 ;;
*) echo "Unsupported preset: $SPEC_PRESET" >&2; exit 1 ;;
esac
}
SLOT_PER_EPOCH=$(get_spec_preset_value $SPEC_PRESET)
echo "slot_per_epoch=$SLOT_PER_EPOCH"
genesis_file=$1
# Update future hardforks time in the EL genesis file based on the CL genesis time
GENESIS_TIME=$(lcli pretty-ssz --spec $SPEC_PRESET --testnet-dir $TESTNET_DIR BeaconState $TESTNET_DIR/genesis.ssz | jq | grep -Po 'genesis_time": "\K.*\d')
echo $GENESIS_TIME
CAPELLA_TIME=$((GENESIS_TIME + (CAPELLA_FORK_EPOCH * $SLOT_PER_EPOCH * SECONDS_PER_SLOT)))
echo $CAPELLA_TIME
sed -i 's/"shanghaiTime".*$/"shanghaiTime": '"$CAPELLA_TIME"',/g' $genesis_file
CANCUN_TIME=$((GENESIS_TIME + (DENEB_FORK_EPOCH * $SLOT_PER_EPOCH * SECONDS_PER_SLOT)))
echo $CANCUN_TIME
sed -i 's/"cancunTime".*$/"cancunTime": '"$CANCUN_TIME"',/g' $genesis_file
PRAGUE_TIME=$((GENESIS_TIME + (ELECTRA_FORK_EPOCH * $SLOT_PER_EPOCH * SECONDS_PER_SLOT)))
echo $PRAGUE_TIME
sed -i 's/"pragueTime".*$/"pragueTime": '"$PRAGUE_TIME"',/g' $genesis_file
cat $genesis_file

View File

@@ -1,147 +1,83 @@
#!/usr/bin/env bash
# Start all processes necessary to create a local testnet
# Requires `docker`, `kurtosis`, `yq`
set -Eeuo pipefail
source ./vars.env
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ENCLAVE_NAME=local-testnet
NETWORK_PARAMS_FILE=$SCRIPT_DIR/network_params.yaml
# Set a higher ulimit in case we want to import 1000s of validators.
ulimit -n 65536
# VC_COUNT is defaulted in vars.env
DEBUG_LEVEL=${DEBUG_LEVEL:-info}
BUILDER_PROPOSALS=
BUILD_IMAGE=true
BUILDER_PROPOSALS=false
CI=false
# Get options
while getopts "v:d:ph" flag; do
while getopts "e:b:n:phc" flag; do
case "${flag}" in
v) VC_COUNT=${OPTARG};;
d) DEBUG_LEVEL=${OPTARG};;
p) BUILDER_PROPOSALS="-p";;
e) ENCLAVE_NAME=${OPTARG};;
b) BUILD_IMAGE=${OPTARG};;
n) NETWORK_PARAMS_FILE=${OPTARG};;
p) BUILDER_PROPOSALS=true;;
c) CI=true;;
h)
validators=$(( $VALIDATOR_COUNT / $BN_COUNT ))
echo "Start local testnet, defaults: 1 eth1 node, $BN_COUNT beacon nodes,"
echo "and $VC_COUNT validator clients with each vc having $validators validators."
echo "Start a local testnet with kurtosis."
echo
echo "usage: $0 <Options>"
echo
echo "Options:"
echo " -v: VC_COUNT default: $VC_COUNT"
echo " -d: DEBUG_LEVEL default: info"
echo " -p: enable builder proposals"
echo " -h: this help"
echo " -e: enclave name default: $ENCLAVE_NAME"
echo " -b: whether to build Lighthouse docker image default: $BUILD_IMAGE"
echo " -n: kurtosis network params file path default: $NETWORK_PARAMS_FILE"
echo " -p: enable builder proposals"
echo " -c: CI mode, run without other additional services like Grafana and Dora explorer"
echo " -h: this help"
exit
;;
esac
done
if (( $VC_COUNT > $BN_COUNT )); then
echo "Error $VC_COUNT is too large, must be <= BN_COUNT=$BN_COUNT"
LH_IMAGE_NAME=$(yq eval ".participants[0].cl_image" $NETWORK_PARAMS_FILE)
if ! command -v docker &> /dev/null; then
echo "Docker is not installed. Please install Docker and try again."
exit 1
fi
if ! command -v kurtosis &> /dev/null; then
echo "kurtosis command not found. Please install kurtosis and try again."
exit
fi
genesis_file=${@:$OPTIND+0:1}
if ! command -v yq &> /dev/null; then
echo "yq not found. Please install yq and try again."
fi
# Init some constants
PID_FILE=$TESTNET_DIR/PIDS.pid
LOG_DIR=$TESTNET_DIR
if [ "$BUILDER_PROPOSALS" = true ]; then
yq eval '.participants[0].vc_extra_params = ["--builder-proposals"]' -i $NETWORK_PARAMS_FILE
echo "--builder-proposals VC flag added to network_params.yaml"
fi
# Stop local testnet and remove $PID_FILE
./stop_local_testnet.sh
if [ "$CI" = true ]; then
# TODO: run assertoor tests
yq eval '.additional_services = []' -i $NETWORK_PARAMS_FILE
echo "Running without additional services (CI mode)."
else
yq eval '.additional_services = ["dora", "prometheus_grafana"]' -i $NETWORK_PARAMS_FILE
echo "Additional services dora and prometheus_grafana added to network_params.yaml"
fi
# Clean $DATADIR and create empty log files so the
# user can "tail -f" right after starting this script
# even before its done.
./clean.sh
mkdir -p $LOG_DIR
for (( bn=1; bn<=$BN_COUNT; bn++ )); do
touch $LOG_DIR/beacon_node_$bn.log
done
for (( el=1; el<=$BN_COUNT; el++ )); do
touch $LOG_DIR/geth_$el.log
done
for (( vc=1; vc<=$VC_COUNT; vc++ )); do
touch $LOG_DIR/validator_node_$vc.log
done
if [ "$BUILD_IMAGE" = true ]; then
echo "Building Lighthouse Docker image."
ROOT_DIR="$SCRIPT_DIR/../.."
docker build --build-arg FEATURES=portable -f $ROOT_DIR/Dockerfile -t $LH_IMAGE_NAME $ROOT_DIR
else
echo "Not rebuilding Lighthouse Docker image."
fi
# Sleep with a message
sleeping() {
echo sleeping $1
sleep $1
}
# Stop local testnet
kurtosis enclave rm -f $ENCLAVE_NAME 2>/dev/null || true
# Execute the command with logs saved to a file.
#
# First parameter is log file name
# Second parameter is executable name
# Remaining parameters are passed to executable
execute_command() {
LOG_NAME=$1
EX_NAME=$2
shift
shift
CMD="$EX_NAME $@ >> $LOG_DIR/$LOG_NAME 2>&1"
echo "executing: $CMD"
echo "$CMD" > "$LOG_DIR/$LOG_NAME"
eval "$CMD &"
}
# Execute the command with logs saved to a file
# and is PID is saved to $PID_FILE.
#
# First parameter is log file name
# Second parameter is executable name
# Remaining parameters are passed to executable
execute_command_add_PID() {
execute_command $@
echo "$!" >> $PID_FILE
}
# Setup data
echo "executing: ./setup.sh >> $LOG_DIR/setup.log"
./setup.sh >> $LOG_DIR/setup.log 2>&1
# Call setup_time.sh to update future hardforks time in the EL genesis file based on the CL genesis time
./setup_time.sh $genesis_file
# Delay to let boot_enr.yaml to be created
execute_command_add_PID bootnode.log ./bootnode.sh
sleeping 3
execute_command_add_PID el_bootnode.log ./el_bootnode.sh
sleeping 3
# Start beacon nodes
BN_udp_tcp_base=9000
BN_http_port_base=8000
EL_base_network=7000
EL_base_http=6000
EL_base_auth_http=5000
(( $VC_COUNT < $BN_COUNT )) && SAS=-s || SAS=
for (( el=1; el<=$BN_COUNT; el++ )); do
execute_command_add_PID geth_$el.log ./geth.sh $DATADIR/geth_datadir$el $((EL_base_network + $el)) $((EL_base_http + $el)) $((EL_base_auth_http + $el)) $genesis_file
done
sleeping 20
# Reset the `genesis.json` config file fork times.
sed -i 's/"shanghaiTime".*$/"shanghaiTime": 0,/g' $genesis_file
sed -i 's/"cancunTime".*$/"cancunTime": 0,/g' $genesis_file
sed -i 's/"pragueTime".*$/"pragueTime": 0,/g' $genesis_file
for (( bn=1; bn<=$BN_COUNT; bn++ )); do
secret=$DATADIR/geth_datadir$bn/geth/jwtsecret
echo $secret
execute_command_add_PID beacon_node_$bn.log ./beacon_node.sh $SAS -d $DEBUG_LEVEL $DATADIR/node_$bn $((BN_udp_tcp_base + $bn)) $((BN_udp_tcp_base + $bn + 100)) $((BN_http_port_base + $bn)) http://localhost:$((EL_base_auth_http + $bn)) $secret
done
# Start requested number of validator clients
for (( vc=1; vc<=$VC_COUNT; vc++ )); do
execute_command_add_PID validator_node_$vc.log ./validator_client.sh $BUILDER_PROPOSALS -d $DEBUG_LEVEL $DATADIR/node_$vc http://localhost:$((BN_http_port_base + $vc))
done
kurtosis run --enclave $ENCLAVE_NAME github.com/kurtosis-tech/ethereum-package --args-file $NETWORK_PARAMS_FILE
echo "Started!"

View File

@@ -1,10 +1,15 @@
#!/usr/bin/env bash
# Stop all processes that were started with start_local_testnet.sh
set -Eeuo pipefail
source ./vars.env
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ENCLAVE_NAME=${1:-local-testnet}
LOGS_PATH=$SCRIPT_DIR/logs
LOGS_SUBDIR=$LOGS_PATH/$ENCLAVE_NAME
PID_FILE=$TESTNET_DIR/PIDS.pid
./kill_processes.sh $PID_FILE
rm -f $PID_FILE
# Delete existing logs directory and make sure parent directory exists.
rm -rf $LOGS_SUBDIR && mkdir -p $LOGS_PATH
kurtosis enclave dump $ENCLAVE_NAME $LOGS_SUBDIR
echo "Local testnet logs stored to $LOGS_SUBDIR."
kurtosis enclave rm -f $ENCLAVE_NAME
echo "Local testnet stopped."

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
#
# Starts a validator client based upon a genesis state created by
# `./setup.sh`.
#
# Usage: ./validator_client.sh <DATADIR> <BEACON-NODE-HTTP> <OPTIONAL-DEBUG-LEVEL>
set -Eeuo pipefail
source ./vars.env
DEBUG_LEVEL=info
BUILDER_PROPOSALS=
# Get options
while getopts "pd:" flag; do
case "${flag}" in
p) BUILDER_PROPOSALS="--builder-proposals";;
d) DEBUG_LEVEL=${OPTARG};;
esac
done
exec lighthouse \
--debug-level $DEBUG_LEVEL \
vc \
$BUILDER_PROPOSALS \
--datadir ${@:$OPTIND:1} \
--testnet-dir $TESTNET_DIR \
--init-slashing-protection \
--beacon-nodes ${@:$OPTIND+1:1} \
--suggested-fee-recipient 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990 \
$VC_ARGS

View File

@@ -1,69 +0,0 @@
# Path to the geth binary
GETH_BINARY=geth
EL_BOOTNODE_BINARY=bootnode
# Base directories for the validator keys and secrets
DATADIR=~/.lighthouse/local-testnet
# Directory for the eth2 config
TESTNET_DIR=$DATADIR/testnet
# Mnemonic for generating validator keys
MNEMONIC_PHRASE="vast thought differ pull jewel broom cook wrist tribe word before omit"
EL_BOOTNODE_ENODE="enode://51ea9bb34d31efc3491a842ed13b8cab70e753af108526b57916d716978b380ed713f4336a80cdb85ec2a115d5a8c0ae9f3247bed3c84d3cb025c6bab311062c@127.0.0.1:0?discport=30301"
# Hardcoded deposit contract
DEPOSIT_CONTRACT_ADDRESS=4242424242424242424242424242424242424242
GENESIS_FORK_VERSION=0x42424242
# Block hash generated from genesis.json in directory
ETH1_BLOCK_HASH=4b0e17cf5c04616d64526d292b80a1f2720cf2195d990006e4ea6950c5bbcb9f
VALIDATOR_COUNT=80
GENESIS_VALIDATOR_COUNT=80
# Number of beacon_node instances that you intend to run
BN_COUNT=4
# Number of validator clients
VC_COUNT=$BN_COUNT
# Number of seconds to delay to start genesis block.
# If started by a script this can be 0, if starting by hand
# use something like 180.
GENESIS_DELAY=0
# Port for P2P communication with bootnode
BOOTNODE_PORT=4242
# Network ID and Chain ID of local eth1 test network
CHAIN_ID=4242
# Hard fork configuration
ALTAIR_FORK_EPOCH=0
BELLATRIX_FORK_EPOCH=0
CAPELLA_FORK_EPOCH=0
DENEB_FORK_EPOCH=1
ELECTRA_FORK_EPOCH=9999999
TTD=0
# Spec version (mainnet or minimal)
SPEC_PRESET=mainnet
# Seconds per Eth2 slot
SECONDS_PER_SLOT=3
# Seconds per Eth1 block
SECONDS_PER_ETH1_BLOCK=3
# Proposer score boost percentage
PROPOSER_SCORE_BOOST=40
# Command line arguments for beacon node client
BN_ARGS=""
# Command line arguments for validator client
VC_ARGS=""

View File

@@ -1,101 +1,129 @@
#!/usr/bin/env bash
# Requires `lighthouse`, `lcli`, `geth`, `bootnode`, `curl`, `jq`
# Requires `docker`, `kurtosis`, `yq`, `curl`, `jq`
set -Eeuo pipefail
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
NETWORK_PARAMS_FILE=$SCRIPT_DIR/network_params.yaml
BEHAVIOR=$1
ENCLAVE_NAME=local-testnet-$BEHAVIOR
SECONDS_PER_SLOT=$(yq eval ".network_params.seconds_per_slot" $NETWORK_PARAMS_FILE)
KEYS_PER_NODE=$(yq eval ".network_params.num_validator_keys_per_node" $NETWORK_PARAMS_FILE)
LH_IMAGE_NAME=$(yq eval ".participants[0].cl_image" $NETWORK_PARAMS_FILE)
if [[ "$BEHAVIOR" != "success" ]] && [[ "$BEHAVIOR" != "failure" ]]; then
echo "Usage: doppelganger_protection.sh [success|failure]"
exit 1
fi
exit_if_fails() {
echo $@
$@
EXIT_CODE=$?
if [[ $EXIT_CODE -eq 1 ]]; then
exit 1
fi
function exit_and_dump_logs() {
local exit_code=$1
echo "Shutting down..."
$SCRIPT_DIR/../local_testnet/stop_local_testnet.sh $ENCLAVE_NAME
echo "Test completed with exit code $exit_code."
exit $exit_code
}
genesis_file=$2
source ./vars.env
function get_service_status() {
local service_name=$1
kurtosis service inspect $ENCLAVE_NAME $service_name | grep Status | cut -d':' -f2 | xargs
}
exit_if_fails ../local_testnet/clean.sh
function run_command_without_exit() {
local command=$1
set +e
eval "$command"
local exit_code=$?
set -e
echo $exit_code
}
# Start local testnet
$SCRIPT_DIR/../local_testnet/start_local_testnet.sh -e $ENCLAVE_NAME -b false -c -n $NETWORK_PARAMS_FILE
echo "Setting up local testnet"
# Immediately stop node 4 (as we only need the node 4 validator keys generated for later use)
kurtosis service stop $ENCLAVE_NAME cl-4-lighthouse-geth el-4-geth-lighthouse vc-4-geth-lighthouse > /dev/null
exit_if_fails ../local_testnet/setup.sh
# Get the http port to get the config
BN1_HTTP_ADDRESS=`kurtosis port print $ENCLAVE_NAME cl-1-lighthouse-geth http`
# Duplicate this directory so slashing protection doesn't keep us from re-using validator keys
exit_if_fails cp -R $HOME/.lighthouse/local-testnet/node_1 $HOME/.lighthouse/local-testnet/node_1_doppelganger
# Get the genesis time and genesis delay
MIN_GENESIS_TIME=`curl -s $BN1_HTTP_ADDRESS/eth/v1/config/spec | jq '.data.MIN_GENESIS_TIME|tonumber'`
GENESIS_DELAY=`curl -s $BN1_HTTP_ADDRESS/eth/v1/config/spec | jq '.data.GENESIS_DELAY|tonumber'`
echo "Starting bootnode"
CURRENT_TIME=`date +%s`
# Note: doppelganger protection can only be started post epoch 0
echo "Waiting until next epoch before starting the next validator client..."
DELAY=$(( $SECONDS_PER_SLOT * 32 + $GENESIS_DELAY + $MIN_GENESIS_TIME - $CURRENT_TIME))
sleep $DELAY
exit_if_fails ../local_testnet/bootnode.sh &> /dev/null &
exit_if_fails ../local_testnet/el_bootnode.sh &> /dev/null &
# wait for the bootnode to start
sleep 10
echo "Starting local execution nodes"
exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir1 6000 5000 4000 $genesis_file &> geth.log &
exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir2 6100 5100 4100 $genesis_file &> /dev/null &
exit_if_fails ../local_testnet/geth.sh $HOME/.lighthouse/local-testnet/geth_datadir3 6200 5200 4200 $genesis_file &> /dev/null &
sleep 20
exit_if_fails ../local_testnet/beacon_node.sh -d debug $HOME/.lighthouse/local-testnet/node_1 8000 7000 9000 http://localhost:4000 $HOME/.lighthouse/local-testnet/geth_datadir1/geth/jwtsecret &> /dev/null &
exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_2 8100 7100 9100 http://localhost:4100 $HOME/.lighthouse/local-testnet/geth_datadir2/geth/jwtsecret &> /dev/null &
exit_if_fails ../local_testnet/beacon_node.sh $HOME/.lighthouse/local-testnet/node_3 8200 7200 9200 http://localhost:4200 $HOME/.lighthouse/local-testnet/geth_datadir3/geth/jwtsecret &> /dev/null &
echo "Starting local validator clients"
exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1 http://localhost:9000 &> /dev/null &
exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_2 http://localhost:9100 &> /dev/null &
exit_if_fails ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_3 http://localhost:9200 &> /dev/null &
echo "Waiting an epoch before starting the next validator client"
sleep $(( $SECONDS_PER_SLOT * 32 ))
# Use BN2 for the next validator client
bn_2_url=$(kurtosis service inspect $ENCLAVE_NAME cl-2-lighthouse-geth | grep 'enr-address' | cut -d'=' -f2)
bn_2_port=4000
if [[ "$BEHAVIOR" == "failure" ]]; then
echo "Starting the doppelganger validator client"
echo "Starting the doppelganger validator client."
# Use same keys as keys from VC1 and connect to BN2
# This process should not last longer than 2 epochs
timeout $(( $SECONDS_PER_SLOT * 32 * 2 )) ../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_1_doppelganger http://localhost:9100
DOPPELGANGER_EXIT=$?
vc_1_range_start=0
vc_1_range_end=$(($KEYS_PER_NODE - 1))
vc_1_keys_artifact_id="1-lighthouse-geth-$vc_1_range_start-$vc_1_range_end-0"
service_name=vc-1-doppelganger
echo "Shutting down"
kurtosis service add \
--files /validator_keys:$vc_1_keys_artifact_id,/testnet:el_cl_genesis_data \
$ENCLAVE_NAME $service_name $LH_IMAGE_NAME -- lighthouse \
vc \
--debug-level debug \
--testnet-dir=/testnet \
--validators-dir=/validator_keys/keys \
--secrets-dir=/validator_keys/secrets \
--init-slashing-protection \
--beacon-nodes=http://$bn_2_url:$bn_2_port \
--enable-doppelganger-protection \
--suggested-fee-recipient 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990
# Cleanup
killall geth
killall lighthouse
killall bootnode
# Check if doppelganger VC has stopped and exited. Exit code 1 means the check timed out and VC is still running.
check_exit_cmd="until [ \$(get_service_status $service_name) != 'RUNNING' ]; do sleep 1; done"
doppelganger_exit=$(run_command_without_exit "timeout $(( $SECONDS_PER_SLOT * 32 * 2 )) bash -c \"$check_exit_cmd\"")
echo "Done"
# We expect to find a doppelganger, exit with success error code if doppelganger was found
# and failure if no doppelganger was found.
if [[ $DOPPELGANGER_EXIT -eq 1 ]]; then
exit 0
if [[ $doppelganger_exit -eq 1 ]]; then
echo "Test failed: expected doppelganger but VC is still running. Check the logs for details."
exit_and_dump_logs 1
else
exit 1
echo "Test passed: doppelganger found and VC process stopped successfully."
exit_and_dump_logs 0
fi
fi
if [[ "$BEHAVIOR" == "success" ]]; then
echo "Starting the last validator client"
echo "Starting the last validator client."
../local_testnet/validator_client.sh $HOME/.lighthouse/local-testnet/node_4 http://localhost:9100 &
DOPPELGANGER_FAILURE=0
vc_4_range_start=$(($KEYS_PER_NODE * 3))
vc_4_range_end=$(($KEYS_PER_NODE * 4 - 1))
vc_4_keys_artifact_id="4-lighthouse-geth-$vc_4_range_start-$vc_4_range_end-0"
service_name=vc-4
kurtosis service add \
--files /validator_keys:$vc_4_keys_artifact_id,/testnet:el_cl_genesis_data \
$ENCLAVE_NAME $service_name $LH_IMAGE_NAME -- lighthouse \
vc \
--debug-level debug \
--testnet-dir=/testnet \
--validators-dir=/validator_keys/keys \
--secrets-dir=/validator_keys/secrets \
--init-slashing-protection \
--beacon-nodes=http://$bn_2_url:$bn_2_port \
--enable-doppelganger-protection \
--suggested-fee-recipient 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990
doppelganger_failure=0
# Sleep three epochs, then make sure all validators were active in epoch 2. Use
# `is_previous_epoch_target_attester` from epoch 3 for a complete view of epoch 2 inclusion.
@@ -104,20 +132,27 @@ if [[ "$BEHAVIOR" == "success" ]]; then
echo "Waiting three epochs..."
sleep $(( $SECONDS_PER_SLOT * 32 * 3 ))
PREVIOUS_DIR=$(pwd)
cd $HOME/.lighthouse/local-testnet/node_4/validators
# Get VC4 validator keys
keys_path=$SCRIPT_DIR/$ENCLAVE_NAME/node_4/validators
rm -rf $keys_path && mkdir -p $keys_path
kurtosis files download $ENCLAVE_NAME $vc_4_keys_artifact_id $keys_path
cd $keys_path/keys
for val in 0x*; do
[[ -e $val ]] || continue
curl -s localhost:9100/lighthouse/validator_inclusion/3/$val | jq | grep -q '"is_previous_epoch_target_attester": false'
IS_ATTESTER=$?
if [[ $IS_ATTESTER -eq 0 ]]; then
is_attester=$(run_command_without_exit "curl -s $BN1_HTTP_ADDRESS/lighthouse/validator_inclusion/3/$val | jq | grep -q '\"is_previous_epoch_target_attester\": false'")
if [[ $is_attester -eq 0 ]]; then
echo "$val did not attest in epoch 2."
else
echo "ERROR! $val did attest in epoch 2."
DOPPELGANGER_FAILURE=1
doppelganger_failure=1
fi
done
if [[ $doppelganger_failure -eq 1 ]]; then
exit_and_dump_logs 1
fi
# Sleep two epochs, then make sure all validators were active in epoch 4. Use
# `is_previous_epoch_target_attester` from epoch 5 for a complete view of epoch 4 inclusion.
#
@@ -126,30 +161,18 @@ if [[ "$BEHAVIOR" == "success" ]]; then
sleep $(( $SECONDS_PER_SLOT * 32 * 2 ))
for val in 0x*; do
[[ -e $val ]] || continue
curl -s localhost:9100/lighthouse/validator_inclusion/5/$val | jq | grep -q '"is_previous_epoch_target_attester": true'
IS_ATTESTER=$?
if [[ $IS_ATTESTER -eq 0 ]]; then
is_attester=$(run_command_without_exit "curl -s $BN1_HTTP_ADDRESS/lighthouse/validator_inclusion/5/$val | jq | grep -q '\"is_previous_epoch_target_attester\": true'")
if [[ $is_attester -eq 0 ]]; then
echo "$val attested in epoch 4."
else
echo "ERROR! $val did not attest in epoch 4."
DOPPELGANGER_FAILURE=1
doppelganger_failure=1
fi
done
echo "Shutting down"
# Cleanup
cd $PREVIOUS_DIR
killall geth
killall lighthouse
killall bootnode
echo "Done"
if [[ $DOPPELGANGER_FAILURE -eq 1 ]]; then
exit 1
if [[ $doppelganger_failure -eq 1 ]]; then
exit_and_dump_logs 1
fi
fi
exit 0
exit_and_dump_logs 0

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
# Full configuration reference [here](https://github.com/kurtosis-tech/ethereum-package?tab=readme-ov-file#configuration).
participants:
- el_type: geth
el_image: ethereum/client-go:latest
cl_type: lighthouse
cl_image: lighthouse:local
cl_extra_params:
- --target-peers=3
count: 4
network_params:
deneb_fork_epoch: 0
seconds_per_slot: 3
num_validator_keys_per_node: 20
global_log_level: debug
snooper_enabled: false
additional_services: []

View File

@@ -1,66 +0,0 @@
# Path to the geth binary
GETH_BINARY=geth
EL_BOOTNODE_BINARY=bootnode
# Base directories for the validator keys and secrets
DATADIR=~/.lighthouse/local-testnet
# Directory for the eth2 config
TESTNET_DIR=$DATADIR/testnet
EL_BOOTNODE_ENODE="enode://51ea9bb34d31efc3491a842ed13b8cab70e753af108526b57916d716978b380ed713f4336a80cdb85ec2a115d5a8c0ae9f3247bed3c84d3cb025c6bab311062c@127.0.0.1:0?discport=30301"
# Hardcoded deposit contract
DEPOSIT_CONTRACT_ADDRESS=4242424242424242424242424242424242424242
GENESIS_FORK_VERSION=0x42424242
# Block hash generated from genesis.json in directory
ETH1_BLOCK_HASH=7a5c656343c3a66dcf75415958b500e8873f9dab0cd588e6cf0785b52a06dd34
VALIDATOR_COUNT=80
GENESIS_VALIDATOR_COUNT=80
# Number of beacon_node instances that you intend to run
BN_COUNT=4
# Number of validator clients
VC_COUNT=$BN_COUNT
# Number of seconds to delay to start genesis block.
# If started by a script this can be 0, if starting by hand
# use something like 180.
GENESIS_DELAY=0
# Port for P2P communication with bootnode
BOOTNODE_PORT=4242
# Network ID and Chain ID of local eth1 test network
CHAIN_ID=4242
# Hard fork configuration
ALTAIR_FORK_EPOCH=0
BELLATRIX_FORK_EPOCH=0
CAPELLA_FORK_EPOCH=0
DENEB_FORK_EPOCH=0
ELECTRA_FORK_EPOCH=18446744073709551615
TTD=0
# Spec version (mainnet or minimal)
SPEC_PRESET=mainnet
# Seconds per Eth2 slot
SECONDS_PER_SLOT=3
# Seconds per Eth1 block
SECONDS_PER_ETH1_BLOCK=1
# Proposer score boost percentage
PROPOSER_SCORE_BOOST=70
# Command line arguments for beacon node client
BN_ARGS=""
# Enable doppelganger detection
VC_ARGS=" --enable-doppelganger-protection "

View File

@@ -95,7 +95,7 @@ impl DepositContract {
.await
.map_err(|e| {
format!(
"Failed to deploy contract: {}. Is scripts/anvil_tests_node.sh running?.",
"Failed to deploy contract: {}. Is the RPC server running?.",
e
)
})?;