From e45ba846aef5ce77571dbf71497029eef5c97271 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 30 Jun 2025 20:47:49 +1000 Subject: [PATCH 01/13] Increase http client default timeout to 2s in `http-api` tests. (#7673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `sync_contributions_across_fork_with_skip_slots` test have been quite flaky recently on CI, we suspect it might be caused by the recent introduction of a `default` timeout in #7400, and CI is failing to consistently process those http requests within 1s likely due to limited resources. This PR increases the `default` timeout to 2s in tests to avoid flaky tests, but keeps the remaining timeout the same (1s). https://github.com/sigp/lighthouse/actions/runs/15965113170/job/45023976021 ``` FAIL [ 8.945s] http_api::bn_http_api_tests fork_tests::sync_contributions_across_fork_with_skip_slots stdout ─── running 1 test test fork_tests::sync_contributions_across_fork_with_skip_slots ... FAILED failures: failures: fork_tests::sync_contributions_across_fork_with_skip_slots test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 175 filtered out; finished in 8.91s stderr ─── thread 'fork_tests::sync_contributions_across_fork_with_skip_slots' panicked at beacon_node/http_api/tests/fork_tests.rs:239:10: called `Result::unwrap()` on an `Err` value: HttpClient(url: http://127.0.0.1:41793/, kind: timeout, detail: operation timed out) note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` --- beacon_node/http_api/src/test_utils.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index 0ea8588125..a52df6c863 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -103,6 +103,13 @@ impl InteractiveTester { tokio::spawn(server); + // Override the default timeout to 2s to timeouts on CI, as CI seems to require longer + // to process. The 1s timeouts for other tasks have been working for a long time, so we'll + // keep it as it is, as it may help identify a performance regression. + let timeouts = Timeouts { + default: Duration::from_secs(2), + ..Timeouts::set_all(Duration::from_secs(1)) + }; let client = BeaconNodeHttpClient::new( SensitiveUrl::parse(&format!( "http://{}:{}", @@ -110,7 +117,7 @@ impl InteractiveTester { listening_socket.port() )) .unwrap(), - Timeouts::set_all(Duration::from_secs(1)), + timeouts, ); Self { From 25ea8a83b77bc78c7c0b0e3859db4e3a753a4a27 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Jun 2025 20:47:52 +1000 Subject: [PATCH 02/13] Add Michael as codeowner for store crate (#7667) I'm adding myself as a codeowner for the `store` crate so that I can more easily keep track of database-related PRs. --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a8919337a9..cdec442276 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ /beacon_node/network/ @jxs /beacon_node/lighthouse_network/ @jxs +/beacon_node/store/ @michaelsproul From c1f94d9b7bf8b6c1936ae26fcc86ac9b6180603a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 30 Jun 2025 20:55:47 +1000 Subject: [PATCH 03/13] Test database schema stability (#7669) This PR implements some heuristics to check for breaking database changes. The goal is to prevent accidental changes to the database schema occurring without a version bump. --- beacon_node/beacon_chain/src/lib.rs | 2 +- beacon_node/beacon_chain/tests/main.rs | 1 + .../beacon_chain/tests/schema_stability.rs | 151 ++++++++++++++++++ beacon_node/network/src/persisted_dht.rs | 4 + 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 beacon_node/beacon_chain/tests/schema_stability.rs diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4a7a430532..df253bf72c 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -45,7 +45,7 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; -mod persisted_beacon_chain; +pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; mod pre_finalization_cache; diff --git a/beacon_node/beacon_chain/tests/main.rs b/beacon_node/beacon_chain/tests/main.rs index 942ce81684..f0978c5f05 100644 --- a/beacon_node/beacon_chain/tests/main.rs +++ b/beacon_node/beacon_chain/tests/main.rs @@ -7,6 +7,7 @@ mod events; mod op_verification; mod payload_invalidation; mod rewards; +mod schema_stability; mod store_tests; mod sync_committee_verification; mod tests; diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs new file mode 100644 index 0000000000..00d75a554d --- /dev/null +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -0,0 +1,151 @@ +use beacon_chain::{ + persisted_beacon_chain::PersistedBeaconChain, + persisted_custody::PersistedCustody, + test_utils::{test_spec, BeaconChainHarness, DiskHarnessType}, + ChainConfig, +}; +use logging::create_test_tracing_subscriber; +use operation_pool::PersistedOperationPool; +use ssz::Encode; +use std::sync::{Arc, LazyLock}; +use store::{ + database::interface::BeaconNodeBackend, hot_cold_store::Split, metadata::DataColumnInfo, + DBColumn, HotColdDB, StoreConfig, StoreItem, +}; +use strum::IntoEnumIterator; +use tempfile::{tempdir, TempDir}; +use types::{ChainSpec, Hash256, Keypair, MainnetEthSpec}; + +type E = MainnetEthSpec; +type Store = Arc, BeaconNodeBackend>>; +type TestHarness = BeaconChainHarness>; + +const VALIDATOR_COUNT: usize = 32; + +/// A cached set of keys. +static KEYPAIRS: LazyLock> = + LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT)); + +fn get_store(db_path: &TempDir, config: StoreConfig, spec: Arc) -> Store { + create_test_tracing_subscriber(); + let hot_path = db_path.path().join("chain_db"); + let cold_path = db_path.path().join("freezer_db"); + let blobs_path = db_path.path().join("blobs_db"); + + HotColdDB::open( + &hot_path, + &cold_path, + &blobs_path, + |_, _, _| Ok(()), + config, + spec, + ) + .expect("disk store should initialize") +} + +/// This test checks the database schema stability against previous versions of Lighthouse's code. +/// +/// If you are changing something about how Lighthouse stores data on disk, you almost certainly +/// need to implement a database schema change. This is true even if the data being stored only +/// applies to an upcoming fork that isn't live on mainnet. We never want to be in the situation +/// where commit A writes data in some format, and then a later commit B changes that format +/// without a schema change. This is liable to break any nodes that update from A to B, even if +/// these nodes are just testnet nodes. +/// +/// This test implements partial, imperfect checks on the DB schema which are designed to quickly +/// catch common changes. +/// +/// This test uses hardcoded values, rather than trying to access previous versions of Lighthouse's +/// code. If you've successfully implemented a schema change and you're sure that the new values are +/// correct, you can update the hardcoded values here. +#[tokio::test] +async fn schema_stability() { + let spec = Arc::new(test_spec::()); + + let datadir = tempdir().unwrap(); + let store_config = StoreConfig::default(); + let store = get_store(&datadir, store_config, spec.clone()); + + let chain_config = ChainConfig { + reconstruct_historic_states: true, + ..ChainConfig::default() + }; + + let harness = TestHarness::builder(MainnetEthSpec) + .spec(spec) + .keypairs(KEYPAIRS.to_vec()) + .fresh_disk_store(store.clone()) + .mock_execution_layer() + .chain_config(chain_config) + .build(); + harness.advance_slot(); + + let chain = &harness.chain; + + chain.persist_op_pool().unwrap(); + chain.persist_custody_context().unwrap(); + + check_db_columns(); + check_metadata_sizes(&store); + check_op_pool(&store); + check_custody_context(&store); + check_persisted_chain(&store); + + // Not covered here: + // - Fork choice (not tested) + // - DBColumn::DhtEnrs (tested in network crate) +} + +/// Check that the set of database columns is unchanged. +fn check_db_columns() { + let current_columns: Vec<&'static str> = DBColumn::iter().map(|c| c.as_str()).collect(); + let expected_columns = vec![ + "bma", "blk", "blb", "bdc", "ste", "hsd", "hsn", "bsn", "bsd", "bss", "bs3", "bcs", "bst", + "exp", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", "bhr", "brm", + "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", + ]; + assert_eq!(expected_columns, current_columns); +} + +/// Check the SSZ sizes of known on-disk metadata. +/// +/// New types can be added here as the schema evolves. +fn check_metadata_sizes(store: &Store) { + assert_eq!(Split::default().ssz_bytes_len(), 40); + assert_eq!(store.get_anchor_info().ssz_bytes_len(), 64); + assert_eq!( + store.get_blob_info().ssz_bytes_len(), + if store.get_chain_spec().deneb_fork_epoch.is_some() { + 14 + } else { + 6 + } + ); + assert_eq!(DataColumnInfo::default().ssz_bytes_len(), 5); +} + +fn check_op_pool(store: &Store) { + let op_pool = store + .get_item::>(&Hash256::ZERO) + .unwrap() + .unwrap(); + assert!(matches!(op_pool, PersistedOperationPool::V20(_))); + assert_eq!(op_pool.ssz_bytes_len(), 28); + assert_eq!(op_pool.as_store_bytes().len(), 28); +} + +fn check_custody_context(store: &Store) { + let custody_context = store + .get_item::(&Hash256::ZERO) + .unwrap() + .unwrap(); + assert_eq!(custody_context.as_store_bytes().len(), 9); +} + +fn check_persisted_chain(store: &Store) { + let chain = store + .get_item::(&Hash256::ZERO) + .unwrap() + .unwrap(); + assert_eq!(chain.as_store_bytes().len(), 32); +} diff --git a/beacon_node/network/src/persisted_dht.rs b/beacon_node/network/src/persisted_dht.rs index 9c112dba86..938b08a315 100644 --- a/beacon_node/network/src/persisted_dht.rs +++ b/beacon_node/network/src/persisted_dht.rs @@ -86,5 +86,9 @@ mod tests { .unwrap(); let dht: PersistedDht = store.get_item(&DHT_DB_KEY).unwrap().unwrap(); assert_eq!(dht.enrs, enrs); + + // This hardcoded length check is for database schema compatibility. If the on-disk format + // of `PersistedDht` changes, we need a DB schema change. + assert_eq!(dht.as_store_bytes().len(), 136); } } From 257d2707182c2c1d073c53811b38e9944e07d932 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:07:49 +0800 Subject: [PATCH 04/13] Add voluntary exit via validator manager (#6612) * #4303 * #4804 -Add voluntary exit feature to the validator manager -Add delete all validators by using the keyword "all" --- Cargo.lock | 3 + book/src/help_vm.md | 4 + book/src/validator_manager.md | 2 +- book/src/validator_manager_api.md | 77 +++ book/src/validator_voluntary_exit.md | 2 + lighthouse/tests/validator_manager.rs | 112 ++++ validator_client/http_api/src/test_utils.rs | 25 +- validator_manager/Cargo.toml | 3 + validator_manager/src/delete_validators.rs | 24 +- validator_manager/src/exit_validators.rs | 585 ++++++++++++++++++++ validator_manager/src/import_validators.rs | 6 +- validator_manager/src/lib.rs | 7 +- validator_manager/src/list_validators.rs | 154 +++++- wordlist.txt | 2 + 14 files changed, 983 insertions(+), 23 deletions(-) create mode 100644 validator_manager/src/exit_validators.rs diff --git a/Cargo.lock b/Cargo.lock index 7d77ce4044..5e22c9742a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9933,6 +9933,7 @@ name = "validator_manager" version = "0.1.0" dependencies = [ "account_utils", + "beacon_chain", "clap", "clap_utils", "derivative", @@ -9942,9 +9943,11 @@ dependencies = [ "eth2_wallet", "ethereum_serde_utils", "hex", + "http_api", "regex", "serde", "serde_json", + "slot_clock", "tempfile", "tokio", "tree_hash", diff --git a/book/src/help_vm.md b/book/src/help_vm.md index 8ff54122ef..f58537ae1c 100644 --- a/book/src/help_vm.md +++ b/book/src/help_vm.md @@ -28,6 +28,10 @@ Commands: delete Deletes one or more validators from a validator client using the HTTP API. + exit + Exits one or more validators using the HTTP API. It can also be used + to generate a presigned voluntary exit message for a particular future + epoch. help Print this message or the help of the given subcommand(s) diff --git a/book/src/validator_manager.md b/book/src/validator_manager.md index b0190c1812..609f176901 100644 --- a/book/src/validator_manager.md +++ b/book/src/validator_manager.md @@ -32,4 +32,4 @@ The `validator-manager` boasts the following features: - [Creating and importing validators using the `create` and `import` commands.](./validator_manager_create.md) - [Moving validators between two VCs using the `move` command.](./validator_manager_move.md) -- [Managing validators such as delete, import and list validators.](./validator_manager_api.md) +- [Managing validators such as exit, delete, import and list validators.](./validator_manager_api.md) diff --git a/book/src/validator_manager_api.md b/book/src/validator_manager_api.md index 7bc5be8557..0542008463 100644 --- a/book/src/validator_manager_api.md +++ b/book/src/validator_manager_api.md @@ -2,6 +2,54 @@ The `lighthouse validator-manager` uses the [Keymanager API](https://ethereum.github.io/keymanager-APIs/#/) to list, import and delete keystores via the HTTP API. This requires the validator client running with the flag `--http`. By default, the validator client HTTP address is `http://localhost:5062`. If a different IP address or port is used, add the flag `--vc-url http://IP:port_number` to the command below. +## Exit + +The `exit` command exits one or more validators from the validator client. To `exit`: + +> **Important note: Once the --beacon-node flag is used, it will publish the voluntary exit to the network. This action is irreversible.** + +```bash +lighthouse vm exit --vc-token --validators pubkey1,pubkey2 --beacon-node http://beacon-node-url:5052 +``` + +Example: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4,0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f --beacon-node http://localhost:5052 +``` + +If successful, the following log will be returned: + +```text +Successfully validated and published voluntary exit for validator 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4 +Successfully validated and published voluntary exit for validator +0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f +``` + +To exit all validators on the validator client, use the keyword `all`: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --beacon-node http://localhost:5052 +``` + +To check the voluntary exit status, refer to [the list command](./validator_manager_api.md#list). + +The following command will only generate a presigned voluntary exit message and save it to a file named `{validator_pubkey}.json`. It **will not** publish the voluntary exit to the network. + +To generate a presigned exit message and save it to a file, use the flag `--presign`: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --presign +``` + +To generate a presigned exit message for a particular (future) epoch, use the flag `--exit-epoch`: + +```bash +lighthouse vm exit --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all --presign --exit-epoch 1234567 +``` + +The generated presigned exit message will only be valid at or after the specified exit-epoch, in this case, epoch 1234567. + ## Delete The `delete` command deletes one or more validators from the validator client. It will also modify the `validator_definitions.yml` file automatically so there is no manual action required from the user after the delete. To `delete`: @@ -16,6 +64,12 @@ Example: lighthouse vm delete --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4,0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f ``` +To delete all validators on the validator client, use the keyword `all`: + +```bash +lighthouse vm delete --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators all +``` + ## Import The `import` command imports validator keystores generated by the `ethstaker-deposit-cli`. To import a validator keystore: @@ -37,3 +91,26 @@ To list the validators running on the validator client: ```bash lighthouse vm list --vc-token ~/.lighthouse/mainnet/validators/api-token.txt ``` + +The `list` command can also be used to check the voluntary exit status of validators. To do so, use both `--beacon-node` and `--validators` flags. The `--validators` flag accepts a comma-separated list of validator public keys, or the keyword `all` to check the voluntary exit status of all validators attached to the validator client. + +```bash +lighthouse vm list --vc-token ~/.lighthouse/mainnet/validators/api-token.txt --validators 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 --beacon-node http://localhost:5052 +``` + +If the validator voluntary exit has been accepted by the chain, the following log will be returned: + +```text +Voluntary exit for validator 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 has been accepted into the beacon chain, but not yet finalized. Finalization may take several minutes or longer. Before finalization there is a low probability that the exit may be reverted. +Current epoch: 2, Exit epoch: 7, Withdrawable epoch: 263 +Please keep your validator running till exit epoch +Exit epoch in approximately 480 secs +``` + +When the exit epoch is reached, querying the status will return: + +```text +Validator 0x8de7ec501d574152f52a962bf588573df2fc3563fd0c6077651208ed20f24f3d8572425706b343117b48bdca56808416 has exited at epoch: 7 +``` + +You can safely shut down the validator client at this point. diff --git a/book/src/validator_voluntary_exit.md b/book/src/validator_voluntary_exit.md index 2a45852f32..ff404518b7 100644 --- a/book/src/validator_voluntary_exit.md +++ b/book/src/validator_voluntary_exit.md @@ -10,6 +10,8 @@ A validator can initiate a voluntary exit provided that the validator is current It takes at a minimum 5 epochs (32 minutes) for a validator to exit after initiating a voluntary exit. This number can be much higher depending on how many other validators are queued to exit. +You can also perform voluntary exit for one or more validators using the validator manager, see [Managing Validators](./validator_manager_api.md#exit) for more details. + ## Initiating a voluntary exit In order to initiate an exit, users can use the `lighthouse account validator exit` command. diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 04e3eafe6e..5ee9b0263a 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -10,6 +10,7 @@ use types::*; use validator_manager::{ create_validators::CreateConfig, delete_validators::DeleteConfig, + exit_validators::ExitConfig, import_validators::ImportConfig, list_validators::ListConfig, move_validators::{MoveConfig, PasswordSource, Validators}, @@ -119,6 +120,12 @@ impl CommandLineTest { } } +impl CommandLineTest { + fn validators_exit() -> Self { + Self::default().flag("exit", None) + } +} + #[test] pub fn validator_create_without_output_path() { CommandLineTest::validators_create().assert_failed(); @@ -443,6 +450,8 @@ pub fn validator_list_defaults() { let expected = ListConfig { vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), vc_token_path: PathBuf::from("./token.json"), + beacon_url: None, + validators_to_display: vec![], }; assert_eq!(expected, config); }); @@ -468,3 +477,106 @@ pub fn validator_delete_defaults() { assert_eq!(expected, config); }); } + +#[test] +pub fn validator_delete_missing_validator_flag() { + CommandLineTest::validators_delete() + .flag("--vc-token", Some("./token.json")) + .assert_failed(); +} + +#[test] +pub fn validator_exit_defaults() { + CommandLineTest::validators_exit() + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--vc-token", Some("./token.json")) + .flag("--beacon-node", Some("http://localhost:5052")) + .assert_success(|config| { + let expected = ExitConfig { + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + validators_to_exit: vec![ + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(), + ], + beacon_url: Some(SensitiveUrl::parse("http://localhost:5052").unwrap()), + exit_epoch: None, + presign: false, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_exit_exit_epoch_and_presign_flags() { + CommandLineTest::validators_exit() + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--vc-token", Some("./token.json")) + .flag("--exit-epoch", Some("1234567")) + .flag("--presign", None) + .assert_success(|config| { + let expected = ExitConfig { + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + validators_to_exit: vec![ + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(), + ], + beacon_url: None, + exit_epoch: Some(Epoch::new(1234567)), + presign: true, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_exit_missing_validator_flag() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .assert_failed(); +} + +#[test] +pub fn validator_exit_using_beacon_and_presign_flags() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--beacon-node", Some("http://localhost:1001")) + .flag("--presign", None) + .assert_failed(); +} + +#[test] +pub fn validator_exit_using_beacon_and_exit_epoch_flags() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--beacon-node", Some("http://localhost:1001")) + .flag("--exit-epoch", Some("1234567")) + .assert_failed(); +} + +#[test] +pub fn validator_exit_exit_epoch_flag_without_presign_flag() { + CommandLineTest::validators_exit() + .flag("--vc-token", Some("./token.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--exit-epoch", Some("1234567")) + .assert_failed(); +} diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index 8c23f79fd3..feb71c3a46 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -26,6 +26,7 @@ use std::time::Duration; use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use tokio::sync::oneshot; +use types::ChainSpec; use validator_services::block_service::BlockService; use zeroize::Zeroizing; @@ -61,6 +62,7 @@ pub struct ApiTester { pub _server_shutdown: oneshot::Sender<()>, pub validator_dir: TempDir, pub secrets_dir: TempDir, + pub spec: Arc, } impl ApiTester { @@ -69,6 +71,19 @@ impl ApiTester { } pub async fn new_with_http_config(http_config: HttpConfig) -> Self { + let slot_clock = + TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); + let genesis_validators_root = Hash256::repeat_byte(42); + let spec = Arc::new(E::default_spec()); + Self::new_with_options(http_config, slot_clock, genesis_validators_root, spec).await + } + + pub async fn new_with_options( + http_config: HttpConfig, + slot_clock: TestingSlotClock, + genesis_validators_root: Hash256, + spec: Arc, + ) -> Self { let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); let token_path = tempdir().unwrap().path().join(PK_FILENAME); @@ -91,20 +106,15 @@ impl ApiTester { ..Default::default() }; - let spec = Arc::new(E::default_spec()); - let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); - let slot_clock = - TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); - let test_runtime = TestRuntime::default(); let validator_store = Arc::new(LighthouseValidatorStore::new( initialized_validators, slashing_protection, - Hash256::repeat_byte(42), + genesis_validators_root, spec.clone(), Some(Arc::new(DoppelgangerService::default())), slot_clock.clone(), @@ -127,7 +137,7 @@ impl ApiTester { validator_store: Some(validator_store.clone()), graffiti_file: None, graffiti_flag: Some(Graffiti::default()), - spec, + spec: spec.clone(), config: http_config, sse_logging_components: None, slot_clock, @@ -161,6 +171,7 @@ impl ApiTester { _server_shutdown: shutdown_tx, validator_dir, secrets_dir, + spec, } } diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 7cb05616f4..9192f0e86b 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -17,12 +17,15 @@ ethereum_serde_utils = { workspace = true } hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +slot_clock = { workspace = true } tokio = { workspace = true } tree_hash = { workspace = true } types = { workspace = true } zeroize = { workspace = true } [dev-dependencies] +beacon_chain = { workspace = true } +http_api = { workspace = true } regex = { workspace = true } tempfile = { workspace = true } validator_http_api = { workspace = true } diff --git a/validator_manager/src/delete_validators.rs b/validator_manager/src/delete_validators.rs index 5ef647c5af..cb0557427c 100644 --- a/validator_manager/src/delete_validators.rs +++ b/validator_manager/src/delete_validators.rs @@ -45,7 +45,10 @@ pub fn cli_app() -> Command { Arg::new(VALIDATOR_FLAG) .long(VALIDATOR_FLAG) .value_name("STRING") - .help("Comma-separated list of validators (pubkey) that will be deleted.") + .help( + "Comma-separated list of validators (pubkey) that will be deleted. \ + To delete all validators, use the keyword \"all\".", + ) .action(ArgAction::Set) .required(true) .display_order(0), @@ -64,10 +67,14 @@ impl DeleteConfig { let validators_to_delete_str = clap_utils::parse_required::(matches, VALIDATOR_FLAG)?; - let validators_to_delete = validators_to_delete_str - .split(',') - .map(|s| s.trim().parse()) - .collect::, _>>()?; + let validators_to_delete = if validators_to_delete_str.trim() == "all" { + Vec::new() + } else { + validators_to_delete_str + .split(',') + .map(|s| s.trim().parse()) + .collect::, _>>()? + }; Ok(Self { vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, @@ -90,11 +97,16 @@ async fn run(config: DeleteConfig) -> Result<(), String> { let DeleteConfig { vc_url, vc_token_path, - validators_to_delete, + mut validators_to_delete, } = config; let (http_client, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; + // Delete all validators on the VC + if validators_to_delete.is_empty() { + validators_to_delete = validators.iter().map(|v| v.validating_pubkey).collect(); + } + for validator_to_delete in &validators_to_delete { if !validators .iter() diff --git a/validator_manager/src/exit_validators.rs b/validator_manager/src/exit_validators.rs new file mode 100644 index 0000000000..30d8c5c47d --- /dev/null +++ b/validator_manager/src/exit_validators.rs @@ -0,0 +1,585 @@ +use crate::{common::vc_http_client, DumpConfig}; + +use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap_utils::FLAG_HEADER; +use eth2::types::{ConfigAndPreset, Epoch, StateId, ValidatorId, ValidatorStatus}; +use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts}; +use serde::{Deserialize, Serialize}; +use serde_json; +use slot_clock::{SlotClock, SystemTimeSlotClock}; +use std::fs::write; +use std::path::PathBuf; +use std::time::Duration; +use types::{ChainSpec, EthSpec, PublicKeyBytes}; + +pub const CMD: &str = "exit"; +pub const BEACON_URL_FLAG: &str = "beacon-node"; +pub const VC_URL_FLAG: &str = "vc-url"; +pub const VC_TOKEN_FLAG: &str = "vc-token"; +pub const VALIDATOR_FLAG: &str = "validators"; +pub const EXIT_EPOCH_FLAG: &str = "exit-epoch"; +pub const PRESIGN_FLAG: &str = "presign"; + +pub fn cli_app() -> Command { + Command::new(CMD) + .about( + "Exits one or more validators using the HTTP API. It can \ + also be used to generate a presigned voluntary exit message for a particular future epoch.", + ) + .arg( + Arg::new(BEACON_URL_FLAG) + .long(BEACON_URL_FLAG) + .value_name("NETWORK_ADDRESS") + .help("Address to a beacon node HTTP API") + .action(ArgAction::Set) + .display_order(0) + .conflicts_with(PRESIGN_FLAG), + ) + .arg( + Arg::new(VC_URL_FLAG) + .long(VC_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help("A HTTP(S) address of a validator client using the keymanager-API.") + .default_value("http://localhost:5062") + .requires(VC_TOKEN_FLAG) + .action(ArgAction::Set) + .display_order(0), + ) + .arg( + Arg::new(VC_TOKEN_FLAG) + .long(VC_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the validator client.") + .action(ArgAction::Set) + .display_order(0), + ) + .arg( + Arg::new(VALIDATOR_FLAG) + .long(VALIDATOR_FLAG) + .value_name("STRING") + .help( + "Comma-separated list of validators (pubkey) to exit. \ + To exit all validators, use the keyword \"all\".", + ) + .action(ArgAction::Set) + .required(true) + .display_order(0), + ) + .arg( + Arg::new(EXIT_EPOCH_FLAG) + .long(EXIT_EPOCH_FLAG) + .value_name("EPOCH") + .help( + "Provide the minimum epoch for processing voluntary exit. \ + This flag is required to be used in combination with `--presign` to \ + save the voluntary exit presign to a file for future use.", + ) + .action(ArgAction::Set) + .display_order(0) + .requires(PRESIGN_FLAG) + .conflicts_with(BEACON_URL_FLAG), + ) + .arg( + Arg::new(PRESIGN_FLAG) + .long(PRESIGN_FLAG) + .help( + "Generate the voluntary exit presign and save it to a file \ + named {validator_pubkey}.json. Note: Using this without the \ + `--beacon-node` flag will not publish the voluntary exit to the network.", + ) + .help_heading(FLAG_HEADER) + .action(ArgAction::SetTrue) + .display_order(0) + .conflicts_with(BEACON_URL_FLAG), + ) +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct ExitConfig { + pub vc_url: SensitiveUrl, + pub vc_token_path: PathBuf, + pub validators_to_exit: Vec, + pub beacon_url: Option, + pub exit_epoch: Option, + pub presign: bool, +} + +impl ExitConfig { + fn from_cli(matches: &ArgMatches) -> Result { + let validators_to_exit_str = clap_utils::parse_required::(matches, VALIDATOR_FLAG)?; + + // Keyword "all" to exit all validators, vector to be created later + let validators_to_exit = if validators_to_exit_str.trim() == "all" { + Vec::new() + } else { + validators_to_exit_str + .split(',') + .map(|s| s.trim().parse()) + .collect::, _>>()? + }; + + Ok(Self { + vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, + vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, + validators_to_exit, + beacon_url: clap_utils::parse_optional(matches, BEACON_URL_FLAG)?, + exit_epoch: clap_utils::parse_optional(matches, EXIT_EPOCH_FLAG)?, + presign: matches.get_flag(PRESIGN_FLAG), + }) + } +} + +pub async fn cli_run( + matches: &ArgMatches, + dump_config: DumpConfig, +) -> Result<(), String> { + let config = ExitConfig::from_cli(matches)?; + + if dump_config.should_exit_early(&config)? { + Ok(()) + } else { + run::(config).await + } +} + +async fn run(config: ExitConfig) -> Result<(), String> { + let ExitConfig { + vc_url, + vc_token_path, + mut validators_to_exit, + beacon_url, + exit_epoch, + presign, + } = config; + + let (http_client, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; + + if validators_to_exit.is_empty() { + validators_to_exit = validators.iter().map(|v| v.validating_pubkey).collect(); + } + + for validator_to_exit in validators_to_exit { + // Check that the validators_to_exit is in the validator client + if !validators + .iter() + .any(|validator| validator.validating_pubkey == validator_to_exit) + { + return Err(format!("Validator {} doesn't exist", validator_to_exit)); + } + + let exit_message = http_client + .post_validator_voluntary_exit(&validator_to_exit, exit_epoch) + .await + .map_err(|e| format!("Failed to generate voluntary exit message: {}", e))?; + + if presign { + let exit_message_json = serde_json::to_string(&exit_message.data); + + match exit_message_json { + Ok(json) => { + // Save the exit message to JSON file(s) + let file_path = format!("{}.json", validator_to_exit); + write(&file_path, json).map_err(|e| { + format!("Failed to write voluntary exit message to file: {}", e) + })?; + println!("Voluntary exit message saved to {}", file_path); + } + Err(e) => eprintln!("Failed to serialize voluntary exit message: {}", e), + } + } + + // Only publish the voluntary exit if the --beacon-node flag is present + if let Some(ref beacon_url) = beacon_url { + let beacon_node = BeaconNodeHttpClient::new( + SensitiveUrl::parse(beacon_url.as_ref()) + .map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?, + Timeouts::set_all(Duration::from_secs(12)), + ); + + if beacon_node + .get_node_syncing() + .await + .map_err(|e| format!("Failed to get beacon node sync status: {:?}", e))? + .data + .is_syncing + { + return Err( + "Beacon node is syncing, submit the voluntary exit later when beacon node is synced" + .to_string(), + ); + } + + let genesis_data = beacon_node + .get_beacon_genesis() + .await + .map_err(|e| format!("Failed to get genesis data: {}", e))? + .data; + + let config_and_preset = beacon_node + .get_config_spec::() + .await + .map_err(|e| format!("Failed to get config spec: {}", e))? + .data; + + let spec = ChainSpec::from_config::(config_and_preset.config()) + .ok_or("Failed to create chain spec")?; + + let validator_data = beacon_node + .get_beacon_states_validator_id( + StateId::Head, + &ValidatorId::PublicKey(validator_to_exit), + ) + .await + .map_err(|e| format!("Failed to get validator details: {:?}", e))? + .ok_or_else(|| { + format!( + "Validator {} is not present in the beacon state. \ + Please ensure that your beacon node is synced \ + and the validator has been deposited.", + validator_to_exit + ) + })? + .data; + + let activation_epoch = validator_data.validator.activation_epoch; + let current_epoch = get_current_epoch::(genesis_data.genesis_time, &spec) + .ok_or("Failed to get current epoch. Please check your system time")?; + + // Check if validator is eligible for exit + if validator_data.status == ValidatorStatus::ActiveOngoing + && current_epoch < activation_epoch + spec.shard_committee_period + { + eprintln!( + "Validator {} is not eligible for exit. It will become eligible at epoch {}", + validator_to_exit, + activation_epoch + spec.shard_committee_period + ) + } else if validator_data.status != ValidatorStatus::ActiveOngoing { + eprintln!( + "Validator {} is not eligible for exit. Validator status is: {:?}", + validator_to_exit, validator_data.status + ) + } else { + // Only publish voluntary exit if validator status is ActiveOngoing + beacon_node + .post_beacon_pool_voluntary_exits(&exit_message.data) + .await + .map_err(|e| format!("Failed to publish voluntary exit: {}", e))?; + eprintln!( + "Successfully validated and published voluntary exit for validator {}", + validator_to_exit + ); + } + } + } + + Ok(()) +} + +pub fn get_current_epoch(genesis_time: u64, spec: &ChainSpec) -> Option { + let slot_clock = SystemTimeSlotClock::new( + spec.genesis_slot, + Duration::from_secs(genesis_time), + Duration::from_secs(spec.seconds_per_slot), + ); + slot_clock.now().map(|s| s.epoch(E::slots_per_epoch())) +} + +#[cfg(not(debug_assertions))] +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, + }; + use account_utils::eth2_keystore::KeystoreBuilder; + use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; + use eth2::lighthouse_vc::types::KeystoreJsonStr; + use http_api::test_utils::InteractiveTester; + use std::{ + fs::{self, File}, + io::Write, + sync::Arc, + }; + use types::{ChainSpec, MainnetEthSpec}; + use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; + use zeroize::Zeroizing; + type E = MainnetEthSpec; + + struct TestBuilder { + exit_config: Option, + src_import_builder: Option, + http_config: HttpConfig, + vc_token: Option, + validators: Vec, + beacon_node: InteractiveTester, + index_of_validators_to_exit: Vec, + spec: Arc, + } + + impl TestBuilder { + async fn new() -> Self { + let mut spec = ChainSpec::mainnet(); + spec.shard_committee_period = 1; + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(1)); + spec.capella_fork_epoch = Some(Epoch::new(2)); + spec.deneb_fork_epoch = Some(Epoch::new(3)); + + let beacon_node = InteractiveTester::new(Some(spec.clone()), 64).await; + + let harness = &beacon_node.harness; + let mock_el = harness.mock_execution_layer.as_ref().unwrap(); + let execution_ctx = mock_el.server.ctx.clone(); + + // Move to terminal block. + mock_el.server.all_payloads_valid(); + execution_ctx + .execution_block_generator + .write() + .move_to_terminal_block() + .unwrap(); + + Self { + exit_config: None, + src_import_builder: None, + http_config: ApiTester::default_http_config(), + vc_token: None, + validators: vec![], + beacon_node, + index_of_validators_to_exit: vec![], + spec: spec.into(), + } + } + + async fn with_validators(mut self, index_of_validators_to_exit: Vec) -> Self { + // Ensure genesis validators root matches the beacon node. + let genesis_validators_root = self + .beacon_node + .harness + .get_current_state() + .genesis_validators_root(); + // And use a single slot clock and same spec for BN and VC to keep things simple. + let slot_clock = self.beacon_node.harness.chain.slot_clock.clone(); + let vc = ApiTester::new_with_options( + self.http_config.clone(), + slot_clock, + genesis_validators_root, + self.spec.clone(), + ) + .await; + let mut builder = ImportTestBuilder::new_with_vc(vc).await; + + self.vc_token = + Some(fs::read_to_string(builder.get_import_config().vc_token_path).unwrap()); + + let local_validators: Vec = index_of_validators_to_exit + .iter() + .map(|&index| { + let keystore = KeystoreBuilder::new( + &self.beacon_node.harness.validator_keypairs[index], + "password".as_bytes(), + "".into(), + ) + .unwrap() + .build() + .unwrap(); + + ValidatorSpecification { + voting_keystore: KeystoreJsonStr(keystore), + voting_keystore_password: Zeroizing::new("password".into()), + slashing_protection: None, + fee_recipient: None, + gas_limit: None, + builder_proposals: None, + builder_boost_factor: None, + prefer_builder_proposals: None, + enabled: Some(true), + } + }) + .collect(); + + let beacon_url = SensitiveUrl::parse(self.beacon_node.client.as_ref()).unwrap(); + + let validators_to_exit = index_of_validators_to_exit + .iter() + .map(|&index| { + self.beacon_node.harness.validator_keypairs[index] + .pk + .clone() + .into() + }) + .collect(); + + let import_config = builder.get_import_config(); + + let validators_dir = import_config.vc_token_path.parent().unwrap(); + let validators_file = validators_dir.join("validators.json"); + + builder = builder.mutate_import_config(|config| { + config.validators_file_path = Some(validators_file.clone()); + }); + + fs::write( + &validators_file, + serde_json::to_string(&local_validators).unwrap(), + ) + .unwrap(); + + self.exit_config = Some(ExitConfig { + vc_url: import_config.vc_url, + vc_token_path: import_config.vc_token_path, + validators_to_exit, + beacon_url: Some(beacon_url), + exit_epoch: None, + presign: false, + }); + + self.validators = local_validators.clone(); + self.src_import_builder = Some(builder); + self.index_of_validators_to_exit = index_of_validators_to_exit; + self + } + + pub async fn run_test(self) -> TestResult { + let import_builder = self.src_import_builder.unwrap(); + let initialized_validators = import_builder.vc.initialized_validators.clone(); + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + + // only assign the validator index after validator is imported to the VC + for &index in &self.index_of_validators_to_exit { + initialized_validators.write().set_index( + &self.beacon_node.harness.validator_keypairs[index] + .pk + .compress(), + index as u64, + ); + } + + let path = self.exit_config.clone().unwrap().vc_token_path; + let parent = path.parent().unwrap(); + + fs::create_dir_all(parent).expect("Was not able to create parent directory"); + + File::options() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(path.clone()) + .unwrap() + .write_all(self.vc_token.clone().unwrap().as_bytes()) + .unwrap(); + + // Advance beacon chain + self.beacon_node.harness.advance_slot(); + + self.beacon_node + .harness + .extend_chain( + 100, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let result = run::(self.exit_config.clone().unwrap()).await; + + self.beacon_node.harness.advance_slot(); + + self.beacon_node + .harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let validator_data = self + .index_of_validators_to_exit + .iter() + .map(|&index| { + self.beacon_node + .harness + .get_current_state() + .get_validator(index) + .unwrap() + .clone() + }) + .collect::>(); + + let validator_exit_epoch = validator_data + .iter() + .map(|validator| validator.exit_epoch) + .collect::>(); + + let validator_withdrawable_epoch = validator_data + .iter() + .map(|validator| validator.withdrawable_epoch) + .collect::>(); + + let current_epoch = self.beacon_node.harness.get_current_state().current_epoch(); + let max_seed_lookahead = self.beacon_node.harness.spec.max_seed_lookahead; + let min_withdrawability_delay = self + .beacon_node + .harness + .spec + .min_validator_withdrawability_delay; + + // As per the spec: + // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_activation_exit_epoch + let beacon_exit_epoch = current_epoch + 1 + max_seed_lookahead; + let beacon_withdrawable_epoch = beacon_exit_epoch + min_withdrawability_delay; + + assert!(validator_exit_epoch + .iter() + .all(|&epoch| epoch == beacon_exit_epoch)); + + assert!(validator_withdrawable_epoch + .iter() + .all(|&epoch| epoch == beacon_withdrawable_epoch)); + + if result.is_ok() { + return TestResult { result: Ok(()) }; + } + + TestResult { + result: Err(result.unwrap_err()), + } + } + } + + #[must_use] + struct TestResult { + result: Result<(), String>, + } + + impl TestResult { + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + } + #[tokio::test] + async fn exit_single_validator() { + TestBuilder::new() + .await + .with_validators(vec![0]) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn exit_multiple_validators() { + TestBuilder::new() + .await + .with_validators(vec![10, 20, 30]) + .await + .run_test() + .await + .assert_ok(); + } +} diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 6cfbf7b54e..e5047f3f37 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -404,8 +404,12 @@ pub mod tests { } pub async fn new_with_http_config(http_config: HttpConfig) -> Self { - let dir = tempdir().unwrap(); let vc = ApiTester::new_with_http_config(http_config).await; + Self::new_with_vc(vc).await + } + + pub async fn new_with_vc(vc: ApiTester) -> Self { + let dir = tempdir().unwrap(); let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); fs::write(&vc_token_path, &vc.api_token).unwrap(); diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 9beccd3bde..fb74779304 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -9,6 +9,7 @@ use types::EthSpec; pub mod common; pub mod create_validators; pub mod delete_validators; +pub mod exit_validators; pub mod import_validators; pub mod list_validators; pub mod move_validators; @@ -51,6 +52,7 @@ pub fn cli_app() -> Command { .subcommand(move_validators::cli_app()) .subcommand(list_validators::cli_app()) .subcommand(delete_validators::cli_app()) + .subcommand(exit_validators::cli_app()) } /// Run the account manager, returning an error if the operation did not succeed. @@ -79,11 +81,14 @@ pub fn run(matches: &ArgMatches, env: Environment) -> Result<(), move_validators::cli_run(matches, dump_config).await } Some((list_validators::CMD, matches)) => { - list_validators::cli_run(matches, dump_config).await + list_validators::cli_run::(matches, dump_config).await } Some((delete_validators::CMD, matches)) => { delete_validators::cli_run(matches, dump_config).await } + Some((exit_validators::CMD, matches)) => { + exit_validators::cli_run::(matches, dump_config).await + } Some(("", _)) => Err("No command supplied. See --help.".to_string()), Some((unknown, _)) => Err(format!( "{} is not a valid {} command. See --help.", diff --git a/validator_manager/src/list_validators.rs b/validator_manager/src/list_validators.rs index a0a1c5fb40..6016b89eea 100644 --- a/validator_manager/src/list_validators.rs +++ b/validator_manager/src/list_validators.rs @@ -1,14 +1,20 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use eth2::lighthouse_vc::types::SingleKeystoreResponse; -use eth2::SensitiveUrl; +use eth2::types::{ConfigAndPreset, StateId, ValidatorId, ValidatorStatus}; +use eth2::{BeaconNodeHttpClient, SensitiveUrl, Timeouts}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::time::Duration; +use types::{ChainSpec, EthSpec, PublicKeyBytes}; +use crate::exit_validators::get_current_epoch; use crate::{common::vc_http_client, DumpConfig}; pub const CMD: &str = "list"; pub const VC_URL_FLAG: &str = "vc-url"; pub const VC_TOKEN_FLAG: &str = "vc-token"; +pub const BEACON_URL_FLAG: &str = "beacon-node"; +pub const VALIDATOR_FLAG: &str = "validators"; pub fn cli_app() -> Command { Command::new(CMD) @@ -31,47 +37,177 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0), ) + .arg( + Arg::new(BEACON_URL_FLAG) + .long(BEACON_URL_FLAG) + .value_name("NETWORK_ADDRESS") + .help( + "Address to a beacon node HTTP API. When supplied, \ + the status of validators (with regard to voluntary exit) \ + will be displayed. This flag is to be used together with \ + the --validators flag.", + ) + .action(ArgAction::Set) + .display_order(0) + .requires(VALIDATOR_FLAG), + ) + .arg( + Arg::new(VALIDATOR_FLAG) + .long(VALIDATOR_FLAG) + .value_name("STRING") + .help( + "Comma-separated list of validators (pubkey) to display status for. \ + To display the status for all validators, use the keyword \"all\". \ + This flag is to be used together with the --beacon-node flag.", + ) + .action(ArgAction::Set) + .display_order(0) + .requires(BEACON_URL_FLAG), + ) } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct ListConfig { pub vc_url: SensitiveUrl, pub vc_token_path: PathBuf, + pub beacon_url: Option, + pub validators_to_display: Vec, } impl ListConfig { fn from_cli(matches: &ArgMatches) -> Result { + let validators_to_display_str = + clap_utils::parse_optional::(matches, VALIDATOR_FLAG)?; + + // Keyword "all" to list all validators, vector to be created later + let validators_to_display = match validators_to_display_str { + Some(str) => { + if str.trim() == "all" { + Vec::new() + } else { + str.split(',') + .map(|s| s.trim().parse()) + .collect::, _>>()? + } + } + None => Vec::new(), + }; + Ok(Self { vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, + beacon_url: clap_utils::parse_optional(matches, BEACON_URL_FLAG)?, + validators_to_display, }) } } -pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<(), String> { +pub async fn cli_run( + matches: &ArgMatches, + dump_config: DumpConfig, +) -> Result<(), String> { let config = ListConfig::from_cli(matches)?; if dump_config.should_exit_early(&config)? { Ok(()) } else { - run(config).await?; + run::(config).await?; Ok(()) } } -async fn run(config: ListConfig) -> Result, String> { +async fn run(config: ListConfig) -> Result, String> { let ListConfig { vc_url, vc_token_path, + beacon_url, + mut validators_to_display, } = config; let (_, validators) = vc_http_client(vc_url.clone(), &vc_token_path).await?; println!("List of validators ({}):", validators.len()); - for validator in &validators { - println!("{}", validator.validating_pubkey); + if validators_to_display.is_empty() { + validators_to_display = validators.iter().map(|v| v.validating_pubkey).collect(); } + if let Some(ref beacon_url) = beacon_url { + for validator in &validators_to_display { + let beacon_node = BeaconNodeHttpClient::new( + SensitiveUrl::parse(beacon_url.as_ref()) + .map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?, + Timeouts::set_all(Duration::from_secs(12)), + ); + + let validator_data = beacon_node + .get_beacon_states_validator_id(StateId::Head, &ValidatorId::PublicKey(*validator)) + .await + .map_err(|e| format!("Failed to get updated validator details: {:?}", e))? + .ok_or_else(|| { + format!("Validator {} is not present in the beacon state", validator) + })? + .data; + + match validator_data.status { + ValidatorStatus::ActiveExiting => { + let exit_epoch = validator_data.validator.exit_epoch; + let withdrawal_epoch = validator_data.validator.withdrawable_epoch; + + let genesis_data = beacon_node + .get_beacon_genesis() + .await + .map_err(|e| format!("Failed to get genesis data: {}", e))? + .data; + + let config_and_preset = beacon_node + .get_config_spec::() + .await + .map_err(|e| format!("Failed to get config spec: {}", e))? + .data; + + let spec = ChainSpec::from_config::(config_and_preset.config()) + .ok_or("Failed to create chain spec")?; + + let current_epoch = get_current_epoch::(genesis_data.genesis_time, &spec) + .ok_or("Failed to get current epoch. Please check your system time")?; + + eprintln!( + "Voluntary exit for validator {} has been accepted into the beacon chain. \ + Note that the voluntary exit is subject chain finalization. \ + Before the chain has finalized, there is a low \ + probability that the exit may be reverted.", + validator + ); + eprintln!( + "Current epoch: {}, Exit epoch: {}, Withdrawable epoch: {}", + current_epoch, exit_epoch, withdrawal_epoch + ); + eprintln!("Please keep your validator running till exit epoch"); + eprintln!( + "Exit epoch in approximately {} secs", + (exit_epoch - current_epoch) * spec.seconds_per_slot * E::slots_per_epoch() + ); + } + ValidatorStatus::ExitedSlashed | ValidatorStatus::ExitedUnslashed => { + eprintln!( + "Validator {} has exited at epoch: {}", + validator, validator_data.validator.exit_epoch + ); + } + _ => { + eprintln!( + "Validator {} has not initiated voluntary exit or the voluntary exit \ + is yet to be accepted into the beacon chain. Validator status is: {}", + validator, validator_data.status + ) + } + } + } + } else { + for validator in &validators { + println!("{}", validator.validating_pubkey); + } + } Ok(validators) } @@ -87,7 +223,9 @@ mod test { use crate::{ common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, }; + use types::MainnetEthSpec; use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; + type E = MainnetEthSpec; struct TestBuilder { list_config: Option, @@ -116,6 +254,8 @@ mod test { self.list_config = Some(ListConfig { vc_url: builder.get_import_config().vc_url, vc_token_path: builder.get_import_config().vc_token_path, + beacon_url: None, + validators_to_display: vec![], }); self.vc_token = @@ -152,7 +292,7 @@ mod test { .write_all(self.vc_token.clone().unwrap().as_bytes()) .unwrap(); - let result = run(self.list_config.clone().unwrap()).await; + let result = run::(self.list_config.clone().unwrap()).await; if result.is_ok() { let result_ref = result.as_ref().unwrap(); diff --git a/wordlist.txt b/wordlist.txt index 3c7070c642..ada0384d36 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -196,6 +196,8 @@ pem performant pid pre +presign +presigned pubkey pubkeys rc From e305cb1b921f544dfd05f769aeb7e37cd482933f Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 30 Jun 2025 23:06:37 -0700 Subject: [PATCH 05/13] Custody persist fix (#7661) N/A Persist the epoch -> cgc values. This is to ensure that `ValidatorRegistrations::latest_validator_custody_requirement` always returns a `Some` value post restart assuming the `epoch_validator_custody_requirements` map has been updated in the previous runs. --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 + .../beacon_chain/src/persisted_custody.rs | 2 +- beacon_node/beacon_chain/src/schema_change.rs | 9 ++ .../src/schema_change/migration_schema_v26.rs | 91 +++++++++++++++++++ .../beacon_chain/src/validator_custody.rs | 20 +++- .../beacon_chain/tests/schema_stability.rs | 15 +-- beacon_node/store/src/metadata.rs | 2 +- 7 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 beacon_node/beacon_chain/src/schema_change/migration_schema_v26.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index de377dab97..65318835cc 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -654,6 +654,10 @@ impl BeaconChain { /// Persists the custody information to disk. pub fn persist_custody_context(&self) -> Result<(), Error> { + if !self.spec.is_peer_das_scheduled() { + return Ok(()); + } + let custody_context: CustodyContextSsz = self .data_availability_checker .custody_context() diff --git a/beacon_node/beacon_chain/src/persisted_custody.rs b/beacon_node/beacon_chain/src/persisted_custody.rs index 6ede473b36..b685ea36b7 100644 --- a/beacon_node/beacon_chain/src/persisted_custody.rs +++ b/beacon_node/beacon_chain/src/persisted_custody.rs @@ -7,7 +7,7 @@ use types::{EthSpec, Hash256}; /// 32-byte key for accessing the `CustodyContext`. All zero because `CustodyContext` has its own column. pub const CUSTODY_DB_KEY: Hash256 = Hash256::ZERO; -pub struct PersistedCustody(CustodyContextSsz); +pub struct PersistedCustody(pub CustodyContextSsz); pub fn load_custody_context, Cold: ItemStore>( store: Arc>, diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 0abb48494a..317b89cbdd 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -2,6 +2,7 @@ mod migration_schema_v23; mod migration_schema_v24; mod migration_schema_v25; +mod migration_schema_v26; use crate::beacon_chain::BeaconChainTypes; use std::sync::Arc; @@ -58,6 +59,14 @@ pub fn migrate_schema( let ops = migration_schema_v25::downgrade_from_v25()?; db.store_schema_version_atomically(to, ops) } + (SchemaVersion(25), SchemaVersion(26)) => { + let ops = migration_schema_v26::upgrade_to_v26::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(26), SchemaVersion(25)) => { + let ops = migration_schema_v26::downgrade_from_v26::(db.clone())?; + db.store_schema_version_atomically(to, ops) + } // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v26.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v26.rs new file mode 100644 index 0000000000..2e2a6bdc4f --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v26.rs @@ -0,0 +1,91 @@ +use crate::persisted_custody::{PersistedCustody, CUSTODY_DB_KEY}; +use crate::validator_custody::CustodyContextSsz; +use crate::BeaconChainTypes; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::sync::Arc; +use store::{DBColumn, Error, HotColdDB, KeyValueStoreOp, StoreItem}; +use tracing::info; + +#[derive(Debug, Encode, Decode, Clone)] +pub(crate) struct CustodyContextSszV24 { + pub(crate) validator_custody_at_head: u64, + pub(crate) persisted_is_supernode: bool, +} + +pub(crate) struct PersistedCustodyV24(CustodyContextSszV24); + +impl StoreItem for PersistedCustodyV24 { + fn db_column() -> DBColumn { + DBColumn::CustodyContext + } + + fn as_store_bytes(&self) -> Vec { + self.0.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + let custody_context = CustodyContextSszV24::from_ssz_bytes(bytes)?; + Ok(PersistedCustodyV24(custody_context)) + } +} + +/// Upgrade the `CustodyContext` entry to v26. +pub fn upgrade_to_v26( + db: Arc>, +) -> Result, Error> { + let ops = if db.spec.is_peer_das_scheduled() { + match db.get_item::(&CUSTODY_DB_KEY) { + Ok(Some(PersistedCustodyV24(ssz_v24))) => { + info!("Migrating `CustodyContext` to v26 schema"); + let custody_context_v2 = CustodyContextSsz { + validator_custody_at_head: ssz_v24.validator_custody_at_head, + persisted_is_supernode: ssz_v24.persisted_is_supernode, + epoch_validator_custody_requirements: vec![], + }; + vec![KeyValueStoreOp::PutKeyValue( + DBColumn::CustodyContext, + CUSTODY_DB_KEY.as_slice().to_vec(), + PersistedCustody(custody_context_v2).as_store_bytes(), + )] + } + _ => { + vec![] + } + } + } else { + // Delete it from db if PeerDAS hasn't been scheduled + vec![KeyValueStoreOp::DeleteKey( + DBColumn::CustodyContext, + CUSTODY_DB_KEY.as_slice().to_vec(), + )] + }; + + Ok(ops) +} + +pub fn downgrade_from_v26( + db: Arc>, +) -> Result, Error> { + let res = db.get_item::(&CUSTODY_DB_KEY); + let ops = match res { + Ok(Some(PersistedCustody(ssz_v26))) => { + info!("Migrating `CustodyContext` back from v26 schema"); + let custody_context_v24 = CustodyContextSszV24 { + validator_custody_at_head: ssz_v26.validator_custody_at_head, + persisted_is_supernode: ssz_v26.persisted_is_supernode, + }; + vec![KeyValueStoreOp::PutKeyValue( + DBColumn::CustodyContext, + CUSTODY_DB_KEY.as_slice().to_vec(), + PersistedCustodyV24(custody_context_v24).as_store_bytes(), + )] + } + _ => { + // no op if it's not on the db, as previous versions gracefully handle data missing from disk. + vec![] + } + }; + + Ok(ops) +} diff --git a/beacon_node/beacon_chain/src/validator_custody.rs b/beacon_node/beacon_chain/src/validator_custody.rs index 1169b64537..5f037fabf3 100644 --- a/beacon_node/beacon_chain/src/validator_custody.rs +++ b/beacon_node/beacon_chain/src/validator_custody.rs @@ -163,7 +163,13 @@ impl CustodyContext { validator_custody_count: AtomicU64::new(ssz_context.validator_custody_at_head), current_is_supernode: is_supernode, persisted_is_supernode: ssz_context.persisted_is_supernode, - validator_registrations: Default::default(), + validator_registrations: RwLock::new(ValidatorRegistrations { + validators: Default::default(), + epoch_validator_custody_requirements: ssz_context + .epoch_validator_custody_requirements + .into_iter() + .collect(), + }), } } @@ -263,8 +269,9 @@ pub struct CustodyCountChanged { /// The custody information that gets persisted across runs. #[derive(Debug, Encode, Decode, Clone)] pub struct CustodyContextSsz { - validator_custody_at_head: u64, - persisted_is_supernode: bool, + pub validator_custody_at_head: u64, + pub persisted_is_supernode: bool, + pub epoch_validator_custody_requirements: Vec<(Epoch, u64)>, } impl From<&CustodyContext> for CustodyContextSsz { @@ -272,6 +279,13 @@ impl From<&CustodyContext> for CustodyContextSsz { CustodyContextSsz { validator_custody_at_head: context.validator_custody_count.load(Ordering::Relaxed), persisted_is_supernode: context.persisted_is_supernode, + epoch_validator_custody_requirements: context + .validator_registrations + .read() + .epoch_validator_custody_requirements + .iter() + .map(|(epoch, count)| (*epoch, *count)) + .collect(), } } } diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs index 00d75a554d..fc37a1159b 100644 --- a/beacon_node/beacon_chain/tests/schema_stability.rs +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -88,7 +88,7 @@ async fn schema_stability() { check_db_columns(); check_metadata_sizes(&store); check_op_pool(&store); - check_custody_context(&store); + check_custody_context(&store, &harness.spec); check_persisted_chain(&store); // Not covered here: @@ -134,12 +134,13 @@ fn check_op_pool(store: &Store) { assert_eq!(op_pool.as_store_bytes().len(), 28); } -fn check_custody_context(store: &Store) { - let custody_context = store - .get_item::(&Hash256::ZERO) - .unwrap() - .unwrap(); - assert_eq!(custody_context.as_store_bytes().len(), 9); +fn check_custody_context(store: &Store, spec: &ChainSpec) { + let custody_context_opt = store.get_item::(&Hash256::ZERO).unwrap(); + if spec.is_peer_das_scheduled() { + assert_eq!(custody_context_opt.unwrap().as_store_bytes().len(), 13); + } else { + assert!(custody_context_opt.is_none()); + } } fn check_persisted_chain(store: &Store) { diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 63cb4661cd..39a46451fc 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -4,7 +4,7 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use types::{Hash256, Slot}; -pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(25); +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(26); // All the keys that get stored under the `BeaconMeta` column. // From 41742ce2bde924e4dc6684b430815ca1895ae225 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 2 Jul 2025 10:08:40 +1000 Subject: [PATCH 06/13] Update `SAMPLES_PER_SLOT` to be number of custody groups instead of data columns (#7683) Update `SAMPLES_PER_SLOT` to be number of custody groups instead of data columns. This should have no impact on the current implementation as config currently maintains a `group:subnet:column` ratio of `1:1:1`. **In short, this PR doesn't change anything for Fusaka, but ensures compliance with the spec and potential future changes.** I've added separate methods to compute sampling columns and custody groups for clarity: `spec.sampling_size_columns` and `spec.sampling_size_custod_groups` See the clarifications in this PR for more details: https://github.com/ethereum/consensus-specs/pull/4251 --- .../overflow_lru_cache.rs | 21 +++++---- beacon_node/beacon_chain/src/test_utils.rs | 2 +- .../beacon_chain/src/validator_custody.rs | 45 ++++++++++++++----- .../lighthouse_network/src/types/globals.rs | 18 +++++--- consensus/types/src/chain_spec.rs | 15 +++++-- 5 files changed, 71 insertions(+), 30 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index deaea3eb24..3c1fd1e7bc 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -481,7 +481,8 @@ impl DataAvailabilityCheckerInner { if let Some(available_block) = pending_components.make_available( &self.spec, - self.custody_context.sampling_size(Some(epoch), &self.spec), + self.custody_context + .num_of_data_columns_to_sample(Some(epoch), &self.spec), |block| self.state_cache.recover_pending_executed_block(block), )? { // We keep the pending components in the availability cache during block import (#5845). @@ -526,7 +527,9 @@ impl DataAvailabilityCheckerInner { // Merge in the data columns. pending_components.merge_data_columns(kzg_verified_data_columns)?; - let num_expected_columns = self.custody_context.sampling_size(Some(epoch), &self.spec); + let num_expected_columns = self + .custody_context + .num_of_data_columns_to_sample(Some(epoch), &self.spec); debug!( component = "data_columns", ?block_root, @@ -622,7 +625,9 @@ impl DataAvailabilityCheckerInner { // Merge in the block. pending_components.merge_block(diet_executed_block); - let num_expected_columns = self.custody_context.sampling_size(Some(epoch), &self.spec); + let num_expected_columns = self + .custody_context + .num_of_data_columns_to_sample(Some(epoch), &self.spec); debug!( component = "block", ?block_root, @@ -631,11 +636,11 @@ impl DataAvailabilityCheckerInner { ); // Check if we have all components and entire set is consistent. - if let Some(available_block) = pending_components.make_available( - &self.spec, - self.custody_context.sampling_size(Some(epoch), &self.spec), - |block| self.state_cache.recover_pending_executed_block(block), - )? { + if let Some(available_block) = + pending_components.make_available(&self.spec, num_expected_columns, |block| { + self.state_cache.recover_pending_executed_block(block) + })? + { // We keep the pending components in the availability cache during block import (#5845). write_lock.put(block_root, pending_components); drop(write_lock); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index db4e2fab26..2c4981078d 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -777,7 +777,7 @@ where self.chain .data_availability_checker .custody_context() - .sampling_size(None, &self.chain.spec) as usize + .num_of_data_columns_to_sample(None, &self.chain.spec) as usize } pub fn slots_per_epoch(&self) -> u64 { diff --git a/beacon_node/beacon_chain/src/validator_custody.rs b/beacon_node/beacon_chain/src/validator_custody.rs index 5f037fabf3..7dc5b18ae4 100644 --- a/beacon_node/beacon_chain/src/validator_custody.rs +++ b/beacon_node/beacon_chain/src/validator_custody.rs @@ -215,7 +215,8 @@ impl CustodyContext { ); return Some(CustodyCountChanged { new_custody_group_count: updated_cgc, - sampling_count: self.sampling_size(Some(effective_epoch), spec), + sampling_count: self + .num_of_custody_groups_to_sample(Some(effective_epoch), spec), }); } } @@ -240,9 +241,13 @@ impl CustodyContext { } } - /// Returns the count of custody columns this node must sample for a block at `epoch` to import. - /// If an `epoch` is not specified, returns the *current* validator custody requirement. - pub fn sampling_size(&self, epoch_opt: Option, spec: &ChainSpec) -> u64 { + /// This function is used to determine the custody group count at a given epoch. + /// + /// This differs from the number of custody groups sampled per slot, as the spec requires a + /// minimum sampling size which may exceed the custody group count (CGC). + /// + /// See also: [`Self::num_of_custody_groups_to_sample`]. + fn custody_group_count_at_epoch(&self, epoch_opt: Option, spec: &ChainSpec) -> u64 { let custody_group_count = if self.current_is_supernode { spec.number_of_custody_groups } else if let Some(epoch) = epoch_opt { @@ -253,8 +258,26 @@ impl CustodyContext { } else { self.custody_group_count_at_head(spec) }; + custody_group_count + } - spec.sampling_size(custody_group_count) + /// Returns the count of custody groups this node must _sample_ for a block at `epoch` to import. + /// If an `epoch` is not specified, returns the *current* validator custody requirement. + pub fn num_of_custody_groups_to_sample( + &self, + epoch_opt: Option, + spec: &ChainSpec, + ) -> u64 { + let custody_group_count = self.custody_group_count_at_epoch(epoch_opt, spec); + spec.sampling_size_custody_groups(custody_group_count) + .expect("should compute node sampling size from valid chain spec") + } + + /// Returns the count of columns this node must _sample_ for a block at `epoch` to import. + /// If an `epoch` is not specified, returns the *current* validator custody requirement. + pub fn num_of_data_columns_to_sample(&self, epoch_opt: Option, spec: &ChainSpec) -> u64 { + let custody_group_count = self.custody_group_count_at_epoch(epoch_opt, spec); + spec.sampling_size_columns(custody_group_count) .expect("should compute node sampling size from valid chain spec") } } @@ -307,7 +330,7 @@ mod tests { spec.number_of_custody_groups ); assert_eq!( - custody_context.sampling_size(None, &spec), + custody_context.num_of_custody_groups_to_sample(None, &spec), spec.number_of_custody_groups ); } @@ -322,7 +345,7 @@ mod tests { "head custody count should be minimum spec custody requirement" ); assert_eq!( - custody_context.sampling_size(None, &spec), + custody_context.num_of_custody_groups_to_sample(None, &spec), spec.samples_per_slot ); } @@ -412,7 +435,7 @@ mod tests { register_validators_and_assert_cgc(&custody_context, validators_and_expected_cgc, &spec); assert_eq!( - custody_context.sampling_size(None, &spec), + custody_context.num_of_custody_groups_to_sample(None, &spec), spec.number_of_custody_groups ); } @@ -423,7 +446,7 @@ mod tests { let spec = E::default_spec(); let current_slot = Slot::new(10); let current_epoch = current_slot.epoch(E::slots_per_epoch()); - let default_sampling_size = custody_context.sampling_size(None, &spec); + let default_sampling_size = custody_context.num_of_custody_groups_to_sample(None, &spec); let validator_custody_units = 10; let _cgc_changed = custody_context.register_validators::( @@ -437,12 +460,12 @@ mod tests { // CGC update is not applied for `current_epoch`. assert_eq!( - custody_context.sampling_size(Some(current_epoch), &spec), + custody_context.num_of_custody_groups_to_sample(Some(current_epoch), &spec), default_sampling_size ); // CGC update is applied for the next epoch. assert_eq!( - custody_context.sampling_size(Some(current_epoch + 1), &spec), + custody_context.num_of_custody_groups_to_sample(Some(current_epoch + 1), &spec), validator_custody_units ); } diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index d1ed1c33b0..cc4d758b4a 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -66,7 +66,7 @@ impl NetworkGlobals { // The below `expect` calls will panic on start up if the chain spec config values used // are invalid let sampling_size = spec - .sampling_size(custody_group_count) + .sampling_size_custody_groups(custody_group_count) .expect("should compute node sampling size from valid chain spec"); let custody_groups = get_custody_groups(node_id, sampling_size, &spec) .expect("should compute node custody groups"); @@ -114,7 +114,7 @@ impl NetworkGlobals { // are invalid let sampling_size = self .spec - .sampling_size(custody_group_count) + .sampling_size_custody_groups(custody_group_count) .expect("should compute node sampling size from valid chain spec"); let custody_groups = get_custody_groups(self.local_enr().node_id().raw(), sampling_size, &self.spec) @@ -298,7 +298,13 @@ mod test { spec.fulu_fork_epoch = Some(Epoch::new(0)); let custody_group_count = spec.number_of_custody_groups / 2; - let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); + let sampling_size_custody_groups = spec + .sampling_size_custody_groups(custody_group_count) + .unwrap(); + let expected_sampling_subnet_count = sampling_size_custody_groups + * spec.data_column_sidecar_subnet_count + / spec.number_of_custody_groups; + let metadata = get_metadata(custody_group_count); let config = Arc::new(NetworkConfig::default()); @@ -310,7 +316,7 @@ mod test { ); assert_eq!( globals.sampling_subnets.read().len(), - subnet_sampling_size as usize + expected_sampling_subnet_count as usize ); } @@ -321,7 +327,7 @@ mod test { spec.fulu_fork_epoch = Some(Epoch::new(0)); let custody_group_count = spec.number_of_custody_groups / 2; - let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); + let expected_sampling_columns = spec.sampling_size_columns(custody_group_count).unwrap(); let metadata = get_metadata(custody_group_count); let config = Arc::new(NetworkConfig::default()); @@ -333,7 +339,7 @@ mod test { ); assert_eq!( globals.sampling_columns.read().len(), - subnet_sampling_size as usize + expected_sampling_columns as usize ); } diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b4fd5afe87..38cd4b9217 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -720,17 +720,24 @@ impl ChainSpec { } /// Returns the number of column sidecars to sample per slot. - pub fn sampling_size(&self, custody_group_count: u64) -> Result { + pub fn sampling_size_columns(&self, custody_group_count: u64) -> Result { + let sampling_size_groups = self.sampling_size_custody_groups(custody_group_count)?; + let columns_per_custody_group = self .number_of_columns .safe_div(self.number_of_custody_groups) .map_err(|_| "number_of_custody_groups must be greater than 0")?; - let custody_column_count = columns_per_custody_group - .safe_mul(custody_group_count) + let sampling_size_columns = columns_per_custody_group + .safe_mul(sampling_size_groups) .map_err(|_| "Computing sampling size should not overflow")?; - Ok(std::cmp::max(custody_column_count, self.samples_per_slot)) + Ok(sampling_size_columns) + } + + /// Returns the number of custody groups to sample per slot. + pub fn sampling_size_custody_groups(&self, custody_group_count: u64) -> Result { + Ok(std::cmp::max(custody_group_count, self.samples_per_slot)) } pub fn all_data_column_sidecar_subnets(&self) -> impl Iterator { From 69c9c7038af79e5f2c538c7d023fce85114673f2 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 1 Jul 2025 19:38:22 -0700 Subject: [PATCH 07/13] Use prepare_beacon_proposer endpoint for validator custody registration (#7681) N/A This PR switches to using `prepare_beacon_proposer` instead of `beacon_committee_subscriptions` endpoint to register validators with the custody context. We currently use the `beacon_committee_subscriptions` endpoint for registering validators in the custody context. Using the subscriptions endpoint has a few disadvantages: 1. The lighthouse VC tries to optimise the number of calls it makes to this endpoint to reduce the load on the subscriptions endpoint. So we would be getting different a subset of the total number of validators in each call. This will lead to a ramp up of the validator custody units instead of a one time bump. For e.g. see these logs ``` Jun 30 22:36:05.012 DEBUG Validator count at head updated old_count: 0, new_count: 19 Jun 30 22:36:11.016 DEBUG Validator count at head updated old_count: 19, new_count: 24 Jun 30 22:36:17.017 DEBUG Validator count at head updated old_count: 24, new_count: 27 Jun 30 22:36:23.020 DEBUG Validator count at head updated old_count: 27, new_count: 32 Jun 30 22:36:29.016 DEBUG Validator count at head updated old_count: 32, new_count: 36 Jun 30 22:36:35.005 DEBUG Validator count at head updated old_count: 36, new_count: 42 Jun 30 22:36:41.014 DEBUG Validator count at head updated old_count: 42, new_count: 44 Jun 30 22:36:47.017 DEBUG Validator count at head updated old_count: 44, new_count: 46 Jun 30 22:36:53.007 DEBUG Validator count at head updated old_count: 46, new_count: 48 Jun 30 22:36:59.009 DEBUG Validator count at head updated old_count: 48, new_count: 49 Jun 30 22:37:05.014 DEBUG Validator count at head updated old_count: 49, new_count: 50 Jun 30 22:37:11.007 DEBUG Validator count at head updated old_count: 50, new_count: 53 Jun 30 22:37:17.007 DEBUG Validator count at head updated old_count: 53, new_count: 55 Jun 30 22:37:35.008 DEBUG Validator count at head updated old_count: 55, new_count: 58 Jun 30 22:37:41.007 DEBUG Validator count at head updated old_count: 58, new_count: 59 Jun 30 22:37:53.010 DEBUG Validator count at head updated old_count: 59, new_count: 60 Jun 30 22:38:05.013 DEBUG Validator count at head updated old_count: 60, new_count: 61 Jun 30 22:38:23.006 DEBUG Validator count at head updated old_count: 61, new_count: 62 Jun 30 22:38:29.009 DEBUG Validator count at head updated old_count: 62, new_count: 63 Jun 30 22:38:41.009 DEBUG Validator count at head updated old_count: 63, new_count: 64 ``` 2. Different VCs would probably have different behaviours in terms of sending subscriptions In contrast, the `prepare_beacon_proposer` endpoint usage would be more standard across different VCs without any filtering of validators. Not doing so could mean potentially missing proposals so VCs are incentivised to make this call on any change in the validators managed by them. Lighthouse calls this endpoint every slot. --- beacon_node/http_api/src/lib.rs | 76 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index a627fb0353..c757ca035b 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -3721,13 +3721,11 @@ pub fn serve( .and(warp::path::end()) .and(warp_utils::json::json()) .and(validator_subscription_tx_filter.clone()) - .and(network_tx_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .then( |committee_subscriptions: Vec, validator_subscription_tx: Sender, - network_tx: UnboundedSender>, task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { @@ -3761,42 +3759,6 @@ pub fn serve( )); } - if chain.spec.is_peer_das_scheduled() { - let (finalized_beacon_state, _, _) = - StateId(CoreStateId::Finalized).state(&chain)?; - let validators_and_balances = committee_subscriptions - .iter() - .filter_map(|subscription| { - if let Ok(effective_balance) = finalized_beacon_state - .get_effective_balance(subscription.validator_index as usize) - { - Some((subscription.validator_index as usize, effective_balance)) - } else { - None - } - }) - .collect::>(); - - let current_slot = - chain.slot().map_err(warp_utils::reject::unhandled_error)?; - if let Some(cgc_change) = chain - .data_availability_checker - .custody_context() - .register_validators::( - validators_and_balances, - current_slot, - &chain.spec, - ) { - network_tx.send(NetworkMessage::CustodyCountChanged { - new_custody_group_count: cgc_change.new_custody_group_count, - sampling_count: cgc_change.sampling_count, - }).unwrap_or_else(|e| { - debug!(error = %e, "Could not send message to the network service. \ - Likely shutdown") - }); - } - } - Ok(()) }) }, @@ -3808,11 +3770,13 @@ pub fn serve( .and(warp::path("prepare_beacon_proposer")) .and(warp::path::end()) .and(not_while_syncing_filter.clone()) + .and(network_tx_filter.clone()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(warp_utils::json::json()) .then( |not_synced_filter: Result<(), Rejection>, + network_tx: UnboundedSender>, task_spawner: TaskSpawner, chain: Arc>, preparation_data: Vec| { @@ -3849,6 +3813,42 @@ pub fn serve( )) })?; + if chain.spec.is_peer_das_scheduled() { + let (finalized_beacon_state, _, _) = + StateId(CoreStateId::Finalized).state(&chain)?; + let validators_and_balances = preparation_data + .iter() + .filter_map(|preparation| { + if let Ok(effective_balance) = finalized_beacon_state + .get_effective_balance(preparation.validator_index as usize) + { + Some((preparation.validator_index as usize, effective_balance)) + } else { + None + } + }) + .collect::>(); + + let current_slot = + chain.slot().map_err(warp_utils::reject::unhandled_error)?; + if let Some(cgc_change) = chain + .data_availability_checker + .custody_context() + .register_validators::( + validators_and_balances, + current_slot, + &chain.spec, + ) { + network_tx.send(NetworkMessage::CustodyCountChanged { + new_custody_group_count: cgc_change.new_custody_group_count, + sampling_count: cgc_change.sampling_count, + }).unwrap_or_else(|e| { + debug!(error = %e, "Could not send message to the network service. \ + Likely shutdown") + }); + } + } + Ok::<_, warp::reject::Rejection>(warp::reply::json(&()).into_response()) }) }, From fcc602a7872a867c8b0c60401b6e8e397e4d4332 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 2 Jul 2025 12:38:25 +1000 Subject: [PATCH 08/13] Update fulu network configs and add `MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS` (#7646) - #6240 - Bring built-in network configs up to date with latest consensus-spec PeerDAS configs. - Add `MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS` and use it to determine data availability window after the Fulu fork. --- .../src/data_availability_checker.rs | 29 +++--- beacon_node/store/src/hot_cold_store.rs | 15 ++- .../holesky/config.yaml | 5 +- .../hoodi/config.yaml | 3 +- .../mainnet/config.yaml | 5 +- .../sepolia/config.yaml | 5 +- consensus/types/src/chain_spec.rs | 92 +++++++++++++++++++ 7 files changed, 126 insertions(+), 28 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 1bc95c22ac..5404718048 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -486,14 +486,9 @@ impl DataAvailabilityChecker { /// The epoch at which we require a data availability check in block processing. /// `None` if the `Deneb` fork is disabled. pub fn data_availability_boundary(&self) -> Option { - let fork_epoch = self.spec.deneb_fork_epoch?; - let current_slot = self.slot_clock.now()?; - Some(std::cmp::max( - fork_epoch, - current_slot - .epoch(T::EthSpec::slots_per_epoch()) - .saturating_sub(self.spec.min_epochs_for_blob_sidecars_requests), - )) + let current_epoch = self.slot_clock.now()?.epoch(T::EthSpec::slots_per_epoch()); + self.spec + .min_epoch_data_availability_boundary(current_epoch) } /// Returns true if the given epoch lies within the da boundary and false otherwise. @@ -670,15 +665,17 @@ async fn availability_cache_maintenance_service( .fork_choice_read_lock() .finalized_checkpoint() .epoch; + + let Some(min_epochs_for_blobs) = chain + .spec + .min_epoch_data_availability_boundary(current_epoch) + else { + // Shutdown service if deneb fork epoch not set. Unreachable as the same check is performed above. + break; + }; + // any data belonging to an epoch before this should be pruned - let cutoff_epoch = std::cmp::max( - finalized_epoch + 1, - std::cmp::max( - current_epoch - .saturating_sub(chain.spec.min_epochs_for_blob_sidecars_requests), - deneb_fork_epoch, - ), - ); + let cutoff_epoch = std::cmp::max(finalized_epoch + 1, min_epochs_for_blobs); if let Err(e) = overflow_cache.do_maintenance(cutoff_epoch) { error!(error = ?e,"Failed to maintain availability cache"); diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index f5e44f7ac9..0c230494b8 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -3074,18 +3074,17 @@ impl, Cold: ItemStore> HotColdDB /// Try to prune blobs, approximating the current epoch from the split slot. pub fn try_prune_most_blobs(&self, force: bool) -> Result<(), Error> { - let Some(deneb_fork_epoch) = self.spec.deneb_fork_epoch else { - debug!("Deneb fork is disabled"); - return Ok(()); - }; // The current epoch is >= split_epoch + 2. It could be greater if the database is // configured to delay updating the split or finalization has ceased. In this instance we // choose to also delay the pruning of blobs (we never prune without finalization anyway). let min_current_epoch = self.get_split_slot().epoch(E::slots_per_epoch()) + 2; - let min_data_availability_boundary = std::cmp::max( - deneb_fork_epoch, - min_current_epoch.saturating_sub(self.spec.min_epochs_for_blob_sidecars_requests), - ); + let Some(min_data_availability_boundary) = self + .spec + .min_epoch_data_availability_boundary(min_current_epoch) + else { + debug!("Deneb fork is disabled"); + return Ok(()); + }; self.try_prune_blobs(force, min_data_availability_boundary) } diff --git a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml index 19a3f79cc0..76d8d482c2 100644 --- a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -141,6 +141,9 @@ MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +VALIDATOR_CUSTODY_REQUIREMENT: 8 +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml b/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml index 5cca1cd037..a1365e3464 100644 --- a/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/hoodi/config.yaml @@ -156,7 +156,8 @@ DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +VALIDATOR_CUSTODY_REQUIREMENT: 8 +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 # EIP7732 diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 886e5d12ed..0b68a27f4d 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -156,6 +156,9 @@ MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +VALIDATOR_CUSTODY_REQUIREMENT: 8 +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 diff --git a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml index 10be107263..ccd71cdce9 100644 --- a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml @@ -142,6 +142,9 @@ MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152 NUMBER_OF_COLUMNS: 128 NUMBER_OF_CUSTODY_GROUPS: 128 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 +MAX_REQUEST_DATA_COLUMN_SIDECARS: 16384 SAMPLES_PER_SLOT: 8 CUSTODY_REQUIREMENT: 4 -MAX_BLOBS_PER_BLOCK_FULU: 12 +VALIDATOR_CUSTODY_REQUIREMENT: 8 +BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000 +MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096 diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 38cd4b9217..631389ce43 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -246,6 +246,7 @@ pub struct ChainSpec { * Networking Fulu */ blob_schedule: BlobSchedule, + min_epochs_for_data_column_sidecars_requests: u64, /* * Networking Derived @@ -740,6 +741,20 @@ impl ChainSpec { Ok(std::cmp::max(custody_group_count, self.samples_per_slot)) } + /// Returns the min epoch for blob / data column sidecar requests based on the current epoch. + /// Switch to use the column sidecar config once the `blob_retention_epoch` has passed Fulu fork epoch. + pub fn min_epoch_data_availability_boundary(&self, current_epoch: Epoch) -> Option { + let fork_epoch = self.deneb_fork_epoch?; + let blob_retention_epoch = + current_epoch.saturating_sub(self.min_epochs_for_blob_sidecars_requests); + match self.fulu_fork_epoch { + Some(fulu_fork_epoch) if blob_retention_epoch > fulu_fork_epoch => Some( + current_epoch.saturating_sub(self.min_epochs_for_data_column_sidecars_requests), + ), + _ => Some(std::cmp::max(fork_epoch, blob_retention_epoch)), + } + } + pub fn all_data_column_sidecar_subnets(&self) -> impl Iterator { (0..self.data_column_sidecar_subnet_count).map(DataColumnSubnetId::new) } @@ -1027,6 +1042,8 @@ impl ChainSpec { * Networking Fulu specific */ blob_schedule: BlobSchedule::default(), + min_epochs_for_data_column_sidecars_requests: + default_min_epochs_for_data_column_sidecars_requests(), /* * Application specific @@ -1363,6 +1380,8 @@ impl ChainSpec { * Networking Fulu specific */ blob_schedule: BlobSchedule::default(), + min_epochs_for_data_column_sidecars_requests: + default_min_epochs_for_data_column_sidecars_requests(), /* * Application specific @@ -1661,6 +1680,9 @@ pub struct Config { #[serde(default = "default_balance_per_additional_custody_group")] #[serde(with = "serde_utils::quoted_u64")] balance_per_additional_custody_group: u64, + #[serde(default = "default_min_epochs_for_data_column_sidecars_requests")] + #[serde(with = "serde_utils::quoted_u64")] + min_epochs_for_data_column_sidecars_requests: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -1834,6 +1856,10 @@ const fn default_balance_per_additional_custody_group() -> u64 { 32000000000 } +const fn default_min_epochs_for_data_column_sidecars_requests() -> u64 { + 4096 +} + fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize { let max_request_blocks = max_request_blocks as usize; RuntimeVariableList::::from_vec( @@ -2045,6 +2071,8 @@ impl Config { blob_schedule: spec.blob_schedule.clone(), validator_custody_requirement: spec.validator_custody_requirement, balance_per_additional_custody_group: spec.balance_per_additional_custody_group, + min_epochs_for_data_column_sidecars_requests: spec + .min_epochs_for_data_column_sidecars_requests, } } @@ -2126,6 +2154,7 @@ impl Config { ref blob_schedule, validator_custody_requirement, balance_per_additional_custody_group, + min_epochs_for_data_column_sidecars_requests, } = self; if preset_base != E::spec_name().to_string().as_str() { @@ -2212,6 +2241,7 @@ impl Config { blob_schedule: blob_schedule.clone(), validator_custody_requirement, balance_per_additional_custody_group, + min_epochs_for_data_column_sidecars_requests, ..chain_spec.clone() }) @@ -2350,6 +2380,7 @@ mod tests { mod yaml_tests { use super::*; use paste::paste; + use std::sync::Arc; use tempfile::NamedTempFile; #[test] @@ -2649,4 +2680,65 @@ mod yaml_tests { let _ = spec.max_message_size(); let _ = spec.max_compressed_len(); } + + #[test] + fn min_epochs_for_data_sidecar_requests_deneb() { + type E = MainnetEthSpec; + let spec = Arc::new(ForkName::Deneb.make_genesis_spec(E::default_spec())); + let blob_retention_epochs = spec.min_epochs_for_blob_sidecars_requests; + + // `min_epochs_for_data_sidecar_requests` cannot be earlier than Deneb fork epoch. + assert_eq!( + spec.deneb_fork_epoch, + spec.min_epoch_data_availability_boundary(Epoch::new(blob_retention_epochs / 2)) + ); + + let current_epoch = Epoch::new(blob_retention_epochs * 2); + let expected_min_blob_epoch = current_epoch - blob_retention_epochs; + assert_eq!( + Some(expected_min_blob_epoch), + spec.min_epoch_data_availability_boundary(current_epoch) + ); + } + + #[test] + fn min_epochs_for_data_sidecar_requests_fulu() { + type E = MainnetEthSpec; + let spec = { + let mut spec = ForkName::Deneb.make_genesis_spec(E::default_spec()); + // 4096 * 2 = 8192 + spec.fulu_fork_epoch = Some(Epoch::new(spec.min_epochs_for_blob_sidecars_requests * 2)); + // set a different value for testing purpose, 4096 / 2 = 2048 + spec.min_epochs_for_data_column_sidecars_requests = + spec.min_epochs_for_blob_sidecars_requests / 2; + Arc::new(spec) + }; + let blob_retention_epochs = spec.min_epochs_for_blob_sidecars_requests; + let data_column_retention_epochs = spec.min_epochs_for_data_column_sidecars_requests; + + // `min_epochs_for_data_sidecar_requests` at fulu fork epoch still uses `min_epochs_for_blob_sidecars_requests` + let fulu_fork_epoch = spec.fulu_fork_epoch.unwrap(); + let expected_blob_retention_epoch = fulu_fork_epoch - blob_retention_epochs; + assert_eq!( + Some(expected_blob_retention_epoch), + spec.min_epoch_data_availability_boundary(fulu_fork_epoch) + ); + + // `min_epochs_for_data_sidecar_requests` at fulu fork epoch + min_epochs_for_blob_sidecars_request + let blob_retention_epoch_after_fulu = fulu_fork_epoch + blob_retention_epochs; + let expected_blob_retention_epoch = blob_retention_epoch_after_fulu - blob_retention_epochs; + assert_eq!( + Some(expected_blob_retention_epoch), + spec.min_epoch_data_availability_boundary(blob_retention_epoch_after_fulu) + ); + + // After the final blob retention epoch, `min_epochs_for_data_sidecar_requests` should be calculated + // using `min_epochs_for_data_column_sidecars_request` + let current_epoch = blob_retention_epoch_after_fulu + 1; + let expected_data_column_retention_epoch = current_epoch - data_column_retention_epochs; + assert_eq!( + Some(expected_data_column_retention_epoch), + spec.min_epoch_data_availability_boundary(current_epoch) + ); + } } From a459a9af98c9da7dbdf11e36ab2472a11cac4c52 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 2 Jul 2025 14:50:33 +1000 Subject: [PATCH 09/13] Fix and test checkpoint sync from genesis (#7689) Fix a bug involving checkpoint sync from genesis reported by Sunnyside labs. Ensure that the store's `anchor` is initialised prior to storing the genesis state. In the case of checkpoint sync from genesis, the genesis state will be in the _hot DB_, so we need the hot DB metadata to be initialised in order to store it. I've extended the existing checkpoint sync tests to cover this case as well. There are some subtleties around what the `state_upper_limit` should be set to in this case. I've opted to just enable state reconstruction from the start in the test so it gets set to 0, which results in an end state more consistent with the other test cases (full state reconstruction). This is required because we can't meaningfully do any state reconstruction when the split slot is 0 (there is no range of frozen slots to reconstruct). --- beacon_node/beacon_chain/src/builder.rs | 35 +-- beacon_node/beacon_chain/tests/store_tests.rs | 220 +++++++++++------- beacon_node/store/src/reconstruct.rs | 6 + 3 files changed, 158 insertions(+), 103 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index ce4264d550..c46cc015c9 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -514,9 +514,26 @@ where "Storing split from weak subjectivity state" ); - // Set the store's split point *before* storing genesis so that genesis is stored - // immediately in the freezer DB. + // Set the store's split point *before* storing genesis so that if the genesis state + // is prior to the split slot, it will immediately be stored in the freezer DB. store.set_split(weak_subj_slot, weak_subj_state_root, weak_subj_block_root); + + // It is also possible for the checkpoint state to be equal to the genesis state, in which + // case it will be stored in the hot DB. In this case, we need to ensure the store's anchor + // is initialised prior to storing the state, as the anchor is required for working out + // hdiff storage strategies. + let retain_historic_states = self.chain_config.reconstruct_historic_states; + self.pending_io_batch.push( + store + .init_anchor_info( + weak_subj_block.parent_root(), + weak_subj_block.slot(), + weak_subj_slot, + retain_historic_states, + ) + .map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?, + ); + let (_, updated_builder) = self.set_genesis_state(genesis_state)?; self = updated_builder; @@ -541,20 +558,6 @@ where "Stored frozen block roots at skipped slots" ); - // Write the anchor to memory before calling `put_state` otherwise hot hdiff can't store - // states that do not align with the `start_slot` grid. - let retain_historic_states = self.chain_config.reconstruct_historic_states; - self.pending_io_batch.push( - store - .init_anchor_info( - weak_subj_block.parent_root(), - weak_subj_block.slot(), - weak_subj_slot, - retain_historic_states, - ) - .map_err(|e| format!("Failed to initialize anchor info: {:?}", e))?, - ); - // Write the state, block and blobs non-atomically, it doesn't matter if they're forgotten // about on a crash restart. store diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 1be2879e1a..d1a53d9b66 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2281,6 +2281,19 @@ async fn weak_subjectivity_sync_skips_at_genesis() { weak_subjectivity_sync_test(slots, checkpoint_slot).await } +// Checkpoint sync from the genesis state. +// +// This is a regression test for a bug we had involving the storage of the genesis state in the hot +// DB. +#[tokio::test] +async fn weak_subjectivity_sync_from_genesis() { + let start_slot = 1; + let end_slot = E::slots_per_epoch() * 2; + let slots = (start_slot..end_slot).map(Slot::new).collect(); + let checkpoint_slot = Slot::new(0); + weak_subjectivity_sync_test(slots, checkpoint_slot).await +} + async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { // Build an initial chain on one harness, representing a synced node with full history. let num_final_blocks = E::slots_per_epoch() * 2; @@ -2367,7 +2380,15 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { ); slot_clock.set_slot(harness.get_current_slot().as_u64()); + let chain_config = ChainConfig { + // Set reconstruct_historic_states to true from the start in the genesis case. This makes + // some of the later checks more uniform across the genesis/non-genesis cases. + reconstruct_historic_states: checkpoint_slot == 0, + ..ChainConfig::default() + }; + let beacon_chain = BeaconChainBuilder::>::new(MinimalEthSpec, kzg) + .chain_config(chain_config) .store(store.clone()) .custom_spec(test_spec::().into()) .task_executor(harness.chain.task_executor.clone()) @@ -2381,7 +2402,6 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .store_migrator_config(MigratorConfig::default().blocking()) .slot_clock(slot_clock) .shutdown_sender(shutdown_tx) - .chain_config(ChainConfig::default()) .event_handler(Some(ServerSentEventHandler::new_with_capacity(1))) .execution_layer(Some(mock.el)) .rng(Box::new(StdRng::seed_from_u64(42))) @@ -2449,96 +2469,118 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { assert_eq!(state.update_tree_hash_cache().unwrap(), state_root); } - // Forwards iterator from 0 should fail as we lack blocks. - assert!(matches!( - beacon_chain.forwards_iter_block_roots(Slot::new(0)), - Err(BeaconChainError::HistoricalBlockOutOfRange { .. }) - )); - - // Simulate processing of a `StatusMessage` with an older finalized epoch by calling - // `block_root_at_slot` with an old slot for which we don't know the block root. It should - // return `None` rather than erroring. - assert_eq!( - beacon_chain - .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) - .unwrap(), - None - ); - - // Simulate querying the API for a historic state that is unknown. It should also return - // `None` rather than erroring. - assert_eq!(beacon_chain.state_root_at_slot(Slot::new(1)).unwrap(), None); - - // Supply blocks backwards to reach genesis. Omit the genesis block to check genesis handling. - let historical_blocks = chain_dump[..wss_block.slot().as_usize()] - .iter() - .filter(|s| s.beacon_block.slot() != 0) - .map(|s| s.beacon_block.clone()) - .collect::>(); - - let mut available_blocks = vec![]; - for blinded in historical_blocks { - let block_root = blinded.canonical_root(); - let full_block = harness - .chain - .get_block(&block_root) - .await - .expect("should get block") - .expect("should get block"); - - if let MaybeAvailableBlock::Available(block) = harness - .chain - .data_availability_checker - .verify_kzg_for_rpc_block( - harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), - ) - .expect("should verify kzg") - { - available_blocks.push(block); - } + if checkpoint_slot != 0 { + // Forwards iterator from 0 should fail as we lack blocks (unless checkpoint slot is 0). + assert!(matches!( + beacon_chain.forwards_iter_block_roots(Slot::new(0)), + Err(BeaconChainError::HistoricalBlockOutOfRange { .. }) + )); + } else { + assert_eq!( + beacon_chain + .forwards_iter_block_roots(Slot::new(0)) + .unwrap() + .next() + .unwrap() + .unwrap(), + (wss_block_root, Slot::new(0)) + ); } - // Corrupt the signature on the 1st block to ensure that the backfill processor is checking - // signatures correctly. Regression test for https://github.com/sigp/lighthouse/pull/5120. - let mut batch_with_invalid_first_block = - available_blocks.iter().map(clone_block).collect::>(); - batch_with_invalid_first_block[0] = { - let (block_root, block, data) = clone_block(&available_blocks[0]).deconstruct(); - let mut corrupt_block = (*block).clone(); - *corrupt_block.signature_mut() = Signature::empty(); - AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), data, Arc::new(spec)) - }; + // The checks in this block only make sense if some data is missing as a result of the + // checkpoint sync, i.e. if we are not just checkpoint syncing from genesis. + if checkpoint_slot != 0 { + // Simulate processing of a `StatusMessage` with an older finalized epoch by calling + // `block_root_at_slot` with an old slot for which we don't know the block root. It should + // return `None` rather than erroring. + assert_eq!( + beacon_chain + .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) + .unwrap(), + None + ); - // Importing the invalid batch should error. - assert!(matches!( + // Simulate querying the API for a historic state that is unknown. It should also return + // `None` rather than erroring. + assert_eq!(beacon_chain.state_root_at_slot(Slot::new(1)).unwrap(), None); + + // Supply blocks backwards to reach genesis. Omit the genesis block to check genesis handling. + let historical_blocks = chain_dump[..wss_block.slot().as_usize()] + .iter() + .filter(|s| s.beacon_block.slot() != 0) + .map(|s| s.beacon_block.clone()) + .collect::>(); + + let mut available_blocks = vec![]; + for blinded in historical_blocks { + let block_root = blinded.canonical_root(); + let full_block = harness + .chain + .get_block(&block_root) + .await + .expect("should get block") + .expect("should get block"); + + if let MaybeAvailableBlock::Available(block) = harness + .chain + .data_availability_checker + .verify_kzg_for_rpc_block( + harness + .build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), + ) + .expect("should verify kzg") + { + available_blocks.push(block); + } + } + + // Corrupt the signature on the 1st block to ensure that the backfill processor is checking + // signatures correctly. Regression test for https://github.com/sigp/lighthouse/pull/5120. + let mut batch_with_invalid_first_block = + available_blocks.iter().map(clone_block).collect::>(); + batch_with_invalid_first_block[0] = { + let (block_root, block, data) = clone_block(&available_blocks[0]).deconstruct(); + let mut corrupt_block = (*block).clone(); + *corrupt_block.signature_mut() = Signature::empty(); + AvailableBlock::__new_for_testing( + block_root, + Arc::new(corrupt_block), + data, + Arc::new(spec), + ) + }; + + // Importing the invalid batch should error. + assert!(matches!( + beacon_chain + .import_historical_block_batch(batch_with_invalid_first_block) + .unwrap_err(), + HistoricalBlockError::InvalidSignature + )); + + let available_blocks_slots = available_blocks + .iter() + .map(|block| (block.block().slot(), block.block().canonical_root())) + .collect::>(); + info!( + ?available_blocks_slots, + "wss_block_slot" = wss_block.slot().as_usize(), + "Importing historical block batch" + ); + + // Importing the batch with valid signatures should succeed. + let available_blocks_dup = available_blocks.iter().map(clone_block).collect::>(); + assert_eq!(beacon_chain.store.get_oldest_block_slot(), wss_block.slot()); beacon_chain - .import_historical_block_batch(batch_with_invalid_first_block) - .unwrap_err(), - HistoricalBlockError::InvalidSignature - )); + .import_historical_block_batch(available_blocks_dup) + .unwrap(); + assert_eq!(beacon_chain.store.get_oldest_block_slot(), 0); - let available_blocks_slots = available_blocks - .iter() - .map(|block| (block.block().slot(), block.block().canonical_root())) - .collect::>(); - info!( - ?available_blocks_slots, - "wss_block_slot" = wss_block.slot().as_usize(), - "Importing historical block batch" - ); - - // Importing the batch with valid signatures should succeed. - let available_blocks_dup = available_blocks.iter().map(clone_block).collect::>(); - assert_eq!(beacon_chain.store.get_oldest_block_slot(), wss_block.slot()); - beacon_chain - .import_historical_block_batch(available_blocks_dup) - .unwrap(); - assert_eq!(beacon_chain.store.get_oldest_block_slot(), 0); - - // Resupplying the blocks should not fail, they can be safely ignored. - beacon_chain - .import_historical_block_batch(available_blocks) - .unwrap(); + // Resupplying the blocks should not fail, they can be safely ignored. + beacon_chain + .import_historical_block_batch(available_blocks) + .unwrap(); + } // Sanity check for non-aligned WSS starts, to make sure the WSS block is persisted properly if wss_block_slot != wss_state_slot { @@ -2615,7 +2657,11 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { assert_eq!(store.get_anchor_info().anchor_slot, wss_aligned_slot); assert_eq!( store.get_anchor_info().state_upper_limit, - Slot::new(u64::MAX) + if checkpoint_slot == 0 { + Slot::new(0) + } else { + Slot::new(u64::MAX) + } ); info!(anchor = ?store.get_anchor_info(), "anchor pre"); diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index ade111983b..4bd8f12ead 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -47,6 +47,12 @@ where let lower_limit_slot = anchor.state_lower_limit; let upper_limit_slot = std::cmp::min(split.slot, anchor.state_upper_limit); + // If the split is at 0 we can't reconstruct historic states. + if split.slot == 0 { + debug!("No state reconstruction possible"); + return Ok(()); + } + // If `num_blocks` is not specified iterate all blocks. Add 1 so that we end on an epoch // boundary when `num_blocks` is a multiple of an epoch boundary. We want to be *inclusive* // of the state at slot `lower_limit_slot + num_blocks`. From b35854b71f04070884856403319c5bf1552f179d Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 2 Jul 2025 18:47:35 +1000 Subject: [PATCH 10/13] Record v2 beacon blocks http api metrics separately (#7692) This PR adds v2 beacon block paths to the function that records http api usage, so they don't just get recorded as "/v2/beacon" like below: image --- beacon_node/http_api/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index c757ca035b..e9b2e8e6bf 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -211,9 +211,11 @@ pub fn prometheus_metrics() -> warp::filters::log::Log Date: Thu, 3 Jul 2025 09:40:04 +1000 Subject: [PATCH 11/13] Fix lookups of the block at `oldest_block_slot` (#7693) Closes: - https://github.com/sigp/lighthouse/issues/7690 Another checkpoint sync related fix! See issue for a description of the bug. We fix it by just loading the block root of the `oldest_block_slot`, rather than trying to load the slot prior, which will always fail. --- beacon_node/beacon_chain/src/beacon_chain.rs | 8 ++ beacon_node/beacon_chain/tests/store_tests.rs | 93 +++++++++++++------ 2 files changed, 74 insertions(+), 27 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 65318835cc..9900535b2c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -991,6 +991,14 @@ impl BeaconChain { return Ok(root_opt); } + // Do not try to access the previous slot if it's older than the oldest block root + // stored in the database. Instead, load just the block root at `oldest_block_slot`, + // under the assumption that the `oldest_block_slot` *is not* a skipped slot (should be + // true because it is set by the oldest *block*). + if request_slot == self.store.get_anchor_info().oldest_block_slot { + return self.block_root_at_slot_skips_prev(request_slot); + } + if let Some(((prev_root, _), (curr_root, curr_slot))) = process_results( self.forwards_iter_block_roots_until(prev_slot, request_slot)?, |iter| iter.tuple_windows().next(), diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index d1a53d9b66..e9b19ee6e0 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2238,7 +2238,15 @@ async fn weak_subjectivity_sync_easy() { let num_initial_slots = E::slots_per_epoch() * 11; let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); let slots = (1..num_initial_slots).map(Slot::new).collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None).await +} + +#[tokio::test] +async fn weak_subjectivity_sync_single_block_batches() { + let num_initial_slots = E::slots_per_epoch() * 11; + let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); + let slots = (1..num_initial_slots).map(Slot::new).collect(); + weak_subjectivity_sync_test(slots, checkpoint_slot, Some(1)).await } #[tokio::test] @@ -2252,7 +2260,7 @@ async fn weak_subjectivity_sync_unaligned_advanced_checkpoint() { slot <= checkpoint_slot - 3 || slot > checkpoint_slot }) .collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None).await } #[tokio::test] @@ -2266,7 +2274,7 @@ async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() { slot <= checkpoint_slot || slot > checkpoint_slot + 3 }) .collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None).await } // Regression test for https://github.com/sigp/lighthouse/issues/4817 @@ -2278,7 +2286,7 @@ async fn weak_subjectivity_sync_skips_at_genesis() { let end_slot = E::slots_per_epoch() * 4; let slots = (start_slot..end_slot).map(Slot::new).collect(); let checkpoint_slot = Slot::new(E::slots_per_epoch() * 2); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None).await } // Checkpoint sync from the genesis state. @@ -2291,10 +2299,14 @@ async fn weak_subjectivity_sync_from_genesis() { let end_slot = E::slots_per_epoch() * 2; let slots = (start_slot..end_slot).map(Slot::new).collect(); let checkpoint_slot = Slot::new(0); - weak_subjectivity_sync_test(slots, checkpoint_slot).await + weak_subjectivity_sync_test(slots, checkpoint_slot, None).await } -async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { +async fn weak_subjectivity_sync_test( + slots: Vec, + checkpoint_slot: Slot, + backfill_batch_size: Option, +) { // Build an initial chain on one harness, representing a synced node with full history. let num_final_blocks = E::slots_per_epoch() * 2; @@ -2557,30 +2569,57 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .unwrap_err(), HistoricalBlockError::InvalidSignature )); - - let available_blocks_slots = available_blocks - .iter() - .map(|block| (block.block().slot(), block.block().canonical_root())) - .collect::>(); - info!( - ?available_blocks_slots, - "wss_block_slot" = wss_block.slot().as_usize(), - "Importing historical block batch" - ); - - // Importing the batch with valid signatures should succeed. - let available_blocks_dup = available_blocks.iter().map(clone_block).collect::>(); assert_eq!(beacon_chain.store.get_oldest_block_slot(), wss_block.slot()); - beacon_chain - .import_historical_block_batch(available_blocks_dup) - .unwrap(); - assert_eq!(beacon_chain.store.get_oldest_block_slot(), 0); - // Resupplying the blocks should not fail, they can be safely ignored. - beacon_chain - .import_historical_block_batch(available_blocks) - .unwrap(); + let batch_size = backfill_batch_size.unwrap_or(available_blocks.len()); + + for batch in available_blocks.rchunks(batch_size) { + let available_blocks_slots = batch + .iter() + .map(|block| (block.block().slot(), block.block().canonical_root())) + .collect::>(); + info!( + ?available_blocks_slots, + "wss_block_slot" = wss_block.slot().as_usize(), + "Importing historical block batch" + ); + + // Importing the batch with valid signatures should succeed. + let available_blocks_batch1 = batch.iter().map(clone_block).collect::>(); + beacon_chain + .import_historical_block_batch(available_blocks_batch1) + .unwrap(); + + // We should be able to load the block root at the `oldest_block_slot`. + // + // This is a regression test for: https://github.com/sigp/lighthouse/issues/7690 + let oldest_block_imported = &batch[0]; + let (oldest_block_slot, oldest_block_root) = + if oldest_block_imported.block().parent_root() == beacon_chain.genesis_block_root { + (Slot::new(0), beacon_chain.genesis_block_root) + } else { + available_blocks_slots[0] + }; + assert_eq!( + beacon_chain.store.get_oldest_block_slot(), + oldest_block_slot + ); + assert_eq!( + beacon_chain + .block_root_at_slot(oldest_block_slot, WhenSlotSkipped::None) + .unwrap() + .unwrap(), + oldest_block_root + ); + + // Resupplying the blocks should not fail, they can be safely ignored. + let available_blocks_batch2 = batch.iter().map(clone_block).collect::>(); + beacon_chain + .import_historical_block_batch(available_blocks_batch2) + .unwrap(); + } } + assert_eq!(beacon_chain.store.get_oldest_block_slot(), 0); // Sanity check for non-aligned WSS starts, to make sure the WSS block is persisted properly if wss_block_slot != wss_state_slot { From 0f895f3066a39b6ba4f05053037d38f141271655 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 4 Jul 2025 15:54:30 -0700 Subject: [PATCH 12/13] Bump default gas limit (#7695) N/A Bump the default gas limit to 45 million based on recommendation from EL teams https://x.com/vdWijden/status/1939234101631856969 and pandas https://ethpandaops.io/posts/gaslimit-scaling/ --- .../src/test_utils/execution_block_generator.rs | 2 +- beacon_node/execution_layer/src/test_utils/mock_builder.rs | 2 +- beacon_node/http_api/tests/tests.rs | 4 ++-- book/src/help_vc.md | 2 +- lighthouse/tests/validator_client.rs | 2 +- validator_client/lighthouse_validator_store/src/lib.rs | 4 ++-- validator_client/src/cli.rs | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index e01b8de9e3..aefb6d6750 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -29,7 +29,7 @@ use super::DEFAULT_TERMINAL_BLOCK; const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz"); const TEST_BLOB_BUNDLE_V2: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle_v2.ssz"); -pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +pub const DEFAULT_GAS_LIMIT: u64 = 45_000_000; const GAS_USED: u64 = DEFAULT_GAS_LIMIT - 1; #[derive(Clone, Debug, PartialEq)] diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 3704bcc592..751e99494c 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -40,7 +40,7 @@ use warp::reply::{self, Reply}; use warp::{Filter, Rejection}; pub const DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); -pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +pub const DEFAULT_GAS_LIMIT: u64 = 45_000_000; pub const DEFAULT_BUILDER_PRIVATE_KEY: &str = "607a11b45a7219cc61a3d9c5fd08c7eebd602a6a19a977f8d3771d5711a550f2"; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 955b44c36c..ecd20f3f79 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -4669,7 +4669,7 @@ impl ApiTester { self.mock_builder .as_ref() .unwrap() - .add_operation(Operation::GasLimit(30_000_000)); + .add_operation(Operation::GasLimit(DEFAULT_GAS_LIMIT as usize)); let slot = self.chain.slot().unwrap(); let epoch = self.chain.epoch().unwrap(); @@ -4692,7 +4692,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 30_000_000); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); self } diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 15b5c209a7..0bc4bbf53d 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -40,7 +40,7 @@ Options: The gas limit to be used in all builder proposals for all validators managed by this validator client. Note this will not necessarily be used if the gas limit set here moves too far from the previous block's - gas limit. [default: 36000000] + gas limit. [default: 45000000] --genesis-state-url A URL of a beacon-API compatible server from which to download the genesis state. Checkpoint sync server URLs can generally be used with diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index f99fc3c460..7bda1868c8 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -505,7 +505,7 @@ fn no_doppelganger_protection_flag() { fn no_gas_limit_flag() { CommandLineTest::new() .run() - .with_config(|config| assert!(config.validator_store.gas_limit == Some(36_000_000))); + .with_config(|config| assert!(config.validator_store.gas_limit == Some(45_000_000))); } #[test] fn gas_limit_flag() { diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 2cb6ba435e..67af1d73fe 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -55,8 +55,8 @@ const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512; /// Currently used as the default gas limit in execution clients. /// -/// https://ethresear.ch/t/on-increasing-the-block-gas-limit-technical-considerations-path-forward/21225. -pub const DEFAULT_GAS_LIMIT: u64 = 36_000_000; +/// https://ethpandaops.io/posts/gaslimit-scaling/. +pub const DEFAULT_GAS_LIMIT: u64 = 45_000_000; pub struct LighthouseValidatorStore { validators: Arc>, diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index cdbf9f8472..e1cce5c9da 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -388,7 +388,7 @@ pub struct ValidatorClient { #[clap( long, value_name = "INTEGER", - default_value_t = 36_000_000, + default_value_t = 45_000_000, requires = "builder_proposals", help = "The gas limit to be used in all builder proposals for all validators managed \ by this validator client. Note this will not necessarily be used if the gas limit \ From 01ec2ec7ad871e2c83ab96a2266701e069e44959 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 7 Jul 2025 14:42:34 +1000 Subject: [PATCH 13/13] Update LH book for v7.1.0 (#7706) Update the book for upcoming v7.1.0 release. This is targeted at `unstable` rather than `release-v7.1.0` because the book is built from `unstable`. --- book/src/advanced_builders.md | 4 ++-- book/src/advanced_database_migrations.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/book/src/advanced_builders.md b/book/src/advanced_builders.md index d9468898b4..de7d02d956 100644 --- a/book/src/advanced_builders.md +++ b/book/src/advanced_builders.md @@ -60,7 +60,7 @@ relays, run one of the following services and configure lighthouse to use it wit ## Validator Client Configuration In the validator client you can configure gas limit and fee recipient on a per-validator basis. If no gas limit is -configured, Lighthouse will use a default gas limit of 30,000,000, which is the current default value used in execution +configured, Lighthouse will use a default gas limit of 45,000,000, which is the current default value used in execution engines. You can also enable or disable use of external builders on a per-validator basis rather than using `--builder-proposals`, `--builder-boost-factor` or `--prefer-builder-proposals`, which apply builder related preferences for all validators. In order to manage these configurations per-validator, you can either make updates to the `validator_definitions.yml` file @@ -75,7 +75,7 @@ transaction within the block to the fee recipient, so a discrepancy in fee recip is something afoot. > Note: The gas limit configured here is effectively a vote on block size, so the configuration should not be taken lightly. -> 30,000,000 is currently seen as a value balancing block size with how expensive it is for +> 45,000,000 is currently seen as a value balancing block size with how expensive it is for > the network to validate blocks. So if you don't feel comfortable making an informed "vote", using the default value is > encouraged. We will update the default value if the community reaches a rough consensus on a new value. diff --git a/book/src/advanced_database_migrations.md b/book/src/advanced_database_migrations.md index f92ae7846b..e29397619c 100644 --- a/book/src/advanced_database_migrations.md +++ b/book/src/advanced_database_migrations.md @@ -17,7 +17,7 @@ validator client or the slasher**. | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|----------------------| -| v7.1.0 | TBD 2025 | v23 | yes | +| v7.1.0 | Jul 2025 | v26 | yes | | v7.0.0 | Apr 2025 | v22 | no | | v6.0.0 | Nov 2024 | v22 | no | @@ -207,7 +207,7 @@ Here are the steps to prune historic states: | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|-------------------------------------| -| v7.1.0 | TBD 2025 | v23 | yes | +| v7.1.0 | Jul 2025 | v26 | yes | | v7.0.0 | Apr 2025 | v22 | no | | v6.0.0 | Nov 2024 | v22 | no | | v5.3.0 | Aug 2024 | v21 | yes before Electra using <= v7.0.0 |