mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -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>
201 lines
8.5 KiB
Go
201 lines
8.5 KiB
Go
package blocks
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers"
|
|
"github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing"
|
|
v "github.com/OffchainLabs/prysm/v6/beacon-chain/core/validators"
|
|
"github.com/OffchainLabs/prysm/v6/beacon-chain/state"
|
|
"github.com/OffchainLabs/prysm/v6/config/params"
|
|
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
|
|
"github.com/OffchainLabs/prysm/v6/runtime/version"
|
|
"github.com/OffchainLabs/prysm/v6/time/slots"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ValidatorAlreadyExitedMsg defines a message saying that a validator has already exited.
|
|
var ValidatorAlreadyExitedMsg = "has already submitted an exit, which will take place at epoch"
|
|
|
|
// ValidatorCannotExitYetMsg defines a message saying that a validator cannot exit
|
|
// because it has not been active long enough.
|
|
var ValidatorCannotExitYetMsg = "validator has not been active long enough to exit"
|
|
|
|
// ProcessVoluntaryExits is one of the operations performed
|
|
// on each processed beacon block to determine which validators
|
|
// should exit the state's validator registry.
|
|
//
|
|
// Spec pseudocode definition:
|
|
//
|
|
// def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None:
|
|
// voluntary_exit = signed_voluntary_exit.message
|
|
// validator = state.validators[voluntary_exit.validator_index]
|
|
// # Verify the validator is active
|
|
// assert is_active_validator(validator, get_current_epoch(state))
|
|
// # Verify exit has not been initiated
|
|
// assert validator.exit_epoch == FAR_FUTURE_EPOCH
|
|
// # Exits must specify an epoch when they become valid; they are not valid before then
|
|
// assert get_current_epoch(state) >= voluntary_exit.epoch
|
|
// # Verify the validator has been active long enough
|
|
// assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD
|
|
// # Verify signature
|
|
// domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch)
|
|
// signing_root = compute_signing_root(voluntary_exit, domain)
|
|
// assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature)
|
|
// # Initiate exit
|
|
// initiate_validator_exit(state, voluntary_exit.validator_index)
|
|
func ProcessVoluntaryExits(
|
|
ctx context.Context,
|
|
beaconState state.BeaconState,
|
|
exits []*ethpb.SignedVoluntaryExit,
|
|
exitInfo *v.ExitInfo,
|
|
) (state.BeaconState, error) {
|
|
// Avoid calculating the epoch churn if no exits exist.
|
|
if len(exits) == 0 {
|
|
return beaconState, nil
|
|
}
|
|
if exitInfo == nil {
|
|
return nil, errors.New("exit info required to process voluntary exits")
|
|
}
|
|
for idx, exit := range exits {
|
|
if exit == nil || exit.Exit == nil {
|
|
return nil, errors.New("nil voluntary exit in block body")
|
|
}
|
|
val, err := beaconState.ValidatorAtIndexReadOnly(exit.Exit.ValidatorIndex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := VerifyExitAndSignature(val, beaconState, exit); err != nil {
|
|
return nil, errors.Wrapf(err, "could not verify exit %d", idx)
|
|
}
|
|
beaconState, err = v.InitiateValidatorExit(ctx, beaconState, exit.Exit.ValidatorIndex, exitInfo)
|
|
if err != nil && !errors.Is(err, v.ErrValidatorAlreadyExited) {
|
|
return nil, err
|
|
}
|
|
}
|
|
return beaconState, nil
|
|
}
|
|
|
|
// VerifyExitAndSignature implements the spec defined validation for voluntary exits.
|
|
//
|
|
// Spec pseudocode definition:
|
|
//
|
|
// def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None:
|
|
// voluntary_exit = signed_voluntary_exit.message
|
|
// validator = state.validators[voluntary_exit.validator_index]
|
|
// # Verify the validator is active
|
|
// assert is_active_validator(validator, get_current_epoch(state))
|
|
// # Verify exit has not been initiated
|
|
// assert validator.exit_epoch == FAR_FUTURE_EPOCH
|
|
// # Exits must specify an epoch when they become valid; they are not valid before then
|
|
// assert get_current_epoch(state) >= voluntary_exit.epoch
|
|
// # Verify the validator has been active long enough
|
|
// assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD
|
|
// # Only exit validator if it has no pending withdrawals in the queue
|
|
// assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 # [New in Electra:EIP7251]
|
|
// # Verify signature
|
|
// domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch)
|
|
// signing_root = compute_signing_root(voluntary_exit, domain)
|
|
// assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature)
|
|
// # Initiate exit
|
|
// initiate_validator_exit(state, voluntary_exit.validator_index)
|
|
func VerifyExitAndSignature(
|
|
validator state.ReadOnlyValidator,
|
|
state state.ReadOnlyBeaconState,
|
|
signed *ethpb.SignedVoluntaryExit,
|
|
) error {
|
|
if signed == nil || signed.Exit == nil {
|
|
return errors.New("nil exit")
|
|
}
|
|
|
|
fork := state.Fork()
|
|
genesisRoot := state.GenesisValidatorsRoot()
|
|
|
|
// EIP-7044: Beginning in Deneb, fix the fork version to Capella.
|
|
// This allows for signed validator exits to be valid forever.
|
|
if state.Version() >= version.Deneb {
|
|
fork = ðpb.Fork{
|
|
PreviousVersion: params.BeaconConfig().CapellaForkVersion,
|
|
CurrentVersion: params.BeaconConfig().CapellaForkVersion,
|
|
Epoch: params.BeaconConfig().CapellaForkEpoch,
|
|
}
|
|
}
|
|
|
|
exit := signed.Exit
|
|
if err := verifyExitConditions(state, validator, exit); err != nil {
|
|
return err
|
|
}
|
|
domain, err := signing.Domain(fork, exit.Epoch, params.BeaconConfig().DomainVoluntaryExit, genesisRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
valPubKey := validator.PublicKey()
|
|
if err := signing.VerifySigningRoot(exit, valPubKey[:], signed.Signature, domain); err != nil {
|
|
return signing.ErrSigFailedToVerify
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// verifyExitConditions implements the spec defined validation for voluntary exits (excluding signatures).
|
|
//
|
|
// Spec pseudocode definition:
|
|
//
|
|
// def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None:
|
|
// voluntary_exit = signed_voluntary_exit.message
|
|
// validator = state.validators[voluntary_exit.validator_index]
|
|
// # Verify the validator is active
|
|
// assert is_active_validator(validator, get_current_epoch(state))
|
|
// # Verify exit has not been initiated
|
|
// assert validator.exit_epoch == FAR_FUTURE_EPOCH
|
|
// # Exits must specify an epoch when they become valid; they are not valid before then
|
|
// assert get_current_epoch(state) >= voluntary_exit.epoch
|
|
// # Verify the validator has been active long enough
|
|
// assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD
|
|
// # Only exit validator if it has no pending withdrawals in the queue
|
|
// assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 # [New in Electra:EIP7251]
|
|
// # Verify signature
|
|
// domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch)
|
|
// signing_root = compute_signing_root(voluntary_exit, domain)
|
|
// assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature)
|
|
// # Initiate exit
|
|
// initiate_validator_exit(state, voluntary_exit.validator_index)
|
|
func verifyExitConditions(st state.ReadOnlyBeaconState, validator state.ReadOnlyValidator, exit *ethpb.VoluntaryExit) error {
|
|
currentEpoch := slots.ToEpoch(st.Slot())
|
|
// Verify the validator is active.
|
|
if !helpers.IsActiveValidatorUsingTrie(validator, currentEpoch) {
|
|
return errors.New("non-active validator cannot exit")
|
|
}
|
|
// Verify the validator has not yet submitted an exit.
|
|
if validator.ExitEpoch() != params.BeaconConfig().FarFutureEpoch {
|
|
return fmt.Errorf("validator with index %d %s: %v", exit.ValidatorIndex, ValidatorAlreadyExitedMsg, validator.ExitEpoch())
|
|
}
|
|
// Exits must specify an epoch when they become valid; they are not valid before then.
|
|
if currentEpoch < exit.Epoch {
|
|
return fmt.Errorf("expected current epoch >= exit epoch, received %d < %d", currentEpoch, exit.Epoch)
|
|
}
|
|
// Verify the validator has been active long enough.
|
|
if currentEpoch < validator.ActivationEpoch()+params.BeaconConfig().ShardCommitteePeriod {
|
|
return fmt.Errorf(
|
|
"%s: %d of %d epochs. Validator will be eligible for exit at epoch %d",
|
|
ValidatorCannotExitYetMsg,
|
|
currentEpoch-validator.ActivationEpoch(),
|
|
params.BeaconConfig().ShardCommitteePeriod,
|
|
validator.ActivationEpoch()+params.BeaconConfig().ShardCommitteePeriod,
|
|
)
|
|
}
|
|
|
|
if st.Version() >= version.Electra {
|
|
// Only exit validator if it has no pending withdrawals in the queue.
|
|
ok, err := st.HasPendingBalanceToWithdraw(exit.ValidatorIndex)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to retrieve pending balance to withdraw for validator %d: %w", exit.ValidatorIndex, err)
|
|
}
|
|
if ok {
|
|
return fmt.Errorf("validator %d must have no pending balance to withdraw", exit.ValidatorIndex)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|