Skip to content
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

feature(state-processor): update validators EffectiveBalance only when epoch turns #2142

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
24 changes: 24 additions & 0 deletions mod/chain-spec/pkg/chain/chain_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ type Spec[
// calculations.
EffectiveBalanceIncrement() uint64

HysteresisQuotient() uint64

HysteresisDownwardMultiplier() uint64

HysteresisUpwardMultiplier() uint64

abi87 marked this conversation as resolved.
Show resolved Hide resolved
// Time parameters constants.

// SlotsPerEpoch returns the number of slots in an epoch.
Expand Down Expand Up @@ -245,6 +251,24 @@ func (c chainSpec[
return c.Data.EffectiveBalanceIncrement
}

func (c chainSpec[
DomainTypeT, EpochT, ExecutionAddressT, SlotT, CometBFTConfigT,
]) HysteresisQuotient() uint64 {
return c.Data.HysteresisQuotient
}

func (c chainSpec[
DomainTypeT, EpochT, ExecutionAddressT, SlotT, CometBFTConfigT,
]) HysteresisDownwardMultiplier() uint64 {
return c.Data.HysteresisDownwardMultiplier
}

func (c chainSpec[
DomainTypeT, EpochT, ExecutionAddressT, SlotT, CometBFTConfigT,
]) HysteresisUpwardMultiplier() uint64 {
return c.Data.HysteresisUpwardMultiplier
}

// SlotsPerEpoch returns the number of slots per epoch.
func (c chainSpec[
DomainTypeT, EpochT, ExecutionAddressT, SlotT, CometBFTConfigT,
Expand Down
5 changes: 5 additions & 0 deletions mod/chain-spec/pkg/chain/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ type SpecData[
// EffectiveBalanceIncrement is the effective balance increment.
EffectiveBalanceIncrement uint64 `mapstructure:"effective-balance-increment"`

HysteresisQuotient uint64 `mapstructure:"hysteresis-quotient"`

HysteresisDownwardMultiplier uint64 `mapstructure:"hysteresis-downward-multiplier"`

HysteresisUpwardMultiplier uint64 `mapstructure:"hysteresis-upward-multiplier"`
abi87 marked this conversation as resolved.
Show resolved Hide resolved
// Time parameters constants.
//
// SlotsPerEpoch is the number of slots per epoch.
Expand Down
11 changes: 7 additions & 4 deletions mod/config/pkg/spec/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@ func BaseSpec() chain.SpecData[
any,
]{
// // Gwei value constants.
MinDepositAmount: uint64(1e9),
MaxEffectiveBalance: uint64(32e9),
EjectionBalance: uint64(16e9),
EffectiveBalanceIncrement: uint64(1e9),
MinDepositAmount: uint64(1e9),
MaxEffectiveBalance: uint64(32e9),
EjectionBalance: uint64(16e9),
EffectiveBalanceIncrement: uint64(1e9),
HysteresisQuotient: uint64(4),
HysteresisDownwardMultiplier: uint64(1),
HysteresisUpwardMultiplier: uint64(5),
Comment on lines +67 to +69
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

default values from Eth 2.0 specs, similar to other quantities here

abi87 marked this conversation as resolved.
Show resolved Hide resolved
// Time parameters constants.
SlotsPerEpoch: 32,
MinEpochsToInactivityPenalty: 4,
Expand Down
110 changes: 73 additions & 37 deletions mod/state-transition/pkg/core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package core
import (
"bytes"

"github.com/berachain/beacon-kit/mod/consensus-types/pkg/types"
"github.com/berachain/beacon-kit/mod/errors"
"github.com/berachain/beacon-kit/mod/primitives/pkg/common"
"github.com/berachain/beacon-kit/mod/primitives/pkg/constants"
Expand Down Expand Up @@ -197,34 +198,26 @@ func (sp *StateProcessor[
]) ProcessSlots(
st BeaconStateT, slot math.Slot,
) (transition.ValidatorUpdates, error) {
var (
validatorUpdates transition.ValidatorUpdates
epochValidatorUpdates transition.ValidatorUpdates
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

nit: reduced epochValidatorUpdates scope (it's only used when processing epochs)

)

var res transition.ValidatorUpdates
stateSlot, err := st.GetSlot()
if err != nil {
return nil, err
}

// Iterate until we are "caught up".
for ; stateSlot < slot; stateSlot++ {
// Process the slot
if err = sp.processSlot(st); err != nil {
return nil, err
}

// Process the Epoch Boundary.
boundary := (stateSlot.Unwrap()+1)%sp.cs.SlotsPerEpoch() == 0
if boundary {
if epochValidatorUpdates, err =
sp.processEpoch(st); err != nil {
var epochUpdates transition.ValidatorUpdates
if epochUpdates, err = sp.processEpoch(st); err != nil {
return nil, err
}
validatorUpdates = append(
validatorUpdates,
epochValidatorUpdates...,
)
res = append(res, epochUpdates...)
}

// We update on the state because we need to
Expand All @@ -234,7 +227,7 @@ func (sp *StateProcessor[
}
}

return validatorUpdates, nil
return res, nil
}

// processSlot is run when a slot is missed.
Expand Down Expand Up @@ -336,6 +329,9 @@ func (sp *StateProcessor[
if err := sp.processRewardsAndPenalties(st); err != nil {
return nil, err
}
if err := sp.processEffectiveBalanceUpdates(st); err != nil {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is one of the main point of the PR: along Eth 2.0 specs, EffectiveBalances must be updated only once per epoch, not every slot

return nil, err
}
if err := sp.processSlashingsReset(st); err != nil {
return nil, err
}
Expand All @@ -362,13 +358,12 @@ func (sp *StateProcessor[
}
if blk.GetSlot() != slot {
return errors.Wrapf(
ErrSlotMismatch,
"expected: %d, got: %d",
ErrSlotMismatch, "expected: %d, got: %d",
slot, blk.GetSlot(),
)
}

// Verify the parent block root is correct.
// Verify that the block is newer than latest block header
latestBlockHeader, err := st.GetLatestBlockHeader()
if err != nil {
return err
Expand All @@ -380,14 +375,6 @@ func (sp *StateProcessor[
)
}

parentBlockRoot := latestBlockHeader.HashTreeRoot()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

nit: just moved below to match better operation ordering in Eth2.0 specs

if parentBlockRoot != blk.GetParentBlockRoot() {
return errors.Wrapf(ErrParentRootMismatch,
"expected: %s, got: %s",
parentBlockRoot.String(), blk.GetParentBlockRoot().String(),
)
}

// Verify that proposer matches with what consensus declares as proposer
proposer, err := st.ValidatorByIndex(blk.GetProposerIndex())
if err != nil {
Expand All @@ -404,26 +391,24 @@ func (sp *StateProcessor[
)
}

// Check to make sure the proposer isn't slashed.
if proposer.IsSlashed() {
// Verify that the parent matches
parentBlockRoot := latestBlockHeader.HashTreeRoot()
if parentBlockRoot != blk.GetParentBlockRoot() {
return errors.Wrapf(
ErrSlashedProposer,
"index: %d",
blk.GetProposerIndex(),
ErrParentRootMismatch, "expected: %s, got: %s",
parentBlockRoot.String(), blk.GetParentBlockRoot().String(),
)
}

// Ensure the block is within the acceptable range.
// TODO: move this is in the wrong spot.
deposits := blk.GetBody().GetDeposits()
if uint64(len(deposits)) > sp.cs.MaxDepositsPerBlock() {
return errors.Wrapf(ErrExceedsBlockDepositLimit,
"expected: %d, got: %d",
sp.cs.MaxDepositsPerBlock(), len(deposits),
Comment on lines -421 to -427
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed todo. Moved this to processDeposit

// Verify proposer is not slashed
if proposer.IsSlashed() {
return errors.Wrapf(
ErrSlashedProposer, "index: %d",
blk.GetProposerIndex(),
)
}

// Calculate the body root to place on the header.
// Cache current block as the new latest block
bodyRoot := blk.GetBody().HashTreeRoot()
var lbh BeaconBlockHeaderT
lbh = lbh.New(
Expand Down Expand Up @@ -516,3 +501,54 @@ func (sp *StateProcessor[

return nil
}

// processEffectiveBalanceUpdates as defined in the Ethereum 2.0 specification.
// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#effective-balances-updates
//
//nolint:lll
func (sp *StateProcessor[
_, _, _, BeaconStateT, _, _, _, _, _, _, _, _, _, _, _, _, _,
]) processEffectiveBalanceUpdates(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is straight from Eth 2.0 specs.
In essence, at each epoch, check if a validator balance has moved from its effective balance more than a certain amount. If so update the balance

st BeaconStateT,
) error {
// Update effective balances with hysteresis
validators, err := st.GetValidators()
if err != nil {
return err
}

var (
hysteresisIncrement = sp.cs.EffectiveBalanceIncrement() / sp.cs.HysteresisQuotient()
downwardThreshold = math.Gwei(hysteresisIncrement * sp.cs.HysteresisDownwardMultiplier())
upwardThreshold = math.Gwei(hysteresisIncrement * sp.cs.HysteresisUpwardMultiplier())

idx math.U64
balance math.Gwei
)

for _, val := range validators {
idx, err = st.ValidatorIndexByPubkey(val.GetPubkey())
if err != nil {
return err
}

balance, err = st.GetBalance(idx)
if err != nil {
return err
}

if balance+downwardThreshold < val.GetEffectiveBalance() ||
val.GetEffectiveBalance()+upwardThreshold < balance {
updatedBalance := types.ComputeEffectiveBalance(
balance,
math.U64(sp.cs.EffectiveBalanceIncrement()),
math.U64(sp.cs.MaxEffectiveBalance()),
)
val.SetEffectiveBalance(updatedBalance)
if err = st.UpdateValidatorAtIndex(idx, val); err != nil {
return err
}
}
}
return nil
}
calbera marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 13 additions & 1 deletion mod/state-transition/pkg/core/state_processor_committee.go
Copy link
Contributor

Choose a reason for hiding this comment

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

Possible TODO

Mainnet follows: If balance below ejection balance after 1 epoch --> trigger withdrawal for validator

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package core

import (
"github.com/berachain/beacon-kit/mod/primitives/pkg/math"
"github.com/berachain/beacon-kit/mod/primitives/pkg/transition"
"github.com/sourcegraph/conc/iter"
)
Expand All @@ -36,8 +37,19 @@ func (sp *StateProcessor[
return nil, err
}

// filter out validators whose effective balance is not sufficient to validate
activeVals := make([]ValidatorT, 0, len(vals))
for _, val := range vals {
if val.GetEffectiveBalance() > math.U64(sp.cs.EjectionBalance()) {
activeVals = append(activeVals, val)
}
}
Comment on lines +41 to +46
Copy link
Collaborator Author

@abi87 abi87 Nov 9, 2024

Choose a reason for hiding this comment

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

you may ask why we didn't do it so far. The reason is so far:

  • Validator are activated as soon as they are added
  • They never reduces their stake
    • In fact this is not exactly true. We do reduce stake if a validator's balance goes beyond MaxEffectiveBalance but we reduce it just enough to get it to MaxEffectiveBalance. So the validator will stay active.

We need to change this to allow a capped validator set where validators may leave due to a validator with more stake coming in

abi87 marked this conversation as resolved.
Show resolved Hide resolved

// TODO: a more efficient handling would be to only send back to consensus
// updated validators (including evicted ones), rather than the full list

abi87 marked this conversation as resolved.
Show resolved Hide resolved
return iter.MapErr(
vals,
activeVals,
func(val *ValidatorT) (*transition.ValidatorUpdate, error) {
v := (*val)
return &transition.ValidatorUpdate{
Expand Down
Loading
Loading