From 1c075dd0a15e62ff30fba8a5696ac9261a65b8f1 Mon Sep 17 00:00:00 2001 From: pavelkrolevets Date: Mon, 6 Aug 2018 16:26:29 +0800 Subject: [PATCH] added dpos draft --- build/env.sh | 2 +- consensus/dpos/api.go | 71 ++++ consensus/dpos/dpos.go | 532 +++++++++++++++++++++++++++ consensus/dpos/dpos_test.go | 118 ++++++ consensus/dpos/epoch_context.go | 221 +++++++++++ consensus/dpos/epoch_context_test.go | 359 ++++++++++++++++++ 6 files changed, 1302 insertions(+), 1 deletion(-) create mode 100644 consensus/dpos/api.go create mode 100644 consensus/dpos/dpos.go create mode 100644 consensus/dpos/dpos_test.go create mode 100644 consensus/dpos/epoch_context.go create mode 100644 consensus/dpos/epoch_context_test.go diff --git a/build/env.sh b/build/env.sh index 3914555d1b..6806fbe6e6 100755 --- a/build/env.sh +++ b/build/env.sh @@ -10,7 +10,7 @@ fi # Create fake Go workspace if it doesn't exist yet. workspace="$PWD/build/_workspace" root="$PWD" -ethdir="$workspace/src/github.com/ethereum" +ethdir="$workspace/src/github.com/pavelkrolevets" if [ ! -L "$ethdir/go-ethereum" ]; then mkdir -p "$ethdir" cd "$ethdir" diff --git a/consensus/dpos/api.go b/consensus/dpos/api.go new file mode 100644 index 0000000000..cc7b8a93db --- /dev/null +++ b/consensus/dpos/api.go @@ -0,0 +1,71 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package dpos + +import ( + "github.com/pavelkrolevets/go-ethereum/common" + "github.com/pavelkrolevets/go-ethereum/consensus" + "github.com/pavelkrolevets/go-ethereum/core/types" + "github.com/pavelkrolevets/go-ethereum/rpc" + + "math/big" +) + +// API is a user facing RPC API to allow controlling the delegate and voting +// mechanisms of the delegated-proof-of-stake +type API struct { + chain consensus.ChainReader + dpos *Dpos +} + +// GetValidators retrieves the list of the validators at specified block +func (api *API) GetValidators(number *rpc.BlockNumber) ([]common.Address, error) { + var header *types.Header + if number == nil || *number == rpc.LatestBlockNumber { + header = api.chain.CurrentHeader() + } else { + header = api.chain.GetHeaderByNumber(uint64(number.Int64())) + } + if header == nil { + return nil, errUnknownBlock + } + + epochTrie, err := types.NewEpochTrie(header.DposContext.EpochHash, api.dpos.db) + if err != nil { + return nil, err + } + dposContext := types.DposContext{} + dposContext.SetEpoch(epochTrie) + validators, err := dposContext.GetValidators() + if err != nil { + return nil, err + } + return validators, nil +} + +// GetConfirmedBlockNumber retrieves the latest irreversible block +func (api *API) GetConfirmedBlockNumber() (*big.Int, error) { + var err error + header := api.dpos.confirmedBlockHeader + if header == nil { + header, err = api.dpos.loadConfirmedBlockHeader(api.chain) + if err != nil { + return nil, err + } + } + return header.Number, nil +} diff --git a/consensus/dpos/dpos.go b/consensus/dpos/dpos.go new file mode 100644 index 0000000000..d8e9f78277 --- /dev/null +++ b/consensus/dpos/dpos.go @@ -0,0 +1,532 @@ +package dpos + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + "sync" + "time" + + "github.com/pavelkrolevets/go-ethereum/accounts" + "github.com/pavelkrolevets/go-ethereum/common" + "github.com/pavelkrolevets/go-ethereum/consensus" + "github.com/pavelkrolevets/go-ethereum/consensus/misc" + "github.com/pavelkrolevets/go-ethereum/core/state" + "github.com/pavelkrolevets/go-ethereum/core/types" + "github.com/pavelkrolevets/go-ethereum/crypto" + "github.com/pavelkrolevets/go-ethereum/crypto/sha3" + "github.com/pavelkrolevets/go-ethereum/ethdb" + "github.com/pavelkrolevets/go-ethereum/log" + "github.com/pavelkrolevets/go-ethereum/params" + "github.com/pavelkrolevets/go-ethereum/rlp" + "github.com/pavelkrolevets/go-ethereum/rpc" + "github.com/pavelkrolevets/go-ethereum/trie" + lru "github.com/hashicorp/golang-lru" +) + +const ( + extraVanity = 32 // Fixed number of extra-data prefix bytes reserved for signer vanity + extraSeal = 65 // Fixed number of extra-data suffix bytes reserved for signer seal + inmemorySignatures = 4096 // Number of recent block signatures to keep in memory + + blockInterval = int64(1) + epochInterval = int64(86400) + maxValidatorSize = 3 + safeSize = maxValidatorSize*2/3 + 1 + consensusSize = maxValidatorSize*2/3 + 1 +) + +var ( + big0 = big.NewInt(0) + big8 = big.NewInt(8) + big32 = big.NewInt(32) + + frontierBlockReward *big.Int = big.NewInt(5e+18) // Block reward in wei for successfully mining a block + byzantiumBlockReward *big.Int = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from Byzantium + + timeOfFirstBlock = int64(0) + + confirmedBlockHead = []byte("confirmed-block-head") +) + +var ( + // errUnknownBlock is returned when the list of signers is requested for a block + // that is not part of the local blockchain. + errUnknownBlock = errors.New("unknown block") + // errMissingVanity is returned if a block's extra-data section is shorter than + // 32 bytes, which is required to store the signer vanity. + errMissingVanity = errors.New("extra-data 32 byte vanity prefix missing") + // errMissingSignature is returned if a block's extra-data section doesn't seem + // to contain a 65 byte secp256k1 signature. + errMissingSignature = errors.New("extra-data 65 byte suffix signature missing") + // errInvalidMixDigest is returned if a block's mix digest is non-zero. + errInvalidMixDigest = errors.New("non-zero mix digest") + // errInvalidUncleHash is returned if a block contains an non-empty uncle list. + errInvalidUncleHash = errors.New("non empty uncle hash") + errInvalidDifficulty = errors.New("invalid difficulty") + + // ErrInvalidTimestamp is returned if the timestamp of a block is lower than + // the previous block's timestamp + the minimum block period. + ErrInvalidTimestamp = errors.New("invalid timestamp") + ErrWaitForPrevBlock = errors.New("wait for last block arrived") + ErrMintFutureBlock = errors.New("mint the future block") + ErrMismatchSignerAndValidator = errors.New("mismatch block signer and validator") + ErrInvalidBlockValidator = errors.New("invalid block validator") + ErrInvalidMintBlockTime = errors.New("invalid time to mint the block") + ErrNilBlockHeader = errors.New("nil block header returned") +) +var ( + uncleHash = types.CalcUncleHash(nil) // Always Keccak256(RLP([])) as uncles are meaningless outside of PoW. +) + +type Dpos struct { + config *params.DposConfig // Consensus engine configuration parameters + db ethdb.Database // Database to store and retrieve snapshot checkpoints + + signer common.Address + signFn SignerFn + signatures *lru.ARCCache // Signatures of recent blocks to speed up mining + confirmedBlockHeader *types.Header + + mu sync.RWMutex + stop chan bool +} + +type SignerFn func(accounts.Account, []byte) ([]byte, error) + +// NOTE: sigHash was copy from clique +// sigHash returns the hash which is used as input for the proof-of-authority +// signing. It is the hash of the entire header apart from the 65 byte signature +// contained at the end of the extra data. +// +// Note, the method requires the extra data to be at least 65 bytes, otherwise it +// panics. This is done to avoid accidentally using both forms (signature present +// or not), which could be abused to produce different hashes for the same header. +func sigHash(header *types.Header) (hash common.Hash) { + hasher := sha3.NewKeccak256() + + rlp.Encode(hasher, []interface{}{ + header.ParentHash, + header.UncleHash, + header.Validator, + header.Coinbase, + header.Root, + header.TxHash, + header.ReceiptHash, + header.Bloom, + header.Difficulty, + header.Number, + header.GasLimit, + header.GasUsed, + header.Time, + header.Extra[:len(header.Extra)-65], // Yes, this will panic if extra is too short + header.MixDigest, + header.Nonce, + header.DposContext.Root(), + }) + hasher.Sum(hash[:0]) + return hash +} + +func New(config *params.DposConfig, db ethdb.Database) *Dpos { + signatures, _ := lru.NewARC(inmemorySignatures) + return &Dpos{ + config: config, + db: db, + signatures: signatures, + } +} + +func (d *Dpos) Author(header *types.Header) (common.Address, error) { + return header.Validator, nil +} + +func (d *Dpos) VerifyHeader(chain consensus.ChainReader, header *types.Header, seal bool) error { + return d.verifyHeader(chain, header, nil) +} + +func (d *Dpos) verifyHeader(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error { + if header.Number == nil { + return errUnknownBlock + } + number := header.Number.Uint64() + // Unnecssary to verify the block from feature + if header.Time.Cmp(big.NewInt(time.Now().Unix())) > 0 { + return consensus.ErrFutureBlock + } + // Check that the extra-data contains both the vanity and signature + if len(header.Extra) < extraVanity { + return errMissingVanity + } + if len(header.Extra) < extraVanity+extraSeal { + return errMissingSignature + } + // Ensure that the mix digest is zero as we don't have fork protection currently + if header.MixDigest != (common.Hash{}) { + return errInvalidMixDigest + } + // Difficulty always 1 + if header.Difficulty.Uint64() != 1 { + return errInvalidDifficulty + } + // Ensure that the block doesn't contain any uncles which are meaningless in DPoS + if header.UncleHash != uncleHash { + return errInvalidUncleHash + } + // If all checks passed, validate any special fields for hard forks + if err := misc.VerifyForkHashes(chain.Config(), header, false); err != nil { + return err + } + + var parent *types.Header + if len(parents) > 0 { + parent = parents[len(parents)-1] + } else { + parent = chain.GetHeader(header.ParentHash, number-1) + } + if parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash { + return consensus.ErrUnknownAncestor + } + if parent.Time.Uint64()+uint64(blockInterval) > header.Time.Uint64() { + return ErrInvalidTimestamp + } + return nil +} + +func (d *Dpos) VerifyHeaders(chain consensus.ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { + abort := make(chan struct{}) + results := make(chan error, len(headers)) + + go func() { + for i, header := range headers { + err := d.verifyHeader(chain, header, headers[:i]) + select { + case <-abort: + return + case results <- err: + } + } + }() + return abort, results +} + +// VerifyUncles implements consensus.Engine, always returning an error for any +// uncles as this consensus mechanism doesn't permit uncles. +func (d *Dpos) VerifyUncles(chain consensus.ChainReader, block *types.Block) error { + if len(block.Uncles()) > 0 { + return errors.New("uncles not allowed") + } + return nil +} + +// VerifySeal implements consensus.Engine, checking whether the signature contained +// in the header satisfies the consensus protocol requirements. +func (d *Dpos) VerifySeal(chain consensus.ChainReader, header *types.Header) error { + return d.verifySeal(chain, header, nil) +} + +func (d *Dpos) verifySeal(chain consensus.ChainReader, header *types.Header, parents []*types.Header) error { + // Verifying the genesis block is not supported + number := header.Number.Uint64() + if number == 0 { + return errUnknownBlock + } + var parent *types.Header + if len(parents) > 0 { + parent = parents[len(parents)-1] + } else { + parent = chain.GetHeader(header.ParentHash, number-1) + } + dposContext, err := types.NewDposContextFromProto(d.db, parent.DposContext) + if err != nil { + return err + } + epochContext := &EpochContext{DposContext: dposContext} + validator, err := epochContext.lookupValidator(header.Time.Int64()) + if err != nil { + return err + } + if err := d.verifyBlockSigner(validator, header); err != nil { + return err + } + return d.updateConfirmedBlockHeader(chain) +} + +func (d *Dpos) verifyBlockSigner(validator common.Address, header *types.Header) error { + signer, err := ecrecover(header, d.signatures) + if err != nil { + return err + } + if bytes.Compare(signer.Bytes(), validator.Bytes()) != 0 { + return ErrInvalidBlockValidator + } + if bytes.Compare(signer.Bytes(), header.Validator.Bytes()) != 0 { + return ErrMismatchSignerAndValidator + } + return nil +} + +func (d *Dpos) updateConfirmedBlockHeader(chain consensus.ChainReader) error { + if d.confirmedBlockHeader == nil { + header, err := d.loadConfirmedBlockHeader(chain) + if err != nil { + header = chain.GetHeaderByNumber(0) + if header == nil { + return err + } + } + d.confirmedBlockHeader = header + } + + curHeader := chain.CurrentHeader() + epoch := int64(-1) + validatorMap := make(map[common.Address]bool) + for d.confirmedBlockHeader.Hash() != curHeader.Hash() && + d.confirmedBlockHeader.Number.Uint64() < curHeader.Number.Uint64() { + curEpoch := curHeader.Time.Int64() / epochInterval + if curEpoch != epoch { + epoch = curEpoch + validatorMap = make(map[common.Address]bool) + } + // fast return + // if block number difference less consensusSize-witnessNum + // there is no need to check block is confirmed + if curHeader.Number.Int64()-d.confirmedBlockHeader.Number.Int64() < int64(consensusSize-len(validatorMap)) { + log.Debug("Dpos fast return", "current", curHeader.Number.String(), "confirmed", d.confirmedBlockHeader.Number.String(), "witnessCount", len(validatorMap)) + return nil + } + validatorMap[curHeader.Validator] = true + if len(validatorMap) >= consensusSize { + d.confirmedBlockHeader = curHeader + if err := d.storeConfirmedBlockHeader(d.db); err != nil { + return err + } + log.Debug("dpos set confirmed block header success", "currentHeader", curHeader.Number.String()) + return nil + } + curHeader = chain.GetHeaderByHash(curHeader.ParentHash) + if curHeader == nil { + return ErrNilBlockHeader + } + } + return nil +} + +func (s *Dpos) loadConfirmedBlockHeader(chain consensus.ChainReader) (*types.Header, error) { + key, err := s.db.Get(confirmedBlockHead) + if err != nil { + return nil, err + } + header := chain.GetHeaderByHash(common.BytesToHash(key)) + if header == nil { + return nil, ErrNilBlockHeader + } + return header, nil +} + +// store inserts the snapshot into the database. +func (s *Dpos) storeConfirmedBlockHeader(db ethdb.Database) error { + return db.Put(confirmedBlockHead, s.confirmedBlockHeader.Hash().Bytes()) +} + +func (d *Dpos) Prepare(chain consensus.ChainReader, header *types.Header) error { + header.Nonce = types.BlockNonce{} + number := header.Number.Uint64() + if len(header.Extra) < extraVanity { + header.Extra = append(header.Extra, bytes.Repeat([]byte{0x00}, extraVanity-len(header.Extra))...) + } + header.Extra = header.Extra[:extraVanity] + header.Extra = append(header.Extra, make([]byte, extraSeal)...) + parent := chain.GetHeader(header.ParentHash, number-1) + if parent == nil { + return consensus.ErrUnknownAncestor + } + header.Difficulty = d.CalcDifficulty(chain, header.Time.Uint64(), parent) + header.Validator = d.signer + return nil +} + +func AccumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) { + // Select the correct block reward based on chain progression + blockReward := frontierBlockReward + if config.IsByzantium(header.Number) { + blockReward = byzantiumBlockReward + } + // Accumulate the rewards for the miner and any included uncles + reward := new(big.Int).Set(blockReward) + state.AddBalance(header.Coinbase, reward) +} + +func (d *Dpos) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, + uncles []*types.Header, receipts []*types.Receipt, dposContext *types.DposContext) (*types.Block, error) { + // Accumulate block rewards and commit the final state root + AccumulateRewards(chain.Config(), state, header, uncles) + header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) + + parent := chain.GetHeaderByHash(header.ParentHash) + epochContext := &EpochContext{ + statedb: state, + DposContext: dposContext, + TimeStamp: header.Time.Int64(), + } + if timeOfFirstBlock == 0 { + if firstBlockHeader := chain.GetHeaderByNumber(1); firstBlockHeader != nil { + timeOfFirstBlock = firstBlockHeader.Time.Int64() + } + } + genesis := chain.GetHeaderByNumber(0) + err := epochContext.tryElect(genesis, parent) + if err != nil { + return nil, fmt.Errorf("got error when elect next epoch, err: %s", err) + } + + //update mint count trie + updateMintCnt(parent.Time.Int64(), header.Time.Int64(), header.Validator, dposContext) + header.DposContext = dposContext.ToProto() + return types.NewBlock(header, txs, uncles, receipts), nil +} + +func (d *Dpos) checkDeadline(lastBlock *types.Block, now int64) error { + prevSlot := PrevSlot(now) + nextSlot := NextSlot(now) + if lastBlock.Time().Int64() >= nextSlot { + return ErrMintFutureBlock + } + // last block was arrived, or time's up + if lastBlock.Time().Int64() == prevSlot || nextSlot-now <= 1 { + return nil + } + return ErrWaitForPrevBlock +} + +func (d *Dpos) CheckValidator(lastBlock *types.Block, now int64) error { + if err := d.checkDeadline(lastBlock, now); err != nil { + return err + } + dposContext, err := types.NewDposContextFromProto(d.db, lastBlock.Header().DposContext) + if err != nil { + return err + } + epochContext := &EpochContext{DposContext: dposContext} + validator, err := epochContext.lookupValidator(now) + if err != nil { + return err + } + if (validator == common.Address{}) || bytes.Compare(validator.Bytes(), d.signer.Bytes()) != 0 { + return ErrInvalidBlockValidator + } + return nil +} + +// Seal generates a new block for the given input block with the local miner's +// seal place on top. +func (d *Dpos) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) { + header := block.Header() + number := header.Number.Uint64() + // Sealing the genesis block is not supported + if number == 0 { + return nil, errUnknownBlock + } + now := time.Now().Unix() + delay := NextSlot(now) - now + if delay > 0 { + select { + case <-stop: + return nil, nil + case <-time.After(time.Duration(delay) * time.Second): + } + } + block.Header().Time.SetInt64(time.Now().Unix()) + + // time's up, sign the block + sighash, err := d.signFn(accounts.Account{Address: d.signer}, sigHash(header).Bytes()) + if err != nil { + return nil, err + } + copy(header.Extra[len(header.Extra)-extraSeal:], sighash) + return block.WithSeal(header), nil +} + +func (d *Dpos) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int { + return big.NewInt(1) +} + +func (d *Dpos) APIs(chain consensus.ChainReader) []rpc.API { + return []rpc.API{{ + Namespace: "dpos", + Version: "1.0", + Service: &API{chain: chain, dpos: d}, + Public: true, + }} +} + +func (d *Dpos) Authorize(signer common.Address, signFn SignerFn) { + d.mu.Lock() + d.signer = signer + d.signFn = signFn + d.mu.Unlock() +} + +// ecrecover extracts the Ethereum account address from a signed header. +func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, error) { + // If the signature's already cached, return that + hash := header.Hash() + if address, known := sigcache.Get(hash); known { + return address.(common.Address), nil + } + // Retrieve the signature from the header extra-data + if len(header.Extra) < extraSeal { + return common.Address{}, errMissingSignature + } + signature := header.Extra[len(header.Extra)-extraSeal:] + // Recover the public key and the Ethereum address + pubkey, err := crypto.Ecrecover(sigHash(header).Bytes(), signature) + if err != nil { + return common.Address{}, err + } + var signer common.Address + copy(signer[:], crypto.Keccak256(pubkey[1:])[12:]) + sigcache.Add(hash, signer) + return signer, nil +} + +func PrevSlot(now int64) int64 { + return int64((now-1)/blockInterval) * blockInterval +} + +func NextSlot(now int64) int64 { + return int64((now+blockInterval-1)/blockInterval) * blockInterval +} + +// update counts in MintCntTrie for the miner of newBlock +func updateMintCnt(parentBlockTime, currentBlockTime int64, validator common.Address, dposContext *types.DposContext) { + currentMintCntTrie := dposContext.MintCntTrie() + currentEpoch := parentBlockTime / epochInterval + currentEpochBytes := make([]byte, 8) + binary.BigEndian.PutUint64(currentEpochBytes, uint64(currentEpoch)) + + cnt := int64(1) + newEpoch := currentBlockTime / epochInterval + // still during the currentEpochID + if currentEpoch == newEpoch { + iter := trie.NewIterator(currentMintCntTrie.NodeIterator(currentEpochBytes)) + + // when current is not genesis, read last count from the MintCntTrie + if iter.Next() { + cntBytes := currentMintCntTrie.Get(append(currentEpochBytes, validator.Bytes()...)) + + // not the first time to mint + if cntBytes != nil { + cnt = int64(binary.BigEndian.Uint64(cntBytes)) + 1 + } + } + } + + newCntBytes := make([]byte, 8) + newEpochBytes := make([]byte, 8) + binary.BigEndian.PutUint64(newEpochBytes, uint64(newEpoch)) + binary.BigEndian.PutUint64(newCntBytes, uint64(cnt)) + dposContext.MintCntTrie().TryUpdate(append(newEpochBytes, validator.Bytes()...), newCntBytes) +} diff --git a/consensus/dpos/dpos_test.go b/consensus/dpos/dpos_test.go new file mode 100644 index 0000000000..7f79ace3aa --- /dev/null +++ b/consensus/dpos/dpos_test.go @@ -0,0 +1,118 @@ +package dpos + +import ( + "testing" + + "encoding/binary" + + "github.com/pavelkrolevets/go-ethereum/common" + "github.com/pavelkrolevets/go-ethereum/core/types" + "github.com/pavelkrolevets/go-ethereum/ethdb" + "github.com/pavelkrolevets/go-ethereum/trie" + "github.com/stretchr/testify/assert" +) + +var ( + MockEpoch = []string{ + "0x44d1ce0b7cb3588bca96151fe1bc05af38f91b6e", + "0xa60a3886b552ff9992cfcd208ec1152079e046c2", + "0x4e080e49f62694554871e669aeb4ebe17c4a9670", + "0xb040353ec0f2c113d5639444f7253681aecda1f8", + "0x14432e15f21237013017fa6ee90fc99433dec82c", + "0x9f30d0e5c9c88cade54cd1adecf6bc2c7e0e5af6", + "0xd83b44a3719720ec54cdb9f54c0202de68f1ebcb", + "0x56cc452e450551b7b9cffe25084a069e8c1e9441", + "0xbcfcb3fa8250be4f2bf2b1e70e1da500c668377b", + "0x9d9667c71bb09d6ca7c3ed12bfe5e7be24e2ffe1", + "0xabde197e97398864ba74511f02832726edad5967", + "0x6f99d97a394fa7a623fdf84fdc7446b99c3cb335", + "0xf78b011e639ce6d8b76f97712118f3fe4a12dd95", + "0x8db3b6c801dddd624d6ddc2088aa64b5a2493661", + "0x751b484bd5296f8d267a8537d33f25a848f7f7af", + "0x646ba1fa42eb940aac67103a71e9a908ef484ec3", + "0x34d4a8d9f6b53a8f5e674516cb8ad66c843b2801", + "0x5b76fff970bf8a351c1c9ebfb5e5a9493e956ddd", + "0x8da3c5aedaf106c61cfee6d8483e1f255fdd60c0", + "0x2cdbe87a1bd7ee60dd6fe97f7b2d1efbacd5d95d", + "0x743415d0e979dc6e426bc8189e40beb65bf5ac1d", + } +) + +func mockNewDposContext(db ethdb.Database) *types.DposContext { + dposContext, err := types.NewDposContextFromProto(db, &types.DposContextProto{}) + if err != nil { + return nil + } + delegator := []byte{} + candidate := []byte{} + addresses := []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + addresses = append(addresses, common.HexToAddress(MockEpoch[i])) + } + dposContext.SetValidators(addresses) + for j := 0; j < len(MockEpoch); j++ { + delegator = common.HexToAddress(MockEpoch[j]).Bytes() + candidate = common.HexToAddress(MockEpoch[j]).Bytes() + dposContext.DelegateTrie().TryUpdate(append(candidate, delegator...), candidate) + dposContext.CandidateTrie().TryUpdate(candidate, candidate) + dposContext.VoteTrie().TryUpdate(candidate, candidate) + } + return dposContext +} + +func setMintCntTrie(epochID int64, candidate common.Address, mintCntTrie *trie.Trie, count int64) { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, uint64(epochID)) + cntBytes := make([]byte, 8) + binary.BigEndian.PutUint64(cntBytes, uint64(count)) + mintCntTrie.TryUpdate(append(key, candidate.Bytes()...), cntBytes) +} + +func getMintCnt(epochID int64, candidate common.Address, mintCntTrie *trie.Trie) int64 { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, uint64(epochID)) + cntBytes := mintCntTrie.Get(append(key, candidate.Bytes()...)) + if cntBytes == nil { + return 0 + } else { + return int64(binary.BigEndian.Uint64(cntBytes)) + } +} + +func TestUpdateMintCnt(t *testing.T) { + db, _ := ethdb.NewMemDatabase() + dposContext := mockNewDposContext(db) + + // new block still in the same epoch with current block, but newMiner is the first time to mint in the epoch + lastTime := int64(epochInterval) + + miner := common.HexToAddress("0xa60a3886b552ff9992cfcd208ec1152079e046c2") + blockTime := int64(epochInterval + blockInterval) + + beforeUpdateCnt := getMintCnt(blockTime/epochInterval, miner, dposContext.MintCntTrie()) + updateMintCnt(lastTime, blockTime, miner, dposContext) + afterUpdateCnt := getMintCnt(blockTime/epochInterval, miner, dposContext.MintCntTrie()) + assert.Equal(t, int64(0), beforeUpdateCnt) + assert.Equal(t, int64(1), afterUpdateCnt) + + // new block still in the same epoch with current block, and newMiner has mint block before in the epoch + setMintCntTrie(blockTime/epochInterval, miner, dposContext.MintCntTrie(), int64(1)) + + blockTime = epochInterval + blockInterval*4 + + // currentBlock has recorded the count for the newMiner before UpdateMintCnt + beforeUpdateCnt = getMintCnt(blockTime/epochInterval, miner, dposContext.MintCntTrie()) + updateMintCnt(lastTime, blockTime, miner, dposContext) + afterUpdateCnt = getMintCnt(blockTime/epochInterval, miner, dposContext.MintCntTrie()) + assert.Equal(t, int64(1), beforeUpdateCnt) + assert.Equal(t, int64(2), afterUpdateCnt) + + // new block come to a new epoch + blockTime = epochInterval * 2 + + beforeUpdateCnt = getMintCnt(blockTime/epochInterval, miner, dposContext.MintCntTrie()) + updateMintCnt(lastTime, blockTime, miner, dposContext) + afterUpdateCnt = getMintCnt(blockTime/epochInterval, miner, dposContext.MintCntTrie()) + assert.Equal(t, int64(0), beforeUpdateCnt) + assert.Equal(t, int64(1), afterUpdateCnt) +} diff --git a/consensus/dpos/epoch_context.go b/consensus/dpos/epoch_context.go new file mode 100644 index 0000000000..cbda686c1e --- /dev/null +++ b/consensus/dpos/epoch_context.go @@ -0,0 +1,221 @@ +package dpos + +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + "math/rand" + "sort" + + "github.com/pavelkrolevets/go-ethereum/common" + "github.com/pavelkrolevets/go-ethereum/core/state" + "github.com/pavelkrolevets/go-ethereum/core/types" + "github.com/pavelkrolevets/go-ethereum/crypto" + "github.com/pavelkrolevets/go-ethereum/log" + "github.com/pavelkrolevets/go-ethereum/trie" +) + +type EpochContext struct { + TimeStamp int64 + DposContext *types.DposContext + statedb *state.StateDB +} + +// countVotes +func (ec *EpochContext) countVotes() (votes map[common.Address]*big.Int, err error) { + votes = map[common.Address]*big.Int{} + delegateTrie := ec.DposContext.DelegateTrie() + candidateTrie := ec.DposContext.CandidateTrie() + statedb := ec.statedb + + iterCandidate := trie.NewIterator(candidateTrie.NodeIterator(nil)) + existCandidate := iterCandidate.Next() + if !existCandidate { + return votes, errors.New("no candidates") + } + for existCandidate { + candidate := iterCandidate.Value + candidateAddr := common.BytesToAddress(candidate) + delegateIterator := trie.NewIterator(delegateTrie.PrefixIterator(candidate)) + existDelegator := delegateIterator.Next() + if !existDelegator { + votes[candidateAddr] = new(big.Int) + existCandidate = iterCandidate.Next() + continue + } + for existDelegator { + delegator := delegateIterator.Value + score, ok := votes[candidateAddr] + if !ok { + score = new(big.Int) + } + delegatorAddr := common.BytesToAddress(delegator) + weight := statedb.GetBalance(delegatorAddr) + score.Add(score, weight) + votes[candidateAddr] = score + existDelegator = delegateIterator.Next() + } + existCandidate = iterCandidate.Next() + } + return votes, nil +} + +func (ec *EpochContext) kickoutValidator(epoch int64) error { + validators, err := ec.DposContext.GetValidators() + if err != nil { + return fmt.Errorf("failed to get validator: %s", err) + } + if len(validators) == 0 { + return errors.New("no validator could be kickout") + } + + epochDuration := epochInterval + // First epoch duration may lt epoch interval, + // while the first block time wouldn't always align with epoch interval, + // so caculate the first epoch duartion with first block time instead of epoch interval, + // prevent the validators were kickout incorrectly. + if ec.TimeStamp-timeOfFirstBlock < epochInterval { + epochDuration = ec.TimeStamp - timeOfFirstBlock + } + + needKickoutValidators := sortableAddresses{} + for _, validator := range validators { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, uint64(epoch)) + key = append(key, validator.Bytes()...) + cnt := int64(0) + if cntBytes := ec.DposContext.MintCntTrie().Get(key); cntBytes != nil { + cnt = int64(binary.BigEndian.Uint64(cntBytes)) + } + if cnt < epochDuration/blockInterval/ maxValidatorSize /2 { + // not active validators need kickout + needKickoutValidators = append(needKickoutValidators, &sortableAddress{validator, big.NewInt(cnt)}) + } + } + // no validators need kickout + needKickoutValidatorCnt := len(needKickoutValidators) + if needKickoutValidatorCnt <= 0 { + return nil + } + sort.Sort(sort.Reverse(needKickoutValidators)) + + candidateCount := 0 + iter := trie.NewIterator(ec.DposContext.CandidateTrie().NodeIterator(nil)) + for iter.Next() { + candidateCount++ + if candidateCount >= needKickoutValidatorCnt+safeSize { + break + } + } + + for i, validator := range needKickoutValidators { + // ensure candidate count greater than or equal to safeSize + if candidateCount <= safeSize { + log.Info("No more candidate can be kickout", "prevEpochID", epoch, "candidateCount", candidateCount, "needKickoutCount", len(needKickoutValidators)-i) + return nil + } + + if err := ec.DposContext.KickoutCandidate(validator.address); err != nil { + return err + } + // if kickout success, candidateCount minus 1 + candidateCount-- + log.Info("Kickout candidate", "prevEpochID", epoch, "candidate", validator.address.String(), "mintCnt", validator.weight.String()) + } + return nil +} + +func (ec *EpochContext) lookupValidator(now int64) (validator common.Address, err error) { + validator = common.Address{} + offset := now % epochInterval + if offset%blockInterval != 0 { + return common.Address{}, ErrInvalidMintBlockTime + } + offset /= blockInterval + + validators, err := ec.DposContext.GetValidators() + if err != nil { + return common.Address{}, err + } + validatorSize := len(validators) + if validatorSize == 0 { + return common.Address{}, errors.New("failed to lookup validator") + } + offset %= int64(validatorSize) + return validators[offset], nil +} + +func (ec *EpochContext) tryElect(genesis, parent *types.Header) error { + genesisEpoch := genesis.Time.Int64() / epochInterval + prevEpoch := parent.Time.Int64() / epochInterval + currentEpoch := ec.TimeStamp / epochInterval + + prevEpochIsGenesis := prevEpoch == genesisEpoch + if prevEpochIsGenesis && prevEpoch < currentEpoch { + prevEpoch = currentEpoch - 1 + } + + prevEpochBytes := make([]byte, 8) + binary.BigEndian.PutUint64(prevEpochBytes, uint64(prevEpoch)) + iter := trie.NewIterator(ec.DposContext.MintCntTrie().PrefixIterator(prevEpochBytes)) + for i := prevEpoch; i < currentEpoch; i++ { + // if prevEpoch is not genesis, kickout not active candidate + if !prevEpochIsGenesis && iter.Next() { + if err := ec.kickoutValidator(prevEpoch); err != nil { + return err + } + } + votes, err := ec.countVotes() + if err != nil { + return err + } + candidates := sortableAddresses{} + for candidate, cnt := range votes { + candidates = append(candidates, &sortableAddress{candidate, cnt}) + } + if len(candidates) < safeSize { + return errors.New("too few candidates") + } + sort.Sort(candidates) + if len(candidates) > maxValidatorSize { + candidates = candidates[:maxValidatorSize] + } + + // shuffle candidates + seed := int64(binary.LittleEndian.Uint32(crypto.Keccak512(parent.Hash().Bytes()))) + i + r := rand.New(rand.NewSource(seed)) + for i := len(candidates) - 1; i > 0; i-- { + j := int(r.Int31n(int32(i + 1))) + candidates[i], candidates[j] = candidates[j], candidates[i] + } + sortedValidators := make([]common.Address, 0) + for _, candidate := range candidates { + sortedValidators = append(sortedValidators, candidate.address) + } + + epochTrie, _ := types.NewEpochTrie(common.Hash{}, ec.DposContext.DB()) + ec.DposContext.SetEpoch(epochTrie) + ec.DposContext.SetValidators(sortedValidators) + log.Info("Come to new epoch", "prevEpoch", i, "nextEpoch", i+1) + } + return nil +} + +type sortableAddress struct { + address common.Address + weight *big.Int +} +type sortableAddresses []*sortableAddress + +func (p sortableAddresses) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p sortableAddresses) Len() int { return len(p) } +func (p sortableAddresses) Less(i, j int) bool { + if p[i].weight.Cmp(p[j].weight) < 0 { + return false + } else if p[i].weight.Cmp(p[j].weight) > 0 { + return true + } else { + return p[i].address.String() < p[j].address.String() + } +} diff --git a/consensus/dpos/epoch_context_test.go b/consensus/dpos/epoch_context_test.go new file mode 100644 index 0000000000..e8a2d2fde7 --- /dev/null +++ b/consensus/dpos/epoch_context_test.go @@ -0,0 +1,359 @@ +package dpos + +import ( + "math/big" + "strconv" + "strings" + "testing" + + "github.com/pavelkrolevets/go-ethereum/common" + "github.com/pavelkrolevets/go-ethereum/core/state" + "github.com/pavelkrolevets/go-ethereum/core/types" + "github.com/pavelkrolevets/go-ethereum/ethdb" + "github.com/pavelkrolevets/go-ethereum/trie" + + "github.com/stretchr/testify/assert" +) + +func TestEpochContextCountVotes(t *testing.T) { + voteMap := map[common.Address][]common.Address{ + common.HexToAddress("0x44d1ce0b7cb3588bca96151fe1bc05af38f91b6e"): { + common.HexToAddress("0xb040353ec0f2c113d5639444f7253681aecda1f8"), + }, + common.HexToAddress("0xa60a3886b552ff9992cfcd208ec1152079e046c2"): { + common.HexToAddress("0x14432e15f21237013017fa6ee90fc99433dec82c"), + common.HexToAddress("0x9f30d0e5c9c88cade54cd1adecf6bc2c7e0e5af6"), + }, + common.HexToAddress("0x4e080e49f62694554871e669aeb4ebe17c4a9670"): { + common.HexToAddress("0xd83b44a3719720ec54cdb9f54c0202de68f1ebcb"), + common.HexToAddress("0x56cc452e450551b7b9cffe25084a069e8c1e9441"), + common.HexToAddress("0xbcfcb3fa8250be4f2bf2b1e70e1da500c668377b"), + }, + common.HexToAddress("0x9d9667c71bb09d6ca7c3ed12bfe5e7be24e2ffe1"): {}, + } + balance := int64(5) + db, _ := ethdb.NewMemDatabase() + stateDB, _ := state.New(common.Hash{}, state.NewDatabase(db)) + dposContext, err := types.NewDposContext(db) + assert.Nil(t, err) + + epochContext := &EpochContext{ + DposContext: dposContext, + statedb: stateDB, + } + _, err = epochContext.countVotes() + assert.NotNil(t, err) + + for candidate, electors := range voteMap { + assert.Nil(t, dposContext.BecomeCandidate(candidate)) + for _, elector := range electors { + stateDB.SetBalance(elector, big.NewInt(balance)) + assert.Nil(t, dposContext.Delegate(elector, candidate)) + } + } + result, err := epochContext.countVotes() + assert.Nil(t, err) + assert.Equal(t, len(voteMap), len(result)) + for candidate, electors := range voteMap { + voteCount, ok := result[candidate] + assert.True(t, ok) + assert.Equal(t, balance*int64(len(electors)), voteCount.Int64()) + } +} + +func TestLookupValidator(t *testing.T) { + db, _ := ethdb.NewMemDatabase() + dposCtx, _ := types.NewDposContext(db) + mockEpochContext := &EpochContext{ + DposContext: dposCtx, + } + validators := []common.Address{ + common.StringToAddress("addr1"), + common.StringToAddress("addr2"), + common.StringToAddress("addr3"), + } + mockEpochContext.DposContext.SetValidators(validators) + for i, expected := range validators { + got, _ := mockEpochContext.lookupValidator(int64(i) * blockInterval) + if got != expected { + t.Errorf("Failed to test lookup validator, %s was expected but got %s", expected.Str(), got.Str()) + } + } + _, err := mockEpochContext.lookupValidator(blockInterval - 1) + if err != ErrInvalidMintBlockTime { + t.Errorf("Failed to test lookup validator. err '%v' was expected but got '%v'", ErrInvalidMintBlockTime, err) + } +} + +func TestEpochContextKickoutValidator(t *testing.T) { + db, _ := ethdb.NewMemDatabase() + stateDB, _ := state.New(common.Hash{}, state.NewDatabase(db)) + dposContext, err := types.NewDposContext(db) + assert.Nil(t, err) + epochContext := &EpochContext{ + TimeStamp: epochInterval, + DposContext: dposContext, + statedb: stateDB, + } + atLeastMintCnt := epochInterval / blockInterval / maxValidatorSize / 2 + testEpoch := int64(1) + + // no validator can be kickout, because all validators mint enough block at least + validators := []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + validator := common.StringToAddress("addr" + strconv.Itoa(i)) + validators = append(validators, validator) + assert.Nil(t, dposContext.BecomeCandidate(validator)) + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt) + } + assert.Nil(t, dposContext.SetValidators(validators)) + assert.Nil(t, dposContext.BecomeCandidate(common.StringToAddress("addr"))) + assert.Nil(t, epochContext.kickoutValidator(testEpoch)) + candidateMap := getCandidates(dposContext.CandidateTrie()) + assert.Equal(t, maxValidatorSize +1, len(candidateMap)) + + // atLeast a safeSize count candidate will reserve + dposContext, err = types.NewDposContext(db) + assert.Nil(t, err) + epochContext = &EpochContext{ + TimeStamp: epochInterval, + DposContext: dposContext, + statedb: stateDB, + } + validators = []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + validator := common.StringToAddress("addr" + strconv.Itoa(i)) + validators = append(validators, validator) + assert.Nil(t, dposContext.BecomeCandidate(validator)) + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt-int64(i)-1) + } + assert.Nil(t, dposContext.SetValidators(validators)) + assert.Nil(t, epochContext.kickoutValidator(testEpoch)) + candidateMap = getCandidates(dposContext.CandidateTrie()) + assert.Equal(t, safeSize, len(candidateMap)) + for i := maxValidatorSize - 1; i >= safeSize; i-- { + assert.False(t, candidateMap[common.StringToAddress("addr"+strconv.Itoa(i))]) + } + + // all validator will be kickout, because all validators didn't mint enough block at least + dposContext, err = types.NewDposContext(db) + assert.Nil(t, err) + epochContext = &EpochContext{ + TimeStamp: epochInterval, + DposContext: dposContext, + statedb: stateDB, + } + validators = []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + validator := common.StringToAddress("addr" + strconv.Itoa(i)) + validators = append(validators, validator) + assert.Nil(t, dposContext.BecomeCandidate(validator)) + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt-1) + } + for i := maxValidatorSize; i < maxValidatorSize *2; i++ { + candidate := common.StringToAddress("addr" + strconv.Itoa(i)) + assert.Nil(t, dposContext.BecomeCandidate(candidate)) + } + assert.Nil(t, dposContext.SetValidators(validators)) + assert.Nil(t, epochContext.kickoutValidator(testEpoch)) + candidateMap = getCandidates(dposContext.CandidateTrie()) + assert.Equal(t, maxValidatorSize, len(candidateMap)) + + // only one validator mint count is not enough + dposContext, err = types.NewDposContext(db) + assert.Nil(t, err) + epochContext = &EpochContext{ + TimeStamp: epochInterval, + DposContext: dposContext, + statedb: stateDB, + } + validators = []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + validator := common.StringToAddress("addr" + strconv.Itoa(i)) + validators = append(validators, validator) + assert.Nil(t, dposContext.BecomeCandidate(validator)) + if i == 0 { + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt-1) + } else { + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt) + } + } + assert.Nil(t, dposContext.BecomeCandidate(common.StringToAddress("addr"))) + assert.Nil(t, dposContext.SetValidators(validators)) + assert.Nil(t, epochContext.kickoutValidator(testEpoch)) + candidateMap = getCandidates(dposContext.CandidateTrie()) + assert.Equal(t, maxValidatorSize, len(candidateMap)) + assert.False(t, candidateMap[common.StringToAddress("addr"+strconv.Itoa(0))]) + + // epochTime is not complete, all validators mint enough block at least + dposContext, err = types.NewDposContext(db) + assert.Nil(t, err) + epochContext = &EpochContext{ + TimeStamp: epochInterval / 2, + DposContext: dposContext, + statedb: stateDB, + } + validators = []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + validator := common.StringToAddress("addr" + strconv.Itoa(i)) + validators = append(validators, validator) + assert.Nil(t, dposContext.BecomeCandidate(validator)) + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt/2) + } + for i := maxValidatorSize; i < maxValidatorSize *2; i++ { + candidate := common.StringToAddress("addr" + strconv.Itoa(i)) + assert.Nil(t, dposContext.BecomeCandidate(candidate)) + } + assert.Nil(t, dposContext.SetValidators(validators)) + assert.Nil(t, epochContext.kickoutValidator(testEpoch)) + candidateMap = getCandidates(dposContext.CandidateTrie()) + assert.Equal(t, maxValidatorSize *2, len(candidateMap)) + + // epochTime is not complete, all validators didn't mint enough block at least + dposContext, err = types.NewDposContext(db) + assert.Nil(t, err) + epochContext = &EpochContext{ + TimeStamp: epochInterval / 2, + DposContext: dposContext, + statedb: stateDB, + } + validators = []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + validator := common.StringToAddress("addr" + strconv.Itoa(i)) + validators = append(validators, validator) + assert.Nil(t, dposContext.BecomeCandidate(validator)) + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt/2-1) + } + for i := maxValidatorSize; i < maxValidatorSize *2; i++ { + candidate := common.StringToAddress("addr" + strconv.Itoa(i)) + assert.Nil(t, dposContext.BecomeCandidate(candidate)) + } + assert.Nil(t, dposContext.SetValidators(validators)) + assert.Nil(t, epochContext.kickoutValidator(testEpoch)) + candidateMap = getCandidates(dposContext.CandidateTrie()) + assert.Equal(t, maxValidatorSize, len(candidateMap)) + + dposContext, err = types.NewDposContext(db) + assert.Nil(t, err) + epochContext = &EpochContext{ + TimeStamp: epochInterval / 2, + DposContext: dposContext, + statedb: stateDB, + } + assert.NotNil(t, epochContext.kickoutValidator(testEpoch)) + dposContext.SetValidators([]common.Address{}) + assert.NotNil(t, epochContext.kickoutValidator(testEpoch)) +} + +func setTestMintCnt(dposContext *types.DposContext, epoch int64, validator common.Address, count int64) { + for i := int64(0); i < count; i++ { + updateMintCnt(epoch*epochInterval, epoch*epochInterval+blockInterval, validator, dposContext) + } +} + +func getCandidates(candidateTrie *trie.Trie) map[common.Address]bool { + candidateMap := map[common.Address]bool{} + iter := trie.NewIterator(candidateTrie.NodeIterator(nil)) + for iter.Next() { + candidateMap[common.BytesToAddress(iter.Value)] = true + } + return candidateMap +} + +func TestEpochContextTryElect(t *testing.T) { + db, _ := ethdb.NewMemDatabase() + stateDB, _ := state.New(common.Hash{}, state.NewDatabase(db)) + dposContext, err := types.NewDposContext(db) + assert.Nil(t, err) + epochContext := &EpochContext{ + TimeStamp: epochInterval, + DposContext: dposContext, + statedb: stateDB, + } + atLeastMintCnt := epochInterval / blockInterval / maxValidatorSize / 2 + testEpoch := int64(1) + validators := []common.Address{} + for i := 0; i < maxValidatorSize; i++ { + validator := common.StringToAddress("addr" + strconv.Itoa(i)) + validators = append(validators, validator) + assert.Nil(t, dposContext.BecomeCandidate(validator)) + assert.Nil(t, dposContext.Delegate(validator, validator)) + stateDB.SetBalance(validator, big.NewInt(1)) + setTestMintCnt(dposContext, testEpoch, validator, atLeastMintCnt-1) + } + dposContext.BecomeCandidate(common.StringToAddress("more")) + assert.Nil(t, dposContext.SetValidators(validators)) + + // genesisEpoch == parentEpoch do not kickout + genesis := &types.Header{ + Time: big.NewInt(0), + } + parent := &types.Header{ + Time: big.NewInt(epochInterval - blockInterval), + } + oldHash := dposContext.EpochTrie().Hash() + assert.Nil(t, epochContext.tryElect(genesis, parent)) + result, err := dposContext.GetValidators() + assert.Nil(t, err) + assert.Equal(t, maxValidatorSize, len(result)) + for _, validator := range result { + assert.True(t, strings.Contains(validator.Str(), "addr")) + } + assert.NotEqual(t, oldHash, dposContext.EpochTrie().Hash()) + + // genesisEpoch != parentEpoch and have none mintCnt do not kickout + genesis = &types.Header{ + Time: big.NewInt(-epochInterval), + } + parent = &types.Header{ + Difficulty: big.NewInt(1), + Time: big.NewInt(epochInterval - blockInterval), + } + epochContext.TimeStamp = epochInterval + oldHash = dposContext.EpochTrie().Hash() + assert.Nil(t, epochContext.tryElect(genesis, parent)) + result, err = dposContext.GetValidators() + assert.Nil(t, err) + assert.Equal(t, maxValidatorSize, len(result)) + for _, validator := range result { + assert.True(t, strings.Contains(validator.Str(), "addr")) + } + assert.NotEqual(t, oldHash, dposContext.EpochTrie().Hash()) + + // genesisEpoch != parentEpoch kickout + genesis = &types.Header{ + Time: big.NewInt(0), + } + parent = &types.Header{ + Time: big.NewInt(epochInterval*2 - blockInterval), + } + epochContext.TimeStamp = epochInterval * 2 + oldHash = dposContext.EpochTrie().Hash() + assert.Nil(t, epochContext.tryElect(genesis, parent)) + result, err = dposContext.GetValidators() + assert.Nil(t, err) + assert.Equal(t, safeSize, len(result)) + moreCnt := 0 + for _, validator := range result { + if strings.Contains(validator.Str(), "more") { + moreCnt++ + } + } + assert.Equal(t, 1, moreCnt) + assert.NotEqual(t, oldHash, dposContext.EpochTrie().Hash()) + + // parentEpoch == currentEpoch do not elect + genesis = &types.Header{ + Time: big.NewInt(0), + } + parent = &types.Header{ + Time: big.NewInt(epochInterval), + } + epochContext.TimeStamp = epochInterval + blockInterval + oldHash = dposContext.EpochTrie().Hash() + assert.Nil(t, epochContext.tryElect(genesis, parent)) + result, err = dposContext.GetValidators() + assert.Nil(t, err) + assert.Equal(t, safeSize, len(result)) + assert.Equal(t, oldHash, dposContext.EpochTrie().Hash()) +}