mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-16 09:50:45 +00:00
File Updated | XDPoS Consensus updated
This commit is contained in:
parent
153c8ef49d
commit
8e89efe6a9
2 changed files with 111 additions and 446 deletions
|
|
@ -13,15 +13,18 @@
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
// You should have received a copy of the GNU Lesser General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Package XDPoS implements the XinFin-DPoS consensus engine.
|
// Package XDPoS implements the delegated-proof-of-stake consensus engine.
|
||||||
package XDPoS
|
package XDPoS
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"math/big"
|
"math/big"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -42,12 +45,12 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/hashicorp/golang-lru"
|
"github.com/hashicorp/golang-lru"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory
|
inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory
|
||||||
M2ByteLength = 4
|
blockSignersCacheLimit = 9000
|
||||||
|
M2ByteLength = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
type Masternode struct {
|
type Masternode struct {
|
||||||
|
|
@ -55,7 +58,7 @@ type Masternode struct {
|
||||||
Stake *big.Int
|
Stake *big.Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// XDPoS XinFin-DPoS protocol constants.
|
// XDPoS delegated-proof-of-stake protocol constants.
|
||||||
var (
|
var (
|
||||||
epochLength = uint64(900) // Default number of blocks after which to checkpoint and reset the pending votes
|
epochLength = uint64(900) // Default number of blocks after which to checkpoint and reset the pending votes
|
||||||
|
|
||||||
|
|
@ -146,7 +149,7 @@ var (
|
||||||
// backing account.
|
// backing account.
|
||||||
//type SignerFn func(accounts.Account, []byte) ([]byte, error)
|
//type SignerFn func(accounts.Account, []byte) ([]byte, error)
|
||||||
|
|
||||||
// sigHash returns the hash which is used as input for the XinFin-DPoS
|
// sigHash returns the hash which is used as input for the delegated-proof-of-stake
|
||||||
// signing. It is the hash of the entire header apart from the 65 byte signature
|
// signing. It is the hash of the entire header apart from the 65 byte signature
|
||||||
// contained at the end of the extra data.
|
// contained at the end of the extra data.
|
||||||
//
|
//
|
||||||
|
|
@ -206,30 +209,31 @@ func ecrecover(header *types.Header, sigcache *lru.ARCCache) (common.Address, er
|
||||||
return signer, nil
|
return signer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// XDPoS is the XinFin-DPoS consensus engine proposed to support the
|
// XDPoS is the delegated-proof-of-stake consensus engine proposed to support the
|
||||||
// Ethereum testnet following the Ropsten attacks.
|
// Ethereum testnet following the Ropsten attacks.
|
||||||
type XDPoS struct {
|
type XDPoS struct {
|
||||||
config *params.XDPoSConfig // Consensus engine configuration parameters
|
config *params.XDPoSConfig // Consensus engine configuration parameters
|
||||||
db ethdb.Database // Database to store and retrieve snapshot checkpoints
|
db ethdb.Database // Database to store and retrieve snapshot checkpoints
|
||||||
|
|
||||||
recents *lru.ARCCache // Snapshots for recent block to speed up reorgs
|
recents *lru.ARCCache // Snapshots for recent block to speed up reorgs
|
||||||
signatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
signatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
||||||
validatorSignatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
validatorSignatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
||||||
verifiedHeaders *lru.ARCCache
|
verifiedHeaders *lru.ARCCache
|
||||||
rewards *lru.ARCCache
|
|
||||||
proposals map[common.Address]bool // Current list of proposals we are pushing
|
proposals map[common.Address]bool // Current list of proposals we are pushing
|
||||||
|
|
||||||
signer common.Address // Ethereum address of the signing key
|
signer common.Address // Ethereum address of the signing key
|
||||||
signFn clique.SignerFn // Signer function to authorize hashes with
|
signFn clique.SignerFn // Signer function to authorize hashes with
|
||||||
lock sync.RWMutex // Protects the signer fields
|
lock sync.RWMutex // Protects the signer fields
|
||||||
|
|
||||||
HookReward func(chain consensus.ChainReader, state *state.StateDB, header *types.Header) (error, map[string]interface{})
|
BlockSigners *lru.Cache
|
||||||
HookPenalty func(chain consensus.ChainReader, blockNumberEpoc uint64) ([]common.Address, error)
|
HookReward func(chain consensus.ChainReader, state *state.StateDB, header *types.Header) (error, map[string]interface{})
|
||||||
HookValidator func(header *types.Header, signers []common.Address) ([]byte, error)
|
HookPenalty func(chain consensus.ChainReader, blockNumberEpoc uint64) ([]common.Address, error)
|
||||||
HookVerifyMNs func(header *types.Header, signers []common.Address) error
|
HookPenaltyTIPSigning func(chain consensus.ChainReader, header *types.Header, candidate []common.Address) ([]common.Address, error)
|
||||||
|
HookValidator func(header *types.Header, signers []common.Address) ([]byte, error)
|
||||||
|
HookVerifyMNs func(header *types.Header, signers []common.Address) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a XDPoS XinFin-DPoS consensus engine with the initial
|
// New creates a XDPoS delegated-proof-of-stake consensus engine with the initial
|
||||||
// signers set to the ones provided by the user.
|
// signers set to the ones provided by the user.
|
||||||
func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS {
|
func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS {
|
||||||
// Set any missing consensus parameters to their defaults
|
// Set any missing consensus parameters to their defaults
|
||||||
|
|
@ -238,19 +242,19 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS {
|
||||||
conf.Epoch = epochLength
|
conf.Epoch = epochLength
|
||||||
}
|
}
|
||||||
// Allocate the snapshot caches and create the engine
|
// Allocate the snapshot caches and create the engine
|
||||||
|
BlockSigners, _ := lru.New(blockSignersCacheLimit)
|
||||||
recents, _ := lru.NewARC(inmemorySnapshots)
|
recents, _ := lru.NewARC(inmemorySnapshots)
|
||||||
signatures, _ := lru.NewARC(inmemorySnapshots)
|
signatures, _ := lru.NewARC(inmemorySnapshots)
|
||||||
validatorSignatures, _ := lru.NewARC(inmemorySnapshots)
|
validatorSignatures, _ := lru.NewARC(inmemorySnapshots)
|
||||||
verifiedHeaders, _ := lru.NewARC(inmemorySnapshots)
|
verifiedHeaders, _ := lru.NewARC(inmemorySnapshots)
|
||||||
rewards, _ := lru.NewARC(inmemorySnapshots)
|
|
||||||
return &XDPoS{
|
return &XDPoS{
|
||||||
config: &conf,
|
config: &conf,
|
||||||
db: db,
|
db: db,
|
||||||
|
BlockSigners: BlockSigners,
|
||||||
recents: recents,
|
recents: recents,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
verifiedHeaders: verifiedHeaders,
|
verifiedHeaders: verifiedHeaders,
|
||||||
validatorSignatures: validatorSignatures,
|
validatorSignatures: validatorSignatures,
|
||||||
rewards: rewards,
|
|
||||||
proposals: make(map[common.Address]bool),
|
proposals: make(map[common.Address]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -304,6 +308,9 @@ func (c *XDPoS) verifyHeaderWithCache(chain consensus.ChainReader, header *types
|
||||||
// looking those up from the database. This is useful for concurrently verifying
|
// looking those up from the database. This is useful for concurrently verifying
|
||||||
// a batch of new headers.
|
// a batch of new headers.
|
||||||
func (c *XDPoS) verifyHeader(chain consensus.ChainReader, header *types.Header, parents []*types.Header, fullVerify bool) error {
|
func (c *XDPoS) verifyHeader(chain consensus.ChainReader, header *types.Header, parents []*types.Header, fullVerify bool) error {
|
||||||
|
if common.IsTestnet {
|
||||||
|
fullVerify = false
|
||||||
|
}
|
||||||
if header.Number == nil {
|
if header.Number == nil {
|
||||||
return errUnknownBlock
|
return errUnknownBlock
|
||||||
}
|
}
|
||||||
|
|
@ -391,9 +398,15 @@ func (c *XDPoS) verifyCascadingFields(chain consensus.ChainReader, header *types
|
||||||
}
|
}
|
||||||
// If the block is a checkpoint block, verify the signer list
|
// If the block is a checkpoint block, verify the signer list
|
||||||
if number%c.config.Epoch == 0 {
|
if number%c.config.Epoch == 0 {
|
||||||
|
signers := snap.GetSigners()
|
||||||
penPenalties := []common.Address{}
|
penPenalties := []common.Address{}
|
||||||
if c.HookPenalty != nil {
|
if c.HookPenalty != nil || c.HookPenaltyTIPSigning != nil {
|
||||||
penPenalties, err = c.HookPenalty(chain, number)
|
var err error = nil
|
||||||
|
if chain.Config().IsTIPSigning(header.Number) {
|
||||||
|
penPenalties, err = c.HookPenaltyTIPSigning(chain, header, signers)
|
||||||
|
} else {
|
||||||
|
penPenalties, err = c.HookPenalty(chain, number)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +418,6 @@ func (c *XDPoS) verifyCascadingFields(chain consensus.ChainReader, header *types
|
||||||
return errInvalidCheckpointPenalties
|
return errInvalidCheckpointPenalties
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
signers := snap.GetSigners()
|
|
||||||
signers = common.RemoveItemFromArray(signers, penPenalties)
|
signers = common.RemoveItemFromArray(signers, penPenalties)
|
||||||
for i := 1; i <= common.LimitPenaltyEpoch; i++ {
|
for i := 1; i <= common.LimitPenaltyEpoch; i++ {
|
||||||
if number > uint64(i)*c.config.Epoch {
|
if number > uint64(i)*c.config.Epoch {
|
||||||
|
|
@ -480,10 +492,16 @@ func whoIsCreator(snap *Snapshot, header *types.Header) (common.Address, error)
|
||||||
|
|
||||||
func (c *XDPoS) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (int, int, int, bool, error) {
|
func (c *XDPoS) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (int, int, int, bool, error) {
|
||||||
masternodes := c.GetMasternodes(chain, parent)
|
masternodes := c.GetMasternodes(chain, parent)
|
||||||
|
|
||||||
if common.IsTestnet {
|
if common.IsTestnet {
|
||||||
// Only three mns for XDC testnet.
|
// Only three mns hard code for XDC testnet.
|
||||||
masternodes = masternodes[:3]
|
masternodes = []common.Address{
|
||||||
|
common.HexToAddress("0xfFC679Dcdf444D2eEb0491A998E7902B411CcF20"),
|
||||||
|
common.HexToAddress("0xd76fd76F7101811726DCE9E43C2617706a4c45c8"),
|
||||||
|
common.HexToAddress("0x8A97753311aeAFACfd76a68Cf2e2a9808d3e65E8"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
snap, err := c.GetSnapshot(chain, parent)
|
snap, err := c.GetSnapshot(chain, parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed when trying to commit new work", "err", err)
|
log.Warn("Failed when trying to commit new work", "err", err)
|
||||||
|
|
@ -775,9 +793,15 @@ func (c *XDPoS) Prepare(chain consensus.ChainReader, header *types.Header) error
|
||||||
}
|
}
|
||||||
header.Extra = header.Extra[:extraVanity]
|
header.Extra = header.Extra[:extraVanity]
|
||||||
masternodes := snap.GetSigners()
|
masternodes := snap.GetSigners()
|
||||||
if number > 0 && number%c.config.Epoch == 0 {
|
if number >= c.config.Epoch && number%c.config.Epoch == 0 {
|
||||||
if c.HookPenalty != nil {
|
if c.HookPenalty != nil || c.HookPenaltyTIPSigning != nil {
|
||||||
penMasternodes, err := c.HookPenalty(chain, number)
|
var penMasternodes []common.Address = nil
|
||||||
|
var err error = nil
|
||||||
|
if chain.Config().IsTIPSigning(header.Number) {
|
||||||
|
penMasternodes, err = c.HookPenaltyTIPSigning(chain, header, masternodes)
|
||||||
|
} else {
|
||||||
|
penMasternodes, err = c.HookPenalty(chain, number)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -785,7 +809,7 @@ func (c *XDPoS) Prepare(chain consensus.ChainReader, header *types.Header) error
|
||||||
// penalize bad masternode(s)
|
// penalize bad masternode(s)
|
||||||
masternodes = common.RemoveItemFromArray(masternodes, penMasternodes)
|
masternodes = common.RemoveItemFromArray(masternodes, penMasternodes)
|
||||||
for _, address := range penMasternodes {
|
for _, address := range penMasternodes {
|
||||||
log.Debug("Penalty status", "address", address, "block number", number)
|
log.Debug("Penalty status", "address", address, "number", number)
|
||||||
}
|
}
|
||||||
header.Penalties = common.ExtractAddressToBytes(penMasternodes)
|
header.Penalties = common.ExtractAddressToBytes(penMasternodes)
|
||||||
}
|
}
|
||||||
|
|
@ -850,12 +874,22 @@ func (c *XDPoS) Finalize(chain consensus.ChainReader, header *types.Header, stat
|
||||||
number := header.Number.Uint64()
|
number := header.Number.Uint64()
|
||||||
rCheckpoint := chain.Config().XDPoS.RewardCheckpoint
|
rCheckpoint := chain.Config().XDPoS.RewardCheckpoint
|
||||||
|
|
||||||
|
// _ = c.CacheData(header, txs, receipts)
|
||||||
|
|
||||||
if c.HookReward != nil && number%rCheckpoint == 0 {
|
if c.HookReward != nil && number%rCheckpoint == 0 {
|
||||||
err, rewardResults := c.HookReward(chain, state, header)
|
err, rewards := c.HookReward(chain, state, header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.rewards.Add(header.Hash(), rewardResults)
|
if len(common.StoreRewardFolder) > 0 {
|
||||||
|
data, err := json.Marshal(rewards)
|
||||||
|
if err == nil {
|
||||||
|
err = ioutil.WriteFile(filepath.Join(common.StoreRewardFolder, header.Number.String()+"."+header.Hash().Hex()), data, 0644)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error when save reward info ", "number", header.Number, "hash", header.Hash().Hex(), "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the state remains as is and uncles are dropped
|
// the state remains as is and uncles are dropped
|
||||||
|
|
@ -887,7 +921,8 @@ func (c *XDPoS) Seal(chain consensus.ChainReader, block *types.Block, stop <-cha
|
||||||
return nil, errUnknownBlock
|
return nil, errUnknownBlock
|
||||||
}
|
}
|
||||||
// For 0-period chains, refuse to seal empty blocks (no reward but would spin sealing)
|
// For 0-period chains, refuse to seal empty blocks (no reward but would spin sealing)
|
||||||
if c.config.Period == 0 && len(block.Transactions()) == 0 {
|
// checkpoint blocks have no tx
|
||||||
|
if c.config.Period == 0 && len(block.Transactions()) == 0 && number%c.config.Epoch != 0 {
|
||||||
return nil, errWaitTransactions
|
return nil, errWaitTransactions
|
||||||
}
|
}
|
||||||
// Don't hold the signer fields for the entire sealing procedure
|
// Don't hold the signer fields for the entire sealing procedure
|
||||||
|
|
@ -923,8 +958,7 @@ func (c *XDPoS) Seal(chain consensus.ChainReader, block *types.Block, stop <-cha
|
||||||
if limit := uint64(2); number < limit || seen > number-limit {
|
if limit := uint64(2); number < limit || seen > number-limit {
|
||||||
// Only take into account the non-epoch blocks
|
// Only take into account the non-epoch blocks
|
||||||
if number%c.config.Epoch != 0 {
|
if number%c.config.Epoch != 0 {
|
||||||
log.Info("Length of MasterNodes", "len(masternodes)", len(masternodes), "number", number, "limit", limit, "seen", seen, "recent", recent.String(), "snap.Recents", snap.Recents)
|
log.Info("Signed recently, must wait for others ", "len(masternodes)", len(masternodes), "number", number, "limit", limit, "seen", seen, "recent", recent.String(), "snap.Recents", snap.Recents)
|
||||||
log.Info("Signed recently, must wait for others")
|
|
||||||
<-stop
|
<-stop
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -1010,9 +1044,56 @@ func (c *XDPoS) GetMasternodesFromCheckpointHeader(preCheckpointHeader *types.He
|
||||||
for i := 0; i < len(masternodes); i++ {
|
for i := 0; i < len(masternodes); i++ {
|
||||||
copy(masternodes[i][:], preCheckpointHeader.Extra[extraVanity+i*common.AddressLength:])
|
copy(masternodes[i][:], preCheckpointHeader.Extra[extraVanity+i*common.AddressLength:])
|
||||||
}
|
}
|
||||||
|
|
||||||
return masternodes
|
return masternodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *XDPoS) CacheData(header *types.Header, txs []*types.Transaction, receipts []*types.Receipt) []*types.Transaction {
|
||||||
|
signTxs := []*types.Transaction{}
|
||||||
|
for _, tx := range txs {
|
||||||
|
if tx.IsSigningTransaction() {
|
||||||
|
var b uint
|
||||||
|
for _, r := range receipts {
|
||||||
|
if r.TxHash == tx.Hash() {
|
||||||
|
if len(r.PostState) > 0 {
|
||||||
|
b = types.ReceiptStatusSuccessful
|
||||||
|
} else {
|
||||||
|
b = r.Status
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == types.ReceiptStatusFailed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
signTxs = append(signTxs, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Save tx signers to cache", "hash", header.Hash().String(), "number", header.Number, "len(txs)", len(signTxs))
|
||||||
|
c.BlockSigners.Add(header.Hash(), signTxs)
|
||||||
|
|
||||||
|
return signTxs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XDPoS) CacheSigner(hash common.Hash, txs []*types.Transaction) []*types.Transaction {
|
||||||
|
signTxs := []*types.Transaction{}
|
||||||
|
for _, tx := range txs {
|
||||||
|
if tx.IsSigningTransaction() {
|
||||||
|
signTxs = append(signTxs, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debug("Save tx signers to cache", "hash", hash.String(), "len(txs)", len(signTxs))
|
||||||
|
c.BlockSigners.Add(hash, signTxs)
|
||||||
|
return signTxs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XDPoS) GetDb() ethdb.Database {
|
||||||
|
return c.db
|
||||||
|
}
|
||||||
|
|
||||||
// Extract validators from byte array.
|
// Extract validators from byte array.
|
||||||
func RemovePenaltiesFromBlock(chain consensus.ChainReader, masternodes []common.Address, epochNumber uint64) []common.Address {
|
func RemovePenaltiesFromBlock(chain consensus.ChainReader, masternodes []common.Address, epochNumber uint64) []common.Address {
|
||||||
if epochNumber <= 0 {
|
if epochNumber <= 0 {
|
||||||
|
|
@ -1085,15 +1166,3 @@ func Hop(len, pre, cur int) int {
|
||||||
return len - 1
|
return len - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XDPoS) GetRewards(hash common.Hash) map[string]interface{} {
|
|
||||||
rewards, ok := c.rewards.Get(hash)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return rewards.(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *XDPoS) InsertRewards(hash common.Hash, rewards map[string]interface{}) {
|
|
||||||
c.rewards.Add(hash, rewards)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,404 +0,0 @@
|
||||||
// Copyright (c) 2018 Xinfin
|
|
||||||
//
|
|
||||||
// This program 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.
|
|
||||||
//
|
|
||||||
// This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package XDPoS
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testerVote struct {
|
|
||||||
signer string
|
|
||||||
voted string
|
|
||||||
auth bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// testerAccountPool is a pool to maintain currently active tester accounts,
|
|
||||||
// mapped from textual names used in the tests below to actual Ethereum private
|
|
||||||
// keys capable of signing transactions.
|
|
||||||
type testerAccountPool struct {
|
|
||||||
accounts map[string]*ecdsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTesterAccountPool() *testerAccountPool {
|
|
||||||
return &testerAccountPool{
|
|
||||||
accounts: make(map[string]*ecdsa.PrivateKey),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap *testerAccountPool) sign(header *types.Header, signer string) {
|
|
||||||
// Ensure we have a persistent key for the signer
|
|
||||||
if ap.accounts[signer] == nil {
|
|
||||||
ap.accounts[signer], _ = crypto.GenerateKey()
|
|
||||||
}
|
|
||||||
// Sign the header and embed the signature in extra data
|
|
||||||
sig, _ := crypto.Sign(sigHash(header).Bytes(), ap.accounts[signer])
|
|
||||||
copy(header.Extra[len(header.Extra)-65:], sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ap *testerAccountPool) address(account string) common.Address {
|
|
||||||
// Ensure we have a persistent key for the account
|
|
||||||
if ap.accounts[account] == nil {
|
|
||||||
ap.accounts[account], _ = crypto.GenerateKey()
|
|
||||||
}
|
|
||||||
// Resolve and return the Ethereum address
|
|
||||||
return crypto.PubkeyToAddress(ap.accounts[account].PublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testerChainReader implements consensus.ChainReader to access the genesis
|
|
||||||
// block. All other methods and requests will panic.
|
|
||||||
type testerChainReader struct {
|
|
||||||
db ethdb.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testerChainReader) Config() *params.ChainConfig { return params.AllXDPoSProtocolChanges }
|
|
||||||
func (r *testerChainReader) CurrentHeader() *types.Header { panic("not supported") }
|
|
||||||
func (r *testerChainReader) GetHeader(common.Hash, uint64) *types.Header { panic("not supported") }
|
|
||||||
func (r *testerChainReader) GetBlock(common.Hash, uint64) *types.Block { panic("not supported") }
|
|
||||||
func (r *testerChainReader) GetHeaderByHash(common.Hash) *types.Header { panic("not supported") }
|
|
||||||
func (r *testerChainReader) GetHeaderByNumber(number uint64) *types.Header {
|
|
||||||
if number == 0 {
|
|
||||||
return core.GetHeader(r.db, core.GetCanonicalHash(r.db, 0), 0)
|
|
||||||
}
|
|
||||||
panic("not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that voting is evaluated correctly for various simple and complex scenarios.
|
|
||||||
func TestVoting(t *testing.T) {
|
|
||||||
// Define the various voting scenarios to test
|
|
||||||
tests := []struct {
|
|
||||||
epoch uint64
|
|
||||||
signers []string
|
|
||||||
votes []testerVote
|
|
||||||
results []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
// Single signer, no votes cast
|
|
||||||
signers: []string{"A"},
|
|
||||||
votes: []testerVote{{signer: "A"}},
|
|
||||||
results: []string{"A"},
|
|
||||||
}, {
|
|
||||||
// Single signer, voting to add two others (only accept first, second needs 2 votes)
|
|
||||||
signers: []string{"A"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "B", auth: true},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A", voted: "C", auth: true},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Two signers, voting to add three others (only accept first two, third needs 3 votes already)
|
|
||||||
signers: []string{"A", "B"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: true},
|
|
||||||
{signer: "B", voted: "C", auth: true},
|
|
||||||
{signer: "A", voted: "D", auth: true},
|
|
||||||
{signer: "B", voted: "D", auth: true},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A", voted: "E", auth: true},
|
|
||||||
{signer: "B", voted: "E", auth: true},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B", "C", "D"},
|
|
||||||
}, {
|
|
||||||
// Single signer, dropping itself (weird, but one less cornercase by explicitly allowing this)
|
|
||||||
signers: []string{"A"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "A", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{},
|
|
||||||
}, {
|
|
||||||
// Two signers, actually needing mutual consent to drop either of them (not fulfilled)
|
|
||||||
signers: []string{"A", "B"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "B", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Two signers, actually needing mutual consent to drop either of them (fulfilled)
|
|
||||||
signers: []string{"A", "B"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "B", auth: false},
|
|
||||||
{signer: "B", voted: "B", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A"},
|
|
||||||
}, {
|
|
||||||
// Three signers, two of them deciding to drop the third
|
|
||||||
signers: []string{"A", "B", "C"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Four signers, consensus of two not being enough to drop anyone
|
|
||||||
signers: []string{"A", "B", "C", "D"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B", "C", "D"},
|
|
||||||
}, {
|
|
||||||
// Four signers, consensus of three already being enough to drop someone
|
|
||||||
signers: []string{"A", "B", "C", "D"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "D", auth: false},
|
|
||||||
{signer: "B", voted: "D", auth: false},
|
|
||||||
{signer: "C", voted: "D", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B", "C"},
|
|
||||||
}, {
|
|
||||||
// Authorizations are counted once per signer per target
|
|
||||||
signers: []string{"A", "B"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: true},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A", voted: "C", auth: true},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A", voted: "C", auth: true},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Authorizing multiple accounts concurrently is permitted
|
|
||||||
signers: []string{"A", "B"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: true},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A", voted: "D", auth: true},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "D", auth: true},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "C", auth: true},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B", "C", "D"},
|
|
||||||
}, {
|
|
||||||
// Deauthorizations are counted once per signer per target
|
|
||||||
signers: []string{"A", "B"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "B", auth: false},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A", voted: "B", auth: false},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A", voted: "B", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Deauthorizing multiple accounts concurrently is permitted
|
|
||||||
signers: []string{"A", "B", "C", "D"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A", voted: "D", auth: false},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "D", auth: false},
|
|
||||||
{signer: "C", voted: "D", auth: false},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Votes from deauthorized signers are discarded immediately (deauth votes)
|
|
||||||
signers: []string{"A", "B", "C"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "C", voted: "B", auth: false},
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
{signer: "A", voted: "B", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Votes from deauthorized signers are discarded immediately (auth votes)
|
|
||||||
signers: []string{"A", "B", "C"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "C", voted: "B", auth: false},
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
{signer: "A", voted: "B", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Cascading changes are not allowed, only the account being voted on may change
|
|
||||||
signers: []string{"A", "B", "C", "D"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A", voted: "D", auth: false},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "D", auth: false},
|
|
||||||
{signer: "C", voted: "D", auth: false},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B", "C"},
|
|
||||||
}, {
|
|
||||||
// Changes reaching consensus out of bounds (via a deauth) execute on touch
|
|
||||||
signers: []string{"A", "B", "C", "D"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A", voted: "D", auth: false},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "D", auth: false},
|
|
||||||
{signer: "C", voted: "D", auth: false},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "C", voted: "C", auth: true},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
}, {
|
|
||||||
// Changes reaching consensus out of bounds (via a deauth) may go out of consensus on first touch
|
|
||||||
signers: []string{"A", "B", "C", "D"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: false},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A", voted: "D", auth: false},
|
|
||||||
{signer: "B", voted: "C", auth: false},
|
|
||||||
{signer: "C"},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "D", auth: false},
|
|
||||||
{signer: "C", voted: "D", auth: false},
|
|
||||||
{signer: "A"},
|
|
||||||
{signer: "B", voted: "C", auth: true},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B", "C"},
|
|
||||||
}, {
|
|
||||||
// Ensure that pending votes don't survive authorization status changes. This
|
|
||||||
// corner case can only appear if a signer is quickly added, removed and then
|
|
||||||
// readded (or the inverse), while one of the original voters dropped. If a
|
|
||||||
// past vote is left cached in the system somewhere, this will interfere with
|
|
||||||
// the final signer outcome.
|
|
||||||
signers: []string{"A", "B", "C", "D", "E"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "F", auth: true}, // Authorize F, 3 votes needed
|
|
||||||
{signer: "B", voted: "F", auth: true},
|
|
||||||
{signer: "C", voted: "F", auth: true},
|
|
||||||
{signer: "D", voted: "F", auth: false}, // Deauthorize F, 4 votes needed (leave A's previous vote "unchanged")
|
|
||||||
{signer: "E", voted: "F", auth: false},
|
|
||||||
{signer: "B", voted: "F", auth: false},
|
|
||||||
{signer: "C", voted: "F", auth: false},
|
|
||||||
{signer: "D", voted: "F", auth: true}, // Almost authorize F, 2/3 votes needed
|
|
||||||
{signer: "E", voted: "F", auth: true},
|
|
||||||
{signer: "B", voted: "A", auth: false}, // Deauthorize A, 3 votes needed
|
|
||||||
{signer: "C", voted: "A", auth: false},
|
|
||||||
{signer: "D", voted: "A", auth: false},
|
|
||||||
{signer: "B", voted: "F", auth: true}, // Finish authorizing F, 3/3 votes needed
|
|
||||||
},
|
|
||||||
results: []string{"B", "C", "D", "E", "F"},
|
|
||||||
}, {
|
|
||||||
// Epoch transitions reset all votes to allow chain checkpointing
|
|
||||||
epoch: 3,
|
|
||||||
signers: []string{"A", "B"},
|
|
||||||
votes: []testerVote{
|
|
||||||
{signer: "A", voted: "C", auth: true},
|
|
||||||
{signer: "B"},
|
|
||||||
{signer: "A"}, // Checkpoint block, (don't vote here, it's validated outside of snapshots)
|
|
||||||
{signer: "B", voted: "C", auth: true},
|
|
||||||
},
|
|
||||||
results: []string{"A", "B"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Run through the scenarios and test them
|
|
||||||
for i, tt := range tests {
|
|
||||||
// Create the account pool and generate the initial set of signers
|
|
||||||
accounts := newTesterAccountPool()
|
|
||||||
|
|
||||||
signers := make([]common.Address, len(tt.signers))
|
|
||||||
for j, signer := range tt.signers {
|
|
||||||
signers[j] = accounts.address(signer)
|
|
||||||
}
|
|
||||||
for j := 0; j < len(signers); j++ {
|
|
||||||
for k := j + 1; k < len(signers); k++ {
|
|
||||||
if bytes.Compare(signers[j][:], signers[k][:]) > 0 {
|
|
||||||
signers[j], signers[k] = signers[k], signers[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Create the genesis block with the initial set of signers
|
|
||||||
genesis := &core.Genesis{
|
|
||||||
ExtraData: make([]byte, extraVanity+common.AddressLength*len(signers)+extraSeal),
|
|
||||||
}
|
|
||||||
for j, signer := range signers {
|
|
||||||
copy(genesis.ExtraData[extraVanity+j*common.AddressLength:], signer[:])
|
|
||||||
}
|
|
||||||
// Create a pristine blockchain with the genesis injected
|
|
||||||
db, _ := ethdb.NewMemDatabase()
|
|
||||||
genesis.Commit(db)
|
|
||||||
|
|
||||||
// Assemble a chain of headers from the cast votes
|
|
||||||
headers := make([]*types.Header, len(tt.votes))
|
|
||||||
for j, vote := range tt.votes {
|
|
||||||
headers[j] = &types.Header{
|
|
||||||
Number: big.NewInt(int64(j) + 1),
|
|
||||||
Time: big.NewInt(int64(j) * int64(blockPeriod)),
|
|
||||||
Coinbase: accounts.address(vote.voted),
|
|
||||||
Extra: make([]byte, extraVanity+extraSeal),
|
|
||||||
}
|
|
||||||
if j > 0 {
|
|
||||||
headers[j].ParentHash = headers[j-1].Hash()
|
|
||||||
}
|
|
||||||
if vote.auth {
|
|
||||||
copy(headers[j].Nonce[:], nonceAuthVote)
|
|
||||||
}
|
|
||||||
accounts.sign(headers[j], vote.signer)
|
|
||||||
}
|
|
||||||
// Pass all the headers through XDPoS and ensure tallying succeeds
|
|
||||||
head := headers[len(headers)-1]
|
|
||||||
|
|
||||||
snap, err := New(¶ms.XDPoSConfig{Epoch: tt.epoch}, db).snapshot(&testerChainReader{db: db}, head.Number.Uint64(), head.Hash(), headers)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d: failed to create voting snapshot: %v", i, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Verify the final list of signers against the expected ones
|
|
||||||
signers = make([]common.Address, len(tt.results))
|
|
||||||
for j, signer := range tt.results {
|
|
||||||
signers[j] = accounts.address(signer)
|
|
||||||
}
|
|
||||||
for j := 0; j < len(signers); j++ {
|
|
||||||
for k := j + 1; k < len(signers); k++ {
|
|
||||||
if bytes.Compare(signers[j][:], signers[k][:]) > 0 {
|
|
||||||
signers[j], signers[k] = signers[k], signers[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result := snap.signers()
|
|
||||||
if len(result) != len(signers) {
|
|
||||||
t.Errorf("test %d: signers mismatch: have %x, want %x", i, result, signers)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for j := 0; j < len(result); j++ {
|
|
||||||
if !bytes.Equal(result[j][:], signers[j][:]) {
|
|
||||||
t.Errorf("test %d, signer %d: signer mismatch: have %x, want %x", i, j, result[j], signers[j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue