mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 21:31:37 +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
|
||||
// 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -42,12 +45,12 @@ import (
|
|||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/hashicorp/golang-lru"
|
||||
|
||||
)
|
||||
|
||||
const (
|
||||
inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory
|
||||
M2ByteLength = 4
|
||||
inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory
|
||||
blockSignersCacheLimit = 9000
|
||||
M2ByteLength = 4
|
||||
)
|
||||
|
||||
type Masternode struct {
|
||||
|
|
@ -55,7 +58,7 @@ type Masternode struct {
|
|||
Stake *big.Int
|
||||
}
|
||||
|
||||
// XDPoS XinFin-DPoS protocol constants.
|
||||
// XDPoS delegated-proof-of-stake protocol constants.
|
||||
var (
|
||||
epochLength = uint64(900) // Default number of blocks after which to checkpoint and reset the pending votes
|
||||
|
||||
|
|
@ -146,7 +149,7 @@ var (
|
|||
// backing account.
|
||||
//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
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
type XDPoS struct {
|
||||
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
|
||||
signatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
||||
validatorSignatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
||||
verifiedHeaders *lru.ARCCache
|
||||
rewards *lru.ARCCache
|
||||
proposals map[common.Address]bool // Current list of proposals we are pushing
|
||||
|
||||
signer common.Address // Ethereum address of the signing key
|
||||
signFn clique.SignerFn // Signer function to authorize hashes with
|
||||
lock sync.RWMutex // Protects the signer fields
|
||||
|
||||
HookReward func(chain consensus.ChainReader, state *state.StateDB, header *types.Header) (error, map[string]interface{})
|
||||
HookPenalty func(chain consensus.ChainReader, blockNumberEpoc uint64) ([]common.Address, error)
|
||||
HookValidator func(header *types.Header, signers []common.Address) ([]byte, error)
|
||||
HookVerifyMNs func(header *types.Header, signers []common.Address) error
|
||||
BlockSigners *lru.Cache
|
||||
HookReward func(chain consensus.ChainReader, state *state.StateDB, header *types.Header) (error, map[string]interface{})
|
||||
HookPenalty func(chain consensus.ChainReader, blockNumberEpoc uint64) ([]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.
|
||||
func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS {
|
||||
// Set any missing consensus parameters to their defaults
|
||||
|
|
@ -238,19 +242,19 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS {
|
|||
conf.Epoch = epochLength
|
||||
}
|
||||
// Allocate the snapshot caches and create the engine
|
||||
BlockSigners, _ := lru.New(blockSignersCacheLimit)
|
||||
recents, _ := lru.NewARC(inmemorySnapshots)
|
||||
signatures, _ := lru.NewARC(inmemorySnapshots)
|
||||
validatorSignatures, _ := lru.NewARC(inmemorySnapshots)
|
||||
verifiedHeaders, _ := lru.NewARC(inmemorySnapshots)
|
||||
rewards, _ := lru.NewARC(inmemorySnapshots)
|
||||
return &XDPoS{
|
||||
config: &conf,
|
||||
db: db,
|
||||
BlockSigners: BlockSigners,
|
||||
recents: recents,
|
||||
signatures: signatures,
|
||||
verifiedHeaders: verifiedHeaders,
|
||||
validatorSignatures: validatorSignatures,
|
||||
rewards: rewards,
|
||||
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
|
||||
// a batch of new headers.
|
||||
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 {
|
||||
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 number%c.config.Epoch == 0 {
|
||||
signers := snap.GetSigners()
|
||||
penPenalties := []common.Address{}
|
||||
if c.HookPenalty != nil {
|
||||
penPenalties, err = c.HookPenalty(chain, number)
|
||||
if c.HookPenalty != nil || c.HookPenaltyTIPSigning != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
@ -405,7 +418,6 @@ func (c *XDPoS) verifyCascadingFields(chain consensus.ChainReader, header *types
|
|||
return errInvalidCheckpointPenalties
|
||||
}
|
||||
}
|
||||
signers := snap.GetSigners()
|
||||
signers = common.RemoveItemFromArray(signers, penPenalties)
|
||||
for i := 1; i <= common.LimitPenaltyEpoch; i++ {
|
||||
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) {
|
||||
masternodes := c.GetMasternodes(chain, parent)
|
||||
|
||||
if common.IsTestnet {
|
||||
// Only three mns for XDC testnet.
|
||||
masternodes = masternodes[:3]
|
||||
// Only three mns hard code for XDC testnet.
|
||||
masternodes = []common.Address{
|
||||
common.HexToAddress("0xfFC679Dcdf444D2eEb0491A998E7902B411CcF20"),
|
||||
common.HexToAddress("0xd76fd76F7101811726DCE9E43C2617706a4c45c8"),
|
||||
common.HexToAddress("0x8A97753311aeAFACfd76a68Cf2e2a9808d3e65E8"),
|
||||
}
|
||||
}
|
||||
|
||||
snap, err := c.GetSnapshot(chain, parent)
|
||||
if err != nil {
|
||||
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]
|
||||
masternodes := snap.GetSigners()
|
||||
if number > 0 && number%c.config.Epoch == 0 {
|
||||
if c.HookPenalty != nil {
|
||||
penMasternodes, err := c.HookPenalty(chain, number)
|
||||
if number >= c.config.Epoch && number%c.config.Epoch == 0 {
|
||||
if c.HookPenalty != nil || c.HookPenaltyTIPSigning != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
@ -785,7 +809,7 @@ func (c *XDPoS) Prepare(chain consensus.ChainReader, header *types.Header) error
|
|||
// penalize bad masternode(s)
|
||||
masternodes = common.RemoveItemFromArray(masternodes, 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)
|
||||
}
|
||||
|
|
@ -850,12 +874,22 @@ func (c *XDPoS) Finalize(chain consensus.ChainReader, header *types.Header, stat
|
|||
number := header.Number.Uint64()
|
||||
rCheckpoint := chain.Config().XDPoS.RewardCheckpoint
|
||||
|
||||
// _ = c.CacheData(header, txs, receipts)
|
||||
|
||||
if c.HookReward != nil && number%rCheckpoint == 0 {
|
||||
err, rewardResults := c.HookReward(chain, state, header)
|
||||
err, rewards := c.HookReward(chain, state, header)
|
||||
if err != nil {
|
||||
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
|
||||
|
|
@ -887,7 +921,8 @@ func (c *XDPoS) Seal(chain consensus.ChainReader, block *types.Block, stop <-cha
|
|||
return nil, errUnknownBlock
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// 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 {
|
||||
// Only take into account the non-epoch blocks
|
||||
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")
|
||||
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)
|
||||
<-stop
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -1010,9 +1044,56 @@ func (c *XDPoS) GetMasternodesFromCheckpointHeader(preCheckpointHeader *types.He
|
|||
for i := 0; i < len(masternodes); i++ {
|
||||
copy(masternodes[i][:], preCheckpointHeader.Extra[extraVanity+i*common.AddressLength:])
|
||||
}
|
||||
|
||||
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.
|
||||
func RemovePenaltiesFromBlock(chain consensus.ChainReader, masternodes []common.Address, epochNumber uint64) []common.Address {
|
||||
if epochNumber <= 0 {
|
||||
|
|
@ -1085,15 +1166,3 @@ func Hop(len, pre, cur int) int {
|
|||
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