mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-08 23:18:15 -05:00
* Avoid unnecessary calls to ExitInformation()
ExitInformation runs a loop over the whole validator set. This is needed
in case that there are slashings or exits to be processed in a block (we
could be caching or avoid this entirely post-Electra though). This PR
removes these calls on normal state transition to this function. h/t to
@terencechain for finding out this bug.
In addition, on processing withdrawal requests and registry updates, we
kept recomputing the exit information at the same time that the state is
updated and the function that updates the state already takes care of
tracking and updating the right exit information. So this PR removes the
calls to compute this exit information on a loop. Notice that this bug
has been present even before we had a function `ExitInformation()` so I
will document here to help the reviewer
Our previous behavior is to do this in a loop
```
st, err = validators.InitiateValidatorExit(ctx, st, vIdx, validators.ExitInformation(st))
```
This is a bit problematic since `ExitInformation` loops over the whole validator set to compute the exit information (and the total active balance) and then the function `InitiateValidatorExit` actually recomputes the total active balance looping again over the whole validator set and overwriting the pointer returned by `ExitInformation`.
On the other hand, the funciton `InitiateValidatorExit` does mutate the state `st` itself. So each call to `ExitInformation(st)` may actually return a different pointer.
The function ExitInformation computes as follows
```
err := s.ReadFromEveryValidator(func(idx int, val state.ReadOnlyValidator) error {
e := val.ExitEpoch()
if e != farFutureEpoch {
if e > exitInfo.HighestExitEpoch {
exitInfo.HighestExitEpoch = e
exitInfo.Churn = 1
} else if e == exitInfo.HighestExitEpoch {
exitInfo.Churn++
}
```
So it simply increases the churn for each validator that has epoch equal to the highest exit epoch.
The function `InitiateValidatorExit` mutates this pointer in the following way
if the state is post-electra, it disregards completely this pointer and computes the highest exit epoch and updates churn inconditionally, so the pointer `exitInfo.HighestExitEpoch` will always have the right value and is not even neded to be computed before. We could even avoid the fist loop even. If the state is pre-Electra then the function itself updates correctly the exit info for the next iteration.
* Only care about exits pre-Electra
* Update beacon-chain/core/transition/transition_no_verify_sig.go
Co-authored-by: terence <terence@prysmaticlabs.com>
* Radek's review
---------
Co-authored-by: terence <terence@prysmaticlabs.com>
160 lines
6.1 KiB
Go
160 lines
6.1 KiB
Go
package blocks
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
|
|
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers"
|
|
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators"
|
|
"github.com/OffchainLabs/prysm/v6/beacon-chain/state"
|
|
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
|
|
"github.com/OffchainLabs/prysm/v6/container/slice"
|
|
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
|
|
"github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1/attestation"
|
|
"github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1/slashings"
|
|
"github.com/OffchainLabs/prysm/v6/time/slots"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ProcessAttesterSlashings is one of the operations performed
|
|
// on each processed beacon block to slash attesters based on
|
|
// Casper FFG slashing conditions if any slashable events occurred.
|
|
//
|
|
// Spec pseudocode definition:
|
|
//
|
|
// def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None:
|
|
// attestation_1 = attester_slashing.attestation_1
|
|
// attestation_2 = attester_slashing.attestation_2
|
|
// assert is_slashable_attestation_data(attestation_1.data, attestation_2.data)
|
|
// assert is_valid_indexed_attestation(state, attestation_1)
|
|
// assert is_valid_indexed_attestation(state, attestation_2)
|
|
//
|
|
// slashed_any = False
|
|
// indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices)
|
|
// for index in sorted(indices):
|
|
// if is_slashable_validator(state.validators[index], get_current_epoch(state)):
|
|
// slash_validator(state, index)
|
|
// slashed_any = True
|
|
// assert slashed_any
|
|
func ProcessAttesterSlashings(
|
|
ctx context.Context,
|
|
beaconState state.BeaconState,
|
|
slashings []ethpb.AttSlashing,
|
|
exitInfo *validators.ExitInfo,
|
|
) (state.BeaconState, error) {
|
|
if exitInfo == nil && len(slashings) > 0 {
|
|
return nil, errors.New("exit info required to process attester slashings")
|
|
}
|
|
var err error
|
|
for _, slashing := range slashings {
|
|
beaconState, err = ProcessAttesterSlashing(ctx, beaconState, slashing, exitInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return beaconState, nil
|
|
}
|
|
|
|
// ProcessAttesterSlashing processes individual attester slashing.
|
|
func ProcessAttesterSlashing(
|
|
ctx context.Context,
|
|
beaconState state.BeaconState,
|
|
slashing ethpb.AttSlashing,
|
|
exitInfo *validators.ExitInfo,
|
|
) (state.BeaconState, error) {
|
|
if exitInfo == nil {
|
|
return nil, errors.New("exit info is required to process attester slashing")
|
|
}
|
|
if err := VerifyAttesterSlashing(ctx, beaconState, slashing); err != nil {
|
|
return nil, errors.Wrap(err, "could not verify attester slashing")
|
|
}
|
|
slashableIndices := SlashableAttesterIndices(slashing)
|
|
sort.SliceStable(slashableIndices, func(i, j int) bool {
|
|
return slashableIndices[i] < slashableIndices[j]
|
|
})
|
|
currentEpoch := slots.ToEpoch(beaconState.Slot())
|
|
var err error
|
|
var slashedAny bool
|
|
var val state.ReadOnlyValidator
|
|
for _, validatorIndex := range slashableIndices {
|
|
val, err = beaconState.ValidatorAtIndexReadOnly(primitives.ValidatorIndex(validatorIndex))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if helpers.IsSlashableValidator(val.ActivationEpoch(), val.WithdrawableEpoch(), val.Slashed(), currentEpoch) {
|
|
beaconState, err = validators.SlashValidator(ctx, beaconState, primitives.ValidatorIndex(validatorIndex), exitInfo)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "could not slash validator index %d", validatorIndex)
|
|
}
|
|
slashedAny = true
|
|
}
|
|
}
|
|
if !slashedAny {
|
|
return nil, errors.New("unable to slash any validator despite confirmed attester slashing")
|
|
}
|
|
return beaconState, nil
|
|
}
|
|
|
|
// VerifyAttesterSlashing validates the attestation data in both attestations in the slashing object.
|
|
func VerifyAttesterSlashing(ctx context.Context, beaconState state.ReadOnlyBeaconState, slashing ethpb.AttSlashing) error {
|
|
if slashing == nil {
|
|
return errors.New("nil slashing")
|
|
}
|
|
if slashing.FirstAttestation() == nil || slashing.SecondAttestation() == nil {
|
|
return errors.New("nil attestation")
|
|
}
|
|
if slashing.FirstAttestation().GetData() == nil || slashing.SecondAttestation().GetData() == nil {
|
|
return errors.New("nil attestation data")
|
|
}
|
|
att1 := slashing.FirstAttestation()
|
|
att2 := slashing.SecondAttestation()
|
|
data1 := att1.GetData()
|
|
data2 := att2.GetData()
|
|
if !IsSlashableAttestationData(data1, data2) {
|
|
return errors.New("attestations are not slashable")
|
|
}
|
|
if err := VerifyIndexedAttestation(ctx, beaconState, att1); err != nil {
|
|
return errors.Wrap(err, "could not validate indexed attestation")
|
|
}
|
|
if err := VerifyIndexedAttestation(ctx, beaconState, att2); err != nil {
|
|
return errors.Wrap(err, "could not validate indexed attestation")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsSlashableAttestationData verifies a slashing against the Casper Proof of Stake FFG rules.
|
|
//
|
|
// Spec pseudocode definition:
|
|
//
|
|
// def is_slashable_attestation_data(data_1: AttestationData, data_2: AttestationData) -> bool:
|
|
// """
|
|
// Check if ``data_1`` and ``data_2`` are slashable according to Casper FFG rules.
|
|
// """
|
|
// return (
|
|
// # Double vote
|
|
// (data_1 != data_2 and data_1.target.epoch == data_2.target.epoch) or
|
|
// # Surround vote
|
|
// (data_1.source.epoch < data_2.source.epoch and data_2.target.epoch < data_1.target.epoch)
|
|
// )
|
|
func IsSlashableAttestationData(data1, data2 *ethpb.AttestationData) bool {
|
|
if data1 == nil || data2 == nil || data1.Target == nil || data2.Target == nil || data1.Source == nil || data2.Source == nil {
|
|
return false
|
|
}
|
|
isDoubleVote := !attestation.AttDataIsEqual(data1, data2) && data1.Target.Epoch == data2.Target.Epoch
|
|
att1 := ðpb.IndexedAttestation{Data: data1}
|
|
att2 := ðpb.IndexedAttestation{Data: data2}
|
|
// Check if att1 is surrounding att2.
|
|
isSurroundVote := slashings.IsSurround(att1, att2)
|
|
return isDoubleVote || isSurroundVote
|
|
}
|
|
|
|
// SlashableAttesterIndices returns the intersection of attester indices from both attestations in this slashing.
|
|
func SlashableAttesterIndices(slashing ethpb.AttSlashing) []uint64 {
|
|
if slashing == nil || slashing.FirstAttestation() == nil || slashing.SecondAttestation() == nil {
|
|
return nil
|
|
}
|
|
indices1 := slashing.FirstAttestation().GetAttestingIndices()
|
|
indices2 := slashing.SecondAttestation().GetAttestingIndices()
|
|
return slice.IntersectionUint64(indices1, indices2)
|
|
}
|