Skip to content

Commit

Permalink
Change candidates/kickoutList state DB implementation (#1858)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
koseoyoung committed Feb 18, 2020
1 parent 89faa03 commit 2e20e5a
Show file tree
Hide file tree
Showing 30 changed files with 1,134 additions and 362 deletions.
254 changes: 189 additions & 65 deletions action/protocol/poll/governance_protocol.go

Large diffs are not rendered by default.

125 changes: 104 additions & 21 deletions action/protocol/poll/governance_protocol_test.go
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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
}
Expand All @@ -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()
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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))
Expand Down Expand Up @@ -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)).
Expand All @@ -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)
Expand Down Expand Up @@ -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)).
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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]))
}

}
62 changes: 37 additions & 25 deletions action/protocol/poll/lifelong_protocol.go
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions action/protocol/poll/lifelong_protocol_test.go
Expand Up @@ -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"
Expand All @@ -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{
Expand Down

0 comments on commit 2e20e5a

Please sign in to comment.