Skip to content

Commit

Permalink
feat(blockchain): introducing validator size cap size (#2119)
Browse files Browse the repository at this point in the history
Signed-off-by: nidhi-singh02 <[email protected]>
Co-authored-by: Cal Bera <[email protected]>
  • Loading branch information
abi87 and calbera authored Dec 2, 2024
1 parent fd7cca5 commit eb7c5e8
Show file tree
Hide file tree
Showing 13 changed files with 754 additions and 15 deletions.
16 changes: 16 additions & 0 deletions mod/chain-spec/pkg/chain/chain_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ type Spec[

// Berachain Values

// ValidatorSetCap retrieves the maximum number of
// validators allowed in the active set.
ValidatorSetCap() uint64

// EVMInflationAddress returns the address on the EVM which will receive
// the inflation amount of native EVM balance through a withdrawal every
// block.
Expand Down Expand Up @@ -254,6 +258,10 @@ func (c *chainSpec[
return ErrInsufficientMaxWithdrawalsPerPayload
}

if c.ValidatorSetCap() > c.ValidatorRegistryLimit() {
return ErrInvalidValidatorSetCap
}

// EVM Inflation values can be zero or non-zero, no validation needed.

// TODO: Add more validation rules here.
Expand Down Expand Up @@ -545,6 +553,14 @@ func (c chainSpec[
return c.Data.CometValues
}

// ValidatorSetCap retrieves the maximum number of
// validators allowed in the active set.
func (c chainSpec[
DomainTypeT, EpochT, ExecutionAddressT, SlotT, CometBFTConfigT,
]) ValidatorSetCap() uint64 {
return c.Data.ValidatorSetCap
}

// EVMInflationAddress returns the address on the EVM which will receive the
// inflation amount of native EVM balance through a withdrawal every block.
func (c chainSpec[
Expand Down
4 changes: 4 additions & 0 deletions mod/chain-spec/pkg/chain/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ type SpecData[

// Berachain Values
//
// ValidatorSetCap is the maximum number of validators that can be active
// for a given epoch
// Note: ValidatorSetCap must be smaller than ValidatorRegistryLimit.
ValidatorSetCap uint64 `mapstructure:"validator-set-cap-size"`
// EVMInflationAddress is the address on the EVM which will receive the
// inflation amount of native EVM balance through a withdrawal every block.
EVMInflationAddress ExecutionAddressT `mapstructure:"evm-inflation-address"`
Expand Down
6 changes: 6 additions & 0 deletions mod/chain-spec/pkg/chain/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@ var (
// per block.
ErrInsufficientMaxWithdrawalsPerPayload = errors.New(
"max withdrawals per payload must be greater than 1")

// ErrInvalidValidatorSetCap is returned when the validator set cap is
// greater than the validator registry limit.
ErrInvalidValidatorSetCap = errors.New(
"validator set cap must be less than the validator registry limit",
)
)
3 changes: 3 additions & 0 deletions mod/config/pkg/spec/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,8 @@ func BaseSpec() chain.SpecData[

// Comet values.
CometValues: cmtConsensusParams,

// Berachain Values
ValidatorSetCap: 256,
}
}
7 changes: 6 additions & 1 deletion mod/config/pkg/spec/boonet.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ func BoonetChainSpec() (chain.Spec[

// BERA per block minting.
//
// TODO: determine correct value for boonet upgrade.
// TODO: Determine correct value for boonet upgrade.
boonetSpec.EVMInflationPerBlock = 2.5e9

// ValidatorSetCap is 256 on the Boonet chain.
//
// TODO: Determine correct value for boonet upgrade.
boonetSpec.ValidatorSetCap = 256

// MaxValidatorsPerWithdrawalsSweep is 43 because we expect at least 46
// validators in the total validators set. We choose a prime number smaller
// than the minimum amount of total validators possible.
Expand Down
5 changes: 5 additions & 0 deletions mod/consensus-types/pkg/types/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ func (v *Validator) SetEffectiveBalance(balance math.Gwei) {
v.EffectiveBalance = balance
}

// SetWithdrawableEpoch sets the epoch when the validator can withdraw.
func (v *Validator) SetWithdrawableEpoch(e math.Epoch) {
v.WithdrawableEpoch = e
}

// GetWithdrawableEpoch returns the epoch when the validator can withdraw.
func (v Validator) GetWithdrawableEpoch() math.Epoch {
return v.WithdrawableEpoch
Expand Down
10 changes: 10 additions & 0 deletions mod/state-transition/pkg/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,13 @@ func buildNextBlock(
Body: nextBlkBody,
}
}

var (
dummyExecutionPayload = &types.ExecutionPayload{
Timestamp: 0,
ExtraData: []byte("testing"),
Transactions: [][]byte{},
Withdrawals: []*engineprimitives.Withdrawal{},
BaseFeePerGas: math.NewU256(0),
}
)
4 changes: 4 additions & 0 deletions mod/state-transition/pkg/core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ package core
import "github.com/berachain/beacon-kit/mod/errors"

var (
// ErrValSetCapExceeded is returned when the number of genesis deposits
// exceeds the validator set cap.
ErrValSetCapExceeded = errors.New("validator set cap exceeded at genesis")

// ErrBlockSlotTooLow is returned when the block slot is too low.
ErrBlockSlotTooLow = errors.New("block slot too low")

Expand Down
124 changes: 121 additions & 3 deletions mod/state-transition/pkg/core/state_processor_staking.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
package core

import (
"bytes"
"slices"

"github.com/berachain/beacon-kit/mod/config/pkg/spec"
"github.com/berachain/beacon-kit/mod/errors"
"github.com/berachain/beacon-kit/mod/primitives/pkg/common"
Expand Down Expand Up @@ -145,15 +148,130 @@ func (sp *StateProcessor[
st BeaconStateT,
dep DepositT,
) error {
var val ValidatorT
val = val.New(
var candidateVal ValidatorT
candidateVal = candidateVal.New(
dep.GetPubkey(),
dep.GetWithdrawalCredentials(),
dep.GetAmount(),
math.Gwei(sp.cs.EffectiveBalanceIncrement()),
math.Gwei(sp.cs.MaxEffectiveBalance()),
)

// BeaconKit enforces a cap on the validator set size. If the deposit
// breaches the cap, we find the validator with the smallest stake and
// mark it as withdrawable so that it will be evicted next epoch and
// its deposits returned.

nextEpochVals, err := sp.nextEpochValidatorSet(st)
if err != nil {
return err
}
//#nosec:G701 // no overflow risk here
if uint64(len(nextEpochVals)) < sp.cs.ValidatorSetCap() {
// cap not hit, just add the validator
return sp.addValidatorInternal(st, candidateVal, dep.GetAmount())
}

// Adding the validator would breach the cap. Find the validator
// with the smallest stake among current and candidate validators
// and kick it out.
lowestStakeVal, err := sp.lowestStakeVal(nextEpochVals)
if err != nil {
return err
}

slot, err := st.GetSlot()
if err != nil {
return err
}
nextEpoch := sp.cs.SlotToEpoch(slot) + 1

if candidateVal.GetEffectiveBalance() <= lowestStakeVal.GetEffectiveBalance() {
// in case of tie-break among candidate validator we prefer
// existing one so we mark candidate as withdrawable
// We wait next epoch to return funds, as a way to curb spamming
candidateVal.SetWithdrawableEpoch(nextEpoch)
return sp.addValidatorInternal(st, candidateVal, dep.GetAmount())
}

// mark existing validator for eviction and add candidate
lowestStakeVal.SetWithdrawableEpoch(nextEpoch)
idx, err := st.ValidatorIndexByPubkey(lowestStakeVal.GetPubkey())
if err != nil {
return err
}
if err = st.UpdateValidatorAtIndex(idx, lowestStakeVal); err != nil {
return err
}
return sp.addValidatorInternal(st, candidateVal, dep.GetAmount())
}

// nextEpochValidatorSet returns the current estimation of what next epoch
// validator set would be.
func (sp *StateProcessor[
_, _, _, BeaconStateT, _, _, _, _, _, _, _, _, ValidatorT, _, _, _, _,
]) nextEpochValidatorSet(st BeaconStateT) ([]ValidatorT, error) {
slot, err := st.GetSlot()
if err != nil {
return nil, err
}
nextEpoch := sp.cs.SlotToEpoch(slot) + 1

vals, err := st.GetValidators()
if err != nil {
return nil, err
}
activeVals := make([]ValidatorT, 0, len(vals))
for _, val := range vals {
if val.GetEffectiveBalance() <= math.U64(sp.cs.EjectionBalance()) {
continue
}
if val.GetWithdrawableEpoch() == nextEpoch {
continue
}
activeVals = append(activeVals, val)
}

return activeVals, nil
}

// TODO: consider moving this to BeaconState directly
func (*StateProcessor[
_, _, _, _, _, _, _, _, _, _, _, _, ValidatorT, _, _, _, _,
]) lowestStakeVal(currentVals []ValidatorT) (
ValidatorT,
error,
) {
// TODO: consider heapifying slice instead. We only care about the smallest
slices.SortFunc(currentVals, func(lhs, rhs ValidatorT) int {
var (
val1Stake = lhs.GetEffectiveBalance()
val2Stake = rhs.GetEffectiveBalance()
)
switch {
case val1Stake < val2Stake:
return -1
case val1Stake > val2Stake:
return 1
default:
// validators pks are guaranteed to be different
var (
val1Pk = lhs.GetPubkey()
val2Pk = rhs.GetPubkey()
)
return bytes.Compare(val1Pk[:], val2Pk[:])
}
})
return currentVals[0], nil
}

func (sp *StateProcessor[
_, _, _, BeaconStateT, _, _, _, _, _, _, _, _, ValidatorT, _, _, _, _,
]) addValidatorInternal(
st BeaconStateT,
val ValidatorT,
depositAmount math.Gwei,
) error {
// TODO: This is a bug that lives on bArtio. Delete this eventually.
if sp.cs.DepositEth1ChainID() == spec.BartioChainID {
// Note in AddValidatorBartio we implicitly increase
Expand All @@ -168,5 +286,5 @@ func (sp *StateProcessor[
if err != nil {
return err
}
return st.IncreaseBalance(idx, dep.GetAmount())
return st.IncreaseBalance(idx, depositAmount)
}
Loading

0 comments on commit eb7c5e8

Please sign in to comment.