Skip to content

Filter out redundant attestations in input builder #100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ fn validate_link(
}

/// Verifies a single attestation returning its attesting validator indices.
fn process_attestation<S: StateReader, E: EthSpec>(
pub fn process_attestation<S: StateReader, E: EthSpec>(
state: &S,
active_validators: &BTreeMap<ValidatorIndex, &ValidatorInfo>,
committees: &CommitteeCache<E>,
Expand Down Expand Up @@ -250,7 +250,7 @@ fn is_valid_indexed_attestation<S: StateReader>(
}

/// Return all the committees for the given epoch.
fn compute_committees<S: StateReader>(
pub fn compute_committees<S: StateReader>(
state_reader: &S,
active_validators: &BTreeMap<ValidatorIndex, &ValidatorInfo>,
epoch: Epoch,
Expand Down
9 changes: 5 additions & 4 deletions host/src/bin/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ async fn main() -> anyhow::Result<()> {
Checkpoint::new(trusted_epoch, trusted_beacon_block.hash_tree_root()?);
info!("Trusted checkpoint: {}", trusted_checkpoint);

let builder = InputBuilder::<Spec, _>::new(beacon_client.clone());
let builder = InputBuilder::<Spec, _, _>::new(&beacon_client, &reader);

let (input, _) = builder.build(trusted_checkpoint).await?;
let (input, _) = builder.build(&DEFAULT_CONFIG, trusted_checkpoint).await?;
info!("Pre-state: {:#?}", input.consensus_state);
debug!("Input: {:?}", input);

Expand Down Expand Up @@ -226,11 +226,12 @@ async fn run_sync<E: EthSpec + Serialize>(

let mut consensus_state = beacon_client.get_consensus_state(start_slot).await?;
info!("Initial Consensus State: {:#?}", consensus_state);
let input_builder = InputBuilder::<E, _>::new(beacon_client.clone());

loop {
let input_builder = InputBuilder::<E, _, _>::new(beacon_client, &sr);

let (input, expected_state) = input_builder
.build(consensus_state.finalized_checkpoint())
.build(&DEFAULT_CONFIG, consensus_state.finalized_checkpoint())
.await?;
debug!("Input: {:?}", input);
let msg = match run_verify(network, mode, &DEFAULT_CONFIG, &sr, input.clone()) {
Expand Down
131 changes: 120 additions & 11 deletions host/src/input_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
use crate::conv_attestation;
use beacon_types::EthSpec;
use ethereum_consensus::{electra::mainnet::SignedBeaconBlockHeader, types::mainnet::BeaconBlock};
use std::collections::HashMap;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt::Display;
use tracing::debug;
use z_core::{Attestation, Checkpoint, ConsensusError, ConsensusState, Epoch, Input, Link, ensure};
use z_core::{
Attestation, Checkpoint, Config, ConsensusError, ConsensusState, Epoch, Input, Link,
StateReader, compute_committees, ensure, get_total_balance, process_attestation,
};

/// A trait to abstract reading data from an instance of a beacon chain
/// This could be an RPC to a node or something else (e.g. test harness)
Expand Down Expand Up @@ -50,17 +53,33 @@ pub enum InputBuilderError {
ConsensusError(#[from] ConsensusError),
#[error("Chain reader error")]
ChainReader(#[from] anyhow::Error),
#[error("Error in called verify method")]
VerifyError(#[from] z_core::VerifyError),
#[error("Arithmetic error: {0:?}")]
ArithError(safe_arith::ArithError),
#[error("State reader error: {0}")]
StateReader(String),
#[error("Duplicate attestations from one-or-more validators detected")]
DuplicateAttesters,
}

pub struct InputBuilder<E, CR> {
chain_reader: CR,
impl From<safe_arith::ArithError> for InputBuilderError {
fn from(e: safe_arith::ArithError) -> Self {
InputBuilderError::ArithError(e)
}
}

pub struct InputBuilder<'a, E, CR, SR> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this has a lifetime now anyways, lets also use chain_reader: &'a CR,

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See changes

chain_reader: &'a CR,
state_reader: &'a SR,
_phantom: std::marker::PhantomData<E>,
}

impl<E: EthSpec, CR: ChainReader> InputBuilder<E, CR> {
pub fn new(chain_reader: CR) -> Self {
impl<'a, E: EthSpec, CR: ChainReader, SR: StateReader<Spec = E>> InputBuilder<'a, E, CR, SR> {
pub fn new(chain_reader: &'a CR, state_reader: &'a SR) -> Self {
Self {
chain_reader,
state_reader,
_phantom: std::marker::PhantomData,
}
}
Expand All @@ -69,6 +88,7 @@ impl<E: EthSpec, CR: ChainReader> InputBuilder<E, CR> {
/// used to evolve the consensus state at this checkpoint to a new consensus state in the "best" way possible
pub async fn build(
&self,
cfg: &Config,
trusted_checkpoint: Checkpoint,
) -> Result<(Input<E>, ConsensusState), InputBuilderError> {
// Find the first consensus state that confirms the finality of the trusted_checkpoint
Expand All @@ -88,7 +108,9 @@ impl<E: EthSpec, CR: ChainReader> InputBuilder<E, CR> {
);

// Concurrently fetch attestations for all required links.
let attestations = self.collect_attestations_for_links(&links).await?;
let attestations = self
.collect_attestations_for_links(cfg, &links, trusted_checkpoint)
.await?;

Ok((
Input {
Expand Down Expand Up @@ -168,7 +190,9 @@ impl<E: EthSpec, CR: ChainReader> InputBuilder<E, CR> {
/// Gathers the attestations for links, looking at block in the range [start_slot, end_slot].
async fn collect_attestations_for_links(
&self,
cfg: &Config,
links: &[Link],
trusted_checkpoint: Checkpoint,
) -> Result<Vec<Attestation<E>>, InputBuilderError> {
if links.is_empty() {
return Ok(vec![]);
Expand Down Expand Up @@ -208,12 +232,97 @@ impl<E: EthSpec, CR: ChainReader> InputBuilder<E, CR> {
}
}

// 3. Assemble the final nested Vec in the correct order
let result = links
.iter()
.flat_map(|link| attestations_by_link.remove(link).unwrap_or_default())
// 3. Assemble the final nested Vec in the correct order and take only as many as are required to meet the threshold requirement
// compute all committees for the target epoch
let mut result = Vec::new();
for link in links {
let link_attestations = attestations_by_link.remove(link).unwrap_or_default();
result.extend(self.get_min_required_attestations(
cfg,
link.target.epoch(),
link_attestations,
trusted_checkpoint,
)?);
}

Ok(result)
}

/// Given a target epoch and a list of attestations related to a single link, return only those
/// that are required to meet the justification threshold for the link as set in the config.
fn get_min_required_attestations(
&self,
cfg: &Config,
target_epoch: Epoch,
attestations: Vec<Attestation<E>>,
trusted_checkpoint: Checkpoint,
) -> Result<Vec<Attestation<E>>, InputBuilderError> {
// We want to use active validators at the target epoch to compute which validators are participating
// but should use their balance/slashed status at the trusted checkpoint epoch.
// This is to ensure compatibility between the different state reader implementations
let mut active_validators: BTreeMap<_, _> = self
.state_reader
.active_validators(target_epoch)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct in general. Only if the ssz reader or an equivalent reader is used. The host reader uses the active balance information about the actual epoch and not only the trusted epoch, so this might significantly diviate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right yes. I have never been happy about how the expected behavior of StateReader is different depending on the type and this is a great example of why it is kind of dangerous. I think using the SszReader and then also sharing the committee cache is a workable solution for now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed how the host state reader operates so now it has a trusted epoch the same as the others. On the plus side this allowed me to remove the hack in AssertStateReader as the results from reading the validators matches now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative approach. This just grabs the balances and slashed status from the trusted state which replicates the SSZ Reader approach for the values used in this function. It is also much simpler than trying to modify the Host reader

.map_err(|e| InputBuilderError::StateReader(e.to_string()))?
.map(|(idx, v)| (idx, v.clone()))
.collect();

// Update effective_balance and slashed status from the trusted checkpoint epoch
for (idx, validator) in self
.state_reader
.active_validators(trusted_checkpoint.epoch())
.map_err(|e| InputBuilderError::StateReader(e.to_string()))?
{
if let Some(v) = active_validators.get_mut(&idx) {
v.effective_balance = validator.effective_balance;
v.slashed = validator.slashed;
}
}

// Prepare a reference map for committee computation
let active_validators: BTreeMap<_, _> =
active_validators.iter().map(|(k, v)| (*k, v)).collect();

let committees = compute_committees(self.state_reader, &active_validators, target_epoch)?;

let total_active_balance =
get_total_balance(self.state_reader.chain_spec(), active_validators.values())?;

// the target balance must be sufficient for a new justification
let rhs = total_active_balance as u128 * cfg.justification_threshold_factor as u128;

// Use a greedy search to build a set of attestations that can finalize. In future this could be replaced with
// a minimum subset sum if we really want to hit the absolute minimum. It isn't expected to make a huge difference though.
let mut target_balance = 0u64;
let mut result = Vec::new();
let mut all_attesting_indices = BTreeSet::new();
for att in attestations {
let lhs = target_balance as u128 * cfg.justification_threshold_quotient as u128;
if lhs < rhs {
let participating_indices =
process_attestation(self.state_reader, &active_validators, &committees, &att)?;

if !all_attesting_indices.is_disjoint(&participating_indices) {
return Err(InputBuilderError::DuplicateAttesters);
}

all_attesting_indices.extend(participating_indices.iter().cloned());

let unslashed_participating_validators = participating_indices
.iter()
.map(|i| active_validators.get(i).unwrap())
.filter(|v| !v.slashed);

target_balance += get_total_balance(
self.state_reader.chain_spec(),
unslashed_participating_validators,
)?;

result.push(att);
} else {
break; // attesting balance is sufficient, no need to add more attestations
}
}
Ok(result)
}
}
7 changes: 5 additions & 2 deletions host/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ async fn test_zkasper_sync(
);

// Build the input and verify it
let builder = InputBuilder::new(harness);
match builder.build(consensus_state.finalized_checkpoint()).await {
let builder = InputBuilder::new(&harness, &state_reader);
match builder
.build(cfg, consensus_state.finalized_checkpoint())
.await
{
Ok((input, _)) => {
dbg!(&input);

Expand Down