Skip to content

Commit b6493d5

Browse files
Enforce Optimistic Sync Conditions & CLI Tests (v2) (#3050)
## Description This PR adds a single, trivial commit (f5d2b27) atop #2986 to resolve a tests compile error. The original author (@ethDreamer) is AFK so I'm getting this one merged ☺️ Please see #2986 for more information about the other, significant changes in this PR. Co-authored-by: Mark Mackey <[email protected]> Co-authored-by: ethDreamer <[email protected]>
1 parent a1b730c commit b6493d5

File tree

16 files changed

+348
-49
lines changed

16 files changed

+348
-49
lines changed

Cargo.lock

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

beacon_node/beacon_chain/src/block_verification.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,8 @@ pub enum ExecutionPayloadError {
312312
///
313313
/// The peer is not necessarily invalid.
314314
PoWParentMissing(ExecutionBlockHash),
315+
/// The execution node is syncing but we fail the conditions for optimistic sync
316+
UnverifiedNonOptimisticCandidate,
315317
}
316318

317319
impl From<execution_layer::Error> for ExecutionPayloadError {
@@ -1128,6 +1130,29 @@ impl<'a, T: BeaconChainTypes> FullyVerifiedBlock<'a, T> {
11281130
// `randao` may change.
11291131
let payload_verification_status = notify_new_payload(chain, &state, block.message())?;
11301132

1133+
// If the payload did not validate or invalidate the block, check to see if this block is
1134+
// valid for optimistic import.
1135+
if payload_verification_status == PayloadVerificationStatus::NotVerified {
1136+
let current_slot = chain
1137+
.slot_clock
1138+
.now()
1139+
.ok_or(BeaconChainError::UnableToReadSlot)?;
1140+
1141+
if !chain
1142+
.fork_choice
1143+
.read()
1144+
.is_optimistic_candidate_block(
1145+
current_slot,
1146+
block.slot(),
1147+
&block.parent_root(),
1148+
&chain.spec,
1149+
)
1150+
.map_err(BeaconChainError::from)?
1151+
{
1152+
return Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into());
1153+
}
1154+
}
1155+
11311156
// If the block is sufficiently recent, notify the validator monitor.
11321157
if let Some(slot) = chain.slot_clock.now() {
11331158
let epoch = slot.epoch(T::EthSpec::slots_per_epoch());

beacon_node/beacon_chain/src/execution_payload.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,25 @@ pub fn validate_merge_block<T: BeaconChainTypes>(
141141
}
142142
.into()),
143143
None => {
144-
debug!(
145-
chain.log,
146-
"Optimistically accepting terminal block";
147-
"block_hash" => ?execution_payload.parent_hash,
148-
"msg" => "the terminal block/parent was unavailable"
149-
);
150-
Ok(())
144+
let current_slot = chain
145+
.slot_clock
146+
.now()
147+
.ok_or(BeaconChainError::UnableToReadSlot)?;
148+
// Check the optimistic sync conditions. Note that because this is the merge block,
149+
// the justified checkpoint can't have execution enabled so we only need to check the
150+
// current slot is at least SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY ahead of the block
151+
// https://github.com/ethereum/consensus-specs/blob/v1.1.9/sync/optimistic.md#when-to-optimistically-import-blocks
152+
if block.slot() + chain.spec.safe_slots_to_import_optimistically <= current_slot {
153+
debug!(
154+
chain.log,
155+
"Optimistically accepting terminal block";
156+
"block_hash" => ?execution_payload.parent_hash,
157+
"msg" => "the terminal block/parent was unavailable"
158+
);
159+
Ok(())
160+
} else {
161+
Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into())
162+
}
151163
}
152164
}
153165
}

beacon_node/network/src/beacon_processor/worker/gossip_methods.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ impl<T: BeaconChainTypes> Worker<T> {
772772
}
773773
// TODO(merge): reconsider peer scoring for this event.
774774
Err(e @BlockError::ExecutionPayloadError(ExecutionPayloadError::RequestFailed(_)))
775+
| Err(e @ BlockError::ExecutionPayloadError(ExecutionPayloadError::UnverifiedNonOptimisticCandidate))
775776
| Err(e @BlockError::ExecutionPayloadError(ExecutionPayloadError::NoExecutionConnection)) => {
776777
debug!(self.log, "Could not verify block for gossip, ignoring the block";
777778
"error" => %e);

boot_node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ hex = "0.4.2"
2323
serde = "1.0.116"
2424
serde_derive = "1.0.116"
2525
serde_json = "1.0.66"
26+
serde_yaml = "0.8.13"
2627
eth2_network_config = { path = "../common/eth2_network_config" }

boot_node/src/lib.rs

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ use clap::ArgMatches;
33
use slog::{o, Drain, Level, Logger};
44

55
use eth2_network_config::Eth2NetworkConfig;
6-
use std::fs::File;
7-
use std::path::PathBuf;
86
mod cli;
97
pub mod config;
108
mod server;
@@ -86,15 +84,13 @@ fn main<T: EthSpec>(
8684
// parse the CLI args into a useable config
8785
let config: BootNodeConfig<T> = BootNodeConfig::new(bn_matches, eth2_network_config)?;
8886

89-
// Dump config if `dump-config` flag is set
90-
let dump_config = clap_utils::parse_optional::<PathBuf>(lh_matches, "dump-config")?;
91-
if let Some(dump_path) = dump_config {
92-
let config_sz = BootNodeConfigSerialization::from_config_ref(&config);
93-
let mut file = File::create(dump_path)
94-
.map_err(|e| format!("Failed to create dumped config: {:?}", e))?;
95-
serde_json::to_writer(&mut file, &config_sz)
96-
.map_err(|e| format!("Error serializing config: {:?}", e))?;
97-
}
87+
// Dump configs if `dump-config` or `dump-chain-config` flags are set
88+
let config_sz = BootNodeConfigSerialization::from_config_ref(&config);
89+
clap_utils::check_dump_configs::<_, T>(
90+
lh_matches,
91+
&config_sz,
92+
&eth2_network_config.chain_spec::<T>()?,
93+
)?;
9894

9995
// Run the boot node
10096
if !lh_matches.is_present("immediate-shutdown") {

common/clap_utils/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ dirs = "3.0.1"
1313
eth2_network_config = { path = "../eth2_network_config" }
1414
eth2_ssz = "0.4.1"
1515
ethereum-types = "0.12.1"
16+
serde = "1.0.116"
17+
serde_json = "1.0.59"
18+
serde_yaml = "0.8.13"
19+
types = { path = "../../consensus/types"}

common/clap_utils/src/lib.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use ethereum_types::U256 as Uint256;
66
use ssz::Decode;
77
use std::path::PathBuf;
88
use std::str::FromStr;
9+
use types::{ChainSpec, Config, EthSpec};
910

1011
pub mod flags;
1112

@@ -52,6 +53,12 @@ pub fn get_eth2_network_config(cli_args: &ArgMatches) -> Result<Eth2NetworkConfi
5253
.terminal_block_hash_activation_epoch = epoch;
5354
}
5455

56+
if let Some(slots) = parse_optional(cli_args, "safe-slots-to-import-optimistically")? {
57+
eth2_network_config
58+
.config
59+
.safe_slots_to_import_optimistically = slots;
60+
}
61+
5562
Ok(eth2_network_config)
5663
}
5764

@@ -157,3 +164,29 @@ pub fn parse_ssz_optional<T: Decode>(
157164
})
158165
.transpose()
159166
}
167+
168+
/// Writes configs to file if `dump-config` or `dump-chain-config` flags are set
169+
pub fn check_dump_configs<S, E>(
170+
matches: &ArgMatches,
171+
config: S,
172+
spec: &ChainSpec,
173+
) -> Result<(), String>
174+
where
175+
S: serde::Serialize,
176+
E: EthSpec,
177+
{
178+
if let Some(dump_path) = parse_optional::<PathBuf>(matches, "dump-config")? {
179+
let mut file = std::fs::File::create(dump_path)
180+
.map_err(|e| format!("Failed to open file for writing config: {:?}", e))?;
181+
serde_json::to_writer(&mut file, &config)
182+
.map_err(|e| format!("Error serializing config: {:?}", e))?;
183+
}
184+
if let Some(dump_path) = parse_optional::<PathBuf>(matches, "dump-chain-config")? {
185+
let chain_config = Config::from_chain_spec::<E>(spec);
186+
let mut file = std::fs::File::create(dump_path)
187+
.map_err(|e| format!("Failed to open file for writing chain config: {:?}", e))?;
188+
serde_yaml::to_writer(&mut file, &chain_config)
189+
.map_err(|e| format!("Error serializing config: {:?}", e))?;
190+
}
191+
Ok(())
192+
}

common/sensitive_url/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub enum SensitiveError {
1010
}
1111

1212
// Wrapper around Url which provides a custom `Display` implementation to protect user secrets.
13-
#[derive(Clone)]
13+
#[derive(Clone, PartialEq)]
1414
pub struct SensitiveUrl {
1515
pub full: Url,
1616
pub redacted: String,

consensus/fork_choice/src/fork_choice.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,54 @@ where
928928
.is_descendant(self.fc_store.finalized_checkpoint().root, block_root)
929929
}
930930

931+
/// Returns `Ok(false)` if a block is not viable to be imported optimistically.
932+
///
933+
/// ## Notes
934+
///
935+
/// Equivalent to the function with the same name in the optimistic sync specs:
936+
///
937+
/// https://github.com/ethereum/consensus-specs/blob/dev/sync/optimistic.md#helpers
938+
pub fn is_optimistic_candidate_block(
939+
&self,
940+
current_slot: Slot,
941+
block_slot: Slot,
942+
block_parent_root: &Hash256,
943+
spec: &ChainSpec,
944+
) -> Result<bool, Error<T::Error>> {
945+
// If the block is sufficiently old, import it.
946+
if block_slot + spec.safe_slots_to_import_optimistically <= current_slot {
947+
return Ok(true);
948+
}
949+
950+
// If the justified block has execution enabled, then optimistically import any block.
951+
if self
952+
.get_justified_block()?
953+
.execution_status
954+
.is_execution_enabled()
955+
{
956+
return Ok(true);
957+
}
958+
959+
// If the block has an ancestor with a verified parent, import this block.
960+
//
961+
// TODO: This condition is not yet merged into the spec. See:
962+
//
963+
// https://github.com/ethereum/consensus-specs/pull/2841
964+
//
965+
// ## Note
966+
//
967+
// If `block_parent_root` is unknown this iter will always return `None`.
968+
if self
969+
.proto_array
970+
.iter_nodes(block_parent_root)
971+
.any(|node| node.execution_status.is_valid())
972+
{
973+
return Ok(true);
974+
}
975+
976+
Ok(false)
977+
}
978+
931979
/// Return the current finalized checkpoint.
932980
pub fn finalized_checkpoint(&self) -> Checkpoint {
933981
*self.fc_store.finalized_checkpoint()

consensus/proto_array/src/proto_array_fork_choice.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::error::Error;
2-
use crate::proto_array::{ProposerBoost, ProtoArray};
2+
use crate::proto_array::{Iter, ProposerBoost, ProtoArray};
33
use crate::ssz_container::SszContainer;
44
use serde_derive::{Deserialize, Serialize};
55
use ssz::{Decode, Encode};
@@ -40,6 +40,10 @@ pub enum ExecutionStatus {
4040
}
4141

4242
impl ExecutionStatus {
43+
pub fn is_execution_enabled(&self) -> bool {
44+
!matches!(self, ExecutionStatus::Irrelevant(_))
45+
}
46+
4347
pub fn irrelevant() -> Self {
4448
ExecutionStatus::Irrelevant(false)
4549
}
@@ -341,6 +345,11 @@ impl ProtoArrayForkChoice {
341345
}
342346
}
343347

348+
/// See `ProtoArray::iter_nodes`
349+
pub fn iter_nodes<'a>(&'a self, block_root: &Hash256) -> Iter<'a> {
350+
self.proto_array.iter_nodes(block_root)
351+
}
352+
344353
pub fn as_bytes(&self) -> Vec<u8> {
345354
SszContainer::from(self).as_ssz_bytes()
346355
}

consensus/types/src/chain_spec.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ pub struct ChainSpec {
146146
pub terminal_total_difficulty: Uint256,
147147
pub terminal_block_hash: ExecutionBlockHash,
148148
pub terminal_block_hash_activation_epoch: Epoch,
149+
pub safe_slots_to_import_optimistically: u64,
149150

150151
/*
151152
* Networking
@@ -551,6 +552,7 @@ impl ChainSpec {
551552
.expect("addition does not overflow"),
552553
terminal_block_hash: ExecutionBlockHash::zero(),
553554
terminal_block_hash_activation_epoch: Epoch::new(u64::MAX),
555+
safe_slots_to_import_optimistically: 128u64,
554556

555557
/*
556558
* Network specific
@@ -748,6 +750,7 @@ impl ChainSpec {
748750
.expect("addition does not overflow"),
749751
terminal_block_hash: ExecutionBlockHash::zero(),
750752
terminal_block_hash_activation_epoch: Epoch::new(u64::MAX),
753+
safe_slots_to_import_optimistically: 128u64,
751754

752755
/*
753756
* Network specific
@@ -791,6 +794,9 @@ pub struct Config {
791794
// TODO(merge): remove this default
792795
#[serde(default = "default_terminal_block_hash_activation_epoch")]
793796
pub terminal_block_hash_activation_epoch: Epoch,
797+
// TODO(merge): remove this default
798+
#[serde(default = "default_safe_slots_to_import_optimistically")]
799+
pub safe_slots_to_import_optimistically: u64,
794800

795801
#[serde(with = "eth2_serde_utils::quoted_u64")]
796802
min_genesis_active_validator_count: u64,
@@ -878,6 +884,10 @@ fn default_terminal_block_hash_activation_epoch() -> Epoch {
878884
Epoch::new(u64::MAX)
879885
}
880886

887+
fn default_safe_slots_to_import_optimistically() -> u64 {
888+
128u64
889+
}
890+
881891
impl Default for Config {
882892
fn default() -> Self {
883893
let chain_spec = MainnetEthSpec::default_spec();
@@ -935,6 +945,7 @@ impl Config {
935945
terminal_total_difficulty: spec.terminal_total_difficulty,
936946
terminal_block_hash: spec.terminal_block_hash,
937947
terminal_block_hash_activation_epoch: spec.terminal_block_hash_activation_epoch,
948+
safe_slots_to_import_optimistically: spec.safe_slots_to_import_optimistically,
938949

939950
min_genesis_active_validator_count: spec.min_genesis_active_validator_count,
940951
min_genesis_time: spec.min_genesis_time,
@@ -985,6 +996,7 @@ impl Config {
985996
terminal_total_difficulty,
986997
terminal_block_hash,
987998
terminal_block_hash_activation_epoch,
999+
safe_slots_to_import_optimistically,
9881000
min_genesis_active_validator_count,
9891001
min_genesis_time,
9901002
genesis_fork_version,
@@ -1040,6 +1052,7 @@ impl Config {
10401052
terminal_total_difficulty,
10411053
terminal_block_hash,
10421054
terminal_block_hash_activation_epoch,
1055+
safe_slots_to_import_optimistically,
10431056
..chain_spec.clone()
10441057
})
10451058
}
@@ -1227,6 +1240,7 @@ mod yaml_tests {
12271240
#TERMINAL_TOTAL_DIFFICULTY: 115792089237316195423570985008687907853269984665640564039457584007913129638911
12281241
#TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000001
12291242
#TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551614
1243+
#SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY: 2
12301244
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384
12311245
MIN_GENESIS_TIME: 1606824000
12321246
GENESIS_FORK_VERSION: 0x00000000
@@ -1266,6 +1280,10 @@ mod yaml_tests {
12661280
chain_spec.terminal_block_hash_activation_epoch,
12671281
default_terminal_block_hash_activation_epoch()
12681282
);
1283+
assert_eq!(
1284+
chain_spec.safe_slots_to_import_optimistically,
1285+
default_safe_slots_to_import_optimistically()
1286+
);
12691287

12701288
assert_eq!(
12711289
chain_spec.bellatrix_fork_epoch,

0 commit comments

Comments
 (0)