Merge remote-tracking branch 'origin/unstable' into tree-states

This commit is contained in:
Michael Sproul
2022-09-14 13:51:23 +10:00
404 changed files with 28947 additions and 12000 deletions

View File

@@ -8,10 +8,13 @@ edition = "2021"
[dependencies]
types = { path = "../types" }
state_processing = { path = "../state_processing" }
proto_array = { path = "../proto_array" }
eth2_ssz = "0.4.1"
eth2_ssz_derive = "0.3.0"
slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] }
[dev-dependencies]
beacon_chain = { path = "../../beacon_node/beacon_chain" }
store = { path = "../../beacon_node/store" }
tokio = { version = "1.14.0", features = ["rt-multi-thread"] }

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
use types::{BeaconBlock, BeaconState, Checkpoint, EthSpec, ExecPayload, Hash256, Slot};
use std::collections::BTreeSet;
use std::fmt::Debug;
use types::{BeaconBlockRef, BeaconState, Checkpoint, EthSpec, ExecPayload, Hash256, Slot};
/// Approximates the `Store` in "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice":
///
@@ -17,7 +19,7 @@ use types::{BeaconBlock, BeaconState, Checkpoint, EthSpec, ExecPayload, Hash256,
/// concrete struct is to allow this crate to be free from "impure" on-disk database logic,
/// hopefully making auditing easier.
pub trait ForkChoiceStore<T: EthSpec>: Sized {
type Error;
type Error: Debug;
/// Returns the last value passed to `Self::set_current_slot`.
fn get_current_slot(&self) -> Slot;
@@ -33,7 +35,7 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
/// choice. Allows the implementer to performing caching or other housekeeping duties.
fn on_verified_block<Payload: ExecPayload<T>>(
&mut self,
block: &BeaconBlock<T, Payload>,
block: BeaconBlockRef<T, Payload>,
block_root: Hash256,
state: &BeaconState<T>,
) -> Result<(), Self::Error>;
@@ -50,6 +52,12 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
/// Returns the `finalized_checkpoint`.
fn finalized_checkpoint(&self) -> &Checkpoint;
/// Returns the `unrealized_justified_checkpoint`.
fn unrealized_justified_checkpoint(&self) -> &Checkpoint;
/// Returns the `unrealized_finalized_checkpoint`.
fn unrealized_finalized_checkpoint(&self) -> &Checkpoint;
/// Returns the `proposer_boost_root`.
fn proposer_boost_root(&self) -> Hash256;
@@ -62,6 +70,18 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
/// Sets the `best_justified_checkpoint`.
fn set_best_justified_checkpoint(&mut self, checkpoint: Checkpoint);
/// Sets the `unrealized_justified_checkpoint`.
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint);
/// Sets the `unrealized_finalized_checkpoint`.
fn set_unrealized_finalized_checkpoint(&mut self, checkpoint: Checkpoint);
/// Sets the proposer boost root.
fn set_proposer_boost_root(&mut self, proposer_boost_root: Hash256);
/// Gets the equivocating indices.
fn equivocating_indices(&self) -> &BTreeSet<u64>;
/// Adds to the set of equivocating indices.
fn extend_equivocating_indices(&mut self, indices: impl IntoIterator<Item = u64>);
}

View File

@@ -2,8 +2,11 @@ mod fork_choice;
mod fork_choice_store;
pub use crate::fork_choice::{
AttestationFromBlock, Error, ForkChoice, InvalidAttestation, InvalidBlock,
PayloadVerificationStatus, PersistedForkChoice, QueuedAttestation,
AttestationFromBlock, CountUnrealized, Error, ForkChoice, ForkChoiceView,
ForkchoiceUpdateParameters, InvalidAttestation, InvalidBlock, PayloadVerificationStatus,
PersistedForkChoice, QueuedAttestation, ResetPayloadStatuses,
};
pub use fork_choice_store::ForkChoiceStore;
pub use proto_array::{Block as ProtoBlock, ExecutionStatus, InvalidationOperation};
pub use proto_array::{
Block as ProtoBlock, CountUnrealizedFull, ExecutionStatus, InvalidationOperation,
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256};
use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot};
#[derive(Clone, PartialEq, Debug)]
pub enum Error {
@@ -52,6 +52,7 @@ pub enum Error {
#[derive(Clone, PartialEq, Debug)]
pub struct InvalidBestNodeInfo {
pub current_slot: Slot,
pub start_root: Hash256,
pub justified_checkpoint: Checkpoint,
pub finalized_checkpoint: Checkpoint,

View File

@@ -3,9 +3,11 @@ mod ffg_updates;
mod no_votes;
mod votes;
use crate::proto_array::CountUnrealizedFull;
use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice};
use crate::InvalidationOperation;
use serde_derive::{Deserialize, Serialize};
use std::collections::BTreeSet;
use types::{
AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
MainnetEthSpec, Slot,
@@ -78,7 +80,7 @@ impl ForkChoiceTestDefinition {
let junk_shuffling_id =
AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero());
let mut fork_choice = ProtoArrayForkChoice::new(
let mut fork_choice = ProtoArrayForkChoice::new::<MainnetEthSpec>(
self.finalized_block_slot,
Hash256::zero(),
self.justified_checkpoint,
@@ -86,8 +88,10 @@ impl ForkChoiceTestDefinition {
junk_shuffling_id.clone(),
junk_shuffling_id,
ExecutionStatus::Optimistic(ExecutionBlockHash::zero()),
CountUnrealizedFull::default(),
)
.expect("should create fork choice struct");
let equivocating_indices = BTreeSet::new();
for (op_index, op) in self.operations.into_iter().enumerate() {
match op.clone() {
@@ -103,9 +107,10 @@ impl ForkChoiceTestDefinition {
finalized_checkpoint,
&justified_state_balances,
Hash256::zero(),
&equivocating_indices,
Slot::new(0),
&spec,
)
.map_err(|e| e)
.unwrap_or_else(|e| {
panic!("find_head op at index {} returned error {}", op_index, e)
});
@@ -130,9 +135,10 @@ impl ForkChoiceTestDefinition {
finalized_checkpoint,
&justified_state_balances,
proposer_boost_root,
&equivocating_indices,
Slot::new(0),
&spec,
)
.map_err(|e| e)
.unwrap_or_else(|e| {
panic!("find_head op at index {} returned error {}", op_index, e)
});
@@ -154,6 +160,8 @@ impl ForkChoiceTestDefinition {
finalized_checkpoint,
&justified_state_balances,
Hash256::zero(),
&equivocating_indices,
Slot::new(0),
&spec,
);
@@ -192,13 +200,17 @@ impl ForkChoiceTestDefinition {
execution_status: ExecutionStatus::Optimistic(
ExecutionBlockHash::from_root(root),
),
unrealized_justified_checkpoint: None,
unrealized_finalized_checkpoint: None,
};
fork_choice.process_block(block).unwrap_or_else(|e| {
panic!(
"process_block op at index {} returned error: {:?}",
op_index, e
)
});
fork_choice
.process_block::<MainnetEthSpec>(block, slot)
.unwrap_or_else(|e| {
panic!(
"process_block op at index {} returned error: {:?}",
op_index, e
)
});
check_bytes_round_trip(&fork_choice);
}
Operation::ProcessAttestation {
@@ -286,8 +298,8 @@ fn get_checkpoint(i: u64) -> Checkpoint {
fn check_bytes_round_trip(original: &ProtoArrayForkChoice) {
let bytes = original.as_bytes();
let decoded =
ProtoArrayForkChoice::from_bytes(&bytes).expect("fork choice should decode from bytes");
let decoded = ProtoArrayForkChoice::from_bytes(&bytes, CountUnrealizedFull::default())
.expect("fork choice should decode from bytes");
assert!(
*original == decoded,
"fork choice should encode and decode without change"

View File

@@ -4,7 +4,7 @@ mod proto_array;
mod proto_array_fork_choice;
mod ssz_container;
pub use crate::proto_array::InvalidationOperation;
pub use crate::proto_array::{CountUnrealizedFull, InvalidationOperation};
pub use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice};
pub use error::Error;

View File

@@ -16,6 +16,7 @@ four_byte_option_impl!(four_byte_option_usize, usize);
four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint);
/// Defines an operation which may invalidate the `execution_status` of some nodes.
#[derive(Clone, Debug)]
pub enum InvalidationOperation {
/// Invalidate only `block_root` and it's descendants. Don't invalidate any ancestors.
InvalidateOne { block_root: Hash256 },
@@ -96,6 +97,10 @@ pub struct ProtoNode {
/// Indicates if an execution node has marked this block as valid. Also contains the execution
/// block hash.
pub execution_status: ExecutionStatus,
#[ssz(with = "four_byte_option_checkpoint")]
pub unrealized_justified_checkpoint: Option<Checkpoint>,
#[ssz(with = "four_byte_option_checkpoint")]
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
}
#[derive(PartialEq, Debug, Encode, Decode, Serialize, Deserialize, Copy, Clone)]
@@ -113,6 +118,24 @@ impl Default for ProposerBoost {
}
}
/// Indicate whether we should strictly count unrealized justification/finalization votes.
#[derive(Default, PartialEq, Eq, Debug, Serialize, Deserialize, Copy, Clone)]
pub enum CountUnrealizedFull {
True,
#[default]
False,
}
impl From<bool> for CountUnrealizedFull {
fn from(b: bool) -> Self {
if b {
CountUnrealizedFull::True
} else {
CountUnrealizedFull::False
}
}
}
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
pub struct ProtoArray {
/// Do not attempt to prune the tree unless it has at least this many nodes. Small prunes
@@ -123,6 +146,7 @@ pub struct ProtoArray {
pub nodes: Vec<ProtoNode>,
pub indices: HashMap<Hash256, usize>,
pub previous_proposer_boost: ProposerBoost,
pub count_unrealized_full: CountUnrealizedFull,
}
impl ProtoArray {
@@ -139,6 +163,7 @@ impl ProtoArray {
/// - Compare the current node with the parents best-child, updating it if the current node
/// should become the best child.
/// - If required, update the parents best-descendant with the current node or its best-descendant.
#[allow(clippy::too_many_arguments)]
pub fn apply_score_changes<E: EthSpec>(
&mut self,
mut deltas: Vec<i64>,
@@ -146,6 +171,7 @@ impl ProtoArray {
finalized_checkpoint: Checkpoint,
new_balances: &[u64],
proposer_boost_root: Hash256,
current_slot: Slot,
spec: &ChainSpec,
) -> Result<(), Error> {
if deltas.len() != self.indices.len() {
@@ -240,7 +266,7 @@ impl ProtoArray {
// not exist.
node.weight = node
.weight
.checked_sub(node_delta.abs() as u64)
.checked_sub(node_delta.unsigned_abs())
.ok_or(Error::DeltaOverflow(node_index))?;
} else {
node.weight = node
@@ -279,7 +305,11 @@ impl ProtoArray {
// If the node has a parent, try to update its best-child and best-descendant.
if let Some(parent_index) = node.parent {
self.maybe_update_best_child_and_descendant(parent_index, node_index)?;
self.maybe_update_best_child_and_descendant::<E>(
parent_index,
node_index,
current_slot,
)?;
}
}
@@ -289,7 +319,7 @@ impl ProtoArray {
/// Register a block with the fork choice.
///
/// It is only sane to supply a `None` parent for the genesis block.
pub fn on_block(&mut self, block: Block) -> Result<(), Error> {
pub fn on_block<E: EthSpec>(&mut self, block: Block, current_slot: Slot) -> Result<(), Error> {
// If the block is already known, simply ignore it.
if self.indices.contains_key(&block.root) {
return Ok(());
@@ -313,6 +343,8 @@ impl ProtoArray {
best_child: None,
best_descendant: None,
execution_status: block.execution_status,
unrealized_justified_checkpoint: block.unrealized_justified_checkpoint,
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint,
};
// If the parent has an invalid execution status, return an error before adding the block to
@@ -334,7 +366,11 @@ impl ProtoArray {
self.nodes.push(node.clone());
if let Some(parent_index) = node.parent {
self.maybe_update_best_child_and_descendant(parent_index, node_index)?;
self.maybe_update_best_child_and_descendant::<E>(
parent_index,
node_index,
current_slot,
)?;
if matches!(block.execution_status, ExecutionStatus::Valid(_)) {
self.propagate_execution_payload_validation_by_index(parent_index)?;
@@ -490,9 +526,6 @@ impl ProtoArray {
node.best_descendant = None
}
// It might be new knowledge that this block is valid, ensure that it and all
// ancestors are marked as valid.
self.propagate_execution_payload_validation_by_index(index)?;
break;
}
}
@@ -606,7 +639,11 @@ impl ProtoArray {
/// been called without a subsequent `Self::apply_score_changes` call. This is because
/// `on_new_block` does not attempt to walk backwards through the tree and update the
/// best-child/best-descendant links.
pub fn find_head(&self, justified_root: &Hash256) -> Result<Hash256, Error> {
pub fn find_head<E: EthSpec>(
&self,
justified_root: &Hash256,
current_slot: Slot,
) -> Result<Hash256, Error> {
let justified_index = self
.indices
.get(justified_root)
@@ -639,8 +676,9 @@ impl ProtoArray {
.ok_or(Error::InvalidBestDescendant(best_descendant_index))?;
// Perform a sanity check that the node is indeed valid to be the head.
if !self.node_is_viable_for_head(best_node) {
if !self.node_is_viable_for_head::<E>(best_node, current_slot) {
return Err(Error::InvalidBestNode(Box::new(InvalidBestNodeInfo {
current_slot,
start_root: *justified_root,
justified_checkpoint: self.justified_checkpoint,
finalized_checkpoint: self.finalized_checkpoint,
@@ -735,10 +773,11 @@ impl ProtoArray {
/// best-descendant.
/// - The child is not the best child but becomes the best child.
/// - The child is not the best child and does not become the best child.
fn maybe_update_best_child_and_descendant(
fn maybe_update_best_child_and_descendant<E: EthSpec>(
&mut self,
parent_index: usize,
child_index: usize,
current_slot: Slot,
) -> Result<(), Error> {
let child = self
.nodes
@@ -750,7 +789,8 @@ impl ProtoArray {
.get(parent_index)
.ok_or(Error::InvalidNodeIndex(parent_index))?;
let child_leads_to_viable_head = self.node_leads_to_viable_head(child)?;
let child_leads_to_viable_head =
self.node_leads_to_viable_head::<E>(child, current_slot)?;
// These three variables are aliases to the three options that we may set the
// `parent.best_child` and `parent.best_descendant` to.
@@ -763,54 +803,54 @@ impl ProtoArray {
);
let no_change = (parent.best_child, parent.best_descendant);
let (new_best_child, new_best_descendant) = if let Some(best_child_index) =
parent.best_child
{
if best_child_index == child_index && !child_leads_to_viable_head {
// If the child is already the best-child of the parent but it's not viable for
// the head, remove it.
change_to_none
} else if best_child_index == child_index {
// If the child is the best-child already, set it again to ensure that the
// best-descendant of the parent is updated.
change_to_child
} else {
let best_child = self
.nodes
.get(best_child_index)
.ok_or(Error::InvalidBestDescendant(best_child_index))?;
let best_child_leads_to_viable_head = self.node_leads_to_viable_head(best_child)?;
if child_leads_to_viable_head && !best_child_leads_to_viable_head {
// The child leads to a viable head, but the current best-child doesn't.
let (new_best_child, new_best_descendant) =
if let Some(best_child_index) = parent.best_child {
if best_child_index == child_index && !child_leads_to_viable_head {
// If the child is already the best-child of the parent but it's not viable for
// the head, remove it.
change_to_none
} else if best_child_index == child_index {
// If the child is the best-child already, set it again to ensure that the
// best-descendant of the parent is updated.
change_to_child
} else if !child_leads_to_viable_head && best_child_leads_to_viable_head {
// The best child leads to a viable head, but the child doesn't.
no_change
} else if child.weight == best_child.weight {
// Tie-breaker of equal weights by root.
if child.root >= best_child.root {
change_to_child
} else {
no_change
}
} else {
// Choose the winner by weight.
if child.weight >= best_child.weight {
let best_child = self
.nodes
.get(best_child_index)
.ok_or(Error::InvalidBestDescendant(best_child_index))?;
let best_child_leads_to_viable_head =
self.node_leads_to_viable_head::<E>(best_child, current_slot)?;
if child_leads_to_viable_head && !best_child_leads_to_viable_head {
// The child leads to a viable head, but the current best-child doesn't.
change_to_child
} else {
} else if !child_leads_to_viable_head && best_child_leads_to_viable_head {
// The best child leads to a viable head, but the child doesn't.
no_change
} else if child.weight == best_child.weight {
// Tie-breaker of equal weights by root.
if child.root >= best_child.root {
change_to_child
} else {
no_change
}
} else {
// Choose the winner by weight.
if child.weight >= best_child.weight {
change_to_child
} else {
no_change
}
}
}
}
} else if child_leads_to_viable_head {
// There is no current best-child and the child is viable.
change_to_child
} else {
// There is no current best-child but the child is not viable.
no_change
};
} else if child_leads_to_viable_head {
// There is no current best-child and the child is viable.
change_to_child
} else {
// There is no current best-child but the child is not viable.
no_change
};
let parent = self
.nodes
@@ -825,7 +865,11 @@ impl ProtoArray {
/// Indicates if the node itself is viable for the head, or if it's best descendant is viable
/// for the head.
fn node_leads_to_viable_head(&self, node: &ProtoNode) -> Result<bool, Error> {
fn node_leads_to_viable_head<E: EthSpec>(
&self,
node: &ProtoNode,
current_slot: Slot,
) -> Result<bool, Error> {
let best_descendant_is_viable_for_head =
if let Some(best_descendant_index) = node.best_descendant {
let best_descendant = self
@@ -833,12 +877,13 @@ impl ProtoArray {
.get(best_descendant_index)
.ok_or(Error::InvalidBestDescendant(best_descendant_index))?;
self.node_is_viable_for_head(best_descendant)
self.node_is_viable_for_head::<E>(best_descendant, current_slot)
} else {
false
};
Ok(best_descendant_is_viable_for_head || self.node_is_viable_for_head(node))
Ok(best_descendant_is_viable_for_head
|| self.node_is_viable_for_head::<E>(node, current_slot))
}
/// This is the equivalent to the `filter_block_tree` function in the eth2 spec:
@@ -847,18 +892,58 @@ impl ProtoArray {
///
/// Any node that has a different finalized or justified epoch should not be viable for the
/// head.
fn node_is_viable_for_head(&self, node: &ProtoNode) -> bool {
fn node_is_viable_for_head<E: EthSpec>(&self, node: &ProtoNode, current_slot: Slot) -> bool {
if node.execution_status.is_invalid() {
return false;
}
if let (Some(node_justified_checkpoint), Some(node_finalized_checkpoint)) =
let genesis_epoch = Epoch::new(0);
let checkpoint_match_predicate =
|node_justified_checkpoint: Checkpoint, node_finalized_checkpoint: Checkpoint| {
let correct_justified = node_justified_checkpoint == self.justified_checkpoint
|| self.justified_checkpoint.epoch == genesis_epoch;
let correct_finalized = node_finalized_checkpoint == self.finalized_checkpoint
|| self.finalized_checkpoint.epoch == genesis_epoch;
correct_justified && correct_finalized
};
if let (
Some(unrealized_justified_checkpoint),
Some(unrealized_finalized_checkpoint),
Some(justified_checkpoint),
Some(finalized_checkpoint),
) = (
node.unrealized_justified_checkpoint,
node.unrealized_finalized_checkpoint,
node.justified_checkpoint,
node.finalized_checkpoint,
) {
let current_epoch = current_slot.epoch(E::slots_per_epoch());
// If previous epoch is justified, pull up all tips to at least the previous epoch
if CountUnrealizedFull::True == self.count_unrealized_full
&& (current_epoch > genesis_epoch
&& self.justified_checkpoint.epoch + 1 == current_epoch)
{
unrealized_justified_checkpoint.epoch + 1 >= current_epoch
// If previous epoch is not justified, pull up only tips from past epochs up to the current epoch
} else {
// If block is from a previous epoch, filter using unrealized justification & finalization information
if node.slot.epoch(E::slots_per_epoch()) < current_epoch {
checkpoint_match_predicate(
unrealized_justified_checkpoint,
unrealized_finalized_checkpoint,
)
// If block is from the current epoch, filter using the head state's justification & finalization information
} else {
checkpoint_match_predicate(justified_checkpoint, finalized_checkpoint)
}
}
} else if let (Some(justified_checkpoint), Some(finalized_checkpoint)) =
(node.justified_checkpoint, node.finalized_checkpoint)
{
(node_justified_checkpoint == self.justified_checkpoint
|| self.justified_checkpoint.epoch == Epoch::new(0))
&& (node_finalized_checkpoint == self.finalized_checkpoint
|| self.finalized_checkpoint.epoch == Epoch::new(0))
checkpoint_match_predicate(justified_checkpoint, finalized_checkpoint)
} else {
false
}
@@ -930,7 +1015,7 @@ impl ProtoArray {
/// Returns `None` if there is an overflow or underflow when calculating the score.
///
/// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#get_latest_attesting_balance
fn calculate_proposer_boost<E: EthSpec>(
pub fn calculate_proposer_boost<E: EthSpec>(
validator_balances: &[u64],
proposer_score_boost: u64,
) -> Option<u64> {

View File

@@ -1,10 +1,13 @@
use crate::error::Error;
use crate::proto_array::{InvalidationOperation, Iter, ProposerBoost, ProtoArray, ProtoNode};
use crate::proto_array::CountUnrealizedFull;
use crate::proto_array::{
calculate_proposer_boost, InvalidationOperation, Iter, ProposerBoost, ProtoArray, ProtoNode,
};
use crate::ssz_container::SszContainer;
use serde_derive::{Deserialize, Serialize};
use ssz::{Decode, Encode};
use ssz_derive::{Decode, Encode};
use std::collections::HashMap;
use std::collections::{BTreeSet, HashMap};
use types::{
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
Slot,
@@ -36,7 +39,7 @@ pub enum ExecutionStatus {
///
/// This `bool` only exists to satisfy our SSZ implementation which requires all variants
/// to have a value. It can be set to anything.
Irrelevant(bool), // TODO(merge): fix bool.
Irrelevant(bool),
}
impl ExecutionStatus {
@@ -87,10 +90,22 @@ impl ExecutionStatus {
///
/// - Has execution enabled, AND
/// - Has a payload that has not yet been verified by an EL.
pub fn is_optimistic(&self) -> bool {
pub fn is_strictly_optimistic(&self) -> bool {
matches!(self, ExecutionStatus::Optimistic(_))
}
/// Returns `true` if the block:
///
/// - Has execution enabled, AND
/// - Has a payload that has not yet been verified by an EL, OR.
/// - Has a payload that has been deemed invalid by an EL.
pub fn is_optimistic_or_invalid(&self) -> bool {
matches!(
self,
ExecutionStatus::Optimistic(_) | ExecutionStatus::Invalid(_)
)
}
/// Returns `true` if the block:
///
/// - Has execution enabled, AND
@@ -124,6 +139,8 @@ pub struct Block {
/// Indicates if an execution node has marked this block as valid. Also contains the execution
/// block hash.
pub execution_status: ExecutionStatus,
pub unrealized_justified_checkpoint: Option<Checkpoint>,
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
}
/// A Vec-wrapper which will grow to match any request.
@@ -162,7 +179,7 @@ pub struct ProtoArrayForkChoice {
impl ProtoArrayForkChoice {
#[allow(clippy::too_many_arguments)]
pub fn new(
pub fn new<E: EthSpec>(
finalized_block_slot: Slot,
finalized_block_state_root: Hash256,
justified_checkpoint: Checkpoint,
@@ -170,6 +187,7 @@ impl ProtoArrayForkChoice {
current_epoch_shuffling_id: AttestationShufflingId,
next_epoch_shuffling_id: AttestationShufflingId,
execution_status: ExecutionStatus,
count_unrealized_full: CountUnrealizedFull,
) -> Result<Self, String> {
let mut proto_array = ProtoArray {
prune_threshold: DEFAULT_PRUNE_THRESHOLD,
@@ -178,6 +196,7 @@ impl ProtoArrayForkChoice {
nodes: Vec::with_capacity(1),
indices: HashMap::with_capacity(1),
previous_proposer_boost: ProposerBoost::default(),
count_unrealized_full,
};
let block = Block {
@@ -193,10 +212,12 @@ impl ProtoArrayForkChoice {
justified_checkpoint,
finalized_checkpoint,
execution_status,
unrealized_justified_checkpoint: Some(justified_checkpoint),
unrealized_finalized_checkpoint: Some(finalized_checkpoint),
};
proto_array
.on_block(block)
.on_block::<E>(block, finalized_block_slot)
.map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?;
Ok(Self {
@@ -242,22 +263,29 @@ impl ProtoArrayForkChoice {
Ok(())
}
pub fn process_block(&mut self, block: Block) -> Result<(), String> {
pub fn process_block<E: EthSpec>(
&mut self,
block: Block,
current_slot: Slot,
) -> Result<(), String> {
if block.parent_root.is_none() {
return Err("Missing parent root".to_string());
}
self.proto_array
.on_block(block)
.on_block::<E>(block, current_slot)
.map_err(|e| format!("process_block_error: {:?}", e))
}
#[allow(clippy::too_many_arguments)]
pub fn find_head<E: EthSpec>(
&mut self,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
justified_state_balances: &[u64],
proposer_boost_root: Hash256,
equivocating_indices: &BTreeSet<u64>,
current_slot: Slot,
spec: &ChainSpec,
) -> Result<Hash256, String> {
let old_balances = &mut self.balances;
@@ -269,6 +297,7 @@ impl ProtoArrayForkChoice {
&mut self.votes,
old_balances,
new_balances,
equivocating_indices,
)
.map_err(|e| format!("find_head compute_deltas failed: {:?}", e))?;
@@ -279,6 +308,7 @@ impl ProtoArrayForkChoice {
finalized_checkpoint,
new_balances,
proposer_boost_root,
current_slot,
spec,
)
.map_err(|e| format!("find_head apply_score_changes failed: {:?}", e))?;
@@ -286,10 +316,121 @@ impl ProtoArrayForkChoice {
*old_balances = new_balances.to_vec();
self.proto_array
.find_head(&justified_checkpoint.root)
.find_head::<E>(&justified_checkpoint.root, current_slot)
.map_err(|e| format!("find_head failed: {:?}", e))
}
/// Returns `true` if there are any blocks in `self` with an `INVALID` execution payload status.
///
/// This will operate on *all* blocks, even those that do not descend from the finalized
/// ancestor.
pub fn contains_invalid_payloads(&mut self) -> bool {
self.proto_array
.nodes
.iter()
.any(|node| node.execution_status.is_invalid())
}
/// For all nodes, regardless of their relationship to the finalized block, set their execution
/// status to be optimistic.
///
/// In practice this means forgetting any `VALID` or `INVALID` statuses.
pub fn set_all_blocks_to_optimistic<E: EthSpec>(
&mut self,
spec: &ChainSpec,
) -> Result<(), String> {
// Iterate backwards through all nodes in the `proto_array`. Whilst it's not strictly
// required to do this process in reverse, it seems natural when we consider how LMD votes
// are counted.
//
// This function will touch all blocks, even those that do not descend from the finalized
// block. Since this function is expected to run at start-up during very rare
// circumstances we prefer simplicity over efficiency.
for node_index in (0..self.proto_array.nodes.len()).rev() {
let node = self
.proto_array
.nodes
.get_mut(node_index)
.ok_or("unreachable index out of bounds in proto_array nodes")?;
match node.execution_status {
ExecutionStatus::Invalid(block_hash) => {
node.execution_status = ExecutionStatus::Optimistic(block_hash);
// Restore the weight of the node, it would have been set to `0` in
// `apply_score_changes` when it was invalidated.
let mut restored_weight: u64 = self
.votes
.0
.iter()
.enumerate()
.filter_map(|(validator_index, vote)| {
if vote.current_root == node.root {
// Any voting validator that does not have a balance should be
// ignored. This is consistent with `compute_deltas`.
self.balances.get(validator_index)
} else {
None
}
})
.sum();
// If the invalid root was boosted, apply the weight to it and
// ancestors.
if let Some(proposer_score_boost) = spec.proposer_score_boost {
if self.proto_array.previous_proposer_boost.root == node.root {
// Compute the score based upon the current balances. We can't rely on
// the `previous_proposr_boost.score` since it is set to zero with an
// invalid node.
let proposer_score =
calculate_proposer_boost::<E>(&self.balances, proposer_score_boost)
.ok_or("Failed to compute proposer boost")?;
// Store the score we've applied here so it can be removed in
// a later call to `apply_score_changes`.
self.proto_array.previous_proposer_boost.score = proposer_score;
// Apply this boost to this node.
restored_weight = restored_weight
.checked_add(proposer_score)
.ok_or("Overflow when adding boost to weight")?;
}
}
// Add the restored weight to the node and all ancestors.
if restored_weight > 0 {
let mut node_or_ancestor = node;
loop {
node_or_ancestor.weight = node_or_ancestor
.weight
.checked_add(restored_weight)
.ok_or("Overflow when adding weight to ancestor")?;
if let Some(parent_index) = node_or_ancestor.parent {
node_or_ancestor = self
.proto_array
.nodes
.get_mut(parent_index)
.ok_or(format!("Missing parent index: {}", parent_index))?;
} else {
// This is either the finalized block or a block that does not
// descend from the finalized block.
break;
}
}
}
}
// There are no balance changes required if the node was either valid or
// optimistic.
ExecutionStatus::Valid(block_hash) | ExecutionStatus::Optimistic(block_hash) => {
node.execution_status = ExecutionStatus::Optimistic(block_hash)
}
// An irrelevant node cannot become optimistic, this is a no-op.
ExecutionStatus::Irrelevant(_) => (),
}
}
Ok(())
}
pub fn maybe_prune(&mut self, finalized_root: Hash256) -> Result<(), String> {
self.proto_array
.maybe_prune(finalized_root)
@@ -341,6 +482,8 @@ impl ProtoArrayForkChoice {
justified_checkpoint,
finalized_checkpoint,
execution_status: block.execution_status,
unrealized_justified_checkpoint: block.unrealized_justified_checkpoint,
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint,
})
} else {
None
@@ -391,8 +534,12 @@ impl ProtoArrayForkChoice {
SszContainer::from(self).as_ssz_bytes()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
pub fn from_bytes(
bytes: &[u8],
count_unrealized_full: CountUnrealizedFull,
) -> Result<Self, String> {
SszContainer::from_ssz_bytes(bytes)
.map(|container| (container, count_unrealized_full))
.map(Into::into)
.map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e))
}
@@ -427,6 +574,7 @@ fn compute_deltas(
votes: &mut ElasticList<VoteTracker>,
old_balances: &[u64],
new_balances: &[u64],
equivocating_indices: &BTreeSet<u64>,
) -> Result<Vec<i64>, Error> {
let mut deltas = vec![0_i64; indices.len()];
@@ -437,6 +585,38 @@ fn compute_deltas(
continue;
}
// Handle newly slashed validators by deducting their weight from their current vote. We
// determine if they are newly slashed by checking whether their `vote.current_root` is
// non-zero. After applying the deduction a single time we set their `current_root` to zero
// and never update it again (thus preventing repeat deductions).
//
// Even if they make new attestations which are processed by `process_attestation` these
// will only update their `vote.next_root`.
if equivocating_indices.contains(&(val_index as u64)) {
// First time we've processed this slashing in fork choice:
//
// 1. Add a negative delta for their `current_root`.
// 2. Set their `current_root` (permanently) to zero.
if !vote.current_root.is_zero() {
let old_balance = old_balances.get(val_index).copied().unwrap_or(0);
if let Some(current_delta_index) = indices.get(&vote.current_root).copied() {
let delta = deltas
.get(current_delta_index)
.ok_or(Error::InvalidNodeDelta(current_delta_index))?
.checked_sub(old_balance as i64)
.ok_or(Error::DeltaOverflow(current_delta_index))?;
// Array access safe due to check on previous line.
deltas[current_delta_index] = delta;
}
vote.current_root = Hash256::zero();
}
// We've handled this slashed validator, continue without applying an ordinary delta.
continue;
}
// If the validator was not included in the _old_ balances (i.e., it did not exist yet)
// then say its balance was zero.
let old_balance = old_balances.get(val_index).copied().unwrap_or(0);
@@ -485,6 +665,7 @@ fn compute_deltas(
#[cfg(test)]
mod test_compute_deltas {
use super::*;
use types::MainnetEthSpec;
/// Gives a hash that is not the zero hash (unless i is `usize::max_value)`.
fn hash_from_index(i: usize) -> Hash256 {
@@ -510,7 +691,7 @@ mod test_compute_deltas {
root: finalized_root,
};
let mut fc = ProtoArrayForkChoice::new(
let mut fc = ProtoArrayForkChoice::new::<MainnetEthSpec>(
genesis_slot,
state_root,
genesis_checkpoint,
@@ -518,39 +699,50 @@ mod test_compute_deltas {
junk_shuffling_id.clone(),
junk_shuffling_id.clone(),
execution_status,
CountUnrealizedFull::default(),
)
.unwrap();
// Add block that is a finalized descendant.
fc.proto_array
.on_block(Block {
slot: genesis_slot + 1,
root: finalized_desc,
parent_root: Some(finalized_root),
state_root,
target_root: finalized_root,
current_epoch_shuffling_id: junk_shuffling_id.clone(),
next_epoch_shuffling_id: junk_shuffling_id.clone(),
justified_checkpoint: genesis_checkpoint,
finalized_checkpoint: genesis_checkpoint,
execution_status,
})
.on_block::<MainnetEthSpec>(
Block {
slot: genesis_slot + 1,
root: finalized_desc,
parent_root: Some(finalized_root),
state_root,
target_root: finalized_root,
current_epoch_shuffling_id: junk_shuffling_id.clone(),
next_epoch_shuffling_id: junk_shuffling_id.clone(),
justified_checkpoint: genesis_checkpoint,
finalized_checkpoint: genesis_checkpoint,
execution_status,
unrealized_justified_checkpoint: Some(genesis_checkpoint),
unrealized_finalized_checkpoint: Some(genesis_checkpoint),
},
genesis_slot + 1,
)
.unwrap();
// Add block that is *not* a finalized descendant.
fc.proto_array
.on_block(Block {
slot: genesis_slot + 1,
root: not_finalized_desc,
parent_root: None,
state_root,
target_root: finalized_root,
current_epoch_shuffling_id: junk_shuffling_id.clone(),
next_epoch_shuffling_id: junk_shuffling_id,
justified_checkpoint: genesis_checkpoint,
finalized_checkpoint: genesis_checkpoint,
execution_status,
})
.on_block::<MainnetEthSpec>(
Block {
slot: genesis_slot + 1,
root: not_finalized_desc,
parent_root: None,
state_root,
target_root: finalized_root,
current_epoch_shuffling_id: junk_shuffling_id.clone(),
next_epoch_shuffling_id: junk_shuffling_id,
justified_checkpoint: genesis_checkpoint,
finalized_checkpoint: genesis_checkpoint,
execution_status,
unrealized_justified_checkpoint: None,
unrealized_finalized_checkpoint: None,
},
genesis_slot + 1,
)
.unwrap();
assert!(!fc.is_descendant(unknown, unknown));
@@ -582,6 +774,7 @@ mod test_compute_deltas {
let mut votes = ElasticList::default();
let mut old_balances = vec![];
let mut new_balances = vec![];
let equivocating_indices = BTreeSet::new();
for i in 0..validator_count {
indices.insert(hash_from_index(i), i);
@@ -594,8 +787,14 @@ mod test_compute_deltas {
new_balances.push(0);
}
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(
deltas.len(),
@@ -626,6 +825,7 @@ mod test_compute_deltas {
let mut votes = ElasticList::default();
let mut old_balances = vec![];
let mut new_balances = vec![];
let equivocating_indices = BTreeSet::new();
for i in 0..validator_count {
indices.insert(hash_from_index(i), i);
@@ -638,8 +838,14 @@ mod test_compute_deltas {
new_balances.push(BALANCE);
}
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(
deltas.len(),
@@ -677,6 +883,7 @@ mod test_compute_deltas {
let mut votes = ElasticList::default();
let mut old_balances = vec![];
let mut new_balances = vec![];
let equivocating_indices = BTreeSet::new();
for i in 0..validator_count {
indices.insert(hash_from_index(i), i);
@@ -689,8 +896,14 @@ mod test_compute_deltas {
new_balances.push(BALANCE);
}
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(
deltas.len(),
@@ -723,6 +936,7 @@ mod test_compute_deltas {
let mut votes = ElasticList::default();
let mut old_balances = vec![];
let mut new_balances = vec![];
let equivocating_indices = BTreeSet::new();
for i in 0..validator_count {
indices.insert(hash_from_index(i), i);
@@ -735,8 +949,14 @@ mod test_compute_deltas {
new_balances.push(BALANCE);
}
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(
deltas.len(),
@@ -774,6 +994,7 @@ mod test_compute_deltas {
let mut indices = HashMap::new();
let mut votes = ElasticList::default();
let equivocating_indices = BTreeSet::new();
// There is only one block.
indices.insert(hash_from_index(1), 0);
@@ -796,8 +1017,14 @@ mod test_compute_deltas {
next_epoch: Epoch::new(0),
});
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(deltas.len(), 1, "deltas should have expected length");
@@ -826,6 +1053,7 @@ mod test_compute_deltas {
let mut votes = ElasticList::default();
let mut old_balances = vec![];
let mut new_balances = vec![];
let equivocating_indices = BTreeSet::new();
for i in 0..validator_count {
indices.insert(hash_from_index(i), i);
@@ -838,8 +1066,14 @@ mod test_compute_deltas {
new_balances.push(NEW_BALANCE);
}
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(
deltas.len(),
@@ -879,6 +1113,7 @@ mod test_compute_deltas {
let mut indices = HashMap::new();
let mut votes = ElasticList::default();
let equivocating_indices = BTreeSet::new();
// There are two blocks.
indices.insert(hash_from_index(1), 0);
@@ -898,8 +1133,14 @@ mod test_compute_deltas {
});
}
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(deltas.len(), 2, "deltas should have expected length");
@@ -928,6 +1169,7 @@ mod test_compute_deltas {
let mut indices = HashMap::new();
let mut votes = ElasticList::default();
let equivocating_indices = BTreeSet::new();
// There are two blocks.
indices.insert(hash_from_index(1), 0);
@@ -947,8 +1189,14 @@ mod test_compute_deltas {
});
}
let deltas = compute_deltas(&indices, &mut votes, &old_balances, &new_balances)
.expect("should compute deltas");
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(deltas.len(), 2, "deltas should have expected length");
@@ -969,4 +1217,72 @@ mod test_compute_deltas {
);
}
}
#[test]
fn validator_equivocates() {
const OLD_BALANCE: u64 = 42;
const NEW_BALANCE: u64 = 43;
let mut indices = HashMap::new();
let mut votes = ElasticList::default();
// There are two blocks.
indices.insert(hash_from_index(1), 0);
indices.insert(hash_from_index(2), 1);
// There are two validators.
let old_balances = vec![OLD_BALANCE; 2];
let new_balances = vec![NEW_BALANCE; 2];
// Both validator move votes from block 1 to block 2.
for _ in 0..2 {
votes.0.push(VoteTracker {
current_root: hash_from_index(1),
next_root: hash_from_index(2),
next_epoch: Epoch::new(0),
});
}
// Validator 0 is slashed.
let equivocating_indices = BTreeSet::from_iter([0]);
let deltas = compute_deltas(
&indices,
&mut votes,
&old_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(deltas.len(), 2, "deltas should have expected length");
assert_eq!(
deltas[0],
-2 * OLD_BALANCE as i64,
"block 1 should have lost two old balances"
);
assert_eq!(
deltas[1], NEW_BALANCE as i64,
"block 2 should have gained one balance"
);
// Validator 0's current root should have been reset.
assert_eq!(votes.0[0].current_root, Hash256::zero());
assert_eq!(votes.0[0].next_root, hash_from_index(2));
// Validator 1's current root should have been updated.
assert_eq!(votes.0[1].current_root, hash_from_index(2));
// Re-computing the deltas should be a no-op (no repeat deduction for the slashed validator).
let deltas = compute_deltas(
&indices,
&mut votes,
&new_balances,
&new_balances,
&equivocating_indices,
)
.expect("should compute deltas");
assert_eq!(deltas, vec![0, 0]);
}
}

View File

@@ -1,6 +1,6 @@
use crate::proto_array::ProposerBoost;
use crate::{
proto_array::{ProtoArray, ProtoNode},
proto_array::{CountUnrealizedFull, ProtoArray, ProtoNode},
proto_array_fork_choice::{ElasticList, ProtoArrayForkChoice, VoteTracker},
};
use ssz::{four_byte_option_impl, Encode};
@@ -41,8 +41,8 @@ impl From<&ProtoArrayForkChoice> for SszContainer {
}
}
impl From<SszContainer> for ProtoArrayForkChoice {
fn from(from: SszContainer) -> Self {
impl From<(SszContainer, CountUnrealizedFull)> for ProtoArrayForkChoice {
fn from((from, count_unrealized_full): (SszContainer, CountUnrealizedFull)) -> Self {
let proto_array = ProtoArray {
prune_threshold: from.prune_threshold,
justified_checkpoint: from.justified_checkpoint,
@@ -50,6 +50,7 @@ impl From<SszContainer> for ProtoArrayForkChoice {
nodes: from.nodes,
indices: from.indices.into_iter().collect::<HashMap<_, _>>(),
previous_proposer_boost: from.previous_proposer_boost,
count_unrealized_full,
};
Self {

View File

@@ -6,6 +6,7 @@ pub mod hex_vec;
pub mod json_str;
pub mod list_of_bytes_lists;
pub mod quoted_u64_vec;
pub mod u256_hex_be;
pub mod u32_hex;
pub mod u64_hex_be;
pub mod u8_hex;

View File

@@ -0,0 +1,144 @@
use ethereum_types::U256;
use serde::de::Visitor;
use serde::{de, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
pub fn serialize<S>(num: &U256, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
num.serialize(serializer)
}
pub struct U256Visitor;
impl<'de> Visitor<'de> for U256Visitor {
type Value = String;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a well formatted hex string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if !value.starts_with("0x") {
return Err(de::Error::custom("must start with 0x"));
}
let stripped = &value[2..];
if stripped.is_empty() {
Err(de::Error::custom(format!(
"quantity cannot be {:?}",
stripped
)))
} else if stripped == "0" {
Ok(value.to_string())
} else if stripped.starts_with('0') {
Err(de::Error::custom("cannot have leading zero"))
} else {
Ok(value.to_string())
}
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<U256, D::Error>
where
D: Deserializer<'de>,
{
let decoded = deserializer.deserialize_string(U256Visitor)?;
U256::from_str(&decoded).map_err(|e| de::Error::custom(format!("Invalid U256 string: {}", e)))
}
#[cfg(test)]
mod test {
use ethereum_types::U256;
use serde::{Deserialize, Serialize};
use serde_json;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
struct Wrapper {
#[serde(with = "super")]
val: U256,
}
#[test]
fn encoding() {
assert_eq!(
&serde_json::to_string(&Wrapper { val: 0.into() }).unwrap(),
"\"0x0\""
);
assert_eq!(
&serde_json::to_string(&Wrapper { val: 1.into() }).unwrap(),
"\"0x1\""
);
assert_eq!(
&serde_json::to_string(&Wrapper { val: 256.into() }).unwrap(),
"\"0x100\""
);
assert_eq!(
&serde_json::to_string(&Wrapper { val: 65.into() }).unwrap(),
"\"0x41\""
);
assert_eq!(
&serde_json::to_string(&Wrapper { val: 1024.into() }).unwrap(),
"\"0x400\""
);
assert_eq!(
&serde_json::to_string(&Wrapper {
val: U256::max_value() - 1
})
.unwrap(),
"\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\""
);
assert_eq!(
&serde_json::to_string(&Wrapper {
val: U256::max_value()
})
.unwrap(),
"\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\""
);
}
#[test]
fn decoding() {
assert_eq!(
serde_json::from_str::<Wrapper>("\"0x0\"").unwrap(),
Wrapper { val: 0.into() },
);
assert_eq!(
serde_json::from_str::<Wrapper>("\"0x41\"").unwrap(),
Wrapper { val: 65.into() },
);
assert_eq!(
serde_json::from_str::<Wrapper>("\"0x400\"").unwrap(),
Wrapper { val: 1024.into() },
);
assert_eq!(
serde_json::from_str::<Wrapper>(
"\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\""
)
.unwrap(),
Wrapper {
val: U256::max_value() - 1
},
);
assert_eq!(
serde_json::from_str::<Wrapper>(
"\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\""
)
.unwrap(),
Wrapper {
val: U256::max_value()
},
);
serde_json::from_str::<Wrapper>("\"0x\"").unwrap_err();
serde_json::from_str::<Wrapper>("\"0x0400\"").unwrap_err();
serde_json::from_str::<Wrapper>("\"400\"").unwrap_err();
serde_json::from_str::<Wrapper>("\"ff\"").unwrap_err();
}
}

View File

@@ -4,7 +4,7 @@ use core::num::NonZeroUsize;
use ethereum_types::{H160, H256, U128, U256};
use itertools::process_results;
use smallvec::SmallVec;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::iter::{self, FromIterator};
use std::sync::Arc;
@@ -431,6 +431,28 @@ where
}
}
impl<T> Decode for BTreeSet<T>
where
T: Decode + Ord,
{
fn is_ssz_fixed_len() -> bool {
false
}
fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
if bytes.is_empty() {
Ok(Self::from_iter(iter::empty()))
} else if T::is_ssz_fixed_len() {
bytes
.chunks(T::ssz_fixed_len())
.map(T::from_ssz_bytes)
.collect()
} else {
decode_list_of_variable_length_items(bytes, None)
}
}
}
/// Decodes `bytes` as if it were a list of variable-length items.
///
/// The `ssz::SszDecoder` can also perform this functionality, however this function is

View File

@@ -1,5 +1,5 @@
use smallvec::SmallVec;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::convert::Infallible;
use std::fmt::Debug;
@@ -62,6 +62,20 @@ where
}
}
impl<T> TryFromIter<T> for BTreeSet<T>
where
T: Ord,
{
type Error = Infallible;
fn try_from_iter<I>(iter: I) -> Result<Self, Self::Error>
where
I: IntoIterator<Item = T>,
{
Ok(Self::from_iter(iter))
}
}
/// Partial variant of `collect`.
pub trait TryCollect: Iterator {
fn try_collect<C>(self) -> Result<C, C::Error>

View File

@@ -2,7 +2,7 @@ use super::*;
use core::num::NonZeroUsize;
use ethereum_types::{H160, H256, U128, U256};
use smallvec::SmallVec;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
macro_rules! impl_encodable_for_uint {
@@ -326,6 +326,23 @@ where
}
}
impl<T> Encode for BTreeSet<T>
where
T: Encode + Ord,
{
fn is_ssz_fixed_len() -> bool {
false
}
fn ssz_bytes_len(&self) -> usize {
sequence_ssz_bytes_len(self.iter())
}
fn ssz_append(&self, buf: &mut Vec<u8>) {
sequence_ssz_append(self.iter(), buf)
}
}
impl Encode for bool {
fn is_ssz_fixed_len() -> bool {
true

View File

@@ -353,7 +353,7 @@ mod test {
let vec = vec![0, 2, 4, 6];
let fixed: FixedVector<u64, U4> = FixedVector::from(vec);
assert_eq!(fixed.get(0), Some(&0));
assert_eq!(fixed.first(), Some(&0));
assert_eq!(fixed.get(3), Some(&6));
assert_eq!(fixed.get(4), None);
}

View File

@@ -335,7 +335,7 @@ mod test {
let vec = vec![0, 2, 4, 6];
let fixed: VariableList<u64, U4> = VariableList::from(vec);
assert_eq!(fixed.get(0), Some(&0));
assert_eq!(fixed.first(), Some(&0));
assert_eq!(fixed.get(3), Some(&6));
assert_eq!(fixed.get(4), None);
}

View File

@@ -7,12 +7,14 @@ edition = "2021"
[dev-dependencies]
env_logger = "0.9.0"
beacon_chain = { path = "../../beacon_node/beacon_chain" }
tokio = { version = "1.14.0", features = ["rt-multi-thread"] }
[dependencies]
bls = { path = "../../crypto/bls" }
integer-sqrt = "0.1.5"
itertools = "0.10.0"
eth2_ssz = "0.4.1"
eth2_ssz_derive = "0.3.0"
eth2_ssz_types = "0.2.2"
merkle_proof = { path = "../merkle_proof" }
safe_arith = { path = "../safe_arith" }
@@ -27,6 +29,7 @@ lighthouse_metrics = { path = "../../common/lighthouse_metrics", optional = true
lazy_static = { version = "1.4.0", optional = true }
rustc-hash = "1.1.0"
vec_map = "0.8.2"
derivative = "2.1.1"
[features]
default = ["legacy-arith", "metrics"]

View File

@@ -2,25 +2,43 @@ use integer_sqrt::IntegerSquareRoot;
use safe_arith::{ArithError, SafeArith};
use types::*;
/// This type exists to avoid confusing `total_active_balance` with `base_reward_per_increment`,
/// since they are used in close proximity and the same type (`u64`).
#[derive(Copy, Clone)]
pub struct BaseRewardPerIncrement(u64);
impl BaseRewardPerIncrement {
pub fn new(total_active_balance: u64, spec: &ChainSpec) -> Result<Self, ArithError> {
get_base_reward_per_increment(total_active_balance, spec).map(Self)
}
pub fn as_u64(&self) -> u64 {
self.0
}
}
/// Returns the base reward for some validator.
///
/// The function has a different interface to the spec since it accepts the
/// `base_reward_per_increment` without computing it each time. Avoiding the re computation has
/// shown to be a significant optimisation.
///
/// Spec v1.1.0
pub fn get_base_reward(
validator_effective_balance: u64,
// Should be == get_total_active_balance(state, spec)
total_active_balance: u64,
base_reward_per_increment: BaseRewardPerIncrement,
spec: &ChainSpec,
) -> Result<u64, Error> {
validator_effective_balance
.safe_div(spec.effective_balance_increment)?
.safe_mul(get_base_reward_per_increment(total_active_balance, spec)?)
.safe_mul(base_reward_per_increment.as_u64())
.map_err(Into::into)
}
/// Returns the base reward for some validator.
///
/// Spec v1.1.0
pub fn get_base_reward_per_increment(
fn get_base_reward_per_increment(
total_active_balance: u64,
spec: &ChainSpec,
) -> Result<u64, ArithError> {

View File

@@ -1,12 +1,10 @@
use types::*;
/// Returns validator indices which participated in the attestation, sorted by increasing index.
///
/// Spec v0.12.1
pub fn get_attesting_indices<T: EthSpec>(
committee: &[usize],
bitlist: &BitList<T::MaxValidatorsPerCommittee>,
) -> Result<Vec<usize>, BeaconStateError> {
) -> Result<Vec<u64>, BeaconStateError> {
if bitlist.len() != committee.len() {
return Err(BeaconStateError::InvalidBitfield);
}
@@ -15,7 +13,7 @@ pub fn get_attesting_indices<T: EthSpec>(
for (i, validator_index) in committee.iter().enumerate() {
if let Ok(true) = bitlist.get(i) {
indices.push(*validator_index)
indices.push(*validator_index as u64)
}
}
@@ -23,3 +21,12 @@ pub fn get_attesting_indices<T: EthSpec>(
Ok(indices)
}
/// Shortcut for getting the attesting indices while fetching the committee from the state's cache.
pub fn get_attesting_indices_from_state<T: EthSpec>(
state: &BeaconState<T>,
att: &Attestation<T>,
) -> Result<Vec<u64>, BeaconStateError> {
let committee = state.get_beacon_committee(att.data.slot, att.data.index)?;
get_attesting_indices::<T>(committee.committee, &att.aggregation_bits)
}

View File

@@ -14,9 +14,7 @@ pub fn get_indexed_attestation<T: EthSpec>(
let attesting_indices = get_attesting_indices::<T>(committee, &attestation.aggregation_bits)?;
Ok(IndexedAttestation {
attesting_indices: VariableList::new(
attesting_indices.into_iter().map(|x| x as u64).collect(),
)?,
attesting_indices: VariableList::new(attesting_indices)?,
data: attestation.data.clone(),
signature: attestation.signature.clone(),
})

View File

@@ -10,7 +10,7 @@ pub mod base;
pub use deposit_data_tree::DepositDataTree;
pub use get_attestation_participation::get_attestation_participation_flag_indices;
pub use get_attesting_indices::get_attesting_indices;
pub use get_attesting_indices::{get_attesting_indices, get_attesting_indices_from_state};
pub use get_indexed_attestation::get_indexed_attestation;
pub use initiate_validator_exit::initiate_validator_exit;
pub use slash_validator::slash_validator;

View File

@@ -41,7 +41,7 @@ use arbitrary::Arbitrary;
/// The strategy to be used when validating the block's signatures.
#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))]
#[derive(PartialEq, Clone, Copy)]
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum BlockSignatureStrategy {
/// Do not validate any signature. Use with caution.
NoVerification,

View File

@@ -1,4 +1,4 @@
use crate::common::{altair::get_base_reward_per_increment, decrease_balance, increase_balance};
use crate::common::{altair::BaseRewardPerIncrement, decrease_balance, increase_balance};
use crate::per_block_processing::errors::{BlockProcessingError, SyncAggregateInvalid};
use crate::{signature_sets::sync_aggregate_signature_set, VerifySignatures};
use safe_arith::SafeArith;
@@ -72,7 +72,8 @@ pub fn compute_sync_aggregate_rewards<T: EthSpec>(
let total_active_balance = state.get_total_active_balance()?;
let total_active_increments =
total_active_balance.safe_div(spec.effective_balance_increment)?;
let total_base_rewards = get_base_reward_per_increment(total_active_balance, spec)?
let total_base_rewards = BaseRewardPerIncrement::new(total_active_balance, spec)?
.as_u64()
.safe_mul(total_active_increments)?;
let max_participant_rewards = total_base_rewards
.safe_mul(SYNC_REWARD_WEIGHT)?

View File

@@ -1,7 +1,8 @@
use super::*;
use crate::common::{
altair::get_base_reward, get_attestation_participation_flag_indices, increase_balance,
initiate_validator_exit, slash_validator,
altair::{get_base_reward, BaseRewardPerIncrement},
get_attestation_participation_flag_indices, increase_balance, initiate_validator_exit,
slash_validator,
};
use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex};
use crate::VerifySignatures;
@@ -129,6 +130,7 @@ pub mod altair {
// Update epoch participation flags.
let total_active_balance = state.get_total_active_balance()?;
let base_reward_per_increment = BaseRewardPerIncrement::new(total_active_balance, spec)?;
let mut proposer_reward_numerator = 0;
for index in &indexed_attestation.attesting_indices {
let index = *index as usize;
@@ -146,7 +148,7 @@ pub mod altair {
validator_participation.add_flag(flag_index)?;
let effective_balance = state.get_validator(index)?.effective_balance;
proposer_reward_numerator.safe_add_assign(
get_base_reward(effective_balance, total_active_balance, spec)?
get_base_reward(effective_balance, base_reward_per_increment, spec)?
.safe_mul(weight)?,
)?;
}

View File

@@ -7,8 +7,8 @@ use crate::per_block_processing::errors::{
ProposerSlashingInvalid,
};
use crate::{
per_block_processing::process_operations, BlockSignatureStrategy, ConsensusContext,
VerifyBlockRoot, VerifySignatures,
per_block_processing::{process_operations, verify_exit::verify_exit},
BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures,
};
use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType};
use lazy_static::lazy_static;
@@ -27,7 +27,7 @@ lazy_static! {
static ref KEYPAIRS: Vec<Keypair> = generate_deterministic_keypairs(MAX_VALIDATOR_COUNT);
}
fn get_harness<E: EthSpec>(
async fn get_harness<E: EthSpec>(
epoch_offset: u64,
num_validators: usize,
) -> BeaconChainHarness<EphemeralHarnessType<E>> {
@@ -41,27 +41,31 @@ fn get_harness<E: EthSpec>(
.build();
let state = harness.get_current_state();
if last_slot_of_epoch > Slot::new(0) {
harness.add_attested_blocks_at_slots(
state,
Hash256::zero(),
(1..last_slot_of_epoch.as_u64())
.map(Slot::new)
.collect::<Vec<_>>()
.as_slice(),
(0..num_validators).collect::<Vec<_>>().as_slice(),
);
harness
.add_attested_blocks_at_slots(
state,
Hash256::zero(),
(1..last_slot_of_epoch.as_u64())
.map(Slot::new)
.collect::<Vec<_>>()
.as_slice(),
(0..num_validators).collect::<Vec<_>>().as_slice(),
)
.await;
}
harness
}
#[test]
fn valid_block_ok() {
#[tokio::test]
async fn valid_block_ok() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let state = harness.get_current_state();
let slot = state.slot();
let (block, mut state) = harness.make_block_return_pre_state(state, slot + Slot::new(1));
let (block, mut state) = harness
.make_block_return_pre_state(state, slot + Slot::new(1))
.await;
let mut ctxt = ConsensusContext::new(block.slot());
let result = per_block_processing(
@@ -76,15 +80,15 @@ fn valid_block_ok() {
assert!(result.is_ok());
}
#[test]
fn invalid_block_header_state_slot() {
#[tokio::test]
async fn invalid_block_header_state_slot() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let state = harness.get_current_state();
let slot = state.slot() + Slot::new(1);
let (signed_block, mut state) = harness.make_block_return_pre_state(state, slot);
let (signed_block, mut state) = harness.make_block_return_pre_state(state, slot).await;
let (mut block, signature) = signed_block.deconstruct();
*block.slot_mut() = slot + Slot::new(1);
@@ -106,15 +110,17 @@ fn invalid_block_header_state_slot() {
);
}
#[test]
fn invalid_parent_block_root() {
#[tokio::test]
async fn invalid_parent_block_root() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let state = harness.get_current_state();
let slot = state.slot();
let (signed_block, mut state) = harness.make_block_return_pre_state(state, slot + Slot::new(1));
let (signed_block, mut state) = harness
.make_block_return_pre_state(state, slot + Slot::new(1))
.await;
let (mut block, signature) = signed_block.deconstruct();
*block.parent_root_mut() = Hash256::from([0xAA; 32]);
@@ -139,14 +145,16 @@ fn invalid_parent_block_root() {
);
}
#[test]
fn invalid_block_signature() {
#[tokio::test]
async fn invalid_block_signature() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let state = harness.get_current_state();
let slot = state.slot();
let (signed_block, mut state) = harness.make_block_return_pre_state(state, slot + Slot::new(1));
let (signed_block, mut state) = harness
.make_block_return_pre_state(state, slot + Slot::new(1))
.await;
let (block, _) = signed_block.deconstruct();
let mut ctxt = ConsensusContext::new(block.slot());
@@ -168,17 +176,19 @@ fn invalid_block_signature() {
);
}
#[test]
fn invalid_randao_reveal_signature() {
#[tokio::test]
async fn invalid_randao_reveal_signature() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let state = harness.get_current_state();
let slot = state.slot();
let (signed_block, mut state) = harness.make_block_with_modifier(state, slot + 1, |block| {
*block.body_mut().randao_reveal_mut() = Signature::empty();
});
let (signed_block, mut state) = harness
.make_block_with_modifier(state, slot + 1, |block| {
*block.body_mut().randao_reveal_mut() = Signature::empty();
})
.await;
let mut ctxt = ConsensusContext::new(signed_block.slot());
let result = per_block_processing(
@@ -194,16 +204,22 @@ fn invalid_randao_reveal_signature() {
assert_eq!(result, Err(BlockProcessingError::RandaoSignatureInvalid));
}
#[test]
fn valid_4_deposits() {
#[tokio::test]
async fn valid_4_deposits() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let (deposits, state) = harness.make_deposits(&mut state, 4, None, None);
let deposits = VariableList::from(deposits);
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
*head_block.to_mut().body_mut().deposits_mut() = deposits;
let result = process_operations::process_deposits(state, head_block.body().deposits(), &spec);
@@ -212,16 +228,22 @@ fn valid_4_deposits() {
assert_eq!(result, Ok(()));
}
#[test]
fn invalid_deposit_deposit_count_too_big() {
#[tokio::test]
async fn invalid_deposit_deposit_count_too_big() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let (deposits, state) = harness.make_deposits(&mut state, 1, None, None);
let deposits = VariableList::from(deposits);
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
*head_block.to_mut().body_mut().deposits_mut() = deposits;
let big_deposit_count = NUM_DEPOSITS + 1;
@@ -238,16 +260,22 @@ fn invalid_deposit_deposit_count_too_big() {
);
}
#[test]
fn invalid_deposit_count_too_small() {
#[tokio::test]
async fn invalid_deposit_count_too_small() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let (deposits, state) = harness.make_deposits(&mut state, 1, None, None);
let deposits = VariableList::from(deposits);
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
*head_block.to_mut().body_mut().deposits_mut() = deposits;
let small_deposit_count = NUM_DEPOSITS - 1;
@@ -264,16 +292,22 @@ fn invalid_deposit_count_too_small() {
);
}
#[test]
fn invalid_deposit_bad_merkle_proof() {
#[tokio::test]
async fn invalid_deposit_bad_merkle_proof() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let (deposits, state) = harness.make_deposits(&mut state, 1, None, None);
let deposits = VariableList::from(deposits);
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
*head_block.to_mut().body_mut().deposits_mut() = deposits;
let bad_index = state.eth1_deposit_index() as usize;
@@ -292,17 +326,23 @@ fn invalid_deposit_bad_merkle_proof() {
);
}
#[test]
fn invalid_deposit_wrong_sig() {
#[tokio::test]
async fn invalid_deposit_wrong_sig() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let (deposits, state) =
harness.make_deposits(&mut state, 1, None, Some(SignatureBytes::empty()));
let deposits = VariableList::from(deposits);
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
*head_block.to_mut().body_mut().deposits_mut() = deposits;
let result = process_operations::process_deposits(state, head_block.body().deposits(), &spec);
@@ -310,17 +350,23 @@ fn invalid_deposit_wrong_sig() {
assert_eq!(result, Ok(()));
}
#[test]
fn invalid_deposit_invalid_pub_key() {
#[tokio::test]
async fn invalid_deposit_invalid_pub_key() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let (deposits, state) =
harness.make_deposits(&mut state, 1, Some(PublicKeyBytes::empty()), None);
let deposits = VariableList::from(deposits);
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
*head_block.to_mut().body_mut().deposits_mut() = deposits;
let result = process_operations::process_deposits(state, head_block.body().deposits(), &spec);
@@ -329,13 +375,19 @@ fn invalid_deposit_invalid_pub_key() {
assert_eq!(result, Ok(()));
}
#[test]
fn invalid_attestation_no_committee_for_index() {
#[tokio::test]
async fn invalid_attestation_no_committee_for_index() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
head_block.to_mut().body_mut().attestations_mut()[0]
.data
.index += 1;
@@ -347,7 +399,7 @@ fn invalid_attestation_no_committee_for_index() {
&spec,
);
// Expecting NoCommitee because we manually set the attestation's index to be invalid
// Expecting NoCommittee because we manually set the attestation's index to be invalid
assert_eq!(
result,
Err(BlockProcessingError::AttestationInvalid {
@@ -357,13 +409,19 @@ fn invalid_attestation_no_committee_for_index() {
);
}
#[test]
fn invalid_attestation_wrong_justified_checkpoint() {
#[tokio::test]
async fn invalid_attestation_wrong_justified_checkpoint() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
let old_justified_checkpoint = head_block.body().attestations()[0].data.source;
let mut new_justified_checkpoint = old_justified_checkpoint;
new_justified_checkpoint.epoch += Epoch::new(1);
@@ -394,13 +452,19 @@ fn invalid_attestation_wrong_justified_checkpoint() {
);
}
#[test]
fn invalid_attestation_bad_aggregation_bitfield_len() {
#[tokio::test]
async fn invalid_attestation_bad_aggregation_bitfield_len() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
head_block.to_mut().body_mut().attestations_mut()[0].aggregation_bits =
Bitfield::with_capacity(spec.target_committee_size).unwrap();
@@ -412,7 +476,7 @@ fn invalid_attestation_bad_aggregation_bitfield_len() {
&spec,
);
// Expecting InvalidBitfield because the size of the aggregation_bitfield is bigger than the commitee size.
// Expecting InvalidBitfield because the size of the aggregation_bitfield is bigger than the committee size.
assert_eq!(
result,
Err(BlockProcessingError::BeaconStateError(
@@ -421,13 +485,19 @@ fn invalid_attestation_bad_aggregation_bitfield_len() {
);
}
#[test]
fn invalid_attestation_bad_signature() {
#[tokio::test]
async fn invalid_attestation_bad_signature() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, 97); // minimal number of required validators for this test
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, 97).await; // minimal number of required validators for this test
let mut state = harness.get_current_state();
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
head_block.to_mut().body_mut().attestations_mut()[0].signature = AggregateSignature::empty();
let result = process_operations::process_attestations(
@@ -449,13 +519,19 @@ fn invalid_attestation_bad_signature() {
);
}
#[test]
fn invalid_attestation_included_too_early() {
#[tokio::test]
async fn invalid_attestation_included_too_early() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
let new_attesation_slot = head_block.body().attestations()[0].data.slot
+ Slot::new(MainnetEthSpec::slots_per_epoch());
head_block.to_mut().body_mut().attestations_mut()[0]
@@ -484,14 +560,20 @@ fn invalid_attestation_included_too_early() {
);
}
#[test]
fn invalid_attestation_included_too_late() {
#[tokio::test]
async fn invalid_attestation_included_too_late() {
let spec = MainnetEthSpec::default_spec();
// note to maintainer: might need to increase validator count if we get NoCommittee
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
let new_attesation_slot = head_block.body().attestations()[0].data.slot
- Slot::new(MainnetEthSpec::slots_per_epoch());
head_block.to_mut().body_mut().attestations_mut()[0]
@@ -517,14 +599,20 @@ fn invalid_attestation_included_too_late() {
);
}
#[test]
fn invalid_attestation_target_epoch_slot_mismatch() {
#[tokio::test]
async fn invalid_attestation_target_epoch_slot_mismatch() {
let spec = MainnetEthSpec::default_spec();
// note to maintainer: might need to increase validator count if we get NoCommittee
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut state = harness.get_current_state();
let mut head_block = harness.chain.head_beacon_block().unwrap().deconstruct().0;
let mut head_block = harness
.chain
.head_beacon_block()
.as_ref()
.clone()
.deconstruct()
.0;
head_block.to_mut().body_mut().attestations_mut()[0]
.data
.target
@@ -549,10 +637,10 @@ fn invalid_attestation_target_epoch_slot_mismatch() {
);
}
#[test]
fn valid_insert_attester_slashing() {
#[tokio::test]
async fn valid_insert_attester_slashing() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let attester_slashing = harness.make_attester_slashing(vec![1, 2]);
@@ -568,10 +656,10 @@ fn valid_insert_attester_slashing() {
assert_eq!(result, Ok(()));
}
#[test]
fn invalid_attester_slashing_not_slashable() {
#[tokio::test]
async fn invalid_attester_slashing_not_slashable() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]);
attester_slashing.attestation_1 = attester_slashing.attestation_2.clone();
@@ -594,10 +682,10 @@ fn invalid_attester_slashing_not_slashable() {
);
}
#[test]
fn invalid_attester_slashing_1_invalid() {
#[tokio::test]
async fn invalid_attester_slashing_1_invalid() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]);
attester_slashing.attestation_1.attesting_indices = VariableList::from(vec![2, 1]);
@@ -623,10 +711,10 @@ fn invalid_attester_slashing_1_invalid() {
);
}
#[test]
fn invalid_attester_slashing_2_invalid() {
#[tokio::test]
async fn invalid_attester_slashing_2_invalid() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]);
attester_slashing.attestation_2.attesting_indices = VariableList::from(vec![2, 1]);
@@ -652,10 +740,10 @@ fn invalid_attester_slashing_2_invalid() {
);
}
#[test]
fn valid_insert_proposer_slashing() {
#[tokio::test]
async fn valid_insert_proposer_slashing() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let proposer_slashing = harness.make_proposer_slashing(1);
let mut state = harness.get_current_state();
let result = process_operations::process_proposer_slashings(
@@ -668,10 +756,10 @@ fn valid_insert_proposer_slashing() {
assert!(result.is_ok());
}
#[test]
fn invalid_proposer_slashing_proposals_identical() {
#[tokio::test]
async fn invalid_proposer_slashing_proposals_identical() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut proposer_slashing = harness.make_proposer_slashing(1);
proposer_slashing.signed_header_1.message = proposer_slashing.signed_header_2.message.clone();
@@ -694,10 +782,10 @@ fn invalid_proposer_slashing_proposals_identical() {
);
}
#[test]
fn invalid_proposer_slashing_proposer_unknown() {
#[tokio::test]
async fn invalid_proposer_slashing_proposer_unknown() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut proposer_slashing = harness.make_proposer_slashing(1);
proposer_slashing.signed_header_1.message.proposer_index = 3_141_592;
@@ -721,10 +809,10 @@ fn invalid_proposer_slashing_proposer_unknown() {
);
}
#[test]
fn invalid_proposer_slashing_duplicate_slashing() {
#[tokio::test]
async fn invalid_proposer_slashing_duplicate_slashing() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let proposer_slashing = harness.make_proposer_slashing(1);
let mut state = harness.get_current_state();
@@ -752,10 +840,10 @@ fn invalid_proposer_slashing_duplicate_slashing() {
);
}
#[test]
fn invalid_bad_proposal_1_signature() {
#[tokio::test]
async fn invalid_bad_proposal_1_signature() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut proposer_slashing = harness.make_proposer_slashing(1);
proposer_slashing.signed_header_1.signature = Signature::empty();
let mut state = harness.get_current_state();
@@ -776,10 +864,10 @@ fn invalid_bad_proposal_1_signature() {
);
}
#[test]
fn invalid_bad_proposal_2_signature() {
#[tokio::test]
async fn invalid_bad_proposal_2_signature() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut proposer_slashing = harness.make_proposer_slashing(1);
proposer_slashing.signed_header_2.signature = Signature::empty();
let mut state = harness.get_current_state();
@@ -800,10 +888,10 @@ fn invalid_bad_proposal_2_signature() {
);
}
#[test]
fn invalid_proposer_slashing_proposal_epoch_mismatch() {
#[tokio::test]
async fn invalid_proposer_slashing_proposal_epoch_mismatch() {
let spec = MainnetEthSpec::default_spec();
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT);
let harness = get_harness::<MainnetEthSpec>(EPOCH_OFFSET, VALIDATOR_COUNT).await;
let mut proposer_slashing = harness.make_proposer_slashing(1);
proposer_slashing.signed_header_1.message.slot = Slot::new(0);
proposer_slashing.signed_header_2.message.slot = Slot::new(128);
@@ -827,3 +915,70 @@ fn invalid_proposer_slashing_proposal_epoch_mismatch() {
})
);
}
#[tokio::test]
async fn fork_spanning_exit() {
let mut spec = MainnetEthSpec::default_spec();
let slots_per_epoch = MainnetEthSpec::slots_per_epoch();
spec.altair_fork_epoch = Some(Epoch::new(2));
spec.bellatrix_fork_epoch = Some(Epoch::new(4));
spec.shard_committee_period = 0;
let harness = BeaconChainHarness::builder(MainnetEthSpec::default())
.spec(spec.clone())
.deterministic_keypairs(VALIDATOR_COUNT)
.mock_execution_layer()
.fresh_ephemeral_store()
.build();
harness.extend_to_slot(slots_per_epoch.into()).await;
/*
* Produce an exit *before* Altair.
*/
let signed_exit = harness.make_voluntary_exit(0, Epoch::new(1));
assert!(signed_exit.message.epoch < spec.altair_fork_epoch.unwrap());
/*
* Ensure the exit verifies before Altair.
*/
let head = harness.chain.canonical_head.cached_head();
let head_state = &head.snapshot.beacon_state;
assert!(head_state.current_epoch() < spec.altair_fork_epoch.unwrap());
verify_exit(head_state, &signed_exit, VerifySignatures::True, &spec)
.expect("phase0 exit verifies against phase0 state");
/*
* Ensure the exit verifies after Altair.
*/
harness
.extend_to_slot(spec.altair_fork_epoch.unwrap().start_slot(slots_per_epoch))
.await;
let head = harness.chain.canonical_head.cached_head();
let head_state = &head.snapshot.beacon_state;
assert!(head_state.current_epoch() >= spec.altair_fork_epoch.unwrap());
assert!(head_state.current_epoch() < spec.bellatrix_fork_epoch.unwrap());
verify_exit(head_state, &signed_exit, VerifySignatures::True, &spec)
.expect("phase0 exit verifies against altair state");
/*
* Ensure the exit no longer verifies after Bellatrix.
*/
harness
.extend_to_slot(
spec.bellatrix_fork_epoch
.unwrap()
.start_slot(slots_per_epoch),
)
.await;
let head = harness.chain.canonical_head.cached_head();
let head_state = &head.snapshot.beacon_state;
assert!(head_state.current_epoch() >= spec.bellatrix_fork_epoch.unwrap());
verify_exit(head_state, &signed_exit, VerifySignatures::True, &spec)
.expect_err("phase0 exit does not verify against bellatrix state");
}

View File

@@ -3,6 +3,7 @@
use crate::metrics;
pub use epoch_processing_summary::EpochProcessingSummary;
use errors::EpochProcessingError as Error;
pub use justification_and_finalization_state::JustificationAndFinalizationState;
pub use registry_updates::process_registry_updates;
use safe_arith::SafeArith;
pub use slashings::process_slashings;
@@ -15,6 +16,7 @@ pub mod effective_balance_updates;
pub mod epoch_processing_summary;
pub mod errors;
pub mod historical_roots_update;
pub mod justification_and_finalization_state;
pub mod registry_updates;
pub mod resets;
pub mod slashings;

View File

@@ -33,7 +33,9 @@ pub fn process_epoch<T: EthSpec>(
let sync_committee = state.current_sync_committee()?.clone();
// Justification and finalization.
process_justification_and_finalization(state, &participation_cache)?;
let justification_and_finalization_state =
process_justification_and_finalization(state, &participation_cache)?;
justification_and_finalization_state.apply_changes_to_state(state);
process_inactivity_updates(state, &mut participation_cache, spec)?;

View File

@@ -12,6 +12,7 @@ pub fn process_inactivity_updates<T: EthSpec>(
participation_cache: &mut ParticipationCache,
spec: &ChainSpec,
) -> Result<(), EpochProcessingError> {
let previous_epoch = state.previous_epoch();
// Score updates based on previous epoch participation, skip genesis epoch
if state.current_epoch() == T::genesis_epoch() {
return Ok(());
@@ -25,7 +26,7 @@ pub fn process_inactivity_updates<T: EthSpec>(
return Ok(());
}
let is_in_inactivity_leak = state.is_in_inactivity_leak(spec);
let is_in_inactivity_leak = state.is_in_inactivity_leak(previous_epoch, spec);
let mut inactivity_scores = state.inactivity_scores_mut()?.iter_cow();

View File

@@ -1,23 +1,27 @@
use super::ParticipationCache;
use crate::per_epoch_processing::weigh_justification_and_finalization;
use crate::per_epoch_processing::Error;
use crate::per_epoch_processing::{
weigh_justification_and_finalization, JustificationAndFinalizationState,
};
use safe_arith::SafeArith;
use types::{BeaconState, EthSpec};
/// Update the justified and finalized checkpoints for matching target attestations.
pub fn process_justification_and_finalization<T: EthSpec>(
state: &mut BeaconState<T>,
state: &BeaconState<T>,
participation_cache: &ParticipationCache,
) -> Result<(), Error> {
) -> Result<JustificationAndFinalizationState<T>, Error> {
let justification_and_finalization_state = JustificationAndFinalizationState::new(state);
if state.current_epoch() <= T::genesis_epoch().safe_add(1)? {
return Ok(());
return Ok(justification_and_finalization_state);
}
let total_active_balance = participation_cache.current_epoch_total_active_balance();
let previous_target_balance = participation_cache.previous_epoch_target_attesting_balance()?;
let current_target_balance = participation_cache.current_epoch_target_attesting_balance()?;
weigh_justification_and_finalization(
state,
justification_and_finalization_state,
total_active_balance,
previous_target_balance,
current_target_balance,

View File

@@ -11,7 +11,7 @@
//! Additionally, this cache is returned from the `altair::process_epoch` function and can be used
//! to get useful summaries about the validator participation in an epoch.
use crate::common::altair::get_base_reward;
use crate::common::altair::{get_base_reward, BaseRewardPerIncrement};
use safe_arith::{ArithError, SafeArith};
use types::milhouse::update_map::{MaxMap, UpdateMap};
use types::{
@@ -31,6 +31,7 @@ pub enum Error {
MissingValidator(usize),
BeaconState(BeaconStateError),
Arith(ArithError),
InvalidValidatorIndex(usize),
}
impl From<BeaconStateError> for Error {
@@ -115,11 +116,13 @@ impl SingleEpochParticipationCache {
val_index: usize,
validator: &Validator,
epoch_participation: &ParticipationFlags,
state: &BeaconState<T>,
// FIXME(sproul): remove state argument
_state: &BeaconState<T>,
current_epoch: Epoch,
relative_epoch: RelativeEpoch,
) -> Result<(), BeaconStateError> {
// Sanity check to ensure the validator is active.
let epoch = relative_epoch.into_epoch(state.current_epoch());
let epoch = relative_epoch.into_epoch(current_epoch);
if !validator.is_active_at(epoch) {
return Err(BeaconStateError::ValidatorIsInactive { val_index });
}
@@ -220,6 +223,8 @@ impl ParticipationCache {
let mut validators = ValidatorInfoCache::new(state.validators().len());
let current_epoch_total_active_balance = state.get_total_active_balance()?;
let base_reward_per_increment =
BaseRewardPerIncrement::new(current_epoch_total_active_balance, spec)?;
// Contains the set of validators which are either:
//
@@ -257,7 +262,7 @@ impl ParticipationCache {
for (val_index, (((val, curr_epoch_flags), prev_epoch_flags), inactivity_score)) in iter {
let is_active_current_epoch = val.is_active_at(current_epoch);
let is_active_previous_epoch = val.is_active_at(previous_epoch);
let is_eligible = state.is_eligible_validator(val);
let is_eligible = state.is_eligible_validator(previous_epoch, val);
if is_active_current_epoch {
current_epoch_participation.process_active_validator(
@@ -265,6 +270,7 @@ impl ParticipationCache {
val,
curr_epoch_flags,
state,
current_epoch,
RelativeEpoch::Current,
)?;
}
@@ -277,6 +283,7 @@ impl ParticipationCache {
val,
prev_epoch_flags,
state,
current_epoch,
RelativeEpoch::Previous,
)?;
}
@@ -326,7 +333,7 @@ impl ParticipationCache {
if is_eligible || is_active_current_epoch {
let effective_balance = val.effective_balance;
let base_reward =
get_base_reward(effective_balance, current_epoch_total_active_balance, spec)?;
get_base_reward(effective_balance, base_reward_per_increment, spec)?;
validator_info.base_reward = base_reward;
validators.info[val_index] = Some(validator_info);
}

View File

@@ -76,6 +76,7 @@ pub fn get_flag_index_deltas<T: EthSpec>(
let unslashed_participating_increments =
unslashed_participating_balance.safe_div(spec.effective_balance_increment)?;
let active_increments = total_active_balance.safe_div(spec.effective_balance_increment)?;
let previous_epoch = state.previous_epoch();
for &index in participation_cache.eligible_validator_indices() {
let validator = participation_cache.get_validator(index)?;
@@ -84,7 +85,7 @@ pub fn get_flag_index_deltas<T: EthSpec>(
let mut delta = Delta::default();
if validator.is_unslashed_participating_index(flag_index)? {
if !state.is_in_inactivity_leak(spec) {
if !state.is_in_inactivity_leak(previous_epoch, spec) {
let reward_numerator = base_reward
.safe_mul(weight)?
.safe_mul(unslashed_participating_increments)?;

View File

@@ -31,7 +31,9 @@ pub fn process_epoch<T: EthSpec>(
validator_statuses.process_attestations(state)?;
// Justification and finalization.
process_justification_and_finalization(state, &validator_statuses.total_balances, spec)?;
let justification_and_finalization_state =
process_justification_and_finalization(state, &validator_statuses.total_balances, spec)?;
justification_and_finalization_state.apply_changes_to_state(state);
// Rewards and Penalties.
process_rewards_and_penalties(state, &mut validator_statuses, spec)?;

View File

@@ -1,21 +1,25 @@
use crate::per_epoch_processing::base::TotalBalances;
use crate::per_epoch_processing::weigh_justification_and_finalization;
use crate::per_epoch_processing::Error;
use crate::per_epoch_processing::{
weigh_justification_and_finalization, JustificationAndFinalizationState,
};
use safe_arith::SafeArith;
use types::{BeaconState, ChainSpec, EthSpec};
/// Update the justified and finalized checkpoints for matching target attestations.
pub fn process_justification_and_finalization<T: EthSpec>(
state: &mut BeaconState<T>,
state: &BeaconState<T>,
total_balances: &TotalBalances,
_spec: &ChainSpec,
) -> Result<(), Error> {
) -> Result<JustificationAndFinalizationState<T>, Error> {
let justification_and_finalization_state = JustificationAndFinalizationState::new(state);
if state.current_epoch() <= T::genesis_epoch().safe_add(1)? {
return Ok(());
return Ok(justification_and_finalization_state);
}
weigh_justification_and_finalization(
state,
justification_and_finalization_state,
total_balances.current_epoch(),
total_balances.previous_epoch_target_attesters(),
total_balances.current_epoch_target_attesters(),

View File

@@ -78,6 +78,7 @@ pub fn get_attestation_deltas<T: EthSpec>(
validator_statuses: &ValidatorStatuses,
spec: &ChainSpec,
) -> Result<Vec<AttestationDelta>, Error> {
let previous_epoch = state.previous_epoch();
let finality_delay = state
.previous_epoch()
.safe_sub(state.finalized_checkpoint().epoch)?
@@ -94,7 +95,7 @@ pub fn get_attestation_deltas<T: EthSpec>(
// eligible.
// FIXME(sproul): this is inefficient
let full_validator = state.get_validator(index)?;
if !state.is_eligible_validator(full_validator) {
if !state.is_eligible_validator(previous_epoch, full_validator) {
continue;
}

View File

@@ -278,8 +278,8 @@ impl ValidatorStatuses {
// Loop through the participating validator indices and update the status vec.
for validator_index in attesting_indices {
self.statuses
.get_mut(validator_index)
.ok_or(BeaconStateError::UnknownValidator(validator_index))?
.get_mut(validator_index as usize)
.ok_or(BeaconStateError::UnknownValidator(validator_index as usize))?
.update(&status);
}
}

View File

@@ -127,7 +127,12 @@ impl<T: EthSpec> EpochProcessingSummary<T> {
EpochProcessingSummary::Altair {
participation_cache,
..
} => participation_cache.is_current_epoch_timely_target_attester(val_index),
} => participation_cache
.is_current_epoch_timely_target_attester(val_index)
.or_else(|e| match e {
ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false),
e => Err(e),
}),
}
}
@@ -218,7 +223,12 @@ impl<T: EthSpec> EpochProcessingSummary<T> {
EpochProcessingSummary::Altair {
participation_cache,
..
} => participation_cache.is_previous_epoch_timely_target_attester(val_index),
} => participation_cache
.is_previous_epoch_timely_target_attester(val_index)
.or_else(|e| match e {
ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false),
e => Err(e),
}),
}
}
@@ -244,7 +254,12 @@ impl<T: EthSpec> EpochProcessingSummary<T> {
EpochProcessingSummary::Altair {
participation_cache,
..
} => participation_cache.is_previous_epoch_timely_head_attester(val_index),
} => participation_cache
.is_previous_epoch_timely_head_attester(val_index)
.or_else(|e| match e {
ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false),
e => Err(e),
}),
}
}
@@ -270,7 +285,12 @@ impl<T: EthSpec> EpochProcessingSummary<T> {
EpochProcessingSummary::Altair {
participation_cache,
..
} => participation_cache.is_previous_epoch_timely_source_attester(val_index),
} => participation_cache
.is_previous_epoch_timely_source_attester(val_index)
.or_else(|e| match e {
ParticipationCacheError::InvalidValidatorIndex(_) => Ok(false),
e => Err(e),
}),
}
}

View File

@@ -0,0 +1,115 @@
use types::{BeaconState, BeaconStateError, BitVector, Checkpoint, Epoch, EthSpec, Hash256};
/// This is a subset of the `BeaconState` which is used to compute justification and finality
/// without modifying the `BeaconState`.
///
/// A `JustificationAndFinalizationState` can be created from a `BeaconState` to compute
/// justification/finality changes and then applied to a `BeaconState` to enshrine those changes.
#[must_use = "this value must be applied to a state or explicitly dropped"]
pub struct JustificationAndFinalizationState<T: EthSpec> {
/*
* Immutable fields.
*/
previous_epoch: Epoch,
previous_epoch_target_root: Result<Hash256, BeaconStateError>,
current_epoch: Epoch,
current_epoch_target_root: Result<Hash256, BeaconStateError>,
/*
* Mutable fields.
*/
previous_justified_checkpoint: Checkpoint,
current_justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
justification_bits: BitVector<T::JustificationBitsLength>,
}
impl<T: EthSpec> JustificationAndFinalizationState<T> {
pub fn new(state: &BeaconState<T>) -> Self {
let previous_epoch = state.previous_epoch();
let current_epoch = state.current_epoch();
Self {
previous_epoch,
previous_epoch_target_root: state.get_block_root_at_epoch(previous_epoch).copied(),
current_epoch,
current_epoch_target_root: state.get_block_root_at_epoch(current_epoch).copied(),
previous_justified_checkpoint: state.previous_justified_checkpoint(),
current_justified_checkpoint: state.current_justified_checkpoint(),
finalized_checkpoint: state.finalized_checkpoint(),
justification_bits: state.justification_bits().clone(),
}
}
pub fn apply_changes_to_state(self, state: &mut BeaconState<T>) {
let Self {
/*
* Immutable fields do not need to be used.
*/
previous_epoch: _,
previous_epoch_target_root: _,
current_epoch: _,
current_epoch_target_root: _,
/*
* Mutable fields *must* be used.
*/
previous_justified_checkpoint,
current_justified_checkpoint,
finalized_checkpoint,
justification_bits,
} = self;
*state.previous_justified_checkpoint_mut() = previous_justified_checkpoint;
*state.current_justified_checkpoint_mut() = current_justified_checkpoint;
*state.finalized_checkpoint_mut() = finalized_checkpoint;
*state.justification_bits_mut() = justification_bits;
}
pub fn previous_epoch(&self) -> Epoch {
self.previous_epoch
}
pub fn current_epoch(&self) -> Epoch {
self.current_epoch
}
pub fn get_block_root_at_epoch(&self, epoch: Epoch) -> Result<Hash256, BeaconStateError> {
if epoch == self.previous_epoch {
self.previous_epoch_target_root.clone()
} else if epoch == self.current_epoch {
self.current_epoch_target_root.clone()
} else {
Err(BeaconStateError::SlotOutOfBounds)
}
}
pub fn previous_justified_checkpoint(&self) -> Checkpoint {
self.previous_justified_checkpoint
}
pub fn previous_justified_checkpoint_mut(&mut self) -> &mut Checkpoint {
&mut self.previous_justified_checkpoint
}
pub fn current_justified_checkpoint_mut(&mut self) -> &mut Checkpoint {
&mut self.current_justified_checkpoint
}
pub fn current_justified_checkpoint(&self) -> Checkpoint {
self.current_justified_checkpoint
}
pub fn finalized_checkpoint(&self) -> Checkpoint {
self.finalized_checkpoint
}
pub fn finalized_checkpoint_mut(&mut self) -> &mut Checkpoint {
&mut self.finalized_checkpoint
}
pub fn justification_bits(&self) -> &BitVector<T::JustificationBitsLength> {
&self.justification_bits
}
pub fn justification_bits_mut(&mut self) -> &mut BitVector<T::JustificationBitsLength> {
&mut self.justification_bits
}
}

View File

@@ -6,8 +6,8 @@ use bls::Hash256;
use env_logger::{Builder, Env};
use types::Slot;
#[test]
fn runs_without_error() {
#[tokio::test]
async fn runs_without_error() {
Builder::from_env(Env::default().default_filter_or("error")).init();
let harness = BeaconChainHarness::builder(MinimalEthSpec)
@@ -22,15 +22,17 @@ fn runs_without_error() {
(MinimalEthSpec::genesis_epoch() + 4).end_slot(MinimalEthSpec::slots_per_epoch());
let state = harness.get_current_state();
harness.add_attested_blocks_at_slots(
state,
Hash256::zero(),
(1..target_slot.as_u64())
.map(Slot::new)
.collect::<Vec<_>>()
.as_slice(),
(0..8).collect::<Vec<_>>().as_slice(),
);
harness
.add_attested_blocks_at_slots(
state,
Hash256::zero(),
(1..target_slot.as_u64())
.map(Slot::new)
.collect::<Vec<_>>()
.as_slice(),
(0..8).collect::<Vec<_>>().as_slice(),
)
.await;
let mut new_head_state = harness.get_current_state();
process_epoch(&mut new_head_state, &spec).unwrap();
@@ -45,8 +47,8 @@ mod release_tests {
use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy};
use types::{Epoch, ForkName, InconsistentFork, MainnetEthSpec};
#[test]
fn altair_state_on_base_fork() {
#[tokio::test]
async fn altair_state_on_base_fork() {
let mut spec = MainnetEthSpec::default_spec();
let slots_per_epoch = MainnetEthSpec::slots_per_epoch();
// The Altair fork happens at epoch 1.
@@ -61,12 +63,14 @@ mod release_tests {
harness.advance_slot();
harness.extend_chain(
// Build out enough blocks so we get an Altair block at the very end of an epoch.
(slots_per_epoch * 2 - 1) as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
);
harness
.extend_chain(
// Build out enough blocks so we get an Altair block at the very end of an epoch.
(slots_per_epoch * 2 - 1) as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
harness.get_current_state()
};
@@ -103,8 +107,8 @@ mod release_tests {
);
}
#[test]
fn base_state_on_altair_fork() {
#[tokio::test]
async fn base_state_on_altair_fork() {
let mut spec = MainnetEthSpec::default_spec();
let slots_per_epoch = MainnetEthSpec::slots_per_epoch();
// The Altair fork never happens.
@@ -119,12 +123,14 @@ mod release_tests {
harness.advance_slot();
harness.extend_chain(
// Build out enough blocks so we get a block at the very end of an epoch.
(slots_per_epoch * 2 - 1) as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
);
harness
.extend_chain(
// Build out enough blocks so we get a block at the very end of an epoch.
(slots_per_epoch * 2 - 1) as usize,
BlockStrategy::OnCanonicalHead,
AttestationStrategy::AllValidators,
)
.await;
harness.get_current_state()
};

View File

@@ -1,16 +1,16 @@
use crate::per_epoch_processing::Error;
use crate::per_epoch_processing::{Error, JustificationAndFinalizationState};
use safe_arith::SafeArith;
use std::ops::Range;
use types::{BeaconState, Checkpoint, EthSpec};
use types::{Checkpoint, EthSpec};
/// Update the justified and finalized checkpoints for matching target attestations.
#[allow(clippy::if_same_then_else)] // For readability and consistency with spec.
pub fn weigh_justification_and_finalization<T: EthSpec>(
state: &mut BeaconState<T>,
mut state: JustificationAndFinalizationState<T>,
total_active_balance: u64,
previous_target_balance: u64,
current_target_balance: u64,
) -> Result<(), Error> {
) -> Result<JustificationAndFinalizationState<T>, Error> {
let previous_epoch = state.previous_epoch();
let current_epoch = state.current_epoch();
@@ -24,7 +24,7 @@ pub fn weigh_justification_and_finalization<T: EthSpec>(
if previous_target_balance.safe_mul(3)? >= total_active_balance.safe_mul(2)? {
*state.current_justified_checkpoint_mut() = Checkpoint {
epoch: previous_epoch,
root: *state.get_block_root_at_epoch(previous_epoch)?,
root: state.get_block_root_at_epoch(previous_epoch)?,
};
state.justification_bits_mut().set(1, true)?;
}
@@ -32,7 +32,7 @@ pub fn weigh_justification_and_finalization<T: EthSpec>(
if current_target_balance.safe_mul(3)? >= total_active_balance.safe_mul(2)? {
*state.current_justified_checkpoint_mut() = Checkpoint {
epoch: current_epoch,
root: *state.get_block_root_at_epoch(current_epoch)?,
root: state.get_block_root_at_epoch(current_epoch)?,
};
state.justification_bits_mut().set(0, true)?;
}
@@ -66,5 +66,5 @@ pub fn weigh_justification_and_finalization<T: EthSpec>(
*state.finalized_checkpoint_mut() = old_current_justified_checkpoint;
}
Ok(())
Ok(state)
}

View File

@@ -32,8 +32,8 @@ pub fn translate_participation<E: EthSpec>(
for index in attesting_indices {
for flag_index in &participation_flag_indices {
epoch_participation
.get_mut(index)
.ok_or(Error::UnknownValidator(index))?
.get_mut(index as usize)
.ok_or(Error::UnknownValidator(index as usize))?
.add_flag(*flag_index)?;
}
}

View File

@@ -5,36 +5,120 @@ use crate::per_block_processing::{
verify_attester_slashing, verify_exit, verify_proposer_slashing,
};
use crate::VerifySignatures;
use derivative::Derivative;
use smallvec::{smallvec, SmallVec};
use ssz::{Decode, Encode};
use ssz_derive::{Decode, Encode};
use std::marker::PhantomData;
use types::{
AttesterSlashing, BeaconState, ChainSpec, EthSpec, ProposerSlashing, SignedVoluntaryExit,
AttesterSlashing, BeaconState, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, ProposerSlashing,
SignedVoluntaryExit,
};
const MAX_FORKS_VERIFIED_AGAINST: usize = 2;
/// Wrapper around an operation type that acts as proof that its signature has been checked.
///
/// The inner field is private, meaning instances of this type can only be constructed
/// The inner `op` field is private, meaning instances of this type can only be constructed
/// by calling `validate`.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SigVerifiedOp<T>(T);
#[derive(Derivative, Debug, Clone, Encode, Decode)]
#[derivative(
PartialEq,
Eq,
Hash(bound = "T: Encode + Decode + std::hash::Hash, E: EthSpec")
)]
pub struct SigVerifiedOp<T: Encode + Decode, E: EthSpec> {
op: T,
verified_against: VerifiedAgainst,
#[ssz(skip_serializing, skip_deserializing)]
_phantom: PhantomData<E>,
}
/// Information about the fork versions that this message was verified against.
///
/// In general it is not safe to assume that a `SigVerifiedOp` constructed at some point in the past
/// will continue to be valid in the presence of a changing `state.fork()`. The reason for this
/// is that the fork versions that the message's epochs map to might change.
///
/// For example a proposer slashing at a phase0 slot verified against an Altair state will use
/// the phase0 fork version, but will become invalid once the Bellatrix fork occurs because that
/// slot will start to map to the Altair fork version. This is because `Fork::get_fork_version` only
/// remembers the most recent two forks.
///
/// In the other direction, a proposer slashing at a Bellatrix slot verified against an Altair state
/// will use the Altair fork version, but will become invalid once the Bellatrix fork occurs because
/// that slot will start to map to the Bellatrix fork version.
///
/// We need to store multiple `ForkVersion`s because attester slashings contain two indexed
/// attestations which may be signed using different versions.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Encode, Decode)]
pub struct VerifiedAgainst {
fork_versions: SmallVec<[ForkVersion; MAX_FORKS_VERIFIED_AGAINST]>,
}
impl<T, E> SigVerifiedOp<T, E>
where
T: VerifyOperation<E>,
E: EthSpec,
{
/// This function must be private because it assumes that `op` has already been verified.
fn new(op: T, state: &BeaconState<E>) -> Self {
let verified_against = VerifiedAgainst {
fork_versions: op
.verification_epochs()
.into_iter()
.map(|epoch| state.fork().get_fork_version(epoch))
.collect(),
};
SigVerifiedOp {
op,
verified_against,
_phantom: PhantomData,
}
}
impl<T> SigVerifiedOp<T> {
pub fn into_inner(self) -> T {
self.0
self.op
}
pub fn as_inner(&self) -> &T {
&self.0
&self.op
}
pub fn signature_is_still_valid(&self, current_fork: &Fork) -> bool {
self.as_inner()
.verification_epochs()
.into_iter()
.zip(self.verified_against.fork_versions.iter())
.all(|(epoch, verified_fork_version)| {
current_fork.get_fork_version(epoch) == *verified_fork_version
})
}
/// Return one of the fork versions this message was verified against.
///
/// This is only required for the v12 schema downgrade and can be deleted once all nodes
/// are upgraded to v12.
pub fn first_fork_verified_against(&self) -> Option<ForkVersion> {
self.verified_against.fork_versions.first().copied()
}
}
/// Trait for operations that can be verified and transformed into a `SigVerifiedOp`.
pub trait VerifyOperation<E: EthSpec>: Sized {
pub trait VerifyOperation<E: EthSpec>: Encode + Decode + Sized {
type Error;
fn validate(
self,
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<SigVerifiedOp<Self>, Self::Error>;
) -> Result<SigVerifiedOp<Self, E>, Self::Error>;
/// Return the epochs at which parts of this message were verified.
///
/// These need to map 1-to-1 to the `SigVerifiedOp::verified_against` for this type.
fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]>;
}
impl<E: EthSpec> VerifyOperation<E> for SignedVoluntaryExit {
@@ -44,9 +128,14 @@ impl<E: EthSpec> VerifyOperation<E> for SignedVoluntaryExit {
self,
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<SigVerifiedOp<Self>, Self::Error> {
) -> Result<SigVerifiedOp<Self, E>, Self::Error> {
verify_exit(state, &self, VerifySignatures::True, spec)?;
Ok(SigVerifiedOp(self))
Ok(SigVerifiedOp::new(self, state))
}
#[allow(clippy::integer_arithmetic)]
fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> {
smallvec![self.message.epoch]
}
}
@@ -57,9 +146,17 @@ impl<E: EthSpec> VerifyOperation<E> for AttesterSlashing<E> {
self,
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<SigVerifiedOp<Self>, Self::Error> {
) -> Result<SigVerifiedOp<Self, E>, Self::Error> {
verify_attester_slashing(state, &self, VerifySignatures::True, spec)?;
Ok(SigVerifiedOp(self))
Ok(SigVerifiedOp::new(self, state))
}
#[allow(clippy::integer_arithmetic)]
fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> {
smallvec![
self.attestation_1.data.target.epoch,
self.attestation_2.data.target.epoch
]
}
}
@@ -70,8 +167,18 @@ impl<E: EthSpec> VerifyOperation<E> for ProposerSlashing {
self,
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<SigVerifiedOp<Self>, Self::Error> {
) -> Result<SigVerifiedOp<Self, E>, Self::Error> {
verify_proposer_slashing(&self, state, VerifySignatures::True, spec)?;
Ok(SigVerifiedOp(self))
Ok(SigVerifiedOp::new(self, state))
}
#[allow(clippy::integer_arithmetic)]
fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> {
// Only need a single epoch because the slots of the two headers must be equal.
smallvec![self
.signed_header_1
.message
.slot
.epoch(E::slots_per_epoch())]
}
}

View File

@@ -17,7 +17,7 @@ fn get_harness<T: EthSpec>() -> BeaconChainHarness<EphemeralHarnessType<T>> {
}
fn build_state<T: EthSpec>() -> BeaconState<T> {
let state = get_harness::<T>().chain.head_beacon_state().unwrap();
let state = get_harness::<T>().chain.head_beacon_state_cloned();
assert_eq!(state.as_base().unwrap().validators.len(), VALIDATOR_COUNT);
assert_eq!(state.as_base().unwrap().balances.len(), VALIDATOR_COUNT);

View File

@@ -48,12 +48,15 @@ serde_json = "1.0.74"
smallvec = "1.8.0"
milhouse = { git = "https://github.com/sigp/milhouse", branch = "main" }
rpds = "0.11.0"
serde_with = "1.13.0"
maplit = "1.0.2"
[dev-dependencies]
criterion = "0.3.3"
beacon_chain = { path = "../../beacon_node/beacon_chain" }
eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" }
state_processing = { path = "../state_processing" }
tokio = "1.14.0"
[features]
default = ["sqlite", "legacy-arith"]

View File

@@ -0,0 +1,16 @@
/// This value is an application index of 0 with the bitmask applied (so it's equivalent to the bit mask).
/// Little endian hex: 0x00000001, Binary: 1000000000000000000000000
pub const APPLICATION_DOMAIN_BUILDER: u32 = 16777216;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ApplicationDomain {
Builder,
}
impl ApplicationDomain {
pub fn get_domain_constant(&self) -> u32 {
match self {
ApplicationDomain::Builder => APPLICATION_DOMAIN_BUILDER,
}
}
}

View File

@@ -38,7 +38,7 @@ use tree_hash_derive::TreeHash;
derive(Debug, PartialEq, TreeHash),
tree_hash(enum_behaviour = "transparent")
),
map_ref_into(BeaconBlockBodyRef),
map_ref_into(BeaconBlockBodyRef, BeaconBlock),
map_ref_mut_into(BeaconBlockBodyRefMut)
)]
#[derive(Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative)]
@@ -541,6 +541,50 @@ impl_from!(BeaconBlockBase, <E, FullPayload<E>>, <E, BlindedPayload<E>>, |body:
impl_from!(BeaconBlockAltair, <E, FullPayload<E>>, <E, BlindedPayload<E>>, |body: BeaconBlockBodyAltair<_, _>| body.into());
impl_from!(BeaconBlockMerge, <E, FullPayload<E>>, <E, BlindedPayload<E>>, |body: BeaconBlockBodyMerge<_, _>| body.into());
// We can clone blocks with payloads to blocks without payloads, without cloning the payload.
macro_rules! impl_clone_as_blinded {
($ty_name:ident, <$($from_params:ty),*>, <$($to_params:ty),*>) => {
impl<E: EthSpec> $ty_name<$($from_params),*>
{
pub fn clone_as_blinded(&self) -> $ty_name<$($to_params),*> {
let $ty_name {
slot,
proposer_index,
parent_root,
state_root,
body,
} = self;
$ty_name {
slot: *slot,
proposer_index: *proposer_index,
parent_root: *parent_root,
state_root: *state_root,
body: body.clone_as_blinded(),
}
}
}
}
}
impl_clone_as_blinded!(BeaconBlockBase, <E, FullPayload<E>>, <E, BlindedPayload<E>>);
impl_clone_as_blinded!(BeaconBlockAltair, <E, FullPayload<E>>, <E, BlindedPayload<E>>);
impl_clone_as_blinded!(BeaconBlockMerge, <E, FullPayload<E>>, <E, BlindedPayload<E>>);
// A reference to a full beacon block can be cloned into a blinded beacon block, without cloning the
// execution payload.
impl<'a, E: EthSpec> From<BeaconBlockRef<'a, E, FullPayload<E>>>
for BeaconBlock<E, BlindedPayload<E>>
{
fn from(
full_block: BeaconBlockRef<'a, E, FullPayload<E>>,
) -> BeaconBlock<E, BlindedPayload<E>> {
map_beacon_block_ref_into_beacon_block!(&'a _, full_block, |inner, cons| {
cons(inner.clone_as_blinded())
})
}
}
impl<E: EthSpec> From<BeaconBlock<E, FullPayload<E>>>
for (
BeaconBlock<E, BlindedPayload<E>>,
@@ -607,19 +651,17 @@ mod tests {
#[test]
fn decode_base_and_altair() {
type E = MainnetEthSpec;
let spec = E::default_spec();
let rng = &mut XorShiftRng::from_seed([42; 16]);
let fork_epoch = Epoch::from_ssz_bytes(&[7, 6, 5, 4, 3, 2, 1, 0]).unwrap();
let fork_epoch = spec.altair_fork_epoch.unwrap();
let base_epoch = fork_epoch.saturating_sub(1_u64);
let base_slot = base_epoch.end_slot(E::slots_per_epoch());
let altair_epoch = fork_epoch;
let altair_slot = altair_epoch.start_slot(E::slots_per_epoch());
let mut spec = E::default_spec();
spec.altair_fork_epoch = Some(fork_epoch);
// BeaconBlockBase
{
let good_base_block = BeaconBlock::Base(BeaconBlockBase {

View File

@@ -251,6 +251,53 @@ impl<E: EthSpec> From<BeaconBlockBodyMerge<E, FullPayload<E>>>
}
}
// We can clone a full block into a blinded block, without cloning the payload.
impl<E: EthSpec> BeaconBlockBodyBase<E, FullPayload<E>> {
pub fn clone_as_blinded(&self) -> BeaconBlockBodyBase<E, BlindedPayload<E>> {
let (block_body, _payload) = self.clone().into();
block_body
}
}
impl<E: EthSpec> BeaconBlockBodyAltair<E, FullPayload<E>> {
pub fn clone_as_blinded(&self) -> BeaconBlockBodyAltair<E, BlindedPayload<E>> {
let (block_body, _payload) = self.clone().into();
block_body
}
}
impl<E: EthSpec> BeaconBlockBodyMerge<E, FullPayload<E>> {
pub fn clone_as_blinded(&self) -> BeaconBlockBodyMerge<E, BlindedPayload<E>> {
let BeaconBlockBodyMerge {
randao_reveal,
eth1_data,
graffiti,
proposer_slashings,
attester_slashings,
attestations,
deposits,
voluntary_exits,
sync_aggregate,
execution_payload: FullPayload { execution_payload },
} = self;
BeaconBlockBodyMerge {
randao_reveal: randao_reveal.clone(),
eth1_data: eth1_data.clone(),
graffiti: *graffiti,
proposer_slashings: proposer_slashings.clone(),
attester_slashings: attester_slashings.clone(),
attestations: attestations.clone(),
deposits: deposits.clone(),
voluntary_exits: voluntary_exits.clone(),
sync_aggregate: sync_aggregate.clone(),
execution_payload: BlindedPayload {
execution_payload_header: From::from(execution_payload),
},
}
}
}
impl<E: EthSpec> From<BeaconBlockBody<E, FullPayload<E>>>
for (
BeaconBlockBody<E, BlindedPayload<E>>,

View File

@@ -985,6 +985,13 @@ impl<T: EthSpec> BeaconState<T> {
}
}
/// Return the minimum epoch for which `get_randao_mix` will return a non-error value.
pub fn min_randao_epoch(&self) -> Epoch {
self.current_epoch()
.saturating_add(1u64)
.saturating_sub(T::EpochsPerHistoricalVector::to_u64())
}
/// XOR-assigns the existing `epoch` randao mix with the hash of the `signature`.
///
/// # Errors:
@@ -1625,15 +1632,17 @@ impl<T: EthSpec> BeaconState<T> {
Ok(self.validators().tree_hash_root())
}
pub fn is_eligible_validator(&self, val: &Validator) -> bool {
let previous_epoch = self.previous_epoch();
/// Passing `previous_epoch` to this function rather than computing it internally provides
/// a tangible speed improvement in state processing.
pub fn is_eligible_validator(&self, previous_epoch: Epoch, val: &Validator) -> bool {
val.is_active_at(previous_epoch)
|| (val.slashed && previous_epoch + Epoch::new(1) < val.withdrawable_epoch)
}
pub fn is_in_inactivity_leak(&self, spec: &ChainSpec) -> bool {
(self.previous_epoch() - self.finalized_checkpoint().epoch)
> spec.min_epochs_to_inactivity_penalty
/// Passing `previous_epoch` to this function rather than computing it internally provides
/// a tangible speed improvement in state processing.
pub fn is_in_inactivity_leak(&self, previous_epoch: Epoch, spec: &ChainSpec) -> bool {
(previous_epoch - self.finalized_checkpoint().epoch) > spec.min_epochs_to_inactivity_penalty
}
/// Get the `SyncCommittee` associated with the next slot. Useful because sync committees

View File

@@ -39,8 +39,18 @@ impl CommitteeCache {
epoch: Epoch,
spec: &ChainSpec,
) -> Result<Arc<CommitteeCache>, Error> {
RelativeEpoch::from_epoch(state.current_epoch(), epoch)
.map_err(|_| Error::EpochOutOfBounds)?;
// Check that the cache is being built for an in-range epoch.
//
// We allow caches to be constructed for historic epochs, per:
//
// https://github.com/sigp/lighthouse/issues/3270
let reqd_randao_epoch = epoch
.saturating_sub(spec.min_seed_lookahead)
.saturating_sub(1u64);
if reqd_randao_epoch < state.min_randao_epoch() || epoch > state.current_epoch() + 1 {
return Err(Error::EpochOutOfBounds);
}
// May cause divide-by-zero errors.
if T::slots_per_epoch() == 0 {

View File

@@ -34,32 +34,34 @@ fn default_values() {
assert!(cache.get_beacon_committees_at_slot(Slot::new(0)).is_err());
}
fn new_state<T: EthSpec>(validator_count: usize, slot: Slot) -> BeaconState<T> {
async fn new_state<T: EthSpec>(validator_count: usize, slot: Slot) -> BeaconState<T> {
let harness = get_harness(validator_count);
let head_state = harness.get_current_state();
if slot > Slot::new(0) {
harness.add_attested_blocks_at_slots(
head_state,
Hash256::zero(),
(1..slot.as_u64())
.map(Slot::new)
.collect::<Vec<_>>()
.as_slice(),
(0..validator_count).collect::<Vec<_>>().as_slice(),
);
harness
.add_attested_blocks_at_slots(
head_state,
Hash256::zero(),
(1..=slot.as_u64())
.map(Slot::new)
.collect::<Vec<_>>()
.as_slice(),
(0..validator_count).collect::<Vec<_>>().as_slice(),
)
.await;
}
harness.get_current_state()
}
#[test]
#[tokio::test]
#[should_panic]
fn fails_without_validators() {
new_state::<MinimalEthSpec>(0, Slot::new(0));
async fn fails_without_validators() {
new_state::<MinimalEthSpec>(0, Slot::new(0)).await;
}
#[test]
fn initializes_with_the_right_epoch() {
let state = new_state::<MinimalEthSpec>(16, Slot::new(0));
#[tokio::test]
async fn initializes_with_the_right_epoch() {
let state = new_state::<MinimalEthSpec>(16, Slot::new(0)).await;
let spec = &MinimalEthSpec::default_spec();
let cache = CommitteeCache::default();
@@ -75,17 +77,20 @@ fn initializes_with_the_right_epoch() {
assert!(cache.is_initialized_at(state.next_epoch().unwrap()));
}
#[test]
fn shuffles_for_the_right_epoch() {
#[tokio::test]
async fn shuffles_for_the_right_epoch() {
let num_validators = MinimalEthSpec::minimum_validator_count() * 2;
let epoch = Epoch::new(6);
let slot = epoch.start_slot(MinimalEthSpec::slots_per_epoch());
let mut state = new_state::<MinimalEthSpec>(num_validators, slot);
let mut state = new_state::<MinimalEthSpec>(num_validators, slot).await;
let spec = &MinimalEthSpec::default_spec();
let distinct_hashes = (0..MinimalEthSpec::epochs_per_historical_vector())
.map(|i| Hash256::from_low_u64_be(i as u64));
assert_eq!(state.current_epoch(), epoch);
let distinct_hashes: Vec<Hash256> = (0..MinimalEthSpec::epochs_per_historical_vector())
.map(|i| Hash256::from_low_u64_be(i as u64))
.collect();
*state.randao_mixes_mut() = FixedVector::try_from_iter(distinct_hashes).unwrap();
@@ -121,15 +126,41 @@ fn shuffles_for_the_right_epoch() {
}
};
let cache = CommitteeCache::initialized(&state, state.current_epoch(), spec).unwrap();
assert_eq!(cache.shuffling(), shuffling_with_seed(current_seed));
assert_shuffling_positions_accurate(&cache);
// We can initialize the committee cache at recent epochs in the past, and one epoch into the
// future.
for e in (0..=epoch.as_u64() + 1).map(Epoch::new) {
let seed = state.get_seed(e, Domain::BeaconAttester, spec).unwrap();
let cache = CommitteeCache::initialized(&state, e, spec)
.unwrap_or_else(|_| panic!("failed at epoch {}", e));
assert_eq!(cache.shuffling(), shuffling_with_seed(seed));
assert_shuffling_positions_accurate(&cache);
}
let cache = CommitteeCache::initialized(&state, state.previous_epoch(), spec).unwrap();
assert_eq!(cache.shuffling(), shuffling_with_seed(previous_seed));
assert_shuffling_positions_accurate(&cache);
let cache = CommitteeCache::initialized(&state, state.next_epoch().unwrap(), spec).unwrap();
assert_eq!(cache.shuffling(), shuffling_with_seed(next_seed));
assert_shuffling_positions_accurate(&cache);
// We should *not* be able to build a committee cache for the epoch after the next epoch.
assert_eq!(
CommitteeCache::initialized(&state, epoch + 2, spec),
Err(BeaconStateError::EpochOutOfBounds)
);
}
#[tokio::test]
async fn min_randao_epoch_correct() {
let num_validators = MinimalEthSpec::minimum_validator_count() * 2;
let current_epoch = Epoch::new(MinimalEthSpec::epochs_per_historical_vector() as u64 * 2);
let mut state = new_state::<MinimalEthSpec>(
num_validators,
Epoch::new(1).start_slot(MinimalEthSpec::slots_per_epoch()),
)
.await;
// Override the epoch so that there's some room to move.
*state.slot_mut() = current_epoch.start_slot(MinimalEthSpec::slots_per_epoch());
assert_eq!(state.current_epoch(), current_epoch);
// The min_randao_epoch should be the minimum epoch such that `get_randao_mix` returns `Ok`.
let min_randao_epoch = state.min_randao_epoch();
state.get_randao_mix(min_randao_epoch).unwrap();
state.get_randao_mix(min_randao_epoch - 1).unwrap_err();
state.get_randao_mix(min_randao_epoch + 1).unwrap();
}

View File

@@ -7,7 +7,9 @@ use beacon_chain::types::{
ChainSpec, Domain, Epoch, EthSpec, FixedVector, Hash256, Keypair, MainnetEthSpec,
MinimalEthSpec, RelativeEpoch, Slot,
};
use safe_arith::SafeArith;
use ssz::{Decode, Encode};
use state_processing::per_slot_processing;
use std::ops::Mul;
use swap_or_not_shuffle::compute_shuffled_index;
use tree_hash::TreeHash;
@@ -20,7 +22,7 @@ lazy_static! {
static ref KEYPAIRS: Vec<Keypair> = generate_deterministic_keypairs(MAX_VALIDATOR_COUNT);
}
fn get_harness<E: EthSpec>(
async fn get_harness<E: EthSpec>(
validator_count: usize,
slot: Slot,
) -> BeaconChainHarness<EphemeralHarnessType<E>> {
@@ -36,24 +38,26 @@ fn get_harness<E: EthSpec>(
.map(Slot::new)
.collect::<Vec<_>>();
let state = harness.get_current_state();
harness.add_attested_blocks_at_slots(
state,
Hash256::zero(),
slots.as_slice(),
(0..validator_count).collect::<Vec<_>>().as_slice(),
);
harness
.add_attested_blocks_at_slots(
state,
Hash256::zero(),
slots.as_slice(),
(0..validator_count).collect::<Vec<_>>().as_slice(),
)
.await;
}
harness
}
fn build_state<E: EthSpec>(validator_count: usize) -> BeaconState<E> {
async fn build_state<E: EthSpec>(validator_count: usize) -> BeaconState<E> {
get_harness(validator_count, Slot::new(0))
.await
.chain
.head_beacon_state()
.unwrap()
.head_beacon_state_cloned()
}
fn test_beacon_proposer_index<T: EthSpec>() {
async fn test_beacon_proposer_index<T: EthSpec>() {
let spec = T::default_spec();
// Get the i'th candidate proposer for the given state and slot
@@ -80,20 +84,20 @@ fn test_beacon_proposer_index<T: EthSpec>() {
// Test where we have one validator per slot.
// 0th candidate should be chosen every time.
let state = build_state(T::slots_per_epoch() as usize);
let state = build_state(T::slots_per_epoch() as usize).await;
for i in 0..T::slots_per_epoch() {
test(&state, Slot::from(i), 0);
}
// Test where we have two validators per slot.
// 0th candidate should be chosen every time.
let state = build_state((T::slots_per_epoch() as usize).mul(2));
let state = build_state((T::slots_per_epoch() as usize).mul(2)).await;
for i in 0..T::slots_per_epoch() {
test(&state, Slot::from(i), 0);
}
// Test with two validators per slot, first validator has zero balance.
let mut state = build_state::<T>((T::slots_per_epoch() as usize).mul(2));
let mut state = build_state::<T>((T::slots_per_epoch() as usize).mul(2)).await;
let slot0_candidate0 = ith_candidate(&state, Slot::new(0), 0, &spec);
state
.validators_mut()
@@ -106,9 +110,9 @@ fn test_beacon_proposer_index<T: EthSpec>() {
}
}
#[test]
fn beacon_proposer_index() {
test_beacon_proposer_index::<MinimalEthSpec>();
#[tokio::test]
async fn beacon_proposer_index() {
test_beacon_proposer_index::<MinimalEthSpec>().await;
}
/// Test that
@@ -143,11 +147,11 @@ fn test_cache_initialization<T: EthSpec>(
);
}
#[test]
fn cache_initialization() {
#[tokio::test]
async fn cache_initialization() {
let spec = MinimalEthSpec::default_spec();
let mut state = build_state::<MinimalEthSpec>(16);
let mut state = build_state::<MinimalEthSpec>(16).await;
*state.slot_mut() =
(MinimalEthSpec::genesis_epoch() + 1).start_slot(MinimalEthSpec::slots_per_epoch());
@@ -236,7 +240,7 @@ mod committees {
assert!(expected_indices_iter.next().is_none());
}
fn committee_consistency_test<T: EthSpec>(
async fn committee_consistency_test<T: EthSpec>(
validator_count: usize,
state_epoch: Epoch,
cache_epoch: RelativeEpoch,
@@ -244,7 +248,7 @@ mod committees {
let spec = &T::default_spec();
let slot = state_epoch.start_slot(T::slots_per_epoch());
let harness = get_harness::<T>(validator_count, slot);
let harness = get_harness::<T>(validator_count, slot).await;
let mut new_head_state = harness.get_current_state();
let distinct_hashes =
@@ -271,7 +275,7 @@ mod committees {
);
}
fn committee_consistency_test_suite<T: EthSpec>(cached_epoch: RelativeEpoch) {
async fn committee_consistency_test_suite<T: EthSpec>(cached_epoch: RelativeEpoch) {
let spec = T::default_spec();
let validator_count = spec
@@ -280,13 +284,15 @@ mod committees {
.mul(spec.target_committee_size)
.add(1);
committee_consistency_test::<T>(validator_count as usize, Epoch::new(0), cached_epoch);
committee_consistency_test::<T>(validator_count as usize, Epoch::new(0), cached_epoch)
.await;
committee_consistency_test::<T>(
validator_count as usize,
T::genesis_epoch() + 4,
cached_epoch,
);
)
.await;
committee_consistency_test::<T>(
validator_count as usize,
@@ -295,38 +301,39 @@ mod committees {
.mul(T::slots_per_epoch())
.mul(4),
cached_epoch,
);
)
.await;
}
#[test]
fn current_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Current);
#[tokio::test]
async fn current_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Current).await;
}
#[test]
fn previous_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Previous);
#[tokio::test]
async fn previous_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Previous).await;
}
#[test]
fn next_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Next);
#[tokio::test]
async fn next_epoch_committee_consistency() {
committee_consistency_test_suite::<MinimalEthSpec>(RelativeEpoch::Next).await;
}
}
mod get_outstanding_deposit_len {
use super::*;
fn state() -> BeaconState<MinimalEthSpec> {
async fn state() -> BeaconState<MinimalEthSpec> {
get_harness(16, Slot::new(0))
.await
.chain
.head_beacon_state()
.unwrap()
.head_beacon_state_cloned()
}
#[test]
fn returns_ok() {
let mut state = state();
#[tokio::test]
async fn returns_ok() {
let mut state = state().await;
assert_eq!(state.get_outstanding_deposit_len(), Ok(0));
state.eth1_data_mut().deposit_count = 17;
@@ -334,9 +341,9 @@ mod get_outstanding_deposit_len {
assert_eq!(state.get_outstanding_deposit_len(), Ok(1));
}
#[test]
fn returns_err_if_the_state_is_invalid() {
let mut state = state();
#[tokio::test]
async fn returns_err_if_the_state_is_invalid() {
let mut state = state().await;
// The state is invalid, deposit count is lower than deposit index.
state.eth1_data_mut().deposit_count = 16;
*state.eth1_deposit_index_mut() = 17;
@@ -354,62 +361,60 @@ mod get_outstanding_deposit_len {
#[test]
fn decode_base_and_altair() {
type E = MainnetEthSpec;
let spec = E::default_spec();
let rng = &mut XorShiftRng::from_seed([42; 16]);
let fork_epoch = Epoch::from_ssz_bytes(&[7, 6, 5, 4, 3, 2, 1, 0]).unwrap();
let fork_epoch = spec.altair_fork_epoch.unwrap();
let base_epoch = fork_epoch.saturating_sub(1_u64);
let base_slot = base_epoch.end_slot(E::slots_per_epoch());
let altair_epoch = fork_epoch;
let altair_slot = altair_epoch.start_slot(E::slots_per_epoch());
let mut spec = E::default_spec();
spec.altair_fork_epoch = Some(altair_epoch);
// BeaconStateBase
{
let good_base_block: BeaconState<MainnetEthSpec> = BeaconState::Base(BeaconStateBase {
let good_base_state: BeaconState<MainnetEthSpec> = BeaconState::Base(BeaconStateBase {
slot: base_slot,
..<_>::random_for_test(rng)
});
// It's invalid to have a base block with a slot higher than the fork slot.
let bad_base_block = {
let mut bad = good_base_block.clone();
// It's invalid to have a base state with a slot higher than the fork slot.
let bad_base_state = {
let mut bad = good_base_state.clone();
*bad.slot_mut() = altair_slot;
bad
};
assert_eq!(
BeaconState::from_ssz_bytes(&good_base_block.as_ssz_bytes(), &spec)
.expect("good base block can be decoded"),
good_base_block
BeaconState::from_ssz_bytes(&good_base_state.as_ssz_bytes(), &spec)
.expect("good base state can be decoded"),
good_base_state
);
<BeaconState<MainnetEthSpec>>::from_ssz_bytes(&bad_base_block.as_ssz_bytes(), &spec)
.expect_err("bad base block cannot be decoded");
<BeaconState<MainnetEthSpec>>::from_ssz_bytes(&bad_base_state.as_ssz_bytes(), &spec)
.expect_err("bad base state cannot be decoded");
}
// BeaconStateAltair
{
let good_altair_block: BeaconState<MainnetEthSpec> =
let good_altair_state: BeaconState<MainnetEthSpec> =
BeaconState::Altair(BeaconStateAltair {
slot: altair_slot,
..<_>::random_for_test(rng)
});
// It's invalid to have an Altair block with a slot lower than the fork slot.
let bad_altair_block = {
let mut bad = good_altair_block.clone();
// It's invalid to have an Altair state with a slot lower than the fork slot.
let bad_altair_state = {
let mut bad = good_altair_state.clone();
*bad.slot_mut() = base_slot;
bad
};
assert_eq!(
BeaconState::from_ssz_bytes(&good_altair_block.as_ssz_bytes(), &spec)
.expect("good altair block can be decoded"),
good_altair_block
BeaconState::from_ssz_bytes(&good_altair_state.as_ssz_bytes(), &spec)
.expect("good altair state can be decoded"),
good_altair_state
);
<BeaconState<MainnetEthSpec>>::from_ssz_bytes(&bad_altair_block.as_ssz_bytes(), &spec)
.expect_err("bad altair block cannot be decoded");
<BeaconState<MainnetEthSpec>>::from_ssz_bytes(&bad_altair_state.as_ssz_bytes(), &spec)
.expect_err("bad altair state cannot be decoded");
}
}

View File

@@ -0,0 +1,70 @@
use crate::{ChainSpec, EthSpec, ExecPayload, ExecutionPayloadHeader, SignedRoot, Uint256};
use bls::PublicKeyBytes;
use bls::Signature;
use serde::{Deserialize as De, Deserializer, Serialize as Ser, Serializer};
use serde_derive::{Deserialize, Serialize};
use serde_with::{serde_as, DeserializeAs, SerializeAs};
use std::marker::PhantomData;
use tree_hash_derive::TreeHash;
#[serde_as]
#[derive(PartialEq, Debug, Serialize, Deserialize, TreeHash, Clone)]
#[serde(bound = "E: EthSpec, Payload: ExecPayload<E>")]
pub struct BuilderBid<E: EthSpec, Payload: ExecPayload<E>> {
#[serde_as(as = "BlindedPayloadAsHeader<E>")]
pub header: Payload,
#[serde(with = "eth2_serde_utils::quoted_u256")]
pub value: Uint256,
pub pubkey: PublicKeyBytes,
#[serde(skip)]
#[tree_hash(skip_hashing)]
_phantom_data: PhantomData<E>,
}
impl<E: EthSpec, Payload: ExecPayload<E>> SignedRoot for BuilderBid<E, Payload> {}
/// Validator registration, for use in interacting with servers implementing the builder API.
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
#[serde(bound = "E: EthSpec, Payload: ExecPayload<E>")]
pub struct SignedBuilderBid<E: EthSpec, Payload: ExecPayload<E>> {
pub message: BuilderBid<E, Payload>,
pub signature: Signature,
}
struct BlindedPayloadAsHeader<E>(PhantomData<E>);
impl<E: EthSpec, Payload: ExecPayload<E>> SerializeAs<Payload> for BlindedPayloadAsHeader<E> {
fn serialize_as<S>(source: &Payload, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
source.to_execution_payload_header().serialize(serializer)
}
}
impl<'de, E: EthSpec, Payload: ExecPayload<E>> DeserializeAs<'de, Payload>
for BlindedPayloadAsHeader<E>
{
fn deserialize_as<D>(deserializer: D) -> Result<Payload, D::Error>
where
D: Deserializer<'de>,
{
let payload_header = ExecutionPayloadHeader::deserialize(deserializer)?;
Payload::try_from(payload_header)
.map_err(|_| serde::de::Error::custom("unable to convert payload header to payload"))
}
}
impl<E: EthSpec, Payload: ExecPayload<E>> SignedBuilderBid<E, Payload> {
pub fn verify_signature(&self, spec: &ChainSpec) -> bool {
self.message
.pubkey
.decompress()
.map(|pubkey| {
let domain = spec.get_builder_domain();
let message = self.message.signing_root(domain);
self.signature.verify(&pubkey, message)
})
.unwrap_or(false)
}
}

View File

@@ -1,3 +1,4 @@
use crate::application_domain::{ApplicationDomain, APPLICATION_DOMAIN_BUILDER};
use crate::*;
use eth2_serde_utils::quoted_u64::MaybeQuoted;
use int_to_bytes::int_to_bytes4;
@@ -20,6 +21,7 @@ pub enum Domain {
SyncCommittee,
ContributionAndProof,
SyncCommitteeSelectionProof,
ApplicationMask(ApplicationDomain),
}
/// Lighthouse's internal configuration struct.
@@ -159,6 +161,11 @@ pub struct ChainSpec {
pub attestation_subnet_count: u64,
pub random_subnets_per_validator: u64,
pub epochs_per_random_subnet_subscription: u64,
/*
* Application params
*/
pub(crate) domain_application_mask: u32,
}
impl ChainSpec {
@@ -333,6 +340,7 @@ impl ChainSpec {
Domain::SyncCommittee => self.domain_sync_committee,
Domain::ContributionAndProof => self.domain_contribution_and_proof,
Domain::SyncCommitteeSelectionProof => self.domain_sync_committee_selection_proof,
Domain::ApplicationMask(application_domain) => application_domain.get_domain_constant(),
}
}
@@ -360,6 +368,17 @@ impl ChainSpec {
self.compute_domain(Domain::Deposit, self.genesis_fork_version, Hash256::zero())
}
// This should be updated to include the current fork and the genesis validators root, but discussion is ongoing:
//
// https://github.com/ethereum/builder-specs/issues/14
pub fn get_builder_domain(&self) -> Hash256 {
self.compute_domain(
Domain::ApplicationMask(ApplicationDomain::Builder),
self.genesis_fork_version,
Hash256::zero(),
)
}
/// Return the 32-byte fork data root for the `current_version` and `genesis_validators_root`.
///
/// This is used primarily in signature domains to avoid collisions across forks/chains.
@@ -549,14 +568,9 @@ impl ChainSpec {
.expect("pow does not overflow"),
proportional_slashing_multiplier_bellatrix: 3,
bellatrix_fork_version: [0x02, 0x00, 0x00, 0x00],
bellatrix_fork_epoch: None,
terminal_total_difficulty: Uint256::MAX
.checked_sub(Uint256::from(2u64.pow(10)))
.expect("subtraction does not overflow")
// Add 1 since the spec declares `2**256 - 2**10` and we use
// `Uint256::MAX` which is `2*256- 1`.
.checked_add(Uint256::one())
.expect("addition does not overflow"),
bellatrix_fork_epoch: Some(Epoch::new(144896)),
terminal_total_difficulty: Uint256::from_dec_str("58750000000000000000000")
.expect("terminal_total_difficulty is a valid integer"),
terminal_block_hash: ExecutionBlockHash::zero(),
terminal_block_hash_activation_epoch: Epoch::new(u64::MAX),
safe_slots_to_import_optimistically: 128u64,
@@ -572,6 +586,11 @@ impl ChainSpec {
maximum_gossip_clock_disparity_millis: 500,
target_aggregators_per_committee: 16,
epochs_per_random_subnet_subscription: 256,
/*
* Application specific
*/
domain_application_mask: APPLICATION_DOMAIN_BUILDER,
}
}
@@ -604,6 +623,13 @@ impl ChainSpec {
// Merge
bellatrix_fork_version: [0x02, 0x00, 0x00, 0x01],
bellatrix_fork_epoch: None,
terminal_total_difficulty: Uint256::MAX
.checked_sub(Uint256::from(2u64.pow(10)))
.expect("subtraction does not overflow")
// Add 1 since the spec declares `2**256 - 2**10` and we use
// `Uint256::MAX` which is `2*256- 1`.
.checked_add(Uint256::one())
.expect("addition does not overflow"),
// Other
network_id: 2, // lighthouse testnet network id
deposit_chain_id: 5,
@@ -770,6 +796,11 @@ impl ChainSpec {
maximum_gossip_clock_disparity_millis: 500,
target_aggregators_per_committee: 16,
epochs_per_random_subnet_subscription: 256,
/*
* Application specific
*/
domain_application_mask: APPLICATION_DOMAIN_BUILDER,
}
}
}
@@ -781,6 +812,10 @@ impl Default for ChainSpec {
}
/// Exact implementation of the *config* object from the Ethereum spec (YAML/JSON).
///
/// Fields relevant to hard forks after Altair should be optional so that we can continue
/// to parse Altair configs. This default approach turns out to be much simpler than trying to
/// make `Config` a superstruct because of the hassle of deserializing an untagged enum.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub struct Config {
@@ -791,17 +826,13 @@ pub struct Config {
#[serde(default)]
pub preset_base: String,
// TODO(merge): remove this default
#[serde(default = "default_terminal_total_difficulty")]
#[serde(with = "eth2_serde_utils::quoted_u256")]
pub terminal_total_difficulty: Uint256,
// TODO(merge): remove this default
#[serde(default = "default_terminal_block_hash")]
pub terminal_block_hash: ExecutionBlockHash,
// TODO(merge): remove this default
#[serde(default = "default_terminal_block_hash_activation_epoch")]
pub terminal_block_hash_activation_epoch: Epoch,
// TODO(merge): remove this default
#[serde(default = "default_safe_slots_to_import_optimistically")]
#[serde(with = "eth2_serde_utils::quoted_u64")]
pub safe_slots_to_import_optimistically: u64,
@@ -821,12 +852,10 @@ pub struct Config {
#[serde(deserialize_with = "deserialize_fork_epoch")]
pub altair_fork_epoch: Option<MaybeQuoted<Epoch>>,
// TODO(merge): remove this default
#[serde(default = "default_bellatrix_fork_version")]
#[serde(with = "eth2_serde_utils::bytes_4_hex")]
bellatrix_fork_version: [u8; 4],
// TODO(merge): remove this default
#[serde(default = "default_bellatrix_fork_epoch")]
#[serde(default)]
#[serde(serialize_with = "serialize_fork_epoch")]
#[serde(deserialize_with = "deserialize_fork_epoch")]
pub bellatrix_fork_epoch: Option<MaybeQuoted<Epoch>>,
@@ -868,10 +897,6 @@ fn default_bellatrix_fork_version() -> [u8; 4] {
[0xff, 0xff, 0xff, 0xff]
}
fn default_bellatrix_fork_epoch() -> Option<MaybeQuoted<Epoch>> {
None
}
/// Placeholder value: 2^256-2^10 (115792089237316195423570985008687907853269984665640564039457584007913129638912).
///
/// Taken from https://github.com/ethereum/consensus-specs/blob/d5e4828aecafaf1c57ef67a5f23c4ae7b08c5137/configs/mainnet.yaml#L15-L16
@@ -1126,6 +1151,27 @@ mod tests {
&spec,
);
test_domain(Domain::SyncCommittee, spec.domain_sync_committee, &spec);
// The builder domain index is zero
let builder_domain_pre_mask = [0; 4];
test_domain(
Domain::ApplicationMask(ApplicationDomain::Builder),
apply_bit_mask(builder_domain_pre_mask, &spec),
&spec,
);
}
fn apply_bit_mask(domain_bytes: [u8; 4], spec: &ChainSpec) -> u32 {
let mut domain = [0; 4];
let mask_bytes = int_to_bytes4(spec.domain_application_mask);
// Apply application bit mask
for (i, (domain_byte, mask_byte)) in domain_bytes.iter().zip(mask_bytes.iter()).enumerate()
{
domain[i] = domain_byte | mask_byte;
}
u32::from_le_bytes(domain)
}
// Test that `fork_name_at_epoch` and `fork_epoch` are consistent.
@@ -1292,10 +1338,7 @@ mod yaml_tests {
default_safe_slots_to_import_optimistically()
);
assert_eq!(
chain_spec.bellatrix_fork_epoch,
default_bellatrix_fork_epoch()
);
assert_eq!(chain_spec.bellatrix_fork_epoch, None);
assert_eq!(
chain_spec.bellatrix_fork_version,
@@ -1312,4 +1355,12 @@ mod yaml_tests {
)
);
}
#[test]
fn test_domain_builder() {
assert_eq!(
int_to_bytes4(ApplicationDomain::Builder.get_domain_constant()),
[0, 0, 0, 1]
);
}
}

View File

@@ -1,12 +1,21 @@
use crate::{AltairPreset, BasePreset, BellatrixPreset, ChainSpec, Config, EthSpec};
use crate::{
consts::altair, AltairPreset, BasePreset, BellatrixPreset, ChainSpec, Config, EthSpec, ForkName,
};
use maplit::hashmap;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use superstruct::superstruct;
/// Fusion of a runtime-config with the compile-time preset values.
///
/// Mostly useful for the API.
#[superstruct(
variants(Altair, Bellatrix),
variant_attributes(derive(Serialize, Deserialize, Debug, PartialEq, Clone))
)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(untagged)]
pub struct ConfigAndPreset {
#[serde(flatten)]
pub config: Config,
@@ -15,76 +24,75 @@ pub struct ConfigAndPreset {
pub base_preset: BasePreset,
#[serde(flatten)]
pub altair_preset: AltairPreset,
// TODO(merge): re-enable
// #[serde(flatten)]
// pub bellatrix_preset: BellatrixPreset,
#[superstruct(only(Bellatrix))]
#[serde(flatten)]
pub bellatrix_preset: BellatrixPreset,
/// The `extra_fields` map allows us to gracefully decode fields intended for future hard forks.
#[serde(flatten)]
pub extra_fields: HashMap<String, Value>,
}
impl ConfigAndPreset {
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec) -> Self {
pub fn from_chain_spec<T: EthSpec>(spec: &ChainSpec, fork_name: Option<ForkName>) -> Self {
let config = Config::from_chain_spec::<T>(spec);
let base_preset = BasePreset::from_chain_spec::<T>(spec);
let altair_preset = AltairPreset::from_chain_spec::<T>(spec);
// TODO(merge): re-enable
let _bellatrix_preset = BellatrixPreset::from_chain_spec::<T>(spec);
let extra_fields = HashMap::new();
let extra_fields = get_extra_fields(spec);
Self {
config,
base_preset,
altair_preset,
extra_fields,
if spec.bellatrix_fork_epoch.is_some()
|| fork_name == None
|| fork_name == Some(ForkName::Merge)
{
let bellatrix_preset = BellatrixPreset::from_chain_spec::<T>(spec);
ConfigAndPreset::Bellatrix(ConfigAndPresetBellatrix {
config,
base_preset,
altair_preset,
bellatrix_preset,
extra_fields,
})
} else {
ConfigAndPreset::Altair(ConfigAndPresetAltair {
config,
base_preset,
altair_preset,
extra_fields,
})
}
}
}
/// Add fields that were previously part of the config but are now constants.
pub fn make_backwards_compat(&mut self, spec: &ChainSpec) {
let hex_string = |value: &[u8]| format!("0x{}", hex::encode(&value));
let u32_hex = |v: u32| hex_string(&v.to_le_bytes());
let u8_hex = |v: u8| hex_string(&v.to_le_bytes());
let fields = vec![
(
"bls_withdrawal_prefix",
u8_hex(spec.bls_withdrawal_prefix_byte),
),
(
"domain_beacon_proposer",
u32_hex(spec.domain_beacon_proposer),
),
(
"domain_beacon_attester",
u32_hex(spec.domain_beacon_attester),
),
("domain_randao", u32_hex(spec.domain_randao)),
("domain_deposit", u32_hex(spec.domain_deposit)),
("domain_voluntary_exit", u32_hex(spec.domain_voluntary_exit)),
(
"domain_selection_proof",
u32_hex(spec.domain_selection_proof),
),
(
"domain_aggregate_and_proof",
u32_hex(spec.domain_aggregate_and_proof),
),
(
"target_aggregators_per_committee",
spec.target_aggregators_per_committee.to_string(),
),
(
"random_subnets_per_validator",
spec.random_subnets_per_validator.to_string(),
),
(
"epochs_per_random_subnet_subscription",
spec.epochs_per_random_subnet_subscription.to_string(),
),
];
for (key, value) in fields {
self.extra_fields.insert(key.to_uppercase(), value.into());
}
/// Get a hashmap of constants to add to the `PresetAndConfig`
pub fn get_extra_fields(spec: &ChainSpec) -> HashMap<String, Value> {
let hex_string = |value: &[u8]| format!("0x{}", hex::encode(&value)).into();
let u32_hex = |v: u32| hex_string(&v.to_le_bytes());
let u8_hex = |v: u8| hex_string(&v.to_le_bytes());
hashmap! {
"bls_withdrawal_prefix".to_uppercase() => u8_hex(spec.bls_withdrawal_prefix_byte),
"domain_beacon_proposer".to_uppercase() => u32_hex(spec.domain_beacon_proposer),
"domain_beacon_attester".to_uppercase() => u32_hex(spec.domain_beacon_attester),
"domain_randao".to_uppercase()=> u32_hex(spec.domain_randao),
"domain_deposit".to_uppercase()=> u32_hex(spec.domain_deposit),
"domain_voluntary_exit".to_uppercase() => u32_hex(spec.domain_voluntary_exit),
"domain_selection_proof".to_uppercase() => u32_hex(spec.domain_selection_proof),
"domain_aggregate_and_proof".to_uppercase() => u32_hex(spec.domain_aggregate_and_proof),
"domain_application_mask".to_uppercase()=> u32_hex(spec.domain_application_mask),
"target_aggregators_per_committee".to_uppercase() =>
spec.target_aggregators_per_committee.to_string().into(),
"random_subnets_per_validator".to_uppercase() =>
spec.random_subnets_per_validator.to_string().into(),
"epochs_per_random_subnet_subscription".to_uppercase() =>
spec.epochs_per_random_subnet_subscription.to_string().into(),
"domain_contribution_and_proof".to_uppercase() =>
u32_hex(spec.domain_contribution_and_proof),
"domain_sync_committee".to_uppercase() => u32_hex(spec.domain_sync_committee),
"domain_sync_committee_selection_proof".to_uppercase() =>
u32_hex(spec.domain_sync_committee_selection_proof),
"sync_committee_subnet_count".to_uppercase() =>
altair::SYNC_COMMITTEE_SUBNET_COUNT.to_string().into(),
"target_aggregators_per_sync_subcommittee".to_uppercase() =>
altair::TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE.to_string().into(),
}
}
@@ -104,15 +112,16 @@ mod test {
.open(tmp_file.as_ref())
.expect("error opening file");
let mainnet_spec = ChainSpec::mainnet();
let mut yamlconfig = ConfigAndPreset::from_chain_spec::<MainnetEthSpec>(&mainnet_spec);
let mut yamlconfig =
ConfigAndPreset::from_chain_spec::<MainnetEthSpec>(&mainnet_spec, None);
let (k1, v1) = ("SAMPLE_HARDFORK_KEY1", "123456789");
let (k2, v2) = ("SAMPLE_HARDFORK_KEY2", "987654321");
let (k3, v3) = ("SAMPLE_HARDFORK_KEY3", 32);
let (k4, v4) = ("SAMPLE_HARDFORK_KEY4", Value::Null);
yamlconfig.extra_fields.insert(k1.into(), v1.into());
yamlconfig.extra_fields.insert(k2.into(), v2.into());
yamlconfig.extra_fields.insert(k3.into(), v3.into());
yamlconfig.extra_fields.insert(k4.into(), v4);
yamlconfig.extra_fields_mut().insert(k1.into(), v1.into());
yamlconfig.extra_fields_mut().insert(k2.into(), v2.into());
yamlconfig.extra_fields_mut().insert(k3.into(), v3.into());
yamlconfig.extra_fields_mut().insert(k4.into(), v4);
serde_yaml::to_writer(writer, &yamlconfig).expect("failed to write or serialize");
@@ -121,8 +130,8 @@ mod test {
.write(false)
.open(tmp_file.as_ref())
.expect("error while opening the file");
let from: ConfigAndPreset =
let from: ConfigAndPresetBellatrix =
serde_yaml::from_reader(reader).expect("error while deserializing");
assert_eq!(from, yamlconfig);
assert_eq!(ConfigAndPreset::Bellatrix(from), yamlconfig);
}
}

View File

@@ -1,12 +1,14 @@
use crate::test_utils::TestRandom;
use crate::Hash256;
use derivative::Derivative;
use rand::RngCore;
use serde_derive::{Deserialize, Serialize};
use ssz::{Decode, DecodeError, Encode};
use std::fmt;
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[derive(Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Hash, Derivative)]
#[derivative(Debug = "transparent")]
#[serde(transparent)]
pub struct ExecutionBlockHash(Hash256);

View File

@@ -106,14 +106,14 @@ macro_rules! map_fork_name_with {
}
impl FromStr for ForkName {
type Err = ();
type Err = String;
fn from_str(fork_name: &str) -> Result<Self, ()> {
fn from_str(fork_name: &str) -> Result<Self, String> {
Ok(match fork_name.to_lowercase().as_ref() {
"phase0" | "base" => ForkName::Base,
"altair" => ForkName::Altair,
"bellatrix" | "merge" => ForkName::Merge,
_ => return Err(()),
_ => return Err(format!("unknown fork name: {}", fork_name)),
})
}
}
@@ -138,7 +138,7 @@ impl TryFrom<String> for ForkName {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::from_str(&s).map_err(|()| format!("Invalid fork name: {}", s))
Self::from_str(&s)
}
}
@@ -178,8 +178,8 @@ mod test {
assert_eq!(ForkName::from_str("AlTaIr"), Ok(ForkName::Altair));
assert_eq!(ForkName::from_str("altair"), Ok(ForkName::Altair));
assert_eq!(ForkName::from_str("NO_NAME"), Err(()));
assert_eq!(ForkName::from_str("no_name"), Err(()));
assert!(ForkName::from_str("NO_NAME").is_err());
assert!(ForkName::from_str("no_name").is_err());
}
#[test]

View File

@@ -18,6 +18,7 @@ extern crate lazy_static;
pub mod test_utils;
pub mod aggregate_and_proof;
pub mod application_domain;
pub mod attestation;
pub mod attestation_data;
pub mod attestation_duty;
@@ -27,6 +28,7 @@ pub mod beacon_block_body;
pub mod beacon_block_header;
pub mod beacon_committee;
pub mod beacon_state;
pub mod builder_bid;
pub mod chain_spec;
pub mod checkpoint;
pub mod consts;
@@ -81,6 +83,7 @@ pub mod sync_committee_contribution;
pub mod sync_committee_message;
pub mod sync_selection_proof;
pub mod sync_subnet_id;
pub mod validator_registration_data;
pub mod slot_data;
#[cfg(feature = "sqlite")]
@@ -106,7 +109,9 @@ pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee};
pub use crate::beacon_state::{Error as BeaconStateError, *};
pub use crate::chain_spec::{ChainSpec, Config, Domain};
pub use crate::checkpoint::Checkpoint;
pub use crate::config_and_preset::ConfigAndPreset;
pub use crate::config_and_preset::{
ConfigAndPreset, ConfigAndPresetAltair, ConfigAndPresetBellatrix,
};
pub use crate::contribution_and_proof::ContributionAndProof;
pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH};
pub use crate::deposit_data::DepositData;
@@ -156,6 +161,7 @@ pub use crate::sync_duty::SyncDuty;
pub use crate::sync_selection_proof::SyncSelectionProof;
pub use crate::sync_subnet_id::SyncSubnetId;
pub use crate::validator::{Validator, ValidatorImmutable};
pub use crate::validator_registration_data::*;
pub use crate::validator_subscription::ValidatorSubscription;
pub use crate::voluntary_exit::VoluntaryExit;

View File

@@ -9,6 +9,7 @@ use std::hash::Hash;
use test_random_derive::TestRandom;
use tree_hash::TreeHash;
#[derive(Debug)]
pub enum BlockType {
Blinded,
Full,
@@ -18,6 +19,7 @@ pub trait ExecPayload<T: EthSpec>:
Debug
+ Clone
+ Encode
+ Debug
+ Decode
+ TestRandom
+ TreeHash
@@ -28,6 +30,8 @@ pub trait ExecPayload<T: EthSpec>:
+ Hash
+ TryFrom<ExecutionPayloadHeader<T>>
+ From<ExecutionPayload<T>>
+ Send
+ 'static
{
fn block_type() -> BlockType;
@@ -42,6 +46,8 @@ pub trait ExecPayload<T: EthSpec>:
fn block_number(&self) -> u64;
fn timestamp(&self) -> u64;
fn block_hash(&self) -> ExecutionBlockHash;
fn fee_recipient(&self) -> Address;
fn gas_limit(&self) -> u64;
}
impl<T: EthSpec> ExecPayload<T> for FullPayload<T> {
@@ -72,6 +78,14 @@ impl<T: EthSpec> ExecPayload<T> for FullPayload<T> {
fn block_hash(&self) -> ExecutionBlockHash {
self.execution_payload.block_hash
}
fn fee_recipient(&self) -> Address {
self.execution_payload.fee_recipient
}
fn gas_limit(&self) -> u64 {
self.execution_payload.gas_limit
}
}
impl<T: EthSpec> ExecPayload<T> for BlindedPayload<T> {
@@ -102,6 +116,14 @@ impl<T: EthSpec> ExecPayload<T> for BlindedPayload<T> {
fn block_hash(&self) -> ExecutionBlockHash {
self.execution_payload_header.block_hash
}
fn fee_recipient(&self) -> Address {
self.execution_payload_header.fee_recipient
}
fn gas_limit(&self) -> u64 {
self.execution_payload_header.gas_limit
}
}
#[derive(Debug, Clone, TestRandom, Serialize, Deserialize, Derivative)]

View File

@@ -18,6 +18,13 @@ pub struct ProposerSlashing {
pub signed_header_2: SignedBeaconBlockHeader,
}
impl ProposerSlashing {
/// Get proposer index, assuming slashing validity has already been checked.
pub fn proposer_index(&self) -> u64 {
self.signed_header_1.message.proposer_index
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -346,6 +346,14 @@ impl<E: EthSpec> From<SignedBeaconBlock<E>> for SignedBlindedBeaconBlock<E> {
}
}
// We can blind borrowed blocks with payloads by converting the payload into a header (without
// cloning the payload contents).
impl<E: EthSpec> SignedBeaconBlock<E> {
pub fn clone_as_blinded(&self) -> SignedBlindedBeaconBlock<E> {
SignedBeaconBlock::from_block(self.message().into(), self.signature().clone())
}
}
#[cfg(test)]
mod test {
use super::*;

View File

@@ -13,8 +13,8 @@ macro_rules! ssz_tests {
($type: ty) => {
#[test]
pub fn test_ssz_round_trip() {
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
use ssz::{ssz_encode, Decode};
use $crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
let mut rng = XorShiftRng::from_seed([42; 16]);
let original = <$type>::random_for_test(&mut rng);
@@ -33,8 +33,8 @@ macro_rules! tree_hash_tests {
($type: ty) => {
#[test]
pub fn test_tree_hash_root() {
use crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
use tree_hash::TreeHash;
use $crate::test_utils::{SeedableRng, TestRandom, XorShiftRng};
let mut rng = XorShiftRng::from_seed([42; 16]);
let original = <$type>::random_for_test(&mut rng);

View File

@@ -129,6 +129,7 @@ macro_rules! impl_test_random_for_u8_array {
};
}
impl_test_random_for_u8_array!(3);
impl_test_random_for_u8_array!(4);
impl_test_random_for_u8_array!(32);
impl_test_random_for_u8_array!(48);

View File

@@ -0,0 +1,23 @@
use crate::*;
use serde::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use tree_hash_derive::TreeHash;
/// Validator registration, for use in interacting with servers implementing the builder API.
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
pub struct SignedValidatorRegistrationData {
pub message: ValidatorRegistrationData,
pub signature: Signature,
}
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode, TreeHash)]
pub struct ValidatorRegistrationData {
pub fee_recipient: Address,
#[serde(with = "eth2_serde_utils::quoted_u64")]
pub gas_limit: u64,
#[serde(with = "eth2_serde_utils::quoted_u64")]
pub timestamp: u64,
pub pubkey: PublicKeyBytes,
}
impl SignedRoot for ValidatorRegistrationData {}