Skip to content

Progressive balances optimisation is incorrect with slashed validators #4826

@michaelsproul

Description

@michaelsproul

Description

While testing Deneb + tree-states, I found a bug in our current progressive balances cache implementation: we update the unslashed participating balances incorrectly for slashed validators. First we remove the balance with on_slashing:

/// When a validator is slashed, we reduce the `current_epoch_target_attesting_balance` by the
/// validator's effective balance to exclude the validator weight.
pub fn on_slashing(
&mut self,
is_previous_epoch_target_attester: bool,
is_current_epoch_target_attester: bool,
effective_balance: u64,
) -> Result<(), BeaconStateError> {

Later, when processing effective balance changes, we update the total by the difference in the slashed validator's effective balance, which is incorrect, because the validator's balance has already been removed:

if old_effective_balance != new_effective_balance {
let is_current_epoch_target_attester =
participation_cache.is_current_epoch_timely_target_attester(index)?;
progressive_balances_cache.on_effective_balance_change(
is_current_epoch_target_attester,
old_effective_balance,
new_effective_balance,
)?;
}

The result is that the total unslashed participating balance can be off by O(num_slashed_validators). This is unlikely to cause issues in practice, because:

  • The progressive balances optimisation is disabled by default currently. If this bug were triggered, we'd get an error log and nothing more.
  • If the user has opted into --progressive-balances fast, the worst-case is a temporary view split, which would require a lot of validators to be slashed. If the slashing is small, it's unlikely to cause a view split, because the attesting balances will only be off by a small amount.

Version

Lighthouse v4.5.0

Steps to resolve

Guard the on_effective_balance_change call by !validator.slashed(). On tree-states, I've forced the caller to pass the validator's is_slashed status to on_effective_balance_change:

// Update progressive balances cache for the *current* epoch, which will soon become the
// previous epoch once the epoch transition completes.
progressive_balances.on_effective_balance_change(
validator.slashed(),
validator_info.current_epoch_participation,
old_effective_balance,
new_effective_balance,
)?;

We should make a similar change on unstable, with an accompanying regression test. The conditions necessary to trigger this are:

  • Validator's balance gets lowered substantially as a result of the amount they are slashed (e.g. validator gets slashed 1 ETH and goes from balance 32 ETH to 31 ETH).
  • Epoch transition occurs after the slashing which applies the balance reduction to their effective balance (e.g. eff. balance 32 -> 31).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingconsensusAn issue/PR that touches consensus code, such as state_processing or block verification.tree-statesOngoing state and database overhaul

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions