EIP-7716 Correlated-failure attestation penalty weighting (Anti-Correlation Attestation Penalties)

Hegota Declined for Inclusion · Consensus & staking · EIP status Draft · created 2024-05-25

When a validator misses an attestation, the penalty is scaled up by as much as 256x depending on how many more other validators than usual stopped at the same time. The penalty for a validator that misses alone stays the same as today.

Why it is needed

Today on Ethereum, the penalty a validator receives for missing an attestation (a vote on a block) does not depend on how other validators behaved. A validator that misses alone and one that misses together with 30% of the network pay exactly the same penalty each epoch. As a result there is almost no economic reason to diversify across nodes, regions, clients, internet providers and cloud vendors, because the only benefit is lowering the probability that all validators stop at once. Historical attestation data shows that validators under the same operator miss together far more often than chance, while solo stakers behave almost independently. Penalizing correlated failure can reduce the economies of scale of large, homogeneous staking operations without having to identify the operating entity. The protocol already prices correlated failure, but only when a third or more of the stake stops and the inactivity leak turns on. In the range below that, from 1% to 33%, correlated failure is effectively free. This EIP smoothly attaches a cost proportional to the size of the event in that range.

What changes

Today

After EIP-7716

Who is affected

In depth

Three constants and spec conventions

The spec adds three constants. MAX_PENALTY_FACTOR is uint64(256), the upper bound of the penalty multiplier and the only intensity knob. PENALTY_SLOPE is uint64(765), the slope of the multiplier with respect to excess offline stake. It is set equal to 3 * (MAX_PENALTY_FACTOR - 1) so that the cap is reached at exactly one third of the stake. OFFLINE_BALANCE_SMOOTHING_FACTOR is uint64(2**16), the smoothing divisor of the offline balance moving average, with a half-life of about 45,500 slots (about 6.3 days). All balances are uint64 in Gwei. SLOTS_PER_EPOCH, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_WEIGHT, TIMELY_TARGET_WEIGHT, WEIGHT_DENOMINATOR, BeaconState and Slot use the definitions and values from the post-Altair consensus spec as they are. The source states that its pseudocode is illustrative and that the executable definitions live in the consensus spec.

Beacon state field and the definition of offline

One field, smoothed_offline_balance: Gwei, is added to BeaconState. It is an exponential moving average of the per-slot offline balance. A validator is 'offline' in an epoch if it is active, not slashed, and has neither the timely source flag nor the timely target flag in that epoch. A validator that voted for the wrong target but got the source right on time is not offline, because it showed it was alive and the cause of the failure is a chain view mismatch rather than an infrastructure outage. get_slot_offline_balance(state, slot) returns, in Gwei, the sum of effective_balance over eligible validators assigned to that slot's committee in the previous epoch that have neither flag.

Computing the per-slot penalty multiplier

At the start of epoch processing, before process_rewards_and_penalties, process_penalty_factors(state) runs and computes one penalty_factor for each slot of the previous epoch. The reference balance get_slot_reference_balance is get_total_active_balance(state) // SLOTS_PER_EPOCH. For each slot it computes offline_balance, then excess = offline_balance - min(offline_balance, state.smoothed_offline_balance), and sets penalty_factor = min(uint64(1) + PENALTY_SLOPE * excess // get_slot_reference_balance(state), MAX_PENALTY_FACTOR). It then updates the moving average. If offline_balance is greater than the average, the difference divided by OFFLINE_BALANCE_SMOOTHING_FACTOR is added, and if it is smaller, the same amount is subtracted in the same way. If the offline balance does not exceed the moving average, the multiplier is exactly 1, and there is no discount region below 1. The moving average only sets the baseline to subtract from, and the slope is normalized by the active balance per slot, so the shape of the curve does not change with network participation.

Scope of penalty application and fork transition

In get_flag_index_deltas, only the timely target penalty of an offline validator is multiplied by the penalty_factor of the slot to which that validator's committee was assigned. If flag_index is TIMELY_TARGET_FLAG_INDEX and is_offline(state, index), then penalties[index] += Gwei(penalty_factor * base_reward * weight // WEIGHT_DENOMINATOR), and every other penalty branch stays as before, at Gwei(base_reward * weight // WEIGHT_DENOMINATOR). Only the existing branch that penalizes a missing flag changes. Who receives a penalty and all rewards stay the same. The timely source penalty is not weighted, and a missed timely head still carries no penalty, as now. Proposer and sync committee rewards and penalties, and the inactivity leak penalty, are also unchanged. At the fork epoch, smoothed_offline_balance is initialized to mean_per_slot_offline_balance_of_previous_epoch(pre_state), planting the baseline observed just before the fork. This is to prevent a spurious penalty multiplier from arising at activation.

Design rationale: moving average baseline, linear multiplier, replacing the draft approach

Setting the multiplier as the gap between the current slot's offline balance and a slowly moving average has three consequences. First, at the start of a simultaneous failure the multiplier jumps in proportion to the size of the outlier, and then falls over roughly a day as the moving average absorbs the event. Operators are penalized for being correlated and less so for recovering slowly. Second, the starting multiplier is 1 + PENALTY_SLOPE * spike, which gives about 9x for a 1% simultaneous failure, about 39x for 5%, about 78x for 10%, and about 154x for 20%. Because PENALTY_SLOPE = 3 * (MAX_PENALTY_FACTOR - 1), the cap is reached at exactly one third. Third, when participation is stable the multiplier is exactly 1 in every slot. The individual multiplier is kept linear. The affected stake and the multiplier are each linear, so the network-wide penalty is already quadratic in the size of the event, and a convex multiplier would push the total to cubic while removing the deterrent in the 1 to 15% range where operator choices matter. The draft's NET_EXCESS_PENALTIES counter was replaced with an explicit moving average because the cap barely affects the total penalty, because at mainnet participation rates its equilibrium sits below 1 and it oscillates near 0, and because at deterrent-scale events its recovery window stretches to several months.

Why only the target penalty, and why only validators that missed both source and target are weighted

The weighting applies only to the timely target penalty of validators that missed both source and target. This choice is made for robustness, not leniency. The target flag has the widest inclusion window, the whole next epoch, so it reflects whether a validator is alive regardless of block space or timing luck. Source timeliness fails after five slots without a block, so builder, relay or proposer failures can cause source misses for healthy validators. The two-flag condition excludes view disagreements. A validator that missed only the target, because the epoch boundary block was late, a proposer split the view, or a correct minority client saw things differently from a buggy majority, pays only today's penalty. In historical replay, 91 to 97% of baseline stake that was alive but had the wrong target sits in epoch slot 0, and under this condition none of it is weighted or counted in the offline balance. EIP-8333 pins the checkpoint to the last block before the epoch and removes this phenomenon at the root. This EIP is compatible with it but does not depend on it. The option of using the source vote as the basis was rejected. At the peak of the May 2023 incident, 5.3% of stake had a target but no source, versus 0.11% for the opposite case. Head misses remain unpenalized, as they are today.

Calibration figures, historical incident replay, and worst-case bounds

The calibration table assumes about 40 million ETH staked and shows the cost per 32 ETH as a multiple of rewards. An uncorrelated 24 hours costs about one day of rewards, the same as today. A 1% simultaneous failure for 24 hours costs about 3x (about half a week of rewards). 10% for 24 hours costs about 21x (about 3.6 weeks of rewards, 0.22% of principal). 20% for 24 hours costs about 41x (about 7 weeks of rewards). 10% for 72 hours costs about 8x, so recovering two days later than the group costs only slightly more than a 24-hour recovery. In replays of four historical incidents, the draft stayed within a few percent of the status quo in all of them, while this revision gives 79x for the May 11 and 12, 2023 finality incident (peak 69%), 3.4x for the 2024-01-06 Besu halt (12.4%), 14x for the 2024-01-21 Nethermind consensus bug (18.8%), and 45x for the 2025-12-04 Prysm outage (29.8%). The cap of 256 was chosen by comparing 128 to 512, and 512 was rejected because its tail is excessive. On security, strategies that create misses through view splitting or timing games gain nothing, because head votes carry no penalty and live validators are not weighted. The maximum penalty flow is (TIMELY_SOURCE_WEIGHT + MAX_PENALTY_FACTOR * TIMELY_TARGET_WEIGHT) / WEIGHT_DENOMINATOR of base reward per epoch, about 0.75% of principal per day. Even the worst case, caught in a one-third failure and never recovering, is bounded at about 6.7% of principal (about 2.6 years of rewards). The increased penalties are burned, not redistributed.

Document status and core developer call decisions

The source status is Draft, so the constants, pseudocode and scope may still change. In fact, this document is a revision that replaces the draft's NET_EXCESS_PENALTIES approach with a moving average approach. The core developer call records show that an updated design for EIP-7716 was presented at 08.06 ACDC #184. It recalibrates the penalty curve upward and drops head vote accuracy so that only source and target are considered. It was discussed at 09.16 AMA #15, but no decision was made. At 10.15 ACDC #189, EIP-7716 was removed from the Hegota candidates, because support was at the level of indifference and there were concerns about touching the beacon state. It therefore will not be included in the Hegota fork.

Original EIP · ← EIPs