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

StakingV4: Create auction list selector subcomponent #4073

Merged
9 changes: 9 additions & 0 deletions epochStart/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,12 @@ var ErrSortAuctionList = errors.New("error while trying to sort auction list")

// ErrReceivedNewListNodeInStakingV4 signals that a new node has been assigned in common.NewList instead of common.AuctionList after staking v4
var ErrReceivedNewListNodeInStakingV4 = errors.New("new node has been assigned in common.NewList instead of common.AuctionList after staking v4")

// ErrNilMaxNodesChangeConfigProvider signals that a nil nodes config provider has been provided
var ErrNilMaxNodesChangeConfigProvider = errors.New("nil nodes config provider has been provided")

// ErrNilAuctionListSelector signals that a nil auction list selector has been provided
var ErrNilAuctionListSelector = errors.New("nil auction list selector has been provided")
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved

// ErrUint32SubtractionOverflow signals uint32 subtraction overflowed
var ErrUint32SubtractionOverflow = errors.New("uint32 subtraction overflowed")
16 changes: 16 additions & 0 deletions epochStart/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/ElrondNetwork/elrond-go-core/data"
"github.com/ElrondNetwork/elrond-go-core/data/block"
"github.com/ElrondNetwork/elrond-go/config"
"github.com/ElrondNetwork/elrond-go/state"
vmcommon "github.com/ElrondNetwork/elrond-vm-common"
)
Expand Down Expand Up @@ -199,3 +200,18 @@ type EpochNotifier interface {
CheckEpoch(epoch uint32)
IsInterfaceNil() bool
}

// MaxNodesChangeConfigProvider provides all config.MaxNodesChangeConfig, as well as
// the current config.MaxNodesChangeConfig based on the current epoch
type MaxNodesChangeConfigProvider interface {
GetAllNodesConfig() []config.MaxNodesChangeConfig
GetCurrentNodesConfig() config.MaxNodesChangeConfig
EpochConfirmed(epoch uint32, round uint64)
IsInterfaceNil() bool
}

// AuctionListSelector handles selection of nodes from auction list to be sent to waiting list, based on their top up
type AuctionListSelector interface {
SelectNodesFromAuctionList(validatorsInfoMap state.ShardValidatorsInfoMapHandler, randomness []byte) error
IsInterfaceNil() bool
}
255 changes: 255 additions & 0 deletions epochStart/metachain/auctionListSelector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package metachain

import (
"bytes"
"encoding/hex"
"fmt"
"math/big"
"sort"

"github.com/ElrondNetwork/elrond-go-core/core"
"github.com/ElrondNetwork/elrond-go-core/core/check"
"github.com/ElrondNetwork/elrond-go-core/display"
logger "github.com/ElrondNetwork/elrond-go-logger"
"github.com/ElrondNetwork/elrond-go/common"
"github.com/ElrondNetwork/elrond-go/epochStart"
"github.com/ElrondNetwork/elrond-go/process"
"github.com/ElrondNetwork/elrond-go/sharding"
"github.com/ElrondNetwork/elrond-go/state"
)

type auctionListSelector struct {
shardCoordinator sharding.Coordinator
stakingDataProvider epochStart.StakingDataProvider
nodesConfigProvider epochStart.MaxNodesChangeConfigProvider
}

// AuctionListSelectorArgs is a struct placeholder for all arguments required to create a NewAuctionListSelector
Copy link
Contributor

Choose a reason for hiding this comment

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

to create an auctionListSelector*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Both are linked by Goland editor, is there a reason to use auctionListSelector instead of NewAuctionListSelector ?

Copy link
Contributor

Choose a reason for hiding this comment

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

you don't create a NewAuctionListSelector.. but rather the argument struct is used to create an auctionListSelector

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, will fix it in the upcoming PR

type AuctionListSelectorArgs struct {
ShardCoordinator sharding.Coordinator
StakingDataProvider epochStart.StakingDataProvider
MaxNodesChangeConfigProvider epochStart.MaxNodesChangeConfigProvider
}

// NewAuctionListSelector will create a new auctionListSelector, which handles selection of nodes from auction list based
// on their top up
func NewAuctionListSelector(args AuctionListSelectorArgs) (*auctionListSelector, error) {
if check.IfNil(args.ShardCoordinator) {
return nil, epochStart.ErrNilShardCoordinator
}
if check.IfNil(args.StakingDataProvider) {
return nil, epochStart.ErrNilStakingDataProvider
}
if check.IfNil(args.MaxNodesChangeConfigProvider) {
return nil, epochStart.ErrNilMaxNodesChangeConfigProvider
}

asl := &auctionListSelector{
shardCoordinator: args.ShardCoordinator,
stakingDataProvider: args.StakingDataProvider,
nodesConfigProvider: args.MaxNodesChangeConfigProvider,
}

return asl, nil
}

// SelectNodesFromAuctionList will select nodes from validatorsInfoMap based on their top up. If two or more validators
// have the same top-up, then sorting will be done based on blsKey XOR randomness. Selected nodes will have their list set
// to common.SelectNodesFromAuctionList
func (als *auctionListSelector) SelectNodesFromAuctionList(validatorsInfoMap state.ShardValidatorsInfoMapHandler, randomness []byte) error {
if len(randomness) == 0 {
return process.ErrNilRandSeed
}

currNodesConfig := als.nodesConfigProvider.GetCurrentNodesConfig()
numOfShuffledNodes := currNodesConfig.NodesToShufflePerShard * (als.shardCoordinator.NumberOfShards() + 1)

auctionList, currNumOfValidators := getAuctionListAndNumOfValidators(validatorsInfoMap)
numOfValidatorsAfterShuffling, err := safeSub(currNumOfValidators, numOfShuffledNodes)
if err != nil {
log.Warn(fmt.Sprintf("%v when trying to compute numOfValidatorsAfterShuffling = %v - %v (currNumOfValidators - numOfShuffledNodes)",
err,
currNumOfValidators,
numOfShuffledNodes,
))
numOfValidatorsAfterShuffling = 0
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
}

maxNumNodes := currNodesConfig.MaxNumNodes
availableSlots, err := safeSub(maxNumNodes, numOfValidatorsAfterShuffling)
if availableSlots == 0 || err != nil {
log.Info(fmt.Sprintf("%v or zero value when trying to compute availableSlots = %v - %v (maxNodes - numOfValidatorsAfterShuffling); skip selecting nodes from auction list",
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
err,
maxNumNodes,
numOfValidatorsAfterShuffling,
))
return nil
}

auctionListSize := uint32(len(auctionList))
log.Info("systemSCProcessor.SelectNodesFromAuctionList",
"max nodes", maxNumNodes,
"current number of validators", currNumOfValidators,
"num of nodes which will be shuffled out", numOfShuffledNodes,
"num of validators after shuffling", numOfValidatorsAfterShuffling,
"auction list size", auctionListSize,
fmt.Sprintf("available slots (%v -%v)", maxNumNodes, numOfValidatorsAfterShuffling), availableSlots,
Copy link
Contributor

Choose a reason for hiding this comment

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

%v - %v*
also, log debug maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

%v - %v*

Any particular reason for this suggestion? I like the current version better

Copy link
Contributor

Choose a reason for hiding this comment

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

Because available slots (3 - 5) is better than available slots (3 -5).
Also, why don't you use %d instead of %v ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because available slots (3 - 5) is better than available slots (3 -5)

Right, I thought you suggested to remove the paranthesis

Also, why don't you use %d instead of %v ?

%v is the default value in golang, and for decimals it translates to %d, so it should make no difference, right?

)

err = als.sortAuctionList(auctionList, randomness)
if err != nil {
return err
}

numOfAvailableNodeSlots := core.MinUint32(auctionListSize, availableSlots)
als.displayAuctionList(auctionList, numOfAvailableNodeSlots)

for i := uint32(0); i < numOfAvailableNodeSlots; i++ {
newNode := auctionList[i]
newNode.SetList(string(common.SelectedFromAuctionList))
err = validatorsInfoMap.Replace(auctionList[i], newNode)
if err != nil {
return err
}
}

return nil
}

// TODO: Move this in elrond-go-core
func safeSub(a, b uint32) (uint32, error) {
if a < b {
return 0, epochStart.ErrUint32SubtractionOverflow
}
return a - b, nil
}

func getAuctionListAndNumOfValidators(validatorsInfoMap state.ShardValidatorsInfoMapHandler) ([]state.ValidatorInfoHandler, uint32) {
auctionList := make([]state.ValidatorInfoHandler, 0)
numOfValidators := uint32(0)

for _, validator := range validatorsInfoMap.GetAllValidatorsInfo() {
if validator.GetList() == string(common.AuctionList) {
auctionList = append(auctionList, validator)
continue
}
if isValidator(validator) {
numOfValidators++
}
}

return auctionList, numOfValidators
}

func (als *auctionListSelector) sortAuctionList(auctionList []state.ValidatorInfoHandler, randomness []byte) error {
if len(auctionList) == 0 {
return nil
}

validatorTopUpMap, err := als.getValidatorTopUpMap(auctionList)
if err != nil {
return fmt.Errorf("%w: %v", epochStart.ErrSortAuctionList, err)
}

pubKeyLen := len(auctionList[0].GetPublicKey())
normRandomness := calcNormalizedRandomness(randomness, pubKeyLen)
sort.SliceStable(auctionList, func(i, j int) bool {
pubKey1 := auctionList[i].GetPublicKey()
pubKey2 := auctionList[j].GetPublicKey()

nodeTopUpPubKey1 := validatorTopUpMap[string(pubKey1)]
nodeTopUpPubKey2 := validatorTopUpMap[string(pubKey2)]

if nodeTopUpPubKey1.Cmp(nodeTopUpPubKey2) == 0 {
return compareByXORWithRandomness(pubKey1, pubKey2, normRandomness)
}

return nodeTopUpPubKey1.Cmp(nodeTopUpPubKey2) > 0
})

return nil
}

func (als *auctionListSelector) getValidatorTopUpMap(validators []state.ValidatorInfoHandler) (map[string]*big.Int, error) {
ret := make(map[string]*big.Int, len(validators))

for _, validator := range validators {
pubKey := validator.GetPublicKey()
topUp, err := als.stakingDataProvider.GetNodeStakedTopUp(pubKey)
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("%w when trying to get top up per node for %s", err, hex.EncodeToString(pubKey))
}

ret[string(pubKey)] = topUp
}

return ret, nil
}

func calcNormalizedRandomness(randomness []byte, expectedLen int) []byte {
rand := randomness
randLen := len(rand)

if expectedLen > randLen {
repeatedCt := expectedLen/randLen + 1
bogdan-rosianu marked this conversation as resolved.
Show resolved Hide resolved
rand = bytes.Repeat(randomness, repeatedCt)
}

rand = rand[:expectedLen]
return rand
}

func compareByXORWithRandomness(pubKey1, pubKey2, randomness []byte) bool {
xorLen := len(randomness)

key1Xor := make([]byte, xorLen)
key2Xor := make([]byte, xorLen)

for idx := 0; idx < xorLen; idx++ {
key1Xor[idx] = pubKey1[idx] ^ randomness[idx]
key2Xor[idx] = pubKey2[idx] ^ randomness[idx]
}

return bytes.Compare(key1Xor, key2Xor) == 1
}

func (als *auctionListSelector) displayAuctionList(auctionList []state.ValidatorInfoHandler, numOfSelectedNodes uint32) {
if log.GetLevel() > logger.LogDebug {
return
}

tableHeader := []string{"Owner", "Registered key", "TopUp per node"}
lines := make([]*display.LineData, 0, len(auctionList))
horizontalLine := false
for idx, validator := range auctionList {
pubKey := validator.GetPublicKey()

owner, err := als.stakingDataProvider.GetBlsKeyOwner(pubKey)
log.LogIfError(err)

topUp, err := als.stakingDataProvider.GetNodeStakedTopUp(pubKey)
log.LogIfError(err)

horizontalLine = uint32(idx) == numOfSelectedNodes-1
line := display.NewLineData(horizontalLine, []string{
hex.EncodeToString([]byte(owner)),
hex.EncodeToString(pubKey),
topUp.String(),
})
lines = append(lines, line)
}

table, err := display.CreateTableString(tableHeader, lines)
if err != nil {
log.Error("could not create table", "error", err)
return
}

message := fmt.Sprintf("Auction list\n%s", table)
log.Debug(message)
}

// IsInterfaceNil checks if the underlying pointer is nil
func (als *auctionListSelector) IsInterfaceNil() bool {
return als == nil
}