mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 10:22:23 +00:00
added dpos draft
This commit is contained in:
parent
5baf5b95f7
commit
1c075dd0a1
6 changed files with 1302 additions and 1 deletions
|
|
@ -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"
|
||||
|
|
|
|||
71
consensus/dpos/api.go
Normal file
71
consensus/dpos/api.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
532
consensus/dpos/dpos.go
Normal file
532
consensus/dpos/dpos.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
118
consensus/dpos/dpos_test.go
Normal file
118
consensus/dpos/dpos_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
221
consensus/dpos/epoch_context.go
Normal file
221
consensus/dpos/epoch_context.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
359
consensus/dpos/epoch_context_test.go
Normal file
359
consensus/dpos/epoch_context_test.go
Normal file
|
|
@ -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())
|
||||
}
|
||||
Loading…
Reference in a new issue