From 2e20e5a88052be6b652b1a94bb0395fcbb0588f6 Mon Sep 17 00:00:00 2001 From: Seoyoung Ko <31757627+koseoyoung@users.noreply.github.com> Date: Tue, 18 Feb 2020 12:24:07 -0800 Subject: [PATCH] Change candidates/kickoutList state DB implementation (#1858) * change candidate/kickout/unproductiveddelegate stateDB implementation * fix a bug by passing genesis through context * add unit test/ add comment/ refactor * handle corner case by comparing state height --- action/protocol/poll/governance_protocol.go | 254 +++++++++++++----- .../protocol/poll/governance_protocol_test.go | 125 +++++++-- action/protocol/poll/lifelong_protocol.go | 62 +++-- .../protocol/poll/lifelong_protocol_test.go | 5 + action/protocol/poll/protocol.go | 24 +- action/protocol/poll/protocol_test.go | 2 + .../protocol/poll/staking_committee_test.go | 17 +- action/protocol/poll/util.go | 95 ++++++- action/protocol/protocol.go | 5 + action/protocol/rewarding/protocol.go | 10 +- action/protocol/rewarding/reward.go | 2 +- action/protocol/vote/blacklist.go | 2 +- action/protocol/vote/blacklist_test.go | 9 + .../vote/candidatesutil/candidatesutil.go | 170 ++++++------ action/protocol/vote/unproductivedelegate.go | 122 +++++++++ .../vote/unproductivedelegate_test.go | 50 ++++ .../unproductivedelegate.pb.go | 140 ++++++++++ .../unproductivedelegate.proto | 21 ++ api/api.go | 27 +- api/api_test.go | 17 +- blockchain/blockchain.go | 6 + blockchain/genesis/genesis.go | 9 +- chainservice/chainservice.go | 6 +- consensus/consensus.go | 23 +- consensus/scheme/rolldpos/rolldpos_test.go | 33 ++- consensus/scheme/rolldpos/rolldposctx.go | 3 +- consensus/scheme/rolldpos/rolldposctx_test.go | 157 +++++++---- consensus/scheme/rolldpos/roundcalculator.go | 18 +- .../scheme/rolldpos/roundcalculator_test.go | 79 +++--- state/factory/factory_test.go | 3 + 30 files changed, 1134 insertions(+), 362 deletions(-) create mode 100644 action/protocol/vote/unproductivedelegate.go create mode 100644 action/protocol/vote/unproductivedelegate_test.go create mode 100644 action/protocol/vote/unproductivedelegatepb/unproductivedelegate.pb.go create mode 100644 action/protocol/vote/unproductivedelegatepb/unproductivedelegate.proto diff --git a/action/protocol/poll/governance_protocol.go b/action/protocol/poll/governance_protocol.go index 71e3d6c5dc..5a467fb88c 100644 --- a/action/protocol/poll/governance_protocol.go +++ b/action/protocol/poll/governance_protocol.go @@ -32,7 +32,9 @@ import ( type governanceChainCommitteeProtocol struct { candidatesByHeight CandidatesByHeight - kickoutListByEpoch KickoutListByEpoch + getCandidates GetCandidates + getKickoutList GetKickoutList + getUnproductiveDelegate GetUnproductiveDelegate getBlockTime GetBlockTime electionCommittee committee.Committee initGravityChainHeight uint64 @@ -45,12 +47,15 @@ type governanceChainCommitteeProtocol struct { productivityThreshold uint64 kickoutEpochPeriod uint64 kickoutIntensity float64 + maxKickoutPeriod uint64 } // NewGovernanceChainCommitteeProtocol creates a Poll Protocol which fetch result from governance chain func NewGovernanceChainCommitteeProtocol( candidatesByHeight CandidatesByHeight, - kickoutListByEpoch KickoutListByEpoch, + getCandidates GetCandidates, + getKickoutList GetKickoutList, + getUnproductiveDelegate GetUnproductiveDelegate, electionCommittee committee.Committee, initGravityChainHeight uint64, getBlockTime GetBlockTime, @@ -62,6 +67,7 @@ func NewGovernanceChainCommitteeProtocol( productivityThreshold uint64, kickoutEpochPeriod uint64, kickoutIntensity float64, + maxKickoutPeriod uint64, ) (Protocol, error) { if electionCommittee == nil { return nil, ErrNoElectionCommittee @@ -77,7 +83,9 @@ func NewGovernanceChainCommitteeProtocol( } return &governanceChainCommitteeProtocol{ candidatesByHeight: candidatesByHeight, - kickoutListByEpoch: kickoutListByEpoch, + getCandidates: getCandidates, + getKickoutList: getKickoutList, + getUnproductiveDelegate: getUnproductiveDelegate, electionCommittee: electionCommittee, initGravityChainHeight: initGravityChainHeight, getBlockTime: getBlockTime, @@ -90,6 +98,7 @@ func NewGovernanceChainCommitteeProtocol( productivityThreshold: productivityThreshold, kickoutEpochPeriod: kickoutEpochPeriod, kickoutIntensity: kickoutIntensity, + maxKickoutPeriod: maxKickoutPeriod, }, nil } @@ -98,6 +107,7 @@ func (p *governanceChainCommitteeProtocol) CreateGenesisStates( sm protocol.StateManager, ) (err error) { blkCtx := protocol.MustGetBlockCtx(ctx) + bcCtx := protocol.MustGetBlockchainCtx(ctx) if blkCtx.BlockHeight != 0 { return errors.Errorf("Cannot create genesis state for height %d", blkCtx.BlockHeight) } @@ -112,7 +122,6 @@ func (p *governanceChainCommitteeProtocol) CreateGenesisStates( log.L().Info("calling committee,waiting for a while", zap.Int64("duration", int64(p.initialCandidatesInterval.Seconds())), zap.String("unit", " seconds")) time.Sleep(p.initialCandidatesInterval) } - if err != nil { return } @@ -120,8 +129,15 @@ func (p *governanceChainCommitteeProtocol) CreateGenesisStates( if err = validateDelegates(ds); err != nil { return } - - return setCandidates(sm, ds, uint64(1)) + hu := config.NewHeightUpgrade(&bcCtx.Genesis) + if hu.IsPost(config.Easter, uint64(1)) { + if err := setNextEpochBlacklist(sm, &vote.Blacklist{ + IntensityRate: p.kickoutIntensity, + }); err != nil { + return err + } + } + return setCandidates(ctx, sm, ds, uint64(1)) } func (p *governanceChainCommitteeProtocol) CreatePostSystemActions(ctx context.Context) ([]action.Envelope, error) { @@ -133,17 +149,31 @@ func (p *governanceChainCommitteeProtocol) CreatePreStates(ctx context.Context, blkCtx := protocol.MustGetBlockCtx(ctx) rp := rolldpos.MustGetProtocol(bcCtx.Registry) epochNum := rp.GetEpochNum(blkCtx.BlockHeight) + epochStartHeight := rp.GetEpochHeight(epochNum) epochLastHeight := rp.GetEpochLastBlockHeight(epochNum) nextEpochStartHeight := rp.GetEpochHeight(epochNum + 1) hu := config.NewHeightUpgrade(&bcCtx.Genesis) if blkCtx.BlockHeight == epochLastHeight && hu.IsPost(config.Easter, nextEpochStartHeight) { // if the block height is the end of epoch and next epoch is after the Easter height, calculate blacklist for kick-out and write into state DB - unqualifiedList, err := p.calculateKickoutBlackList(ctx, epochNum+1) + unqualifiedList, err := p.calculateKickoutBlackList(ctx, sm, epochNum+1) if err != nil { return err } - - return setKickoutBlackList(sm, unqualifiedList, epochNum+1) + return setNextEpochBlacklist(sm, unqualifiedList) + } + if blkCtx.BlockHeight == epochStartHeight && hu.IsPost(config.Easter, epochStartHeight) { + prevHeight, err := shiftCandidates(sm) + if err != nil { + return err + } + afterHeight, err := shiftKickoutList(sm) + if err != nil { + return err + } + if prevHeight != afterHeight { + return errors.Wrap(ErrInconsistentHeight, "shifting candidate height is not same as shifting kickout height") + } + return nil } return nil } @@ -206,13 +236,29 @@ func (p *governanceChainCommitteeProtocol) CalculateCandidatesByHeight(ctx conte } func (p *governanceChainCommitteeProtocol) DelegatesByEpoch(ctx context.Context, epochNum uint64) (state.CandidateList, error) { - return p.readActiveBlockProducersByEpoch(ctx, epochNum) + bcCtx := protocol.MustGetBlockchainCtx(ctx) + rp := rolldpos.MustGetProtocol(bcCtx.Registry) + tipEpochNum := rp.GetEpochNum(bcCtx.Tip.Height) + if tipEpochNum+1 == epochNum { + return p.readActiveBlockProducersByEpoch(ctx, epochNum, true) + } else if tipEpochNum == epochNum { + return p.readActiveBlockProducersByEpoch(ctx, epochNum, false) + } + return nil, errors.Errorf("wrong epochNumber to get delegates, epochNumber %d can't be less than tip epoch number %d", epochNum, tipEpochNum) } func (p *governanceChainCommitteeProtocol) CandidatesByHeight(ctx context.Context, height uint64) (state.CandidateList, error) { bcCtx := protocol.MustGetBlockchainCtx(ctx) rp := rolldpos.MustGetProtocol(bcCtx.Registry) - return p.candidatesByHeight(p.sr, rp.GetEpochHeight(rp.GetEpochNum(height))) + tipEpochNum := rp.GetEpochNum(bcCtx.Tip.Height) + targetEpochNum := rp.GetEpochNum(height) + targetEpochStartHeight := rp.GetEpochHeight(targetEpochNum) + if tipEpochNum+1 == targetEpochNum { + return p.readCandidatesByHeight(ctx, targetEpochStartHeight, true) + } else if tipEpochNum == targetEpochNum { + return p.readCandidatesByHeight(ctx, targetEpochStartHeight, false) + } + return nil, errors.Errorf("wrong epochNumber to get candidatesbyHeight, target epochNumber %d can't be less than tip epoch number %d", targetEpochNum, tipEpochNum) } func (p *governanceChainCommitteeProtocol) ReadState( @@ -221,30 +267,43 @@ func (p *governanceChainCommitteeProtocol) ReadState( method []byte, args ...[]byte, ) ([]byte, error) { + blkCtx := protocol.MustGetBlockCtx(ctx) + bcCtx := protocol.MustGetBlockchainCtx(ctx) + rp := rolldpos.MustGetProtocol(bcCtx.Registry) + tipEpoch := rp.GetEpochNum(blkCtx.BlockHeight) switch string(method) { case "CandidatesByEpoch": - if len(args) != 1 { - return nil, errors.Errorf("invalid number of arguments %d", len(args)) + if len(args) != 0 { + inputEpochNum := byteutil.BytesToUint64(args[0]) + if inputEpochNum != tipEpoch { + return nil, errors.New("previous epoch data isn't available with non-archive node") + } } - delegates, err := p.readCandidatesByEpoch(ctx, byteutil.BytesToUint64(args[0])) + delegates, err := p.readCandidatesByEpoch(ctx, tipEpoch, false) if err != nil { return nil, err } return delegates.Serialize() case "BlockProducersByEpoch": - if len(args) != 1 { - return nil, errors.Errorf("invalid number of arguments %d", len(args)) + if len(args) != 0 { + inputEpochNum := byteutil.BytesToUint64(args[0]) + if inputEpochNum != tipEpoch { + return nil, errors.New("previous epoch data isn't available with non-archive node") + } } - blockProducers, err := p.readBlockProducersByEpoch(ctx, byteutil.BytesToUint64(args[0])) + blockProducers, err := p.readBlockProducersByEpoch(ctx, byteutil.BytesToUint64(args[0]), false) if err != nil { return nil, err } return blockProducers.Serialize() case "ActiveBlockProducersByEpoch": - if len(args) != 1 { - return nil, errors.Errorf("invalid number of arguments %d", len(args)) + if len(args) != 0 { + inputEpochNum := byteutil.BytesToUint64(args[0]) + if inputEpochNum != tipEpoch { + return nil, errors.New("previous epoch data isn't available with non-archive node") + } } - activeBlockProducers, err := p.readActiveBlockProducersByEpoch(ctx, byteutil.BytesToUint64(args[0])) + activeBlockProducers, err := p.readActiveBlockProducersByEpoch(ctx, byteutil.BytesToUint64(args[0]), false) if err != nil { return nil, err } @@ -258,6 +317,18 @@ func (p *governanceChainCommitteeProtocol) ReadState( return nil, err } return byteutil.Uint64ToBytes(gravityStartheight), nil + case "KickoutListByEpoch": + if len(args) != 0 { + inputEpochNum := byteutil.BytesToUint64(args[0]) + if inputEpochNum != tipEpoch { + return nil, errors.New("previous epoch data isn't available with non-archive node") + } + } + kickoutList, err := p.readKickoutList(ctx, tipEpoch, false) + if err != nil { + return nil, err + } + return kickoutList.Serialize() default: return nil, errors.New("corresponding method isn't found") @@ -274,22 +345,39 @@ func (p *governanceChainCommitteeProtocol) ForceRegister(r *protocol.Registry) e return r.ForceRegister(protocolID, p) } -func (p *governanceChainCommitteeProtocol) readCandidatesByEpoch(ctx context.Context, epochNum uint64) (state.CandidateList, error) { +func (p *governanceChainCommitteeProtocol) readCandidatesByEpoch(ctx context.Context, epochNum uint64, readFromNext bool) (state.CandidateList, error) { bcCtx := protocol.MustGetBlockchainCtx(ctx) rp := rolldpos.MustGetProtocol(bcCtx.Registry) - return p.candidatesByHeight(p.sr, rp.GetEpochHeight(epochNum)) + return p.readCandidatesByHeight(ctx, rp.GetEpochHeight(epochNum), readFromNext) } -func (p *governanceChainCommitteeProtocol) readBlockProducersByEpoch(ctx context.Context, epochNum uint64) (state.CandidateList, error) { - candidates, err := p.readCandidatesByEpoch(ctx, epochNum) +func (p *governanceChainCommitteeProtocol) readCandidatesByHeight(ctx context.Context, epochStartHeight uint64, readFromNext bool) (state.CandidateList, error) { + bcCtx := protocol.MustGetBlockchainCtx(ctx) + hu := config.NewHeightUpgrade(&bcCtx.Genesis) + rp := rolldpos.MustGetProtocol(bcCtx.Registry) + if hu.IsPre(config.Easter, epochStartHeight) { + return p.candidatesByHeight(p.sr, epochStartHeight) + } + sc, stateHeight, err := p.getCandidates(p.sr, readFromNext) if err != nil { return nil, err } + // to catch the corner case that since the new block is committed, shift occurs in the middle of processing the request + if epochStartHeight < rp.GetEpochHeight(rp.GetEpochNum(stateHeight)) { + return nil, errors.Wrap(ErrInconsistentHeight, "state factory height became larger than target height") + } + return sc, nil +} + +func (p *governanceChainCommitteeProtocol) readBlockProducersByEpoch(ctx context.Context, epochNum uint64, readFromNext bool) (state.CandidateList, error) { bcCtx := protocol.MustGetBlockchainCtx(ctx) - rp := rolldpos.MustGetProtocol(bcCtx.Registry) - epochHeight := rp.GetEpochHeight(epochNum) + candidates, err := p.readCandidatesByEpoch(ctx, epochNum, readFromNext) + if err != nil { + return nil, err + } hu := config.NewHeightUpgrade(&bcCtx.Genesis) - if hu.IsPre(config.Easter, epochHeight) { + rp := rolldpos.MustGetProtocol(bcCtx.Registry) + if hu.IsPre(config.Easter, rp.GetEpochHeight(epochNum)) || epochNum == 1 { var blockProducers state.CandidateList for i, candidate := range candidates { if uint64(i) >= p.numCandidateDelegates { @@ -301,9 +389,9 @@ func (p *governanceChainCommitteeProtocol) readBlockProducersByEpoch(ctx context } // After Easter height, kick-out unqualified delegates based on productivity - unqualifiedList, err := p.kickoutListByEpoch(p.sr, epochNum) + unqualifiedList, err := p.readKickoutList(ctx, epochNum, readFromNext) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to read kick-out list") } // recalculate the voting power for blacklist delegates candidatesMap := make(map[string]*state.Candidate) @@ -332,8 +420,8 @@ func (p *governanceChainCommitteeProtocol) readBlockProducersByEpoch(ctx context return verifiedCandidates, nil } -func (p *governanceChainCommitteeProtocol) readActiveBlockProducersByEpoch(ctx context.Context, epochNum uint64) (state.CandidateList, error) { - blockProducers, err := p.readBlockProducersByEpoch(ctx, epochNum) +func (p *governanceChainCommitteeProtocol) readActiveBlockProducersByEpoch(ctx context.Context, epochNum uint64, readFromNext bool) (state.CandidateList, error) { + blockProducers, err := p.readBlockProducersByEpoch(ctx, epochNum, readFromNext) if err != nil { return nil, errors.Wrapf(err, "failed to get candidates in epoch %d", epochNum) } @@ -367,6 +455,20 @@ func (p *governanceChainCommitteeProtocol) readActiveBlockProducersByEpoch(ctx c return activeBlockProducers, nil } +func (p *governanceChainCommitteeProtocol) readKickoutList(ctx context.Context, epochNum uint64, readFromNext bool) (*vote.Blacklist, error) { + bcCtx := protocol.MustGetBlockchainCtx(ctx) + rp := rolldpos.MustGetProtocol(bcCtx.Registry) + unqualifiedList, stateHeight, err := p.getKickoutList(p.sr, readFromNext) + if err != nil { + return nil, errors.Wrapf(err, "failed to get kickout list when reading from state DB in epoch %d", epochNum) + } + // to catch the corner case that since the new block is committed, shift occurs in the middle of processing the request + if epochNum < rp.GetEpochNum(stateHeight) { + return nil, errors.Wrap(ErrInconsistentHeight, "state factory tip epoch number became larger than target epoch number") + } + return unqualifiedList, nil +} + func (p *governanceChainCommitteeProtocol) getGravityHeight(ctx context.Context, height uint64) (uint64, error) { bcCtx := protocol.MustGetBlockchainCtx(ctx) rp := rolldpos.MustGetProtocol(bcCtx.Registry) @@ -385,6 +487,7 @@ func (p *governanceChainCommitteeProtocol) getGravityHeight(ctx context.Context, func (p *governanceChainCommitteeProtocol) calculateKickoutBlackList( ctx context.Context, + sm protocol.StateManager, epochNum uint64, ) (*vote.Blacklist, error) { bcCtx := protocol.MustGetBlockchainCtx(ctx) @@ -394,46 +497,67 @@ func (p *governanceChainCommitteeProtocol) calculateKickoutBlackList( nextBlacklist := &vote.Blacklist{ IntensityRate: p.kickoutIntensity, } + upd, err := p.getUnproductiveDelegate(p.sr) + if err != nil { + if errors.Cause(err) == state.ErrStateNotExist { + if upd, err = vote.NewUnproductiveDelegate(p.kickoutEpochPeriod, p.maxKickoutPeriod); err != nil { + return nil, errors.Wrap(err, "failed to make new upd") + } + } else { + return nil, errors.Wrapf(err, "failed to read upd struct from state DB at epoch number %d", epochNum) + } + } unqualifiedDelegates := make(map[string]uint32) if epochNum <= easterEpochNum+p.kickoutEpochPeriod { - // if epoch number is smaller than EasterHeightEpoch+K(kickout period), calculate it one-by-one (initialize). - round := epochNum - easterEpochNum // 0, 1, 2, 3 .. K - for { - if round == 0 { - break - } - // N-1, N-2, ..., N-K - isCurrentEpoch := false - if round == 1 { - isCurrentEpoch = true - } - uq, err := p.calculateUnproductiveDelegatesByEpoch(ctx, epochNum-round, isCurrentEpoch) - if err != nil { - return nil, err - } - for _, addr := range uq { + // if epoch number is smaller than easterEpochNum+K(kickout period), calculate it one-by-one (initialize). + log.L().Debug("Before using kick-out blacklist", + zap.Uint64("epochNum", epochNum), + zap.Uint64("easterEpochNum", easterEpochNum), + zap.Uint64("kickoutEpochPeriod", p.kickoutEpochPeriod), + ) + existinglist := upd.DelegateList() + for _, listByEpoch := range existinglist { + for _, addr := range listByEpoch { if _, ok := unqualifiedDelegates[addr]; !ok { unqualifiedDelegates[addr] = 1 } else { unqualifiedDelegates[addr]++ } } - round-- + } + // calculate upd of epochNum-1 (latest) + uq, err := p.calculateUnproductiveDelegatesByEpoch(ctx, epochNum-1) + if err != nil { + return nil, errors.Wrapf(err, "failed to calculate current epoch upd %d", epochNum-1) + } + for _, addr := range uq { + if _, ok := unqualifiedDelegates[addr]; !ok { + unqualifiedDelegates[addr] = 1 + } else { + unqualifiedDelegates[addr]++ + } + } + if err := upd.AddRecentUPD(uq); err != nil { + return nil, errors.Wrap(err, "failed to add recent upd") } nextBlacklist.BlacklistInfos = unqualifiedDelegates - return nextBlacklist, nil + return nextBlacklist, setUnproductiveDelegates(sm, upd) } - // Blacklist[N] = Blacklist[N-1] - Low-productivity-list[N-K-1] + Low-productivity-list[N-1] - prevBlacklist, err := p.kickoutListByEpoch(p.sr, epochNum-1) + log.L().Debug("Using kick-out blacklist", + zap.Uint64("epochNum", epochNum), + zap.Uint64("easterEpochNum", easterEpochNum), + zap.Uint64("kickoutEpochPeriod", p.kickoutEpochPeriod), + ) + prevBlacklist, _, err := p.getKickoutList(p.sr, false) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to read latest kick-out list") } blacklistMap := prevBlacklist.BlacklistInfos - skipList, err := p.calculateUnproductiveDelegatesByEpoch(ctx, epochNum-p.kickoutEpochPeriod-1, false) - if err != nil { - return nil, err + if blacklistMap == nil { + blacklistMap = make(map[string]uint32) } + skipList := upd.ReadOldestUPD() for _, addr := range skipList { if _, ok := blacklistMap[addr]; !ok { log.L().Fatal("skipping list element doesn't exist among one of existing map") @@ -441,9 +565,12 @@ func (p *governanceChainCommitteeProtocol) calculateKickoutBlackList( } blacklistMap[addr]-- } - addList, err := p.calculateUnproductiveDelegatesByEpoch(ctx, epochNum-1, true) + addList, err := p.calculateUnproductiveDelegatesByEpoch(ctx, epochNum-1) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to calculate current epoch upd %d", epochNum-1) + } + if err := upd.AddRecentUPD(addList); err != nil { + return nil, errors.Wrap(err, "failed to add recent upd") } for _, addr := range addList { if _, ok := blacklistMap[addr]; ok { @@ -459,25 +586,22 @@ func (p *governanceChainCommitteeProtocol) calculateKickoutBlackList( } } nextBlacklist.BlacklistInfos = blacklistMap - - return nextBlacklist, nil + return nextBlacklist, setUnproductiveDelegates(sm, upd) } func (p *governanceChainCommitteeProtocol) calculateUnproductiveDelegatesByEpoch( ctx context.Context, epochNum uint64, - isCurrentEpoch bool, ) ([]string, error) { blkCtx := protocol.MustGetBlockCtx(ctx) numBlks, produce, err := p.productivityByEpoch(ctx, epochNum) if err != nil { return nil, err } - if isCurrentEpoch { - // The current block is not included, so that we need to add it to the stats - numBlks++ - produce[blkCtx.Producer.String()]++ - } + // The current block is not included, so that we need to add it to the stats + numBlks++ + produce[blkCtx.Producer.String()]++ + unqualified := make([]string, 0) expectedNumBlks := numBlks / uint64(len(produce)) for addr, actualNumBlks := range produce { diff --git a/action/protocol/poll/governance_protocol_test.go b/action/protocol/poll/governance_protocol_test.go index 55dc4a3dc1..5310d7fb42 100644 --- a/action/protocol/poll/governance_protocol_test.go +++ b/action/protocol/poll/governance_protocol_test.go @@ -36,6 +36,7 @@ import ( func initConstruct(ctrl *gomock.Controller) (Protocol, context.Context, protocol.StateManager, *types.ElectionResult, error) { cfg := config.Default cfg.Genesis.EasterBlockHeight = 1 // set up testing after Easter Height + cfg.Genesis.KickoutIntensityRate = 0.1 cfg.Genesis.KickoutEpochPeriod = 2 cfg.Genesis.ProductivityThreshold = 85 ctx := protocol.WithBlockCtx( @@ -54,6 +55,9 @@ func initConstruct(ctrl *gomock.Controller) (Protocol, context.Context, protocol protocol.BlockchainCtx{ Genesis: cfg.Genesis, Registry: registry, + Tip: protocol.TipInfo{ + Height: 720, + }, }, ) ctx = protocol.WithActionCtx( @@ -70,7 +74,7 @@ func initConstruct(ctrl *gomock.Controller) (Protocol, context.Context, protocol if err != nil { return 0, err } - val, err := cb.Get("state", cfg.Key) + val, err := cb.Get(cfg.Namespace, cfg.Key) if err != nil { return 0, state.ErrStateNotExist } @@ -86,7 +90,7 @@ func initConstruct(ctrl *gomock.Controller) (Protocol, context.Context, protocol if err != nil { return 0, err } - cb.Put("state", cfg.Key, ss, "failed to put state") + cb.Put(cfg.Namespace, cfg.Key, ss, "failed to put state") return 0, nil }).AnyTimes() sm.EXPECT().Snapshot().Return(1).AnyTimes() @@ -117,7 +121,9 @@ func initConstruct(ctrl *gomock.Controller) (Protocol, context.Context, protocol } p, err := NewGovernanceChainCommitteeProtocol( func(protocol.StateReader, uint64) ([]*state.Candidate, error) { return candidates, nil }, - candidatesutil.KickoutListByEpoch, + func(protocol.StateReader, bool) ([]*state.Candidate, uint64, error) { return candidates, 720, nil }, + candidatesutil.KickoutListFromDB, + candidatesutil.UnproductiveDelegateFromDB, committee, uint64(123456), func(uint64) (time.Time, error) { return time.Now(), nil }, @@ -158,7 +164,12 @@ func initConstruct(ctrl *gomock.Controller) (Protocol, context.Context, protocol cfg.Genesis.ProductivityThreshold, cfg.Genesis.KickoutEpochPeriod, cfg.Genesis.KickoutIntensityRate, + cfg.Genesis.UnproductiveDelegateMaxCacheSize, ) + + if err := setCandidates(ctx, sm, candidates, 1); err != nil { + return nil, nil, nil, nil, err + } return p, ctx, sm, r, err } @@ -170,7 +181,8 @@ func TestCreateGenesisStates(t *testing.T) { require.NoError(err) require.NoError(p.CreateGenesisStates(ctx, sm)) var sc state.CandidateList - _, err = sm.State(&sc, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + candKey := candidatesutil.ConstructKey(candidatesutil.NxtCandidateKey) + _, err = sm.State(&sc, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) require.NoError(err) candidates, err := state.CandidatesToMap(sc) require.NoError(err) @@ -242,6 +254,29 @@ func TestCreatePreStates(t *testing.T) { // testing for kick-out slashing var epochNum uint64 for epochNum = 1; epochNum <= 3; epochNum++ { + if epochNum > 1 { + epochStartHeight := rp.GetEpochHeight(epochNum) + ctx = protocol.WithBlockCtx( + ctx, + protocol.BlockCtx{ + BlockHeight: epochStartHeight, + Producer: identityset.Address(1), + }, + ) + require.NoError(psc.CreatePreStates(ctx, sm)) // shift + bl := &vote.Blacklist{} + candKey := candidatesutil.ConstructKey(candidatesutil.CurKickoutKey) + _, err := sm.State(bl, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) + require.NoError(err) + expected := test[epochNum] + require.Equal(len(expected), len(bl.BlacklistInfos)) + for addr, count := range bl.BlacklistInfos { + val, ok := expected[addr] + require.True(ok) + require.Equal(val, count) + } + } + // at last of epoch, set blacklist into next kickout key epochLastHeight := rp.GetEpochLastBlockHeight(epochNum) ctx = protocol.WithBlockCtx( ctx, @@ -251,8 +286,10 @@ func TestCreatePreStates(t *testing.T) { }, ) require.NoError(psc.CreatePreStates(ctx, sm)) + bl := &vote.Blacklist{} - _, err = sm.State(bl, protocol.LegacyKeyOption(candidatesutil.ConstructBlackListKey(epochNum+1))) + candKey := candidatesutil.ConstructKey(candidatesutil.NxtKickoutKey) + _, err = sm.State(bl, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) require.NoError(err) expected := test[epochNum+1] require.Equal(len(expected), len(bl.BlacklistInfos)) @@ -294,7 +331,9 @@ func TestHandle(t *testing.T) { require.NoError(err) require.NoError(p2.CreateGenesisStates(ctx2, sm2)) var sc2 state.CandidateList - _, err = sm2.State(&sc2, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + candKey := candidatesutil.ConstructKey(candidatesutil.NxtCandidateKey) + _, err = sm2.State(&sc2, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) + require.NoError(err) act2 := action.NewPutPollResult(1, 1, sc2) elp = bd.SetGasLimit(uint64(100000)). SetGasPrice(big.NewInt(10)). @@ -306,7 +345,9 @@ func TestHandle(t *testing.T) { require.NoError(err) require.NotNil(receipt) - candidates, err := candidatesutil.CandidatesByHeight(sm2, 1) + _, err = shiftCandidates(sm2) + require.NoError(err) + candidates, _, err := candidatesutil.CandidatesFromDB(sm2, false) require.NoError(err) require.Equal(2, len(candidates)) require.Equal(candidates[0].Address, sc2[0].Address) @@ -343,7 +384,8 @@ func TestProtocol_Validate(t *testing.T) { require.NoError(err) require.NoError(p2.CreateGenesisStates(ctx2, sm2)) var sc2 state.CandidateList - _, err = sm2.State(&sc2, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + candKey := candidatesutil.ConstructKey(candidatesutil.NxtCandidateKey) + _, err = sm2.State(&sc2, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) require.NoError(err) act2 := action.NewPutPollResult(1, 1, sc2) elp = bd.SetGasLimit(uint64(100000)). @@ -374,7 +416,7 @@ func TestProtocol_Validate(t *testing.T) { require.NoError(err) require.NoError(p3.CreateGenesisStates(ctx3, sm3)) var sc3 state.CandidateList - _, err = sm3.State(&sc3, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm3.State(&sc3, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) require.NoError(err) sc3 = append(sc3, &state.Candidate{"1", big.NewInt(10), "2", nil}) sc3 = append(sc3, &state.Candidate{"1", big.NewInt(10), "2", nil}) @@ -406,7 +448,7 @@ func TestProtocol_Validate(t *testing.T) { require.NoError(err) require.NoError(p4.CreateGenesisStates(ctx4, sm4)) var sc4 state.CandidateList - _, err = sm4.State(&sc4, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm4.State(&sc4, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) require.NoError(err) sc4 = append(sc4, &state.Candidate{"1", big.NewInt(10), "2", nil}) act4 := action.NewPutPollResult(1, 1, sc4) @@ -437,7 +479,7 @@ func TestProtocol_Validate(t *testing.T) { require.NoError(err) require.NoError(p5.CreateGenesisStates(ctx5, sm5)) var sc5 state.CandidateList - _, err = sm5.State(&sc5, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm5.State(&sc5, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) require.NoError(err) sc5[0].Votes = big.NewInt(10) act5 := action.NewPutPollResult(1, 1, sc5) @@ -468,7 +510,7 @@ func TestProtocol_Validate(t *testing.T) { require.NoError(err) require.NoError(p6.CreateGenesisStates(ctx6, sm6)) var sc6 state.CandidateList - _, err = sm6.State(&sc6, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm6.State(&sc6, protocol.KeyOption(candKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) require.NoError(err) act6 := action.NewPutPollResult(1, 1, sc6) bd6 := &action.EnvelopeBuilder{} @@ -503,22 +545,63 @@ func TestDelegatesByEpoch(t *testing.T) { p, ctx, sm, _, err := initConstruct(ctrl) require.NoError(err) - blackListMap := map[string]uint32{ - identityset.Address(1).String(): 1, - identityset.Address(2).String(): 1, - } - + // 1: empty blacklist DelegatesByEpoch() + blackListMap := map[string]uint32{} blackList := &vote.Blacklist{ BlacklistInfos: blackListMap, + IntensityRate: 0.1, } - require.NoError(setKickoutBlackList(sm, blackList, 2)) + require.NoError(setNextEpochBlacklist(sm, blackList)) delegates, err := p.DelegatesByEpoch(ctx, 2) require.NoError(err) - require.Equal(2, len(delegates)) + require.Equal(identityset.Address(2).String(), delegates[0].Address) + require.Equal(identityset.Address(1).String(), delegates[1].Address) + + // 2: not empty blacklist DelegatesByEpoch() + blackListMap2 := map[string]uint32{ + identityset.Address(1).String(): 1, + identityset.Address(2).String(): 1, + } + blackList2 := &vote.Blacklist{ + BlacklistInfos: blackListMap2, + IntensityRate: 0.1, + } + require.NoError(setNextEpochBlacklist(sm, blackList2)) + delegates2, err := p.DelegatesByEpoch(ctx, 2) + require.NoError(err) + require.Equal(2, len(delegates2)) // even though the address 1, 2 have larger amount of votes, it got kicked out because it's on kick-out list - require.Equal(identityset.Address(3).String(), delegates[0].Address) - require.Equal(identityset.Address(4).String(), delegates[1].Address) + require.Equal(identityset.Address(3).String(), delegates2[0].Address) + require.Equal(identityset.Address(4).String(), delegates2[1].Address) + + // 3: kickout out with different blacklist + blackListMap3 := map[string]uint32{ + identityset.Address(1).String(): 1, + identityset.Address(3).String(): 2, + } + blackList3 := &vote.Blacklist{ + BlacklistInfos: blackListMap3, + IntensityRate: 0.1, + } + require.NoError(setNextEpochBlacklist(sm, blackList3)) + + delegates3, err := p.DelegatesByEpoch(ctx, 2) + require.NoError(err) + + require.Equal(2, len(delegates3)) + require.Equal(identityset.Address(2).String(), delegates3[0].Address) + require.Equal(identityset.Address(4).String(), delegates3[1].Address) + + // 4: shift kickout list and Delegates() + _, err = shiftKickoutList(sm) + require.NoError(err) + delegates4, err := p.DelegatesByEpoch(ctx, 2) + require.NoError(err) + require.Equal(len(delegates4), len(delegates3)) + for i, d := range delegates3 { + require.True(d.Equal(delegates4[i])) + } } diff --git a/action/protocol/poll/lifelong_protocol.go b/action/protocol/poll/lifelong_protocol.go index 06900113a1..e5b7d91136 100644 --- a/action/protocol/poll/lifelong_protocol.go +++ b/action/protocol/poll/lifelong_protocol.go @@ -60,7 +60,7 @@ func (p *lifeLongDelegatesProtocol) CreateGenesisStates( return errors.Errorf("Cannot create genesis state for height %d", blkCtx.BlockHeight) } log.L().Info("Creating genesis states for lifelong delegates protocol") - return setCandidates(sm, p.delegates, uint64(1)) + return setCandidates(ctx, sm, p.delegates, uint64(1)) } func (p *lifeLongDelegatesProtocol) Handle(ctx context.Context, act action.Action, sm protocol.StateManager) (*action.Receipt, error) { @@ -76,33 +76,15 @@ func (p *lifeLongDelegatesProtocol) CalculateCandidatesByHeight(ctx context.Cont } func (p *lifeLongDelegatesProtocol) DelegatesByEpoch(ctx context.Context, epochNum uint64) (state.CandidateList, error) { - var blockProducerList []string bcCtx := protocol.MustGetBlockchainCtx(ctx) rp := rolldpos.MustGetProtocol(bcCtx.Registry) - blockProducerMap := make(map[string]*state.Candidate) - delegates := p.delegates - if len(p.delegates) > int(rp.NumCandidateDelegates()) { - delegates = p.delegates[:rp.NumCandidateDelegates()] - } - for _, bp := range delegates { - blockProducerList = append(blockProducerList, bp.Address) - blockProducerMap[bp.Address] = bp + tipEpochNum := rp.GetEpochNum(bcCtx.Tip.Height) + if tipEpochNum+1 == epochNum { + return p.readActiveBlockProducersByEpoch(ctx, epochNum, true) + } else if tipEpochNum == epochNum { + return p.readActiveBlockProducersByEpoch(ctx, epochNum, false) } - - epochHeight := rp.GetEpochHeight(epochNum) - crypto.SortCandidates(blockProducerList, epochHeight, crypto.CryptoSeed) - // TODO: kick-out unqualified delegates based on productivity - length := int(rp.NumDelegates()) - if len(blockProducerList) < length { - // TODO: if the number of delegates is smaller than expected, should it return error or not? - length = len(blockProducerList) - } - - var activeBlockProducers state.CandidateList - for i := 0; i < length; i++ { - activeBlockProducers = append(activeBlockProducers, blockProducerMap[blockProducerList[i]]) - } - return activeBlockProducers, nil + return nil, errors.Errorf("wrong epochNumber to get delegates, epochNumber %d can't be less than tip epoch number %d", epochNum, tipEpochNum) } func (p *lifeLongDelegatesProtocol) CandidatesByHeight(ctx context.Context, height uint64) (state.CandidateList, error) { @@ -145,3 +127,33 @@ func (p *lifeLongDelegatesProtocol) ForceRegister(r *protocol.Registry) error { func (p *lifeLongDelegatesProtocol) readBlockProducers() ([]byte, error) { return p.delegates.Serialize() } + +func (p *lifeLongDelegatesProtocol) readActiveBlockProducersByEpoch(ctx context.Context, epochNum uint64, _ bool) (state.CandidateList, error) { + var blockProducerList []string + bcCtx := protocol.MustGetBlockchainCtx(ctx) + rp := rolldpos.MustGetProtocol(bcCtx.Registry) + blockProducerMap := make(map[string]*state.Candidate) + delegates := p.delegates + if len(p.delegates) > int(rp.NumCandidateDelegates()) { + delegates = p.delegates[:rp.NumCandidateDelegates()] + } + for _, bp := range delegates { + blockProducerList = append(blockProducerList, bp.Address) + blockProducerMap[bp.Address] = bp + } + + epochHeight := rp.GetEpochHeight(epochNum) + crypto.SortCandidates(blockProducerList, epochHeight, crypto.CryptoSeed) + // TODO: kick-out unqualified delegates based on productivity + length := int(rp.NumDelegates()) + if len(blockProducerList) < length { + // TODO: if the number of delegates is smaller than expected, should it return error or not? + length = len(blockProducerList) + } + + var activeBlockProducers state.CandidateList + for i := 0; i < length; i++ { + activeBlockProducers = append(activeBlockProducers, blockProducerMap[blockProducerList[i]]) + } + return activeBlockProducers, nil +} diff --git a/action/protocol/poll/lifelong_protocol_test.go b/action/protocol/poll/lifelong_protocol_test.go index 85bd4ec5eb..96c9454825 100644 --- a/action/protocol/poll/lifelong_protocol_test.go +++ b/action/protocol/poll/lifelong_protocol_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "github.com/iotexproject/iotex-core/action/protocol" + "github.com/iotexproject/iotex-core/action/protocol/rolldpos" "github.com/iotexproject/iotex-core/config" "github.com/iotexproject/iotex-core/db/batch" "github.com/iotexproject/iotex-core/state" @@ -25,6 +26,10 @@ func initLifeLongDelegateProtocol(ctrl *gomock.Controller) (Protocol, context.Co delegates := genesisConfig.Delegates p := NewLifeLongDelegatesProtocol(delegates) registry := protocol.NewRegistry() + err := registry.Register("rolldpos", rolldpos.NewProtocol(36, 36, 20)) + if err != nil { + return nil, nil, nil, err + } ctx := protocol.WithBlockchainCtx( context.Background(), protocol.BlockchainCtx{ diff --git a/action/protocol/poll/protocol.go b/action/protocol/poll/protocol.go index 4d45fb5273..d21e92bcd7 100644 --- a/action/protocol/poll/protocol.go +++ b/action/protocol/poll/protocol.go @@ -25,6 +25,9 @@ const ( protocolID = "poll" ) +// ErrInconsistentHeight is an error that result of "readFromStateDB" is not consistent with others +var ErrInconsistentHeight = errors.New("data is inconsistent because the state height has been changed") + // ErrNoElectionCommittee is an error that the election committee is not specified var ErrNoElectionCommittee = errors.New("no election committee specified") @@ -40,8 +43,14 @@ var ErrDelegatesNotExist = errors.New("delegates cannot be found") // CandidatesByHeight returns the candidates of a given height type CandidatesByHeight func(protocol.StateReader, uint64) ([]*state.Candidate, error) -// KickoutListByEpoch returns the blacklist for kickout of a given epoch -type KickoutListByEpoch func(protocol.StateReader, uint64) (*vote.Blacklist, error) +// GetCandidates returns the current candidates +type GetCandidates func(protocol.StateReader, bool) ([]*state.Candidate, uint64, error) + +// GetKickoutList returns current the blacklist +type GetKickoutList func(protocol.StateReader, bool) (*vote.Blacklist, uint64, error) + +// GetUnproductiveDelegate returns unproductiveDelegate struct which contains a cache of upd info by epochs +type GetUnproductiveDelegate func(protocol.StateReader) (*vote.UnproductiveDelegate, error) // GetBlockTime defines a function to get block creation time type GetBlockTime func(uint64) (time.Time, error) @@ -53,12 +62,10 @@ type ProductivityByEpoch func(context.Context, uint64) (uint64, map[string]uint6 type Protocol interface { protocol.Protocol protocol.GenesisStateCreator - // DelegatesByEpoch returns the delegates by epoch DelegatesByEpoch(context.Context, uint64) (state.CandidateList, error) + CandidatesByHeight(context.Context, uint64) (state.CandidateList, error) // CalculateCandidatesByHeight calculates candidate and returns candidates by chain height CalculateCandidatesByHeight(context.Context, uint64) (state.CandidateList, error) - // CandidatesByHeight returns a list of delegate candidates - CandidatesByHeight(context.Context, uint64) (state.CandidateList, error) } // FindProtocol finds the registered protocol from registry @@ -100,7 +107,9 @@ func NewProtocol( cfg config.Config, readContract ReadContract, candidatesByHeight CandidatesByHeight, - kickoutListByEpoch KickoutListByEpoch, + getCandidates GetCandidates, + kickoutListByEpoch GetKickoutList, + getUnproductiveDelegate GetUnproductiveDelegate, electionCommittee committee.Committee, getBlockTimeFunc GetBlockTime, sr protocol.StateReader, @@ -121,7 +130,9 @@ func NewProtocol( var err error if governance, err = NewGovernanceChainCommitteeProtocol( candidatesByHeight, + getCandidates, kickoutListByEpoch, + getUnproductiveDelegate, electionCommittee, genesisConfig.GravityChainStartHeight, getBlockTimeFunc, @@ -133,6 +144,7 @@ func NewProtocol( genesisConfig.ProductivityThreshold, genesisConfig.KickoutEpochPeriod, genesisConfig.KickoutIntensityRate, + genesisConfig.UnproductiveDelegateMaxCacheSize, ); err != nil { return nil, err } diff --git a/action/protocol/poll/protocol_test.go b/action/protocol/poll/protocol_test.go index 6febb76617..22762b73b9 100644 --- a/action/protocol/poll/protocol_test.go +++ b/action/protocol/poll/protocol_test.go @@ -34,6 +34,8 @@ func TestNewProtocol(t *testing.T) { func(context.Context, string, []byte, bool) ([]byte, error) { return nil, nil }, nil, nil, + nil, + nil, committee, func(uint64) (time.Time, error) { return time.Now(), nil }, sm, diff --git a/action/protocol/poll/staking_committee_test.go b/action/protocol/poll/staking_committee_test.go index 297b990fa3..6e61152c05 100644 --- a/action/protocol/poll/staking_committee_test.go +++ b/action/protocol/poll/staking_committee_test.go @@ -94,6 +94,8 @@ func initConstructStakingCommittee(ctrl *gomock.Controller) (Protocol, context.C committee.EXPECT().ResultByHeight(uint64(123456)).Return(r, nil).AnyTimes() committee.EXPECT().HeightByTime(gomock.Any()).Return(uint64(123456), nil).AnyTimes() gs, err := NewGovernanceChainCommitteeProtocol( + nil, + nil, nil, nil, committee, @@ -109,6 +111,7 @@ func initConstructStakingCommittee(ctrl *gomock.Controller) (Protocol, context.C cfg.Genesis.ProductivityThreshold, cfg.Genesis.KickoutEpochPeriod, cfg.Genesis.KickoutIntensityRate, + cfg.Genesis.UnproductiveDelegateMaxCacheSize, ) scoreThreshold, ok := new(big.Int).SetString("0", 10) if !ok { @@ -138,7 +141,7 @@ func TestCreateGenesisStates_StakingCommittee(t *testing.T) { require.NoError(err) require.NoError(p.CreateGenesisStates(ctx, sm)) var candlist state.CandidateList - _, err = sm.State(&candlist, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm.State(&candlist, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(1))) require.NoError(err) candidates, err := state.CandidatesToMap(candlist) require.NoError(err) @@ -209,7 +212,7 @@ func TestHandle_StakingCommittee(t *testing.T) { require.NoError(err) require.NoError(p2.CreateGenesisStates(ctx2, sm2)) var sc2 state.CandidateList - _, err = sm2.State(&sc2, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm2.State(&sc2, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(1))) require.NoError(err) act2 := action.NewPutPollResult(1, 1, sc2) elp = bd.SetGasLimit(uint64(100000)). @@ -259,7 +262,7 @@ func TestProtocol_Validate_StakingCommittee(t *testing.T) { require.NoError(err) require.NoError(p2.CreateGenesisStates(ctx2, sm2)) var sc2 state.CandidateList - _, err = sm2.State(&sc2, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm2.State(&sc2, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(1))) require.NoError(err) act2 := action.NewPutPollResult(1, 1, sc2) elp = bd.SetGasLimit(uint64(100000)). @@ -290,7 +293,7 @@ func TestProtocol_Validate_StakingCommittee(t *testing.T) { require.NoError(err) require.NoError(p3.CreateGenesisStates(ctx3, sm3)) var sc3 state.CandidateList - _, err = sm3.State(&sc3, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm3.State(&sc3, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(1))) require.NoError(err) sc3 = append(sc3, &state.Candidate{"1", big.NewInt(10), "2", nil}) sc3 = append(sc3, &state.Candidate{"1", big.NewInt(10), "2", nil}) @@ -322,7 +325,7 @@ func TestProtocol_Validate_StakingCommittee(t *testing.T) { require.NoError(err) require.NoError(p4.CreateGenesisStates(ctx4, sm4)) var sc4 state.CandidateList - _, err = sm4.State(&sc4, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm4.State(&sc4, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(1))) require.NoError(err) sc4 = append(sc4, &state.Candidate{"1", big.NewInt(10), "2", nil}) act4 := action.NewPutPollResult(1, 1, sc4) @@ -353,7 +356,7 @@ func TestProtocol_Validate_StakingCommittee(t *testing.T) { require.NoError(err) require.NoError(p5.CreateGenesisStates(ctx5, sm5)) var sc5 state.CandidateList - _, err = sm5.State(&sc5, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm5.State(&sc5, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(1))) sc5[0].Votes = big.NewInt(10) act5 := action.NewPutPollResult(1, 1, sc5) bd5 := &action.EnvelopeBuilder{} @@ -383,7 +386,7 @@ func TestProtocol_Validate_StakingCommittee(t *testing.T) { require.NoError(err) require.NoError(p6.CreateGenesisStates(ctx6, sm6)) var sc6 state.CandidateList - _, err = sm6.State(&sc6, protocol.LegacyKeyOption(candidatesutil.ConstructKey(1))) + _, err = sm6.State(&sc6, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(1))) require.NoError(err) act6 := action.NewPutPollResult(1, 1, sc6) bd6 := &action.EnvelopeBuilder{} diff --git a/action/protocol/poll/util.go b/action/protocol/poll/util.go index 6bf5812471..b522f842d0 100644 --- a/action/protocol/poll/util.go +++ b/action/protocol/poll/util.go @@ -21,6 +21,7 @@ import ( "github.com/iotexproject/iotex-core/action/protocol/rolldpos" "github.com/iotexproject/iotex-core/action/protocol/vote" "github.com/iotexproject/iotex-core/action/protocol/vote/candidatesutil" + "github.com/iotexproject/iotex-core/config" "github.com/iotexproject/iotex-core/pkg/log" "github.com/iotexproject/iotex-core/state" ) @@ -54,7 +55,7 @@ func handle(ctx context.Context, act action.Action, sm protocol.StateManager, pr } zap.L().Debug("Handle PutPollResult Action", zap.Uint64("height", r.Height())) - if err := setCandidates(sm, r.Candidates(), r.Height()); err != nil { + if err := setCandidates(ctx, sm, r.Candidates(), r.Height()); err != nil { return nil, errors.Wrap(err, "failed to set candidates") } return &action.Receipt{ @@ -147,19 +148,24 @@ func createPostSystemActions(ctx context.Context, p Protocol) ([]action.Envelope // setCandidates sets the candidates for the given state manager func setCandidates( + ctx context.Context, sm protocol.StateManager, candidates state.CandidateList, - height uint64, + height uint64, // epoch start height ) error { + bcCtx := protocol.MustGetBlockchainCtx(ctx) + rp := rolldpos.MustGetProtocol(bcCtx.Registry) + epochNum := rp.GetEpochNum(height) + if height != rp.GetEpochHeight(epochNum) { + return errors.New("put poll result height should be epoch start height") + } + for _, candidate := range candidates { delegate, err := accountutil.LoadOrCreateAccount(sm, candidate.Address) if err != nil { return errors.Wrapf(err, "failed to load or create the account for delegate %s", candidate.Address) } delegate.IsCandidate = true - if err := candidatesutil.LoadAndAddCandidates(sm, height, candidate.Address); err != nil { - return err - } if err := accountutil.StoreAccount(sm, candidate.Address, delegate); err != nil { return errors.Wrap(err, "failed to update pending account changes to trie") } @@ -170,17 +176,84 @@ func setCandidates( zap.String("score", candidate.Votes.String()), ) } - _, err := sm.PutState(&candidates, protocol.LegacyKeyOption(candidatesutil.ConstructKey(height))) + hu := config.NewHeightUpgrade(&bcCtx.Genesis) + if hu.IsPre(config.Easter, height) { + _, err := sm.PutState(&candidates, protocol.LegacyKeyOption(candidatesutil.ConstructLegacyKey(height))) + return err + } + nextKey := candidatesutil.ConstructKey(candidatesutil.NxtCandidateKey) + _, err := sm.PutState(&candidates, protocol.KeyOption(nextKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) return err } -// setKickoutBlackList sets the blacklist for kick-out for corresponding epoch -func setKickoutBlackList( +// setNextEpochBlacklist sets the blacklist for kick-out with next key +func setNextEpochBlacklist( sm protocol.StateManager, blackList *vote.Blacklist, - epochNum uint64, ) error { - blackListKey := candidatesutil.ConstructBlackListKey(epochNum) - _, err := sm.PutState(blackList, protocol.LegacyKeyOption(blackListKey)) + blackListKey := candidatesutil.ConstructKey(candidatesutil.NxtKickoutKey) + _, err := sm.PutState(blackList, protocol.KeyOption(blackListKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) return err } + +// setUnproductiveDelegates sets the upd struct with updkey +func setUnproductiveDelegates( + sm protocol.StateManager, + upd *vote.UnproductiveDelegate, +) error { + updKey := candidatesutil.ConstructKey(candidatesutil.UnproductiveDelegateKey) + _, err := sm.PutState(upd, protocol.KeyOption(updKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)) + return err +} + +// shiftCandidates updates current data with next data of candidate list +func shiftCandidates(sm protocol.StateManager) (uint64, error) { + zap.L().Debug("Shift candidatelist from next key to current key") + var next state.CandidateList + var err error + var stateHeight, putStateHeight uint64 + nextKey := candidatesutil.ConstructKey(candidatesutil.NxtCandidateKey) + if stateHeight, err = sm.State(&next, protocol.KeyOption(nextKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)); err != nil { + return 0, errors.Wrap( + err, + "failed to read next blacklist when shifting to current blacklist", + ) + } + curKey := candidatesutil.ConstructKey(candidatesutil.CurCandidateKey) + if putStateHeight, err = sm.PutState(&next, protocol.KeyOption(curKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)); err != nil { + return 0, errors.Wrap( + err, + "failed to write current blacklist when shifting from next blacklist to current blacklist", + ) + } + if stateHeight != putStateHeight { + return 0, errors.Wrap(ErrInconsistentHeight, "failed to shift candidates") + } + return stateHeight, nil +} + +// shiftKickoutList updates current data with next data of kickout list +func shiftKickoutList(sm protocol.StateManager) (uint64, error) { + zap.L().Debug("Shift kickoutList from next key to current key") + var err error + var stateHeight, putStateHeight uint64 + next := &vote.Blacklist{} + nextKey := candidatesutil.ConstructKey(candidatesutil.NxtKickoutKey) + if stateHeight, err = sm.State(next, protocol.KeyOption(nextKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)); err != nil { + return 0, errors.Wrap( + err, + "failed to read next blacklist when shifting to current blacklist", + ) + } + curKey := candidatesutil.ConstructKey(candidatesutil.CurKickoutKey) + if putStateHeight, err = sm.PutState(next, protocol.KeyOption(curKey[:]), protocol.NamespaceOption(protocol.SystemNamespace)); err != nil { + return 0, errors.Wrap( + err, + "failed to write current blacklist when shifting from next blacklist to current blacklist", + ) + } + if stateHeight != putStateHeight { + return 0, errors.Wrap(ErrInconsistentHeight, "failed to shift candidates") + } + return stateHeight, nil +} diff --git a/action/protocol/protocol.go b/action/protocol/protocol.go index a72f9b82a1..4f3cdcaad5 100644 --- a/action/protocol/protocol.go +++ b/action/protocol/protocol.go @@ -19,6 +19,11 @@ var ( ErrUnimplemented = errors.New("method is unimplemented") ) +const ( + // SystemNamespace is the namespace to store system information such as candidates/blacklist/unproductiveDelegates + SystemNamespace = "System" +) + // Protocol defines the protocol interfaces atop IoTeX blockchain type Protocol interface { ActionValidator diff --git a/action/protocol/rewarding/protocol.go b/action/protocol/rewarding/protocol.go index 9ef421cffe..58ccf2e2f0 100644 --- a/action/protocol/rewarding/protocol.go +++ b/action/protocol/rewarding/protocol.go @@ -43,15 +43,15 @@ var ( // ProductivityByEpoch returns the number of produced blocks per delegate in an epoch type ProductivityByEpoch func(context.Context, uint64) (uint64, map[string]uint64, error) -// KickoutListByEpoch returns the blacklist for kickout of a given epoch -type KickoutListByEpoch func(protocol.StateReader, uint64) (*vote.Blacklist, error) +// GetKickoutList returns the current blacklist +type GetKickoutList func(protocol.StateReader, bool) (*vote.Blacklist, uint64, error) // Protocol defines the protocol of the rewarding fund and the rewarding process. It allows the admin to config the // reward amount, users to donate tokens to the fund, block producers to grant them block and epoch reward and, // beneficiaries to claim the balance into their personal account. type Protocol struct { productivityByEpoch ProductivityByEpoch - kickoutListByEpoch KickoutListByEpoch + getKickoutList GetKickoutList keyPrefix []byte addr address.Address kickoutIntensity float64 @@ -60,7 +60,7 @@ type Protocol struct { // NewProtocol instantiates a rewarding protocol instance. func NewProtocol( kickoutIntensityRate float64, - kickoutListByEpoch KickoutListByEpoch, + getKickoutList GetKickoutList, productivityByEpoch ProductivityByEpoch, ) *Protocol { h := hash.Hash160b([]byte(protocolID)) @@ -70,7 +70,7 @@ func NewProtocol( } return &Protocol{ productivityByEpoch: productivityByEpoch, - kickoutListByEpoch: kickoutListByEpoch, + getKickoutList: getKickoutList, keyPrefix: h[:], addr: addr, kickoutIntensity: kickoutIntensityRate, diff --git a/action/protocol/rewarding/reward.go b/action/protocol/rewarding/reward.go index 95e33e4873..7a9a7dd531 100644 --- a/action/protocol/rewarding/reward.go +++ b/action/protocol/rewarding/reward.go @@ -167,7 +167,7 @@ func (p *Protocol) GrantEpochReward( } } else { // Get Kick-out List from DB - kickoutList, err := p.kickoutListByEpoch(sm, epochNum) + kickoutList, _, err := p.getKickoutList(sm, false) if err != nil { return nil, err } diff --git a/action/protocol/vote/blacklist.go b/action/protocol/vote/blacklist.go index 254b97f16a..52ab913c0c 100644 --- a/action/protocol/vote/blacklist.go +++ b/action/protocol/vote/blacklist.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019 IoTeX Foundation +// Copyright (c) 2020 IoTeX Foundation // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache diff --git a/action/protocol/vote/blacklist_test.go b/action/protocol/vote/blacklist_test.go index a7cc96767e..0e247b2917 100644 --- a/action/protocol/vote/blacklist_test.go +++ b/action/protocol/vote/blacklist_test.go @@ -35,4 +35,13 @@ func TestBlackListSerializeAndDeserialize(t *testing.T) { r.True(blacklist2.IntensityRate == blacklist1.IntensityRate) r.True(blacklist2.IntensityRate == 0.5) + + blacklist3 := &Blacklist{} + sbytes, err = blacklist3.Serialize() + r.NoError(err) + blacklist4 := &Blacklist{} + err = blacklist4.Deserialize(sbytes) + r.NoError(err) + + r.True(len(blacklist4.BlacklistInfos) == 0) } diff --git a/action/protocol/vote/candidatesutil/candidatesutil.go b/action/protocol/vote/candidatesutil/candidatesutil.go index daadd7add9..85280fa898 100644 --- a/action/protocol/vote/candidatesutil/candidatesutil.go +++ b/action/protocol/vote/candidatesutil/candidatesutil.go @@ -8,13 +8,10 @@ package candidatesutil import ( "go.uber.org/zap" - "math/big" - "sort" "github.com/pkg/errors" "github.com/iotexproject/go-pkgs/hash" - "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-core/action/protocol" "github.com/iotexproject/iotex-core/action/protocol/vote" "github.com/iotexproject/iotex-core/pkg/log" @@ -25,14 +22,26 @@ import ( // CandidatesPrefix is the prefix of the key of candidateList const CandidatesPrefix = "Candidates." -// KickoutPrefix is the prefix of the key of blackList for kick-out -const KickoutPrefix = "KickoutList." +// CurCandidateKey is the key of current candidate list +const CurCandidateKey = "CurrentCandidateList." -// CandidatesByHeight returns array of Candidates in candidate pool of a given height +// NxtCandidateKey is the key of next candidate list +const NxtCandidateKey = "NextCandidateList." + +// CurKickoutKey is the key of current kickout list +const CurKickoutKey = "CurrentKickoutKey." + +// NxtKickoutKey is the key of next kickout list +const NxtKickoutKey = "NextKickoutKey." + +// UnproductiveDelegateKey is the key of unproductive Delegate struct +const UnproductiveDelegateKey = "UnproductiveDelegateKey." + +// CandidatesByHeight returns array of Candidates in candidate pool of a given height (deprecated version) func CandidatesByHeight(sr protocol.StateReader, height uint64) ([]*state.Candidate, error) { var candidates state.CandidateList // Load Candidates on the given height from underlying db - candidatesKey := ConstructKey(height) + candidatesKey := ConstructLegacyKey(height) _, err := sr.State(&candidates, protocol.LegacyKeyOption(candidatesKey)) log.L().Debug( "CandidatesByHeight", @@ -53,102 +62,97 @@ func CandidatesByHeight(sr protocol.StateReader, height uint64) ([]*state.Candid ) } -// KickoutListByEpoch returns array of unqualified delegate address in delegate pool for the given epochNum -func KickoutListByEpoch(sr protocol.StateReader, epochNum uint64) (*vote.Blacklist, error) { - blackList := &vote.Blacklist{} - if epochNum == 1 { - return blackList, nil +// CandidatesFromDB returns array of Candidates at current epoch +func CandidatesFromDB(sr protocol.StateReader, epochStartPoint bool) ([]*state.Candidate, uint64, error) { + var candidates state.CandidateList + candidatesKey := ConstructKey(CurCandidateKey) + if epochStartPoint { + // if not shifted yet + candidatesKey = ConstructKey(NxtCandidateKey) } - // Load kick out list on the given epochNum from underlying db - blackListKey := ConstructBlackListKey(epochNum) - _, err := sr.State(blackList, protocol.LegacyKeyOption(blackListKey)) + stateHeight, err := sr.State( + &candidates, + protocol.KeyOption(candidatesKey[:]), + protocol.NamespaceOption(protocol.SystemNamespace), + ) log.L().Debug( - "KickoutListByEpoch", - zap.Uint64("epoch number", epochNum), - zap.Any("kick out list ", blackList), + "GetCandidates", + zap.Any("candidates", candidates), + zap.Uint64("state height", stateHeight), zap.Error(err), ) - if err == nil { - return blackList, nil + if errors.Cause(err) == nil { + if len(candidates) > 0 { + return candidates, stateHeight, nil + } + err = state.ErrStateNotExist } - return nil, errors.Wrapf( + return nil, stateHeight, errors.Wrapf( err, - "failed to get state of kick-out list for epoch number %d", - epochNum, + "failed to get candidates with epochStartEpoch: %t", + epochStartPoint, ) } -// LoadAndAddCandidates loads candidates from trie and adds a new candidate -func LoadAndAddCandidates(sm protocol.StateManager, blkHeight uint64, addr string) error { - candidateMap, err := GetMostRecentCandidateMap(sm, blkHeight) - if err != nil { - return errors.Wrap(err, "failed to get most recent candidates from trie") +// KickoutListFromDB returns array of kickout list at current epoch +func KickoutListFromDB(sr protocol.StateReader, epochStartPoint bool) (*vote.Blacklist, uint64, error) { + blackList := &vote.Blacklist{} + blackListKey := ConstructKey(CurKickoutKey) + if epochStartPoint { + // if not shifted yet + blackListKey = ConstructKey(NxtKickoutKey) } - if err := addCandidate(candidateMap, addr); err != nil { - return errors.Wrap(err, "failed to add candidate to candidate map") + stateHeight, err := sr.State( + blackList, + protocol.KeyOption(blackListKey[:]), + protocol.NamespaceOption(protocol.SystemNamespace), + ) + log.L().Debug( + "GetKickoutList", + zap.Any("kick out list", blackList.BlacklistInfos), + zap.Uint64("state height", stateHeight), + zap.Error(err), + ) + if err == nil { + return blackList, stateHeight, nil } - return storeCandidates(candidateMap, sm, blkHeight) + return nil, stateHeight, errors.Wrapf( + err, + "failed to get kick-out list with epochStartPoint: %t", + epochStartPoint, + ) } -// GetMostRecentCandidateMap gets the most recent candidateMap from trie -func GetMostRecentCandidateMap(sm protocol.StateManager, blkHeight uint64) (map[hash.Hash160]*state.Candidate, error) { - var sc state.CandidateList - for h := int(blkHeight); h >= 0; h-- { - candidatesKey := ConstructKey(uint64(h)) - var err error - if _, err = sm.State(&sc, protocol.LegacyKeyOption(candidatesKey)); err == nil { - return state.CandidatesToMap(sc) - } - if errors.Cause(err) != state.ErrStateNotExist { - return nil, errors.Wrap(err, "failed to get most recent state of candidateList") - } - } - if blkHeight == uint64(0) || blkHeight == uint64(1) { - return make(map[hash.Hash160]*state.Candidate), nil +// UnproductiveDelegateFromDB returns latest UnproductiveDelegate struct +func UnproductiveDelegateFromDB(sr protocol.StateReader) (*vote.UnproductiveDelegate, error) { + upd := &vote.UnproductiveDelegate{} + updKey := ConstructKey(UnproductiveDelegateKey) + stateHeight, err := sr.State( + upd, + protocol.KeyOption(updKey[:]), + protocol.NamespaceOption(protocol.SystemNamespace), + ) + log.L().Debug( + "GetUnproductiveDelegate", + zap.Uint64("state height", stateHeight), + zap.Error(err), + ) + if err == nil { + return upd, nil } - return nil, errors.Wrap(state.ErrStateNotExist, "failed to get most recent state of candidateList") + return nil, err } -// ConstructKey constructs a key for candidates storage -func ConstructKey(height uint64) hash.Hash160 { +// ConstructLegacyKey constructs a key for candidates storage (deprecated version) +func ConstructLegacyKey(height uint64) hash.Hash160 { heightInBytes := byteutil.Uint64ToBytes(height) k := []byte(CandidatesPrefix) k = append(k, heightInBytes...) return hash.Hash160b(k) } -// ConstructBlackListKey constructs a key for kick-out blacklist storage -func ConstructBlackListKey(epochNum uint64) hash.Hash160 { - epochInBytes := byteutil.Uint64ToBytes(epochNum) - k := []byte(KickoutPrefix) - k = append(k, epochInBytes...) - return hash.Hash160b(k) -} - -// addCandidate adds a new candidate to candidateMap -func addCandidate(candidateMap map[hash.Hash160]*state.Candidate, encodedAddr string) error { - addr, err := address.FromString(encodedAddr) - if err != nil { - return errors.Wrap(err, "failed to get public key hash from account address") - } - addrHash := hash.BytesToHash160(addr.Bytes()) - if _, ok := candidateMap[addrHash]; !ok { - candidateMap[addrHash] = &state.Candidate{ - Address: encodedAddr, - Votes: big.NewInt(0), - } - } - return nil -} - -// storeCandidates puts updated candidates to trie -func storeCandidates(candidateMap map[hash.Hash160]*state.Candidate, sm protocol.StateManager, blkHeight uint64) error { - candidateList, err := state.MapToCandidates(candidateMap) - if err != nil { - return errors.Wrap(err, "failed to convert candidate map to candidate list") - } - sort.Sort(candidateList) - candidatesKey := ConstructKey(blkHeight) - _, err = sm.PutState(&candidateList, protocol.LegacyKeyOption(candidatesKey)) - return err +// ConstructKey constructs a const key +func ConstructKey(key string) hash.Hash256 { + bytesKey := []byte(key) + return hash.Hash256b(bytesKey) } diff --git a/action/protocol/vote/unproductivedelegate.go b/action/protocol/vote/unproductivedelegate.go new file mode 100644 index 0000000000..48d61682ea --- /dev/null +++ b/action/protocol/vote/unproductivedelegate.go @@ -0,0 +1,122 @@ +// Copyright (c) 2020 IoTeX Foundation +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package vote + +import ( + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + + updpb "github.com/iotexproject/iotex-core/action/protocol/vote/unproductivedelegatepb" +) + +// UnproductiveDelegate defines unproductive delegates information within kickout period +type UnproductiveDelegate struct { + delegatelist [][]string + kickoutPeriod uint64 + cacheSize uint64 +} + +// NewUnproductiveDelegate creates new UnproductiveDelegate with kickoutperiod and cacheSize +func NewUnproductiveDelegate(kickoutPeriod uint64, cacheSize uint64) (*UnproductiveDelegate, error) { + if kickoutPeriod > cacheSize { + return nil, errors.New("cache size of unproductiveDelegate should be bigger than kickout period + 1") + } + return &UnproductiveDelegate{ + delegatelist: make([][]string, cacheSize), + kickoutPeriod: kickoutPeriod, + cacheSize: cacheSize, + }, nil +} + +// AddRecentUPD adds new epoch upd-list at the leftmost and shift existing lists to the right +func (upd *UnproductiveDelegate) AddRecentUPD(new []string) error { + upd.delegatelist = append([][]string{new}, upd.delegatelist[0:upd.kickoutPeriod-1]...) + if len(upd.delegatelist) > int(upd.kickoutPeriod) { + return errors.New("wrong length of UPD delegatelist") + } + return nil +} + +// ReadOldestUPD returns the last upd-list +func (upd *UnproductiveDelegate) ReadOldestUPD() []string { + return upd.delegatelist[upd.kickoutPeriod-1] +} + +// Serialize serializes unproductvieDelegate struct to bytes +func (upd *UnproductiveDelegate) Serialize() ([]byte, error) { + return proto.Marshal(upd.Proto()) +} + +// Proto converts the unproductvieDelegate struct to a protobuf message +func (upd *UnproductiveDelegate) Proto() *updpb.UnproductiveDelegate { + delegatespb := make([]*updpb.Delegatelist, 0, len(upd.delegatelist)) + for _, elem := range upd.delegatelist { + data := make([]string, len(elem)) + copy(data, elem) + listpb := &updpb.Delegatelist{ + Delegates: data, + } + delegatespb = append(delegatespb, listpb) + } + return &updpb.UnproductiveDelegate{ + DelegateList: delegatespb, + KickoutPeriod: upd.kickoutPeriod, + CacheSize: upd.cacheSize, + } +} + +// Deserialize deserializes bytes to UnproductiveDelegate struct +func (upd *UnproductiveDelegate) Deserialize(buf []byte) error { + unproductivedelegatePb := &updpb.UnproductiveDelegate{} + if err := proto.Unmarshal(buf, unproductivedelegatePb); err != nil { + return errors.Wrap(err, "failed to unmarshal blacklist") + } + return upd.LoadProto(unproductivedelegatePb) +} + +// LoadProto converts protobuf message to unproductvieDelegate struct +func (upd *UnproductiveDelegate) LoadProto(updPb *updpb.UnproductiveDelegate) error { + var delegates [][]string + for _, delegatelistpb := range updPb.DelegateList { + var delegateElem []string + for _, str := range delegatelistpb.Delegates { + delegateElem = append(delegateElem, str) + } + delegates = append(delegates, delegateElem) + } + upd.delegatelist = delegates + upd.kickoutPeriod = updPb.KickoutPeriod + upd.cacheSize = updPb.CacheSize + + return nil +} + +// Equal compares with other upd struct and returns true if it's equal +func (upd *UnproductiveDelegate) Equal(upd2 *UnproductiveDelegate) bool { + if upd.kickoutPeriod != upd2.kickoutPeriod { + return false + } + if upd.cacheSize != upd2.cacheSize { + return false + } + if len(upd.delegatelist) != len(upd2.delegatelist) { + return false + } + for i, list := range upd.delegatelist { + for j, str := range list { + if str != upd2.delegatelist[i][j] { + return false + } + } + } + return true +} + +// DelegateList returns delegate list 2D array +func (upd *UnproductiveDelegate) DelegateList() [][]string { + return upd.delegatelist +} diff --git a/action/protocol/vote/unproductivedelegate_test.go b/action/protocol/vote/unproductivedelegate_test.go new file mode 100644 index 0000000000..a1033da181 --- /dev/null +++ b/action/protocol/vote/unproductivedelegate_test.go @@ -0,0 +1,50 @@ +// Copyright (c) 2020 IoTeX Foundation +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package vote + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUnproductiveDelegate(t *testing.T) { + r := require.New(t) + upd, err := NewUnproductiveDelegate(2, 10) + r.NoError(err) + + str1 := []string{"a", "b", "c", "d", "e"} + str2 := []string{"d"} + str3 := []string{"f", "g"} + str4 := []string{"a", "f", "g"} + + r.NoError(upd.AddRecentUPD(str1)) + r.NoError(upd.AddRecentUPD(str2)) + r.NoError(upd.AddRecentUPD(str3)) + oldestData := upd.ReadOldestUPD() + r.Equal(len(str2), len(oldestData)) + for i, data := range oldestData { + r.Equal(str2[i], data) + } + + r.NoError(upd.AddRecentUPD(str4)) + oldestData = upd.ReadOldestUPD() + r.Equal(len(str3), len(oldestData)) + for i, data := range oldestData { + r.Equal(str3[i], data) + } + + sbytes, err := upd.Serialize() + r.NoError(err) + + upd2, err := NewUnproductiveDelegate(3, 10) + r.NoError(err) + err = upd2.Deserialize(sbytes) + r.NoError(err) + + r.True(upd.Equal(upd2)) +} diff --git a/action/protocol/vote/unproductivedelegatepb/unproductivedelegate.pb.go b/action/protocol/vote/unproductivedelegatepb/unproductivedelegate.pb.go new file mode 100644 index 0000000000..24bb31cc4b --- /dev/null +++ b/action/protocol/vote/unproductivedelegatepb/unproductivedelegate.pb.go @@ -0,0 +1,140 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: action/protocol/vote/unproductivedelegatepb/unproductivedelegate.proto + +package unproductivedelegatepb + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type UnproductiveDelegate struct { + CacheSize uint64 `protobuf:"varint,1,opt,name=cacheSize,proto3" json:"cacheSize,omitempty"` + KickoutPeriod uint64 `protobuf:"varint,2,opt,name=kickoutPeriod,proto3" json:"kickoutPeriod,omitempty"` + DelegateList []*Delegatelist `protobuf:"bytes,3,rep,name=delegateList,proto3" json:"delegateList,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *UnproductiveDelegate) Reset() { *m = UnproductiveDelegate{} } +func (m *UnproductiveDelegate) String() string { return proto.CompactTextString(m) } +func (*UnproductiveDelegate) ProtoMessage() {} +func (*UnproductiveDelegate) Descriptor() ([]byte, []int) { + return fileDescriptor_ef4b0fa66012b010, []int{0} +} + +func (m *UnproductiveDelegate) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_UnproductiveDelegate.Unmarshal(m, b) +} +func (m *UnproductiveDelegate) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_UnproductiveDelegate.Marshal(b, m, deterministic) +} +func (m *UnproductiveDelegate) XXX_Merge(src proto.Message) { + xxx_messageInfo_UnproductiveDelegate.Merge(m, src) +} +func (m *UnproductiveDelegate) XXX_Size() int { + return xxx_messageInfo_UnproductiveDelegate.Size(m) +} +func (m *UnproductiveDelegate) XXX_DiscardUnknown() { + xxx_messageInfo_UnproductiveDelegate.DiscardUnknown(m) +} + +var xxx_messageInfo_UnproductiveDelegate proto.InternalMessageInfo + +func (m *UnproductiveDelegate) GetCacheSize() uint64 { + if m != nil { + return m.CacheSize + } + return 0 +} + +func (m *UnproductiveDelegate) GetKickoutPeriod() uint64 { + if m != nil { + return m.KickoutPeriod + } + return 0 +} + +func (m *UnproductiveDelegate) GetDelegateList() []*Delegatelist { + if m != nil { + return m.DelegateList + } + return nil +} + +type Delegatelist struct { + Delegates []string `protobuf:"bytes,1,rep,name=delegates,proto3" json:"delegates,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Delegatelist) Reset() { *m = Delegatelist{} } +func (m *Delegatelist) String() string { return proto.CompactTextString(m) } +func (*Delegatelist) ProtoMessage() {} +func (*Delegatelist) Descriptor() ([]byte, []int) { + return fileDescriptor_ef4b0fa66012b010, []int{1} +} + +func (m *Delegatelist) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Delegatelist.Unmarshal(m, b) +} +func (m *Delegatelist) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Delegatelist.Marshal(b, m, deterministic) +} +func (m *Delegatelist) XXX_Merge(src proto.Message) { + xxx_messageInfo_Delegatelist.Merge(m, src) +} +func (m *Delegatelist) XXX_Size() int { + return xxx_messageInfo_Delegatelist.Size(m) +} +func (m *Delegatelist) XXX_DiscardUnknown() { + xxx_messageInfo_Delegatelist.DiscardUnknown(m) +} + +var xxx_messageInfo_Delegatelist proto.InternalMessageInfo + +func (m *Delegatelist) GetDelegates() []string { + if m != nil { + return m.Delegates + } + return nil +} + +func init() { + proto.RegisterType((*UnproductiveDelegate)(nil), "unproductivedelegatepb.unproductiveDelegate") + proto.RegisterType((*Delegatelist)(nil), "unproductivedelegatepb.delegatelist") +} + +func init() { + proto.RegisterFile("action/protocol/vote/unproductivedelegatepb/unproductivedelegate.proto", fileDescriptor_ef4b0fa66012b010) +} + +var fileDescriptor_ef4b0fa66012b010 = []byte{ + // 190 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x72, 0x4b, 0x4c, 0x2e, 0xc9, + 0xcc, 0xcf, 0xd3, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x4f, 0xce, 0xcf, 0xd1, 0x2f, 0xcb, 0x2f, 0x49, + 0xd5, 0x2f, 0xcd, 0x2b, 0x28, 0xca, 0x4f, 0x29, 0x4d, 0x2e, 0xc9, 0x2c, 0x4b, 0x4d, 0x49, 0xcd, + 0x49, 0x4d, 0x4f, 0x2c, 0x49, 0x2d, 0x48, 0xc2, 0x2a, 0xac, 0x07, 0xd6, 0x29, 0x24, 0x86, 0x5d, + 0x8b, 0xd2, 0x12, 0x46, 0x2e, 0x11, 0x64, 0x29, 0x17, 0xa8, 0x94, 0x90, 0x0c, 0x17, 0x67, 0x72, + 0x62, 0x72, 0x46, 0x6a, 0x70, 0x66, 0x55, 0xaa, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x4b, 0x10, 0x42, + 0x40, 0x48, 0x85, 0x8b, 0x37, 0x3b, 0x33, 0x39, 0x3b, 0xbf, 0xb4, 0x24, 0x20, 0xb5, 0x28, 0x33, + 0x3f, 0x45, 0x82, 0x09, 0xac, 0x02, 0x55, 0x50, 0xc8, 0x83, 0x8b, 0x07, 0x66, 0x95, 0x4f, 0x66, + 0x71, 0x89, 0x04, 0xb3, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0x8a, 0x1e, 0x76, 0xb7, 0xe8, 0xc1, 0x98, + 0x39, 0x99, 0xc5, 0x25, 0x41, 0x28, 0x3a, 0x95, 0x74, 0x10, 0x26, 0x81, 0x64, 0x41, 0xae, 0x83, + 0xf1, 0x8b, 0x25, 0x18, 0x15, 0x98, 0x35, 0x38, 0x83, 0x10, 0x02, 0x49, 0x6c, 0x60, 0x3f, 0x1b, + 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xf3, 0x67, 0xa0, 0x82, 0x3d, 0x01, 0x00, 0x00, +} diff --git a/action/protocol/vote/unproductivedelegatepb/unproductivedelegate.proto b/action/protocol/vote/unproductivedelegatepb/unproductivedelegate.proto new file mode 100644 index 0000000000..7c28911ee8 --- /dev/null +++ b/action/protocol/vote/unproductivedelegatepb/unproductivedelegate.proto @@ -0,0 +1,21 @@ +// Copyright (c) 2020 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// To compile the proto, run: +// protoc --go_out=plugins=grpc:. *.proto + +syntax ="proto3"; +package unproductivedelegatepb; + +message unproductiveDelegate{ + uint64 cacheSize = 1; + uint64 kickoutPeriod = 2; + repeated delegatelist delegateList = 3; +} + +message delegatelist{ + repeated string delegates = 1; +} \ No newline at end of file diff --git a/api/api.go b/api/api.go index 16115dfa07..f3feb6a174 100644 --- a/api/api.go +++ b/api/api.go @@ -489,20 +489,27 @@ func (api *Server) GetEpochMeta( ctx context.Context, in *iotexapi.GetEpochMetaRequest, ) (*iotexapi.GetEpochMetaResponse, error) { - if in.EpochNumber < 1 { - return nil, status.Error(codes.InvalidArgument, "epoch number cannot be less than one") - } + // TODO : support archive mode to retrieve historic data, now only support tip epoch number rp := rolldpos.FindProtocol(api.registry) if rp == nil { return nil, status.Error(codes.Internal, "rolldpos protocol is not registered") } - epochHeight := rp.GetEpochHeight(in.EpochNumber) + tipHeight := api.bc.TipHeight() + tipEpochNumber := rp.GetEpochNum(tipHeight) + if in.EpochNumber > 0 { + if in.EpochNumber < tipEpochNumber { + return nil, status.Error(codes.InvalidArgument, "old epoch number isn't available with non-archive node") + } else if in.EpochNumber > tipEpochNumber { + return nil, status.Error(codes.InvalidArgument, "future epoch number is invalid argument") + } + } + epochHeight := rp.GetEpochHeight(tipEpochNumber) gravityChainStartHeight, err := api.getGravityChainStartHeight(epochHeight) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } epochData := &iotextypes.EpochData{ - Num: in.EpochNumber, + Num: tipEpochNumber, Height: epochHeight, GravityChainStartHeight: gravityChainStartHeight, } @@ -510,7 +517,7 @@ func (api *Server) GetEpochMeta( if err != nil { return nil, status.Error(codes.Internal, err.Error()) } - numBlks, produce, err := blockchain.ProductivityByEpoch(bcCtx, api.bc, in.EpochNumber) + numBlks, produce, err := blockchain.ProductivityByEpoch(bcCtx, api.bc, tipEpochNumber) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } @@ -520,7 +527,7 @@ func (api *Server) GetEpochMeta( return nil, status.Error(codes.Internal, "poll protocol is not registered") } methodName := []byte("BlockProducersByEpoch") - arguments := [][]byte{byteutil.Uint64ToBytes(in.EpochNumber)} + arguments := [][]byte{byteutil.Uint64ToBytes(tipEpochNumber)} data, err := api.readState(context.Background(), pp, methodName, arguments...) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) @@ -765,12 +772,16 @@ func (api *Server) Stop() error { func (api *Server) readState(ctx context.Context, p protocol.Protocol, methodName []byte, arguments ...[]byte) ([]byte, error) { // TODO: need to complete the context + tipHeight := api.bc.TipHeight() ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ - BlockHeight: api.bc.TipHeight(), + BlockHeight: tipHeight, }) ctx = protocol.WithBlockchainCtx(ctx, protocol.BlockchainCtx{ Registry: api.registry, Genesis: api.cfg.Genesis, + Tip: protocol.TipInfo{ + Height: tipHeight, + }, }) // TODO: need to distinguish user error and system error diff --git a/api/api_test.go b/api/api_test.go index 742cecb713..4691de8481 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -981,6 +981,8 @@ func TestServer_GetChainMeta(t *testing.T) { } else if test.pollProtocolType == "governanceChainCommittee" { committee := mock_committee.NewMockCommittee(ctrl) pol, _ = poll.NewGovernanceChainCommitteeProtocol( + nil, + nil, nil, nil, committee, @@ -996,6 +998,7 @@ func TestServer_GetChainMeta(t *testing.T) { cfg.Genesis.ProductivityThreshold, cfg.Genesis.KickoutEpochPeriod, cfg.Genesis.KickoutIntensityRate, + cfg.Genesis.UnproductiveDelegateMaxCacheSize, ) committee.EXPECT().HeightByTime(gomock.Any()).Return(test.epoch.GravityChainStartHeight, nil) } @@ -1250,6 +1253,8 @@ func TestServer_ReadCandidatesByEpoch(t *testing.T) { pol, _ = poll.NewGovernanceChainCommitteeProtocol( func(protocol.StateReader, uint64) ([]*state.Candidate, error) { return candidates, nil }, nil, + nil, + nil, committee, uint64(123456), func(uint64) (time.Time, error) { return time.Now(), nil }, @@ -1263,6 +1268,7 @@ func TestServer_ReadCandidatesByEpoch(t *testing.T) { cfg.Genesis.ProductivityThreshold, cfg.Genesis.KickoutEpochPeriod, cfg.Genesis.KickoutIntensityRate, + cfg.Genesis.UnproductiveDelegateMaxCacheSize, ) } svr, err := createServer(cfg, false) @@ -1310,6 +1316,8 @@ func TestServer_ReadBlockProducersByEpoch(t *testing.T) { pol, _ = poll.NewGovernanceChainCommitteeProtocol( func(protocol.StateReader, uint64) ([]*state.Candidate, error) { return candidates, nil }, nil, + nil, + nil, committee, uint64(123456), func(uint64) (time.Time, error) { return time.Now(), nil }, @@ -1323,6 +1331,7 @@ func TestServer_ReadBlockProducersByEpoch(t *testing.T) { cfg.Genesis.ProductivityThreshold, cfg.Genesis.KickoutEpochPeriod, cfg.Genesis.KickoutIntensityRate, + cfg.Genesis.UnproductiveDelegateMaxCacheSize, ) } svr, err := createServer(cfg, false) @@ -1372,6 +1381,8 @@ func TestServer_ReadActiveBlockProducersByEpoch(t *testing.T) { pol, _ = poll.NewGovernanceChainCommitteeProtocol( func(protocol.StateReader, uint64) ([]*state.Candidate, error) { return candidates, nil }, nil, + nil, + nil, committee, uint64(123456), func(uint64) (time.Time, error) { return time.Now(), nil }, @@ -1385,6 +1396,7 @@ func TestServer_ReadActiveBlockProducersByEpoch(t *testing.T) { cfg.Genesis.ProductivityThreshold, cfg.Genesis.KickoutEpochPeriod, cfg.Genesis.KickoutIntensityRate, + cfg.Genesis.UnproductiveDelegateMaxCacheSize, ) } svr, err := createServer(cfg, false) @@ -1491,6 +1503,8 @@ func TestServer_GetEpochMeta(t *testing.T) { }, nil }, nil, + nil, + nil, committee, uint64(123456), func(uint64) (time.Time, error) { return time.Now(), nil }, @@ -1504,11 +1518,12 @@ func TestServer_GetEpochMeta(t *testing.T) { cfg.Genesis.ProductivityThreshold, cfg.Genesis.KickoutEpochPeriod, cfg.Genesis.KickoutIntensityRate, + cfg.Genesis.UnproductiveDelegateMaxCacheSize, ) require.NoError(pol.ForceRegister(svr.registry)) committee.EXPECT().HeightByTime(gomock.Any()).Return(test.epochData.GravityChainStartHeight, nil) - mbc.EXPECT().TipHeight().Return(uint64(4)).Times(2) + mbc.EXPECT().TipHeight().Return(uint64(4)).Times(3) ctx := protocol.WithBlockchainCtx( context.Background(), protocol.BlockchainCtx{ diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index 4837556182..5c6b95e5ad 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -498,10 +498,16 @@ func (bc *blockchain) candidatesByHeight(height uint64) (state.CandidateList, er if bc.registry == nil { return nil, nil } + tipInfo, err := bc.tipInfo() + if err != nil { + return nil, err + } ctx := protocol.WithBlockchainCtx( context.Background(), protocol.BlockchainCtx{ Registry: bc.registry, + Genesis: bc.config.Genesis, + Tip: *tipInfo, }) if pp := poll.FindProtocol(bc.registry); pp != nil { diff --git a/blockchain/genesis/genesis.go b/blockchain/genesis/genesis.go index b09b2a6b0a..02e90e3c29 100644 --- a/blockchain/genesis/genesis.go +++ b/blockchain/genesis/genesis.go @@ -60,9 +60,10 @@ func defaultConfig() Genesis { InitBalanceMap: make(map[string]string), }, Poll: Poll{ - EnableGravityChainVoting: true, - KickoutEpochPeriod: 3, - KickoutIntensityRate: 0, + EnableGravityChainVoting: true, + KickoutEpochPeriod: 3, + KickoutIntensityRate: 0, + UnproductiveDelegateMaxCacheSize: 20, }, Rewarding: Rewarding{ InitBalanceStr: unit.ConvertIotxToRau(200000000).String(), @@ -174,6 +175,8 @@ type ( KickoutEpochPeriod uint64 `yaml:"kickoutEpochPeriod"` // KickoutIntensityRate is a intensity rate of kick-out range from [0,1), where 0 is hard-kickout KickoutIntensityRate float64 `yaml:"kickoutIntensityRate"` + // UnproductiveDelegateMaxCacheSize is a max cache size of upd which is stored into state DB (kickoutEpochPeriod <= UnproductiveDelegateMaxCacheSize) + UnproductiveDelegateMaxCacheSize uint64 `yaml:unproductiveDelegateMaxCacheSize` } // Delegate defines a delegate with address and votes Delegate struct { diff --git a/chainservice/chainservice.go b/chainservice/chainservice.go index 10555a4493..20cbb7ebc5 100644 --- a/chainservice/chainservice.go +++ b/chainservice/chainservice.go @@ -229,7 +229,9 @@ func New( return data, err }, candidatesutil.CandidatesByHeight, - candidatesutil.KickoutListByEpoch, + candidatesutil.CandidatesFromDB, + candidatesutil.KickoutListFromDB, + candidatesutil.UnproductiveDelegateFromDB, electionCommittee, func(height uint64) (time.Time, error) { header, err := chain.BlockHeaderByHeight(height) @@ -256,7 +258,7 @@ func New( // TODO: rewarding protocol for standalone mode is weird, rDPoSProtocol could be passed via context rewardingProtocol := rewarding.NewProtocol( cfg.Genesis.KickoutIntensityRate, - candidatesutil.KickoutListByEpoch, + candidatesutil.KickoutListFromDB, func(ctx context.Context, epochNum uint64) (uint64, map[string]uint64, error) { return blockchain.ProductivityByEpoch(ctx, chain, epochNum) }) diff --git a/consensus/consensus.go b/consensus/consensus.go index 84e3ce2f1d..6c9290ee39 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -24,7 +24,6 @@ import ( "github.com/iotexproject/iotex-core/consensus/scheme/rolldpos" "github.com/iotexproject/iotex-core/pkg/lifecycle" "github.com/iotexproject/iotex-core/pkg/log" - "github.com/iotexproject/iotex-core/state" "github.com/iotexproject/iotex-proto/golang/iotextypes" ) @@ -106,19 +105,31 @@ func NewConsensus( SetActPool(ap). SetClock(clock). SetBroadcast(ops.broadcastHandler). - SetDelegatesByEpochFunc(func(ctx context.Context, epochNum uint64) (state.CandidateList, error) { + SetDelegatesByEpochFunc(func(epochNum uint64) ([]string, error) { re := protocol.NewRegistry() if err := ops.rp.Register(re); err != nil { return nil, err } - ctx = protocol.WithBlockchainCtx( - ctx, + tipHeight := bc.TipHeight() + ctx := protocol.WithBlockchainCtx( + context.Background(), protocol.BlockchainCtx{ - Genesis: cfg.Genesis, Registry: re, + Genesis: cfg.Genesis, + Tip: protocol.TipInfo{ + Height: tipHeight, + }, }, ) - return ops.pp.DelegatesByEpoch(ctx, epochNum) + candidatesList, err := ops.pp.DelegatesByEpoch(ctx, epochNum) + if err != nil { + return nil, err + } + addrs := []string{} + for _, candidate := range candidatesList { + addrs = append(addrs, candidate.Address) + } + return addrs, nil }). RegisterProtocol(ops.rp) // TODO: explorer dependency deleted here at #1085, need to revive by migrating to api diff --git a/consensus/scheme/rolldpos/rolldpos_test.go b/consensus/scheme/rolldpos/rolldpos_test.go index 9095af3098..efdd60e6a9 100644 --- a/consensus/scheme/rolldpos/rolldpos_test.go +++ b/consensus/scheme/rolldpos/rolldpos_test.go @@ -38,7 +38,6 @@ import ( cp "github.com/iotexproject/iotex-core/crypto" "github.com/iotexproject/iotex-core/endorsement" "github.com/iotexproject/iotex-core/p2p/node" - "github.com/iotexproject/iotex-core/state" "github.com/iotexproject/iotex-core/state/factory" "github.com/iotexproject/iotex-core/test/identityset" "github.com/iotexproject/iotex-core/test/mock/mock_actpool" @@ -63,7 +62,7 @@ func TestNewRollDPoS(t *testing.T) { cfg.Genesis.NumDelegates, cfg.Genesis.NumSubEpochs, ) - delegatesByEpoch := func(context.Context, uint64) (state.CandidateList, error) { return nil, nil } + delegatesByEpoch := func(uint64) ([]string, error) { return nil, nil } t.Run("normal", func(t *testing.T) { sk := identityset.PrivateKey(0) r, err := NewRollDPoSBuilder(). @@ -219,12 +218,12 @@ func TestValidateBlockFooter(t *testing.T) { SetBroadcast(func(_ proto.Message) error { return nil }). - SetDelegatesByEpochFunc(func(context.Context, uint64) (state.CandidateList, error) { - return []*state.Candidate{ - {Address: candidates[0]}, - {Address: candidates[1]}, - {Address: candidates[2]}, - {Address: candidates[3]}, + SetDelegatesByEpochFunc(func(uint64) ([]string, error) { + return []string{ + candidates[0], + candidates[1], + candidates[2], + candidates[3], }, nil }). SetClock(clock). @@ -299,12 +298,12 @@ func TestRollDPoS_Metrics(t *testing.T) { return nil }). SetClock(clock). - SetDelegatesByEpochFunc(func(context.Context, uint64) (state.CandidateList, error) { - return []*state.Candidate{ - {Address: candidates[0]}, - {Address: candidates[1]}, - {Address: candidates[2]}, - {Address: candidates[3]}, + SetDelegatesByEpochFunc(func(uint64) ([]string, error) { + return []string{ + candidates[0], + candidates[1], + candidates[2], + candidates[3], }, nil }). RegisterProtocol(rp). @@ -399,10 +398,10 @@ func TestRollDPoSConsensus(t *testing.T) { chainAddrs[i] = addressMap[rawAddress] } - delegatesByEpochFunc := func(ctx context.Context, _ uint64) (state.CandidateList, error) { - candidates := make([]*state.Candidate, 0, numNodes) + delegatesByEpochFunc := func(_ uint64) ([]string, error) { + candidates := make([]string, 0, numNodes) for _, addr := range chainAddrs { - candidates = append(candidates, &state.Candidate{Address: addr.encodedAddr}) + candidates = append(candidates, addr.encodedAddr) } return candidates, nil } diff --git a/consensus/scheme/rolldpos/rolldposctx.go b/consensus/scheme/rolldpos/rolldposctx.go index 025fdd6ea1..e28af49647 100644 --- a/consensus/scheme/rolldpos/rolldposctx.go +++ b/consensus/scheme/rolldpos/rolldposctx.go @@ -28,7 +28,6 @@ import ( "github.com/iotexproject/iotex-core/db" "github.com/iotexproject/iotex-core/endorsement" "github.com/iotexproject/iotex-core/pkg/log" - "github.com/iotexproject/iotex-core/state" ) var ( @@ -73,7 +72,7 @@ func init() { } // DelegatesByEpochFunc defines a function to overwrite candidates -type DelegatesByEpochFunc func(context.Context, uint64) (state.CandidateList, error) +type DelegatesByEpochFunc func(uint64) ([]string, error) type rollDPoSCtx struct { consensusfsm.ConsensusConfig diff --git a/consensus/scheme/rolldpos/rolldposctx_test.go b/consensus/scheme/rolldpos/rolldposctx_test.go index aad2415ace..0e3d09ed3f 100644 --- a/consensus/scheme/rolldpos/rolldposctx_test.go +++ b/consensus/scheme/rolldpos/rolldposctx_test.go @@ -22,11 +22,10 @@ import ( "github.com/iotexproject/iotex-core/config" "github.com/iotexproject/iotex-core/consensus/consensusfsm" "github.com/iotexproject/iotex-core/endorsement" - "github.com/iotexproject/iotex-core/state" "github.com/iotexproject/iotex-core/test/identityset" ) -var dummyCandidatesByHeightFunc = func(context.Context, uint64) (state.CandidateList, error) { return nil, nil } +var dummyCandidatesByHeightFunc = func(uint64) ([]string, error) { return nil, nil } func TestRollDPoSCtx(t *testing.T) { require := require.New(t) @@ -91,20 +90,47 @@ func TestCheckVoteEndorser(t *testing.T) { ) c := clock.New() cfg.Genesis.BlockInterval = time.Second * 20 - rctx, err := newRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg), config.Default.DB, true, time.Second, true, b, nil, rp, nil, func(ctx context.Context, epochNum uint64) (state.CandidateList, error) { - re := protocol.NewRegistry() - if err := rp.Register(re); err != nil { - return nil, err - } - ctx = protocol.WithBlockchainCtx( - ctx, - protocol.BlockchainCtx{ - Genesis: cfg.Genesis, - Registry: re, - }, - ) - return pp.DelegatesByEpoch(ctx, epochNum) - }, "", nil, c, config.Default.Genesis.BeringBlockHeight) + rctx, err := newRollDPoSCtx( + consensusfsm.NewConsensusConfig(cfg), + config.Default.DB, + true, + time.Second, + true, + b, + nil, + rp, + nil, + func(epochnum uint64) ([]string, error) { + re := protocol.NewRegistry() + if err := rp.Register(re); err != nil { + return nil, err + } + tipHeight := b.TipHeight() + ctx := protocol.WithBlockchainCtx( + context.Background(), + protocol.BlockchainCtx{ + Genesis: config.Default.Genesis, + Registry: re, + Tip: protocol.TipInfo{ + Height: tipHeight, + }, + }, + ) + var addrs []string + candidatesList, err := pp.DelegatesByEpoch(ctx, epochnum) + if err != nil { + return nil, err + } + for _, cand := range candidatesList { + addrs = append(addrs, cand.Address) + } + return addrs, nil + }, + "", + nil, + c, + config.Default.Genesis.BeringBlockHeight, + ) require.NoError(err) require.NotNil(rctx) @@ -112,12 +138,12 @@ func TestCheckVoteEndorser(t *testing.T) { require.Panics(func() { rctx.CheckVoteEndorser(0, nil, nil) }, "") // case 2:endorser address error - en := endorsement.NewEndorsement(time.Now(), identityset.PrivateKey(0).PublicKey(), nil) - require.Error(rctx.CheckVoteEndorser(0, nil, en)) + en := endorsement.NewEndorsement(time.Now(), identityset.PrivateKey(3).PublicKey(), nil) + require.Error(rctx.CheckVoteEndorser(51, nil, en)) // case 3:normal en = endorsement.NewEndorsement(time.Now(), identityset.PrivateKey(10).PublicKey(), nil) - require.NoError(rctx.CheckVoteEndorser(1, nil, en)) + require.NoError(rctx.CheckVoteEndorser(51, nil, en)) } func TestCheckBlockProposer(t *testing.T) { @@ -126,29 +152,56 @@ func TestCheckBlockProposer(t *testing.T) { b, rp, pp := makeChain(t) c := clock.New() cfg.Genesis.BlockInterval = time.Second * 20 - rctx, err := newRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg), config.Default.DB, true, time.Second, true, b, nil, rp, nil, func(ctx context.Context, epochNum uint64) (state.CandidateList, error) { - re := protocol.NewRegistry() - if err := rp.Register(re); err != nil { - return nil, err - } - ctx = protocol.WithBlockchainCtx( - ctx, - protocol.BlockchainCtx{ - Genesis: cfg.Genesis, - Registry: re, - }, - ) - return pp.DelegatesByEpoch(ctx, epochNum) - }, "", nil, c, config.Default.Genesis.BeringBlockHeight) + rctx, err := newRollDPoSCtx( + consensusfsm.NewConsensusConfig(cfg), + config.Default.DB, + true, + time.Second, + true, + b, + nil, + rp, + nil, + func(epochnum uint64) ([]string, error) { + re := protocol.NewRegistry() + if err := rp.Register(re); err != nil { + return nil, err + } + tipHeight := b.TipHeight() + ctx := protocol.WithBlockchainCtx( + context.Background(), + protocol.BlockchainCtx{ + Genesis: cfg.Genesis, + Registry: re, + Tip: protocol.TipInfo{ + Height: tipHeight, + }, + }, + ) + var addrs []string + candidatesList, err := pp.DelegatesByEpoch(ctx, epochnum) + if err != nil { + return nil, err + } + for _, cand := range candidatesList { + addrs = append(addrs, cand.Address) + } + return addrs, nil + }, + "", + nil, + c, + config.Default.Genesis.BeringBlockHeight, + ) require.NoError(err) require.NotNil(rctx) block := getBlockforctx(t, 0, false) - en := endorsement.NewEndorsement(time.Unix(1562382392, 0), identityset.PrivateKey(10).PublicKey(), nil) + en := endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(10).PublicKey(), nil) bp := newBlockProposal(&block, []*endorsement.Endorsement{en}) // case 1:panic caused by blockproposal is nil require.Panics(func() { - rctx.CheckBlockProposer(1, nil, nil) + rctx.CheckBlockProposer(51, nil, nil) }, "blockproposal is nil") // case 2:height != proposal.block.Height() @@ -156,50 +209,50 @@ func TestCheckBlockProposer(t *testing.T) { // case 3:panic caused by endorsement is nil require.Panics(func() { - rctx.CheckBlockProposer(21, bp, nil) + rctx.CheckBlockProposer(51, bp, nil) }, "endorsement is nil") // case 4:en's address is not proposer of the corresponding round - require.Error(rctx.CheckBlockProposer(21, bp, en)) + require.Error(rctx.CheckBlockProposer(51, bp, en)) // case 5:endorsor is not proposer of the corresponding round - en = endorsement.NewEndorsement(time.Unix(1562382492, 0), identityset.PrivateKey(22).PublicKey(), nil) - require.Error(rctx.CheckBlockProposer(21, bp, en)) + en = endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(22).PublicKey(), nil) + require.Error(rctx.CheckBlockProposer(51, bp, en)) // case 6:invalid block signature - block = getBlockforctx(t, 5, false) - en = endorsement.NewEndorsement(time.Unix(1562382392, 0), identityset.PrivateKey(5).PublicKey(), nil) + block = getBlockforctx(t, 1, false) + en = endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(1).PublicKey(), nil) bp = newBlockProposal(&block, []*endorsement.Endorsement{en}) - require.Error(rctx.CheckBlockProposer(21, bp, en)) + require.Error(rctx.CheckBlockProposer(51, bp, en)) // case 7:invalid endorsement for the vote when call AddVoteEndorsement - block = getBlockforctx(t, 5, true) - en = endorsement.NewEndorsement(time.Unix(1562382392, 0), identityset.PrivateKey(5).PublicKey(), nil) - en2 := endorsement.NewEndorsement(time.Unix(1562382592, 0), identityset.PrivateKey(7).PublicKey(), nil) + block = getBlockforctx(t, 1, true) + en = endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(1).PublicKey(), nil) + en2 := endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(7).PublicKey(), nil) bp = newBlockProposal(&block, []*endorsement.Endorsement{en2, en}) - require.Error(rctx.CheckBlockProposer(21, bp, en2)) + require.Error(rctx.CheckBlockProposer(51, bp, en2)) // case 8:Insufficient endorsements - block = getBlockforctx(t, 5, true) + block = getBlockforctx(t, 1, true) hash := block.HashBlock() vote := NewConsensusVote(hash[:], COMMIT) en2, err = endorsement.Endorse(identityset.PrivateKey(7), vote, time.Unix(1562382592, 0)) require.NoError(err) bp = newBlockProposal(&block, []*endorsement.Endorsement{en2}) - require.Error(rctx.CheckBlockProposer(21, bp, en2)) + require.Error(rctx.CheckBlockProposer(51, bp, en2)) // case 9:normal - block = getBlockforctx(t, 5, true) + block = getBlockforctx(t, 1, true) bp = newBlockProposal(&block, []*endorsement.Endorsement{en}) - require.NoError(rctx.CheckBlockProposer(21, bp, en)) + require.NoError(rctx.CheckBlockProposer(51, bp, en)) } func getBlockforctx(t *testing.T, i int, sign bool) block.Block { require := require.New(t) - ts := ×tamp.Timestamp{Seconds: 1562382392, Nanos: 10} + ts := ×tamp.Timestamp{Seconds: 1596329600, Nanos: 10} hcore := &iotextypes.BlockHeaderCore{ Version: 1, - Height: 21, + Height: 51, Timestamp: ts, PrevBlockHash: []byte(""), TxRoot: []byte(""), diff --git a/consensus/scheme/rolldpos/roundcalculator.go b/consensus/scheme/rolldpos/roundcalculator.go index 0c76f58025..72717dd007 100644 --- a/consensus/scheme/rolldpos/roundcalculator.go +++ b/consensus/scheme/rolldpos/roundcalculator.go @@ -7,7 +7,6 @@ package rolldpos import ( - "context" "time" "github.com/pkg/errors" @@ -43,7 +42,7 @@ func (c *roundCalculator) UpdateRound(round *roundCtx, height uint64, blockInter epochNum = c.rp.GetEpochNum(height) epochStartHeight = c.rp.GetEpochHeight(epochNum) var err error - if delegates, err = c.Delegates(epochStartHeight); err != nil { + if delegates, err = c.Delegates(height); err != nil { return nil, err } } @@ -169,16 +168,7 @@ func (c *roundCalculator) roundInfo( // Delegates returns list of delegates at given height func (c *roundCalculator) Delegates(height uint64) ([]string, error) { epochNum := c.rp.GetEpochNum(height) - candidatesList, err := c.delegatesByEpochFunc(context.Background(), epochNum) - if err != nil { - return nil, errors.Wrapf(err, "failed to get delegate by epoch %d", epochNum) - } - addrs := []string{} - for _, candidate := range candidatesList { - addrs = append(addrs, candidate.Address) - } - - return addrs, nil + return c.delegatesByEpochFunc(epochNum) } // NewRoundWithToleration starts new round with tolerated over time @@ -217,8 +207,8 @@ func (c *roundCalculator) newRound( var roundStartTime time.Time if height != 0 { epochNum = c.rp.GetEpochNum(height) - epochStartHeight := c.rp.GetEpochHeight(epochNum) - if delegates, err = c.Delegates(epochStartHeight); err != nil { + epochStartHeight = c.rp.GetEpochHeight(epochNum) + if delegates, err = c.Delegates(height); err != nil { return } if roundNum, roundStartTime, err = c.roundInfo(height, blockInterval, now, toleratedOvertime); err != nil { diff --git a/consensus/scheme/rolldpos/roundcalculator_test.go b/consensus/scheme/rolldpos/roundcalculator_test.go index 99d89d3a2f..e5ef84aca4 100644 --- a/consensus/scheme/rolldpos/roundcalculator_test.go +++ b/consensus/scheme/rolldpos/roundcalculator_test.go @@ -26,7 +26,6 @@ import ( "github.com/iotexproject/iotex-core/blockchain/genesis" "github.com/iotexproject/iotex-core/config" "github.com/iotexproject/iotex-core/pkg/unit" - "github.com/iotexproject/iotex-core/state" "github.com/iotexproject/iotex-core/state/factory" "github.com/iotexproject/iotex-core/test/identityset" "github.com/iotexproject/iotex-core/testutil" @@ -35,25 +34,25 @@ import ( func TestUpdateRound(t *testing.T) { require := require.New(t) rc := makeRoundCalculator(t) - ra, err := rc.NewRound(1, time.Second, time.Unix(1562382392, 0), nil) + ra, err := rc.NewRound(51, time.Second, time.Unix(1562382522, 0), nil) require.NoError(err) // height < round.Height() - _, err = rc.UpdateRound(ra, 0, time.Second, time.Unix(1562382492, 0), time.Second) + _, err = rc.UpdateRound(ra, 50, time.Second, time.Unix(1562382492, 0), time.Second) require.Error(err) // height == round.Height() and now.Before(round.StartTime()) - _, err = rc.UpdateRound(ra, 1, time.Second, time.Unix(1562382092, 0), time.Second) + _, err = rc.UpdateRound(ra, 51, time.Second, time.Unix(1562382522, 0), time.Second) require.NoError(err) // height >= round.NextEpochStartHeight() Delegates error _, err = rc.UpdateRound(ra, 500, time.Second, time.Unix(1562382092, 0), time.Second) require.Error(err) - // (31+120)%24 - ra, err = rc.UpdateRound(ra, 31, time.Second, time.Unix(1562382522, 0), time.Second) + // (51+100)%24 + ra, err = rc.UpdateRound(ra, 51, time.Second, time.Unix(1562382522, 0), time.Second) require.NoError(err) - require.Equal(identityset.Address(7).String(), ra.proposer) + require.Equal(identityset.Address(10).String(), ra.proposer) } func TestNewRound(t *testing.T) { @@ -74,31 +73,31 @@ func TestNewRound(t *testing.T) { require.NoError(err) require.Equal(validDelegates[2], proposer) - ra, err := rc.NewRound(1, time.Second, time.Unix(1562382392, 0), nil) + ra, err := rc.NewRound(51, time.Second, time.Unix(1562382592, 0), nil) require.NoError(err) - require.Equal(uint32(19), ra.roundNum) - require.Equal(uint64(1), ra.height) + require.Equal(uint32(170), ra.roundNum) + require.Equal(uint64(51), ra.height) // sorted by address hash - require.Equal(identityset.Address(16).String(), ra.proposer) + require.Equal(identityset.Address(7).String(), ra.proposer) rc.timeBasedRotation = true - ra, err = rc.NewRound(1, time.Second, time.Unix(1562382392, 0), nil) + ra, err = rc.NewRound(51, time.Second, time.Unix(1562382592, 0), nil) require.NoError(err) - require.Equal(uint32(19), ra.roundNum) - require.Equal(uint64(1), ra.height) - require.Equal(identityset.Address(5).String(), ra.proposer) + require.Equal(uint32(170), ra.roundNum) + require.Equal(uint64(51), ra.height) + require.Equal(identityset.Address(12).String(), ra.proposer) } func TestDelegates(t *testing.T) { require := require.New(t) rc := makeRoundCalculator(t) - dels, err := rc.Delegates(4) + dels, err := rc.Delegates(51) require.NoError(err) require.Equal(rc.rp.NumDelegates(), uint64(len(dels))) - require.False(rc.IsDelegate(identityset.Address(25).String(), 2)) - require.True(rc.IsDelegate(identityset.Address(5).String(), 2)) + require.False(rc.IsDelegate(identityset.Address(25).String(), 51)) + require.True(rc.IsDelegate(identityset.Address(0).String(), 51)) } func TestRoundInfo(t *testing.T) { @@ -219,20 +218,36 @@ func makeChain(t *testing.T) (blockchain.Blockchain, *rolldpos.Protocol, poll.Pr func makeRoundCalculator(t *testing.T) *roundCalculator { bc, rp, pp := makeChain(t) - return &roundCalculator{bc, true, rp, func(ctx context.Context, epochNum uint64) (state.CandidateList, error) { - re := protocol.NewRegistry() - if err := rp.Register(re); err != nil { - return nil, err - } - ctx = protocol.WithBlockchainCtx( - ctx, - protocol.BlockchainCtx{ - Genesis: config.Default.Genesis, - Registry: re, - }, - ) - return pp.DelegatesByEpoch(ctx, epochNum) - }, + return &roundCalculator{ + bc, + true, + rp, + func(epochNum uint64) ([]string, error) { + re := protocol.NewRegistry() + if err := rp.Register(re); err != nil { + return nil, err + } + tipHeight := bc.TipHeight() + ctx := protocol.WithBlockchainCtx( + context.Background(), + protocol.BlockchainCtx{ + Genesis: config.Default.Genesis, + Registry: re, + Tip: protocol.TipInfo{ + Height: tipHeight, + }, + }, + ) + var addrs []string + candidatesList, err := pp.DelegatesByEpoch(ctx, epochNum) + if err != nil { + return nil, err + } + for _, cand := range candidatesList { + addrs = append(addrs, cand.Address) + } + return addrs, nil + }, 0, } } diff --git a/state/factory/factory_test.go b/state/factory/factory_test.go index 041635cfe2..ada4e7fe01 100644 --- a/state/factory/factory_test.go +++ b/state/factory/factory_test.go @@ -255,6 +255,8 @@ func testCandidates(sf Factory, t *testing.T) { registry := protocol.NewRegistry() require.NoError(t, registry.Register("rolldpos", rolldpos.NewProtocol(36, 36, 20))) p, err := poll.NewGovernanceChainCommitteeProtocol( + nil, + nil, nil, nil, committee, @@ -268,6 +270,7 @@ func testCandidates(sf Factory, t *testing.T) { config.Default.Genesis.ProductivityThreshold, config.Default.Genesis.KickoutEpochPeriod, config.Default.Genesis.KickoutIntensityRate, + config.Default.Genesis.UnproductiveDelegateMaxCacheSize, ) require.NoError(t, registry.Register("poll", p)) require.NoError(t, err)