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 16 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
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 @@ -245,6 +258,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"`
abi87 marked this conversation as resolved.
Show resolved Hide resolved
// Time parameters constants.
//
// SlotsPerEpoch is the number of slots per epoch.
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,5 +26,5 @@ const (
BartioChainID uint64 = 80084

//nolint:lll // temporary.
BArtioValRoot = "0x9147586693b6e8faa837715c0f3071c2000045b54233901c2e7871b15872bc43"
BartioValRoot = "0x9147586693b6e8faa837715c0f3071c2000045b54233901c2e7871b15872bc43"
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

💡 Codebase verification

Create a GitHub issue to track removal of Bartio special cases

No existing issue found. Multiple TODOs referencing Bartio special cases were found in:

  • mod/state-transition/pkg/core/state_processor_payload.go
  • mod/state-transition/pkg/core/state/statedb.go
  • mod/beacon/blockchain/execution_engine.go
🔗 Analysis chain

Track temporary code for future removal

The comment indicates this is temporary special case handling "to be removed". While the constant rename improves consistency, we should ensure this technical debt is tracked.

Let me help create a GitHub issue to track the removal of these special cases. Would you like me to:

  1. Create an issue to track the removal of Bartio special cases
  2. Include a timeline recommendation based on the validator balance processing improvements mentioned in the PR?
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for existing issues about Bartio special cases

# Search for related issues
gh issue list --search "Bartio special cases" --state all

# Check for other references to Bartio in the codebase
rg -i "bartio.*special.*case" 

Length of output: 425

)
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
11 changes: 11 additions & 0 deletions mod/state-transition/pkg/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 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.
- Withdrawals are automatically generated only if a validator effective balance goes beyond `MaxEffectiveBalance`. In this case enough 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.
abi87 marked this conversation as resolved.
Show resolved Hide resolved
- 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.
abi87 marked this conversation as resolved.
Show resolved Hide resolved

[^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. Moreover the deposit should be evicted at the next epoch.
abi87 marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions mod/state-transition/pkg/core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var (
// match the expected value.
ErrSlotMismatch = errors.New("slot mismatch")

// ErrProposerMismatch is returned when block builder does not match
// with the proposer reported by consensus.
ErrProposerMismatch = errors.New("proposer key mismatch")

// ErrParentRootMismatch is returned when the parent root in an execution
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/log"
"github.com/berachain/beacon-kit/mod/primitives/pkg/common"
Expand Down Expand Up @@ -202,34 +203,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 @@ -239,7 +232,7 @@ func (sp *StateProcessor[
}
}

return validatorUpdates, nil
return res, nil
}

// processSlot is run when a slot is missed.
Expand Down Expand Up @@ -341,6 +334,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 @@ -367,13 +363,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 @@ -385,14 +380,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 @@ -409,26 +396,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 @@ -521,3 +506,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