Skip to content

Commit

Permalink
feature(state-processor): update validators EffectiveBalance only w…
Browse files Browse the repository at this point in the history
…hen epoch turns (#2142)
  • Loading branch information
abi87 authored Nov 28, 2024
1 parent 4f43ca9 commit df1912e
Show file tree
Hide file tree
Showing 12 changed files with 580 additions and 198 deletions.
31 changes: 31 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,19 @@ type Spec[
// calculations.
EffectiveBalanceIncrement() uint64

// HysteresisQuotient returns the quotient used in effective balance
// calculations to create hysteresis. This provides resistance to small
// balance changes triggering effective balance updates.
HysteresisQuotient() uint64

// HysteresisDownwardMultiplier returns the multiplier used when checking
// if the effective balance should be decreased.
HysteresisDownwardMultiplier() uint64

// HysteresisUpwardMultiplier returns the multiplier used when checking
// if the effective balance should be increased.
HysteresisUpwardMultiplier() uint64

// Time parameters constants.

// SlotsPerEpoch returns the number of slots in an epoch.
Expand Down Expand Up @@ -271,6 +284,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
6 changes: 6 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,12 @@ type SpecData[
// EffectiveBalanceIncrement is the effective balance increment.
EffectiveBalanceIncrement uint64 `mapstructure:"effective-balance-increment"`

// HysteresisQuotient is the quotient used in effective balance calculations
HysteresisQuotient uint64 `mapstructure:"hysteresis-quotient"`
// HysteresisDownwardMultiplier is the multiplier for downward balance adjustments.
HysteresisDownwardMultiplier uint64 `mapstructure:"hysteresis-downward-multiplier"`
// HysteresisUpwardMultiplier is the multiplier for upward balance adjustments.
HysteresisUpwardMultiplier uint64 `mapstructure:"hysteresis-upward-multiplier"`
// Time parameters constants.
//
// SlotsPerEpoch is the number of slots per epoch.
Expand Down
12 changes: 8 additions & 4 deletions mod/config/pkg/spec/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ func BaseSpec() chain.SpecData[
any,
]{
// Gwei value constants.
MinDepositAmount: 1e9,
MaxEffectiveBalance: 32e9,
EjectionBalance: 16e9,
EffectiveBalanceIncrement: 1e9,
MinDepositAmount: 1e9,
MaxEffectiveBalance: 32e9,
EjectionBalance: 16e9,
EffectiveBalanceIncrement: 1e9,
HysteresisQuotient: 4,
HysteresisDownwardMultiplier: 1,
HysteresisUpwardMultiplier: 5,

// Time parameters constants.
SlotsPerEpoch: 32,
MinEpochsToInactivityPenalty: 4,
Expand Down
2 changes: 1 addition & 1 deletion mod/config/pkg/spec/special_cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const (
BartioChainID = TestnetEth1ChainID

//nolint:lll // temporary.
BArtioValRoot = "0x9147586693b6e8faa837715c0f3071c2000045b54233901c2e7871b15872bc43"
BartioValRoot = "0x9147586693b6e8faa837715c0f3071c2000045b54233901c2e7871b15872bc43"
)

// Planned hard-fork upgrades on boonet. To be removed.
Expand Down
12 changes: 12 additions & 0 deletions mod/state-transition/pkg/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# State Processor

## Validators handling

Currently:

- Any validator whose effective balance is above `EjectionBalance` will stay a validator forever, as we have not (yet) implemented withdrawals facilities, nor we do slash.
- Withdrawals are automatically generated only if a validator effective balance goes beyond `MaxEffectiveBalance`. In this case some of the balance is scheduled for withdrawal, just enough to make validator's effective balance equal to `MaxEffectiveBalance`. Since `MaxEffectiveBalance` > `EjectionBalance`, the validator will keep being a validator.
- If a deposit is made for a validator with a balance smaller or equal to `EjectionBalance`, no validator will be created[^1] because of the insufficient balance. However currently the whole deposited balance is **not** scheduled for withdrawal at the next epoch.
- `EffectiveBalance`s are updated one per epoch. Following Eth2.0 specs, the whole validators list is scanned and `EffectiveBalance` is updated only if the difference among `Balance` and `EffectiveBalance` is larger than a (upward or downward) threshold, set considering `EffectiveBalanceIncrement` and hysteresis.

[^1]: Technically a validator is made in the BeaconKit state to track the deposit, but such a validator is never returned to the consensus engine.
109 changes: 73 additions & 36 deletions mod/state-transition/pkg/core/state_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"fmt"

"github.com/berachain/beacon-kit/mod/config/pkg/spec"
"github.com/berachain/beacon-kit/mod/consensus-types/pkg/types"
"github.com/berachain/beacon-kit/mod/errors"
"github.com/berachain/beacon-kit/mod/log"
"github.com/berachain/beacon-kit/mod/primitives/pkg/common"
Expand Down Expand Up @@ -208,10 +209,7 @@ func (sp *StateProcessor[
]) ProcessSlots(
st BeaconStateT, slot math.Slot,
) (transition.ValidatorUpdates, error) {
var (
validatorUpdates transition.ValidatorUpdates
epochValidatorUpdates transition.ValidatorUpdates
)
var res transition.ValidatorUpdates

stateSlot, err := st.GetSlot()
if err != nil {
Expand All @@ -220,7 +218,6 @@ func (sp *StateProcessor[

// Iterate until we are "caught up".
for ; stateSlot < slot; stateSlot++ {
// Process the slot
if err = sp.processSlot(st); err != nil {
return nil, err
}
Expand All @@ -245,14 +242,11 @@ func (sp *StateProcessor[
// 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 @@ -262,7 +256,7 @@ func (sp *StateProcessor[
}
}

return validatorUpdates, nil
return res, nil
}

// processSlot is run when a slot is missed.
Expand Down Expand Up @@ -364,6 +358,9 @@ func (sp *StateProcessor[
if err := sp.processRewardsAndPenalties(st); err != nil {
return nil, err
}
if err := sp.processEffectiveBalanceUpdates(st); err != nil {
return nil, err
}
if err := sp.processSlashingsReset(st); err != nil {
return nil, err
}
Expand All @@ -390,13 +387,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 @@ -408,14 +404,6 @@ func (sp *StateProcessor[
)
}

parentBlockRoot := latestBlockHeader.HashTreeRoot()
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 @@ -432,26 +420,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),
// 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 @@ -544,3 +530,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(
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
}
14 changes: 13 additions & 1 deletion mod/state-transition/pkg/core/state_processor_committee.go
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)
}
}

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

return iter.MapErr(
vals,
activeVals,
func(val *ValidatorT) (*transition.ValidatorUpdate, error) {
v := (*val)
return &transition.ValidatorUpdate{
Expand Down
Loading

0 comments on commit df1912e

Please sign in to comment.