From 194a94bdc52cb00e0d1070ae812e54d2c387c93f Mon Sep 17 00:00:00 2001 From: anilchinchawale Date: Wed, 28 Jan 2026 20:41:28 +0100 Subject: [PATCH] feat: Add XDPoS consensus engine for XDC Network Port of XDPoS consensus engine from XDC Network's XDPoSChain to modern go-ethereum 1.17.x. Based on PR #1 reference which shows the complete XDPoS implementation against geth 1.8.x. ## Core Consensus Engine (consensus/XDPoS/) ### xdpos.go - Main Engine (~1000 lines) - Full consensus.Engine interface for geth 1.17.x - DPoS masternode rotation with epoch-based cycling - Block sealing with turn-based difficulty adjustment - Hook system for XDC-specific logic: * HookReward - Block reward distribution * HookPenalty - Masternode penalty calculation * HookValidator - Double validation * HookVerifyMNs - Masternode verification - Block signer transaction caching - Recent signer spam protection ### snapshot.go - Validator State Management (~300 lines) - Authorization snapshots at checkpoints - Vote tracking for masternode changes - Persistent storage to database ### api.go - RPC API Endpoints (~150 lines) - xdpos_getSnapshot, xdpos_getSigners, xdpos_getMasternodes - xdpos_propose, xdpos_discard, xdpos_proposals ### constants.go - XDC Network Constants (~60 lines) - Reward percentages, epoch configuration - Masternode limits, penalty configuration ## Configuration (params/config.go) Added XDPoSConfig struct and XDPoS field to ChainConfig ## Account Signing (accounts/) - Added MimetypeXDPoS constant - Added XDPoS signature handling in external signer ## Key Adaptations for geth 1.17.x - Finalize() and FinalizeAndAssemble() separated - VerifyHeader() signature simplified - Seal() uses channel-based results - Modern LRU cache with generics ## Known Limitations / TODOs - XDC-specific header fields (Validators, Penalties) not in types.Header - Smart contracts not included - XDPoS V2 BFT not ported - cmd/XDC CLI not created Reference: https://github.com/XinFinOrg/XDPoSChain (stable101) --- accounts/accounts.go | 1 + accounts/external/backend.go | 6 +- consensus/XDPoS/api.go | 143 +++++ consensus/XDPoS/constants.go | 75 +++ consensus/XDPoS/snapshot.go | 272 ++++++++ consensus/XDPoS/xdpos.go | 1173 ++++++++++++++++++++++++++++++++++ params/config.go | 16 + 7 files changed, 1683 insertions(+), 3 deletions(-) create mode 100644 consensus/XDPoS/api.go create mode 100644 consensus/XDPoS/constants.go create mode 100644 consensus/XDPoS/snapshot.go create mode 100644 consensus/XDPoS/xdpos.go diff --git a/accounts/accounts.go b/accounts/accounts.go index 7bd911577a..eddb96ebf1 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -39,6 +39,7 @@ const ( MimetypeDataWithValidator = "data/validator" MimetypeTypedData = "data/typed" MimetypeClique = "application/x-clique-header" + MimetypeXDPoS = "application/x-xdpos-header" MimetypeTextPlain = "text/plain" ) diff --git a/accounts/external/backend.go b/accounts/external/backend.go index 42eaf661cc..7a1c5eb1fd 100644 --- a/accounts/external/backend.go +++ b/accounts/external/backend.go @@ -163,9 +163,9 @@ func (api *ExternalSigner) SignData(account accounts.Account, mimeType string, d hexutil.Encode(data)); err != nil { return nil, err } - // If V is on 27/28-form, convert to 0/1 for Clique - if mimeType == accounts.MimetypeClique && (res[64] == 27 || res[64] == 28) { - res[64] -= 27 // Transform V from 27/28 to 0/1 for Clique use + // If V is on 27/28-form, convert to 0/1 for Clique and XDPoS + if (mimeType == accounts.MimetypeClique || mimeType == accounts.MimetypeXDPoS) && (res[64] == 27 || res[64] == 28) { + res[64] -= 27 // Transform V from 27/28 to 0/1 for Clique/XDPoS use } return res, nil } diff --git a/consensus/XDPoS/api.go b/consensus/XDPoS/api.go new file mode 100644 index 0000000000..9985ec980d --- /dev/null +++ b/consensus/XDPoS/api.go @@ -0,0 +1,143 @@ +// Copyright (c) 2018 XDCchain +// Copyright 2024 The go-ethereum Authors +// +// 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 . + +package XDPoS + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +// API is a user facing RPC API to allow controlling the signer and voting +// mechanisms of the XDPoS consensus engine. +type API struct { + chain consensus.ChainHeaderReader + xdpos *XDPoS +} + +// GetSnapshot retrieves the state snapshot at a given block. +func (api *API) GetSnapshot(number *rpc.BlockNumber) (*Snapshot, 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 + } + return api.xdpos.GetSnapshot(api.chain, header) +} + +// GetSnapshotAtHash retrieves the state snapshot at a given block hash. +func (api *API) GetSnapshotAtHash(hash common.Hash) (*Snapshot, error) { + header := api.chain.GetHeaderByHash(hash) + if header == nil { + return nil, errUnknownBlock + } + return api.xdpos.GetSnapshot(api.chain, header) +} + +// GetSigners retrieves the list of authorized signers at the specified block. +func (api *API) GetSigners(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 + } + snap, err := api.xdpos.GetSnapshot(api.chain, header) + if err != nil { + return nil, err + } + return snap.GetSigners(), nil +} + +// GetSignersAtHash retrieves the list of authorized signers at the specified block hash. +func (api *API) GetSignersAtHash(hash common.Hash) ([]common.Address, error) { + header := api.chain.GetHeaderByHash(hash) + if header == nil { + return nil, errUnknownBlock + } + snap, err := api.xdpos.GetSnapshot(api.chain, header) + if err != nil { + return nil, err + } + return snap.GetSigners(), nil +} + +// GetMasternodes retrieves the list of masternodes at the specified block. +func (api *API) GetMasternodes(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 + } + return api.xdpos.GetMasternodes(api.chain, header), nil +} + +// Proposals returns the current proposals the node tries to uphold and vote on. +func (api *API) Proposals() map[common.Address]bool { + api.xdpos.lock.RLock() + defer api.xdpos.lock.RUnlock() + + proposals := make(map[common.Address]bool) + for address, auth := range api.xdpos.proposals { + proposals[address] = auth + } + return proposals +} + +// Propose injects a new authorization proposal that the signer will attempt to push through. +func (api *API) Propose(address common.Address, auth bool) { + api.xdpos.lock.Lock() + defer api.xdpos.lock.Unlock() + api.xdpos.proposals[address] = auth +} + +// Discard drops a currently running proposal. +func (api *API) Discard(address common.Address) { + api.xdpos.lock.Lock() + defer api.xdpos.lock.Unlock() + delete(api.xdpos.proposals, address) +} + +// GetCandidates returns the current candidates +func (api *API) GetCandidates(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 + } + snap, err := api.xdpos.GetSnapshot(api.chain, header) + if err != nil { + return nil, err + } + return snap.GetSigners(), nil +} diff --git a/consensus/XDPoS/constants.go b/consensus/XDPoS/constants.go new file mode 100644 index 0000000000..b80a85ddce --- /dev/null +++ b/consensus/XDPoS/constants.go @@ -0,0 +1,75 @@ +// Copyright (c) 2018 XDCchain +// Copyright 2024 The go-ethereum Authors +// +// 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 . + +package XDPoS + +import "math/big" + +// XDC Network constants +const ( + // Reward distribution percentages + RewardMasterPercent = 90 + RewardVoterPercent = 0 + RewardFoundationPercent = 10 + + // Method signatures for contract calls + HexSignMethod = "e341eaa4" + HexSetSecret = "34d38600" + HexSetOpening = "e11f5ba2" + + // Epoch block offsets + EpocBlockSecret = 800 + EpocBlockOpening = 850 + EpocBlockRandomize = 900 + + // Masternode limits + MaxMasternodes = 18 + MaxMasternodesV2 = 108 + + // Penalty configuration + LimitPenaltyEpoch = 4 + + // Blocks per year (assuming 2 second blocks) + BlocksPerYear = uint64(15768000) + + // Queue limits + LimitThresholdNonceInQueue = 10 + + // Gas price + DefaultMinGasPrice = 2500 + + // Block signing + MergeSignRange = 15 + RangeReturnSigner = 150 + + // Minimum miner blocks per epoch + MinimunMinerBlockPerEpoch = 1 +) + +// Fork block numbers +var ( + TIP2019Block = big.NewInt(1) + TIPSigning = big.NewInt(3000000) + TIPRandomize = big.NewInt(3464000) + TIPIncreaseMasternodes = big.NewInt(5000000) // Upgrade MN Count at Block +) + +// Global configuration variables +var ( + IsTestnet bool = false + StoreRewardFolder string + MinGasPrice int64 +) diff --git a/consensus/XDPoS/snapshot.go b/consensus/XDPoS/snapshot.go new file mode 100644 index 0000000000..0b524d6aea --- /dev/null +++ b/consensus/XDPoS/snapshot.go @@ -0,0 +1,272 @@ +// Copyright (c) 2018 XDCchain +// Copyright 2024 The go-ethereum Authors +// +// 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 . + +package XDPoS + +import ( + "bytes" + "encoding/json" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/params" +) + +// Vote represents a single vote that an authorized signer made to modify the +// list of authorizations. +type Vote struct { + Signer common.Address `json:"signer"` // Authorized signer that cast this vote + Block uint64 `json:"block"` // Block number the vote was cast in (expire old votes) + Address common.Address `json:"address"` // Account being voted on to change its authorization + Authorize bool `json:"authorize"` // Whether to authorize or deauthorize the voted account +} + +// Tally is a simple vote tally to keep the current score of votes. +type Tally struct { + Authorize bool `json:"authorize"` // Whether the vote is about authorizing or kicking someone + Votes int `json:"votes"` // Number of votes until now wanting to pass the proposal +} + +// Snapshot is the state of the authorization voting at a given point in time. +type Snapshot struct { + config *params.XDPoSConfig // Consensus engine parameters to fine tune behavior + sigcache *lru.Cache[common.Hash, common.Address] // Cache of recent block signatures to speed up ecrecover + + Number uint64 `json:"number"` // Block number where the snapshot was created + Hash common.Hash `json:"hash"` // Block hash where the snapshot was created + Signers map[common.Address]struct{} `json:"signers"` // Set of authorized signers at this moment + Recents map[uint64]common.Address `json:"recents"` // Set of recent signers for spam protections + Votes []*Vote `json:"votes"` // List of votes cast in chronological order + Tally map[common.Address]Tally `json:"tally"` // Current vote tally to avoid recalculating +} + +// newSnapshot creates a new snapshot with the specified startup parameters. +func newSnapshot(config *params.XDPoSConfig, sigcache *lru.Cache[common.Hash, common.Address], number uint64, hash common.Hash, signers []common.Address) *Snapshot { + snap := &Snapshot{ + config: config, + sigcache: sigcache, + Number: number, + Hash: hash, + Signers: make(map[common.Address]struct{}), + Recents: make(map[uint64]common.Address), + Tally: make(map[common.Address]Tally), + } + for _, signer := range signers { + snap.Signers[signer] = struct{}{} + } + return snap +} + +// loadSnapshot loads an existing snapshot from the database. +func loadSnapshot(config *params.XDPoSConfig, sigcache *lru.Cache[common.Hash, common.Address], db ethdb.Database, hash common.Hash) (*Snapshot, error) { + blob, err := db.Get(append([]byte("XDPoS-"), hash[:]...)) + if err != nil { + return nil, err + } + snap := new(Snapshot) + if err := json.Unmarshal(blob, snap); err != nil { + return nil, err + } + snap.config = config + snap.sigcache = sigcache + return snap, nil +} + +// store inserts the snapshot into the database. +func (s *Snapshot) store(db ethdb.Database) error { + blob, err := json.Marshal(s) + if err != nil { + return err + } + return db.Put(append([]byte("XDPoS-"), s.Hash[:]...), blob) +} + +// copy creates a deep copy of the snapshot. +func (s *Snapshot) copy() *Snapshot { + cpy := &Snapshot{ + config: s.config, + sigcache: s.sigcache, + Number: s.Number, + Hash: s.Hash, + Signers: make(map[common.Address]struct{}), + Recents: make(map[uint64]common.Address), + Votes: make([]*Vote, len(s.Votes)), + Tally: make(map[common.Address]Tally), + } + for signer := range s.Signers { + cpy.Signers[signer] = struct{}{} + } + for block, signer := range s.Recents { + cpy.Recents[block] = signer + } + for address, tally := range s.Tally { + cpy.Tally[address] = tally + } + copy(cpy.Votes, s.Votes) + return cpy +} + +// validVote returns whether it makes sense to cast the specified vote. +func (s *Snapshot) validVote(address common.Address, authorize bool) bool { + _, signer := s.Signers[address] + return (signer && !authorize) || (!signer && authorize) +} + +// cast adds a new vote into the tally. +func (s *Snapshot) cast(address common.Address, authorize bool) bool { + if !s.validVote(address, authorize) { + return false + } + if old, ok := s.Tally[address]; ok { + old.Votes++ + s.Tally[address] = old + } else { + s.Tally[address] = Tally{Authorize: authorize, Votes: 1} + } + return true +} + +// uncast removes a previously cast vote from the tally. +func (s *Snapshot) uncast(address common.Address, authorize bool) bool { + tally, ok := s.Tally[address] + if !ok { + return false + } + if tally.Authorize != authorize { + return false + } + if tally.Votes > 1 { + tally.Votes-- + s.Tally[address] = tally + } else { + delete(s.Tally, address) + } + return true +} + +// apply creates a new authorization snapshot by applying the given headers. +func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) { + if len(headers) == 0 { + return s, nil + } + for i := 0; i < len(headers)-1; i++ { + if headers[i+1].Number.Uint64() != headers[i].Number.Uint64()+1 { + return nil, errInvalidVotingChain + } + } + if headers[0].Number.Uint64() != s.Number+1 { + return nil, errInvalidVotingChain + } + + snap := s.copy() + + for _, header := range headers { + number := header.Number.Uint64() + if number%s.config.Epoch == 0 { + snap.Votes = nil + snap.Tally = make(map[common.Address]Tally) + } + if limit := uint64(len(snap.Signers)/2 + 1); number >= limit { + delete(snap.Recents, number-limit) + } + signer, err := ecrecover(header, s.sigcache) + if err != nil { + return nil, err + } + snap.Recents[number] = signer + + for i, vote := range snap.Votes { + if vote.Signer == signer && vote.Address == header.Coinbase { + snap.uncast(vote.Address, vote.Authorize) + snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) + break + } + } + + var authorize bool + switch { + case bytes.Equal(header.Nonce[:], nonceAuthVote): + authorize = true + case bytes.Equal(header.Nonce[:], nonceDropVote): + authorize = false + default: + return nil, errInvalidVote + } + if snap.cast(header.Coinbase, authorize) { + snap.Votes = append(snap.Votes, &Vote{ + Signer: signer, + Block: number, + Address: header.Coinbase, + Authorize: authorize, + }) + } + + if tally := snap.Tally[header.Coinbase]; tally.Votes > len(snap.Signers)/2 { + if tally.Authorize { + snap.Signers[header.Coinbase] = struct{}{} + } else { + delete(snap.Signers, header.Coinbase) + if limit := uint64(len(snap.Signers)/2 + 1); number >= limit { + delete(snap.Recents, number-limit) + } + for i := 0; i < len(snap.Votes); i++ { + if snap.Votes[i].Signer == header.Coinbase { + snap.uncast(snap.Votes[i].Address, snap.Votes[i].Authorize) + snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) + i-- + } + } + } + for i := 0; i < len(snap.Votes); i++ { + if snap.Votes[i].Address == header.Coinbase { + snap.Votes = append(snap.Votes[:i], snap.Votes[i+1:]...) + i-- + } + } + delete(snap.Tally, header.Coinbase) + } + } + snap.Number += uint64(len(headers)) + snap.Hash = headers[len(headers)-1].Hash() + return snap, nil +} + +// GetSigners retrieves the list of authorized signers in ascending order. +func (s *Snapshot) GetSigners() []common.Address { + signers := make([]common.Address, 0, len(s.Signers)) + for signer := range s.Signers { + signers = append(signers, signer) + } + for i := 0; i < len(signers); i++ { + for j := i + 1; j < len(signers); j++ { + if bytes.Compare(signers[i][:], signers[j][:]) > 0 { + signers[i], signers[j] = signers[j], signers[i] + } + } + } + return signers +} + +// inturn returns if a signer at a given block height is in-turn or not. +func (s *Snapshot) inturn(number uint64, signer common.Address) bool { + signers, offset := s.GetSigners(), 0 + for offset < len(signers) && signers[offset] != signer { + offset++ + } + return (number % uint64(len(signers))) == uint64(offset) +} diff --git a/consensus/XDPoS/xdpos.go b/consensus/XDPoS/xdpos.go new file mode 100644 index 0000000000..256705be86 --- /dev/null +++ b/consensus/XDPoS/xdpos.go @@ -0,0 +1,1173 @@ +// Copyright (c) 2018 XDCchain +// Copyright 2024 The go-ethereum Authors +// +// 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 . + +// Package XDPoS implements the XDC Network's delegated proof-of-stake consensus engine. +// This is a port from XDPoSChain (geth 1.8.x) to modern go-ethereum (1.17.x). +// +// XDPoS is a DPoS (Delegated Proof of Stake) consensus mechanism where masternodes +// take turns producing blocks in a round-robin fashion within epochs. +// +// Key concepts: +// - Epoch: A fixed number of blocks after which the masternode list may change +// - Masternode: Authorized nodes that can produce blocks +// - Gap: Blocks before epoch end to prepare next epoch's masternode list +// - Validator: Double validation - each block needs creator + validator signatures +// +// This implementation includes: +// - Hook system for rewards, penalties, and validator management +// - Block signer caching for transaction signing +// - Double validation support +// - Penalty system for misbehaving masternodes +package XDPoS + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "math/big" + "math/rand" + "os" + "path/filepath" + "reflect" + "sort" + "strconv" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" + "golang.org/x/crypto/sha3" +) + +const ( + inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory + inmemorySignatures = 4096 // Number of recent block signatures to keep in memory + blockSignersCacheLimit = 9000 // Cache limit for block signers + M2ByteLength = 4 // Length of M2 validator index bytes + + extraVanity = 32 // Fixed number of extra-data prefix bytes reserved for signer vanity + extraSeal = crypto.SignatureLength // Fixed number of extra-data suffix bytes reserved for signer seal (65 bytes) +) + +// Masternode represents a masternode with its address and stake +type Masternode struct { + Address common.Address + Stake *big.Int +} + +// XDPoS proof-of-stake-voting protocol constants. +var ( + defaultEpochLength = uint64(900) // Default number of blocks per epoch + defaultPeriod = uint64(2) // Default block time in seconds + defaultGap = uint64(450) // Default gap blocks before epoch switch + + nonceAuthVote = hexutil.MustDecode("0xffffffffffffffff") // Magic nonce number to vote on adding a new signer + nonceDropVote = hexutil.MustDecode("0x0000000000000000") // Magic nonce number to vote on removing a signer. + + uncleHash = types.CalcUncleHash(nil) // Always Keccak256(RLP([])) as uncles are meaningless outside of PoW. + + diffInTurn = big.NewInt(2) // Block difficulty for in-turn signatures + diffNoTurn = big.NewInt(1) // Block difficulty for out-of-turn signatures +) + +// Various error messages to mark blocks invalid. +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") + + // errInvalidCheckpointBeneficiary is returned if a checkpoint/epoch transition + // block has a beneficiary set to non-zeroes. + errInvalidCheckpointBeneficiary = errors.New("beneficiary in checkpoint block non-zero") + + // errInvalidVote is returned if a nonce value is something else that the two + // allowed constants of 0x00..0 or 0xff..f. + errInvalidVote = errors.New("vote nonce not 0x00..0 or 0xff..f") + + // errInvalidCheckpointVote is returned if a checkpoint/epoch transition block + // has a vote nonce set to non-zeroes. + errInvalidCheckpointVote = errors.New("vote nonce in checkpoint block non-zero") + + // 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") + + // errExtraSigners is returned if non-checkpoint block contain signer data in + // their extra-data fields. + errExtraSigners = errors.New("non-checkpoint block contains extra signer list") + + // errInvalidCheckpointSigners is returned if a checkpoint block contains an + // invalid list of signers (i.e. non divisible by 20 bytes, or not the correct ones). + errInvalidCheckpointSigners = errors.New("invalid signer list on checkpoint block") + + // errInvalidCheckpointPenalties is returned if penalties list is invalid + errInvalidCheckpointPenalties = errors.New("invalid penalty list on checkpoint block") + + // 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 is returned if the difficulty of a block is not either + // of 1 or 2, or if the value does not match the turn of the signer. + 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") + + // errInvalidVotingChain is returned if an authorization list is attempted to + // be modified via out-of-range or non-contiguous headers. + errInvalidVotingChain = errors.New("invalid voting chain") + + // errUnauthorized is returned if a header is signed by a non-authorized entity. + errUnauthorized = errors.New("unauthorized") + + // errFailedDoubleValidation is returned if double validation fails + errFailedDoubleValidation = errors.New("wrong pair of creator-validator in double validation") + + // errWaitTransactions is returned if an empty block is attempted to be sealed + // on an instant chain (0 second period). + errWaitTransactions = errors.New("waiting for transactions") + + // ErrInvalidCheckpointValidators is returned if validators list is invalid + ErrInvalidCheckpointValidators = errors.New("invalid validators list on checkpoint block") +) + +// SignerFn is a signer callback function to request a hash to be signed by a +// backing account. +type SignerFn func(accounts.Account, string, []byte) ([]byte, error) + +// sigHash returns the hash which is used as input for the proof-of-stake-voting +// signing. It is the hash of the entire header apart from the 65 byte signature +// contained at the end of the extra data. +func sigHash(header *types.Header) (hash common.Hash) { + hasher := sha3.NewLegacyKeccak256() + encodeSigHeader(hasher, header) + hasher.Sum(hash[:0]) + return hash +} + +// SealHash returns the hash of a block prior to it being sealed (exported). +func SealHash(header *types.Header) (hash common.Hash) { + return sigHash(header) +} + +// encodeSigHeader encodes the header for signing, excluding the signature +func encodeSigHeader(w io.Writer, header *types.Header) { + enc := []interface{}{ + header.ParentHash, + header.UncleHash, + 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)-extraSeal], // Exclude signature + header.MixDigest, + header.Nonce, + } + // Add post-London fields if present + if header.BaseFee != nil { + enc = append(enc, header.BaseFee) + } + if header.WithdrawalsHash != nil { + enc = append(enc, header.WithdrawalsHash) + } + if header.BlobGasUsed != nil { + enc = append(enc, header.BlobGasUsed) + } + if header.ExcessBlobGas != nil { + enc = append(enc, header.ExcessBlobGas) + } + if header.ParentBeaconRoot != nil { + enc = append(enc, header.ParentBeaconRoot) + } + if err := rlp.Encode(w, enc); err != nil { + panic("can't encode: " + err.Error()) + } +} + +// ecrecover extracts the Ethereum account address from a signed header. +func ecrecover(header *types.Header, sigcache *lru.Cache[common.Hash, common.Address]) (common.Address, error) { + // If the signature's already cached, return that + hash := header.Hash() + if address, known := sigcache.Get(hash); known { + return 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 +} + +// XDPoS is the delegated-proof-of-stake consensus engine. +type XDPoS struct { + config *params.XDPoSConfig // Consensus engine configuration parameters + db ethdb.Database // Database to store and retrieve snapshot checkpoints + + recents *lru.Cache[common.Hash, *Snapshot] // Snapshots for recent block to speed up reorgs + signatures *lru.Cache[common.Hash, common.Address] // Signatures of recent blocks to speed up mining + validatorSignatures *lru.Cache[common.Hash, common.Address] // Validator signatures cache + verifiedHeaders *lru.Cache[common.Hash, bool] // Verified headers cache + proposals map[common.Address]bool // Current list of proposals we are pushing + + signer common.Address // Ethereum address of the signing key + signFn SignerFn // Signer function to authorize hashes with + lock sync.RWMutex // Protects the signer fields + + // Block signers cache for tracking signing transactions + BlockSigners *lru.Cache[common.Hash, []*types.Transaction] + + // Hook functions for XDC-specific logic + // These hooks integrate with the XDC smart contracts for rewards, penalties, etc. + HookReward func(chain consensus.ChainHeaderReader, state *state.StateDB, header *types.Header) (map[string]interface{}, error) + HookPenalty func(chain consensus.ChainHeaderReader, blockNumberEpoc uint64) ([]common.Address, error) + HookPenaltyTIPSigning func(chain consensus.ChainHeaderReader, 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 + + // Testing flags + fakeDiff bool // Skip difficulty verifications (testing) +} + +// New creates a XDPoS delegated-proof-of-stake consensus engine. +func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS { + // Set any missing consensus parameters to their defaults + conf := *config + if conf.Epoch == 0 { + conf.Epoch = defaultEpochLength + } + if conf.Period == 0 { + conf.Period = defaultPeriod + } + if conf.Gap == 0 { + conf.Gap = defaultGap + } + + // Allocate the snapshot caches and create the engine + recents := lru.NewCache[common.Hash, *Snapshot](inmemorySnapshots) + signatures := lru.NewCache[common.Hash, common.Address](inmemorySignatures) + validatorSignatures := lru.NewCache[common.Hash, common.Address](inmemorySignatures) + verifiedHeaders := lru.NewCache[common.Hash, bool](inmemorySnapshots) + blockSigners := lru.NewCache[common.Hash, []*types.Transaction](blockSignersCacheLimit) + + return &XDPoS{ + config: &conf, + db: db, + recents: recents, + signatures: signatures, + validatorSignatures: validatorSignatures, + verifiedHeaders: verifiedHeaders, + BlockSigners: blockSigners, + proposals: make(map[common.Address]bool), + } +} + +// Author implements consensus.Engine, returning the Ethereum address recovered +// from the signature in the header's extra-data section. +func (c *XDPoS) Author(header *types.Header) (common.Address, error) { + return ecrecover(header, c.signatures) +} + +// VerifyHeader checks whether a header conforms to the consensus rules. +func (c *XDPoS) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error { + return c.verifyHeaderWithCache(chain, header, nil) +} + +// VerifyHeaders is similar to VerifyHeader, but verifies a batch of headers. The +// method returns a quit channel to abort the operations and a results channel to +// retrieve the async verifications (the order is that of the input slice). +func (c *XDPoS) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) { + abort := make(chan struct{}) + results := make(chan error, len(headers)) + + go func() { + for i, header := range headers { + err := c.verifyHeaderWithCache(chain, header, headers[:i]) + + select { + case <-abort: + return + case results <- err: + } + } + }() + return abort, results +} + +func (c *XDPoS) verifyHeaderWithCache(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + if _, check := c.verifiedHeaders.Get(header.Hash()); check { + return nil + } + err := c.verifyHeader(chain, header, parents) + if err == nil { + c.verifiedHeaders.Add(header.Hash(), true) + } + return err +} + +// verifyHeader checks whether a header conforms to the consensus rules. +func (c *XDPoS) verifyHeader(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + if header.Number == nil { + return errUnknownBlock + } + number := header.Number.Uint64() + + // Don't waste time checking blocks from the future + if header.Time > uint64(time.Now().Unix()) { + return consensus.ErrFutureBlock + } + + // Checkpoint blocks need to enforce zero beneficiary + checkpoint := (number % c.config.Epoch) == 0 + if checkpoint && header.Coinbase != (common.Address{}) { + return errInvalidCheckpointBeneficiary + } + + // Nonces must be 0x00..0 or 0xff..f, zeroes enforced on checkpoints + if !bytes.Equal(header.Nonce[:], nonceAuthVote) && !bytes.Equal(header.Nonce[:], nonceDropVote) { + return errInvalidVote + } + if checkpoint && !bytes.Equal(header.Nonce[:], nonceDropVote) { + return errInvalidCheckpointVote + } + + // 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 extra-data contains a signer list on checkpoint, but none otherwise + signersBytes := len(header.Extra) - extraVanity - extraSeal + if !checkpoint && signersBytes != 0 { + return errExtraSigners + } + if checkpoint && signersBytes%common.AddressLength != 0 { + return errInvalidCheckpointSigners + } + + // Ensure that the mix digest is zero as we don't have fork protection currently + if header.MixDigest != (common.Hash{}) { + return errInvalidMixDigest + } + + // Ensure that the block doesn't contain any uncles which are meaningless in XDPoS + if header.UncleHash != uncleHash { + return errInvalidUncleHash + } + + // All basic checks passed, verify cascading fields + return c.verifyCascadingFields(chain, header, parents) +} + +// verifyCascadingFields verifies all the header fields that are not standalone, +// rather depend on a batch of previous headers. +func (c *XDPoS) verifyCascadingFields(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + // The genesis block is the always valid dead-end + number := header.Number.Uint64() + if number == 0 { + return nil + } + + // Ensure that the block's timestamp isn't too close to its parent + 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+c.config.Period > header.Time { + return ErrInvalidTimestamp + } + + // Retrieve the snapshot needed to verify this header and cache it + snap, err := c.snapshot(chain, number-1, header.ParentHash, parents) + if err != nil { + return err + } + + // If the block is a checkpoint block, verify the signer list + if number%c.config.Epoch == 0 { + signers := snap.GetSigners() + penPenalties := []common.Address{} + + // Get penalties if hook is configured + if c.HookPenalty != nil || c.HookPenaltyTIPSigning != nil { + var penErr error + if c.HookPenaltyTIPSigning != nil { + penPenalties, penErr = c.HookPenaltyTIPSigning(chain, header, signers) + } else if c.HookPenalty != nil { + penPenalties, penErr = c.HookPenalty(chain, number) + } + if penErr != nil { + return penErr + } + for _, address := range penPenalties { + log.Debug("Penalty Info", "address", address, "number", number) + } + } + + // Remove penalties from signers + signers = removeItemFromArray(signers, penPenalties) + + // Remove penalties from previous epochs (up to LimitPenaltyEpoch) + for i := 1; i <= LimitPenaltyEpoch; i++ { + if number > uint64(i)*c.config.Epoch { + signers = c.removePenaltiesFromBlock(chain, signers, number-uint64(i)*c.config.Epoch) + } + } + + // Verify checkpoint signers match + extraSuffix := len(header.Extra) - extraSeal + masternodesFromCheckpointHeader := extractAddressFromBytes(header.Extra[extraVanity:extraSuffix]) + if !compareSignersLists(masternodesFromCheckpointHeader, signers) { + log.Error("Masternodes lists are different in checkpoint header and snapshot", + "number", number, + "masternodes_from_checkpoint_header", masternodesFromCheckpointHeader, + "masternodes_in_snapshot", signers, + "penList", penPenalties) + return errInvalidCheckpointSigners + } + + // Verify MNs if hook is configured + if c.HookVerifyMNs != nil { + if err := c.HookVerifyMNs(header, signers); err != nil { + return err + } + } + } + + // All basic checks passed, verify the seal and return + return c.verifySeal(chain, header, parents) +} + +// verifySeal checks whether the signature contained in the header satisfies the +// consensus protocol requirements. +func (c *XDPoS) verifySeal(chain consensus.ChainHeaderReader, header *types.Header, parents []*types.Header) error { + // Verifying the genesis block is not supported + number := header.Number.Uint64() + if number == 0 { + return errUnknownBlock + } + + // Retrieve the snapshot needed to verify this header and cache it + snap, err := c.snapshot(chain, number-1, header.ParentHash, parents) + if err != nil { + return err + } + + // Resolve the authorization key and check against signers + creator, err := ecrecover(header, c.signatures) + if 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) + } + + difficulty := c.calcDifficulty(chain, parent, creator) + log.Debug("verify seal block", "number", header.Number, "hash", header.Hash(), "block difficulty", header.Difficulty, "calc difficulty", difficulty, "creator", creator) + + // Ensure that the block's difficulty is meaningful (may not be correct at this point) + if number > 0 && !c.fakeDiff { + if header.Difficulty.Int64() != difficulty.Int64() { + return errInvalidDifficulty + } + } + + // Get masternodes for this header + masternodes := c.GetMasternodes(chain, header) + + // Check if creator is authorized + if _, ok := snap.Signers[creator]; !ok { + valid := false + for _, m := range masternodes { + if m == creator { + valid = true + break + } + } + if !valid { + log.Debug("Unauthorized creator found", "block number", number, "creator", creator.String()) + return errUnauthorized + } + } + + // Check recent signers to prevent spamming + if len(masternodes) > 1 { + for seen, recent := range snap.Recents { + if recent == creator { + if limit := uint64(2); seen > number-limit { + if number%c.config.Epoch != 0 { + return errUnauthorized + } + } + } + } + } + + return nil +} + +// VerifyUncles implements consensus.Engine, always returning an error for any +// uncles as this consensus mechanism doesn't permit uncles. +func (c *XDPoS) VerifyUncles(chain consensus.ChainReader, block *types.Block) error { + if len(block.Uncles()) > 0 { + return errors.New("uncles not allowed") + } + return nil +} + +// Prepare implements consensus.Engine, preparing all the consensus fields of the +// header for running the transactions on top. +func (c *XDPoS) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { + header.Coinbase = common.Address{} + header.Nonce = types.BlockNonce{} + + number := header.Number.Uint64() + + snap, err := c.snapshot(chain, number-1, header.ParentHash, nil) + if err != nil { + return err + } + + if number%c.config.Epoch != 0 { + c.lock.RLock() + addresses := make([]common.Address, 0, len(c.proposals)) + for address, authorize := range c.proposals { + if snap.validVote(address, authorize) { + addresses = append(addresses, address) + } + } + if len(addresses) > 0 { + header.Coinbase = addresses[rand.Intn(len(addresses))] + if c.proposals[header.Coinbase] { + copy(header.Nonce[:], nonceAuthVote) + } else { + copy(header.Nonce[:], nonceDropVote) + } + } + c.lock.RUnlock() + } + + parent := chain.GetHeader(header.ParentHash, number-1) + if parent == nil { + return consensus.ErrUnknownAncestor + } + + header.Difficulty = c.calcDifficulty(chain, parent, c.signer) + log.Debug("CalcDifficulty", "number", header.Number, "difficulty", header.Difficulty) + + if len(header.Extra) < extraVanity { + header.Extra = append(header.Extra, bytes.Repeat([]byte{0x00}, extraVanity-len(header.Extra))...) + } + header.Extra = header.Extra[:extraVanity] + + masternodes := snap.GetSigners() + if number >= c.config.Epoch && number%c.config.Epoch == 0 { + if c.HookPenalty != nil || c.HookPenaltyTIPSigning != nil { + var penMasternodes []common.Address + var penErr error + + if c.HookPenaltyTIPSigning != nil { + penMasternodes, penErr = c.HookPenaltyTIPSigning(chain, header, masternodes) + } else if c.HookPenalty != nil { + penMasternodes, penErr = c.HookPenalty(chain, number) + } + if penErr != nil { + return penErr + } + if len(penMasternodes) > 0 { + masternodes = removeItemFromArray(masternodes, penMasternodes) + for _, address := range penMasternodes { + log.Debug("Penalty status", "address", address, "number", number) + } + } + } + + for i := 1; i <= LimitPenaltyEpoch; i++ { + if number > uint64(i)*c.config.Epoch { + masternodes = c.removePenaltiesFromBlock(chain, masternodes, number-uint64(i)*c.config.Epoch) + } + } + + for _, masternode := range masternodes { + header.Extra = append(header.Extra, masternode[:]...) + } + + if c.HookValidator != nil { + _, err := c.HookValidator(header, masternodes) + if err != nil { + return err + } + } + } + header.Extra = append(header.Extra, make([]byte, extraSeal)...) + + header.MixDigest = common.Hash{} + + header.Time = parent.Time + c.config.Period + if header.Time < uint64(time.Now().Unix()) { + header.Time = uint64(time.Now().Unix()) + } + + return nil +} + +// Finalize implements consensus.Engine. +func (c *XDPoS) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { + header.UncleHash = types.CalcUncleHash(nil) +} + +// FinalizeAndAssemble implements consensus.Engine. +func (c *XDPoS) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, statedb *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { + number := header.Number.Uint64() + rCheckpoint := c.config.RewardCheckpoint + if rCheckpoint == 0 { + rCheckpoint = c.config.Epoch + } + + if c.HookReward != nil && number%rCheckpoint == 0 { + rewards, err := c.HookReward(chain, statedb, header) + if err != nil { + return nil, err + } + if len(StoreRewardFolder) > 0 { + data, err := json.Marshal(rewards) + if err == nil { + err = os.WriteFile(filepath.Join(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) + } + } + } + + header.Root = statedb.IntermediateRoot(chain.Config().IsEIP158(header.Number)) + header.UncleHash = types.CalcUncleHash(nil) + + return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)), nil +} + +// Seal implements consensus.Engine. +func (c *XDPoS) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { + header := block.Header() + + number := header.Number.Uint64() + if number == 0 { + return errUnknownBlock + } + + if c.config.Period == 0 && len(block.Transactions()) == 0 && number%c.config.Epoch != 0 { + return errWaitTransactions + } + + c.lock.RLock() + signer, signFn := c.signer, c.signFn + c.lock.RUnlock() + + snap, err := c.snapshot(chain, number-1, header.ParentHash, nil) + if err != nil { + return err + } + masternodes := c.GetMasternodes(chain, header) + + if _, authorized := snap.Signers[signer]; !authorized { + valid := false + for _, m := range masternodes { + if m == signer { + valid = true + break + } + } + if !valid { + return errUnauthorized + } + } + + if len(masternodes) > 1 { + for seen, recent := range snap.Recents { + if recent == signer { + if limit := uint64(2); number < limit || seen > number-limit { + if number%c.config.Epoch != 0 { + log.Info("Signed recently, must wait for others", "len(masternodes)", len(masternodes), "number", number, "limit", limit, "seen", seen, "recent", recent.String()) + <-stop + return nil + } + } + } + } + } + + select { + case <-stop: + return nil + default: + } + + sighash, err := signFn(accounts.Account{Address: signer}, accounts.MimetypeClique, sigHash(header).Bytes()) + if err != nil { + return err + } + copy(header.Extra[len(header.Extra)-extraSeal:], sighash) + + delay := time.Unix(int64(header.Time), 0).Sub(time.Now()) + if header.Difficulty.Cmp(diffNoTurn) == 0 { + wiggle := time.Duration(len(masternodes)/2+1) * wiggleTime + delay += time.Duration(rand.Int63n(int64(wiggle))) + log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle)) + } + + log.Trace("Waiting for slot to sign and propagate", "delay", common.PrettyDuration(delay)) + go func() { + select { + case <-stop: + return + case <-time.After(delay): + } + + select { + case results <- block.WithSeal(header): + default: + log.Warn("Sealing result is not read by miner", "sealhash", SealHash(header)) + } + }() + + return nil +} + +// CalcDifficulty is the difficulty adjustment algorithm. +func (c *XDPoS) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { + return c.calcDifficulty(chain, parent, c.signer) +} + +func (c *XDPoS) calcDifficulty(chain consensus.ChainHeaderReader, parent *types.Header, signer common.Address) *big.Int { + numMNs, preIndex, curIndex, _, err := c.YourTurn(chain, parent, signer) + if err != nil { + return big.NewInt(int64(numMNs + curIndex - preIndex)) + } + return big.NewInt(int64(numMNs - Hop(numMNs, preIndex, curIndex))) +} + +// SealHash returns the hash of a block prior to it being sealed. +func (c *XDPoS) SealHash(header *types.Header) common.Hash { + return SealHash(header) +} + +// Close implements consensus.Engine. +func (c *XDPoS) Close() error { + return nil +} + +// APIs implements consensus.Engine. +func (c *XDPoS) APIs(chain consensus.ChainHeaderReader) []rpc.API { + return []rpc.API{{ + Namespace: "xdpos", + Service: &API{chain: chain, xdpos: c}, + }} +} + +// Authorize injects a private key into the consensus engine. +func (c *XDPoS) Authorize(signer common.Address, signFn SignerFn) { + c.lock.Lock() + defer c.lock.Unlock() + c.signer = signer + c.signFn = signFn +} + +// GetPeriod returns the configured block period +func (c *XDPoS) GetPeriod() uint64 { return c.config.Period } + +// GetDb returns the database +func (c *XDPoS) GetDb() ethdb.Database { return c.db } + +// GetSnapshot returns the snapshot at a given header +func (c *XDPoS) GetSnapshot(chain consensus.ChainHeaderReader, header *types.Header) (*Snapshot, error) { + number := header.Number.Uint64() + log.Trace("take snapshot", "number", number, "hash", header.Hash()) + return c.snapshot(chain, number, header.Hash(), nil) +} + +// StoreSnapshot stores the snapshot to the database +func (c *XDPoS) StoreSnapshot(snap *Snapshot) error { + return snap.store(c.db) +} + +// YourTurn checks if it's the signer's turn to mine +func (c *XDPoS) YourTurn(chain consensus.ChainHeaderReader, parent *types.Header, signer common.Address) (int, int, int, bool, error) { + masternodes := c.GetMasternodes(chain, parent) + + snap, err := c.GetSnapshot(chain, parent) + if err != nil { + log.Warn("Failed when trying to commit new work", "err", err) + return 0, -1, -1, false, err + } + if len(masternodes) == 0 { + return 0, -1, -1, false, errors.New("Masternodes not found") + } + + pre := common.Address{} + preIndex := -1 + if parent.Number.Uint64() != 0 { + pre, err = whoIsCreator(snap, parent) + if err != nil { + return 0, 0, 0, false, err + } + preIndex = position(masternodes, pre) + } + + curIndex := position(masternodes, signer) + if signer == c.signer { + log.Debug("Masternodes cycle info", "number of masternodes", len(masternodes), "previous", pre, "position", preIndex, "current", signer, "position", curIndex) + } + + if (preIndex+1)%len(masternodes) == curIndex { + return len(masternodes), preIndex, curIndex, true, nil + } + return len(masternodes), preIndex, curIndex, false, nil +} + +// GetMasternodes returns the list of masternodes for a given header +func (c *XDPoS) GetMasternodes(chain consensus.ChainHeaderReader, header *types.Header) []common.Address { + n := header.Number.Uint64() + e := c.config.Epoch + switch { + case n%e == 0: + return c.GetMasternodesFromCheckpointHeader(header, n, e) + case n%e != 0: + h := chain.GetHeaderByNumber(n - (n % e)) + return c.GetMasternodesFromCheckpointHeader(h, n, e) + default: + return []common.Address{} + } +} + +// GetMasternodesFromCheckpointHeader extracts masternode list from checkpoint header +func (c *XDPoS) GetMasternodesFromCheckpointHeader(preCheckpointHeader *types.Header, n, e uint64) []common.Address { + if preCheckpointHeader == nil { + log.Info("Previous checkpoint's header is empty", "block number", n, "epoch", e) + return []common.Address{} + } + masternodes := make([]common.Address, (len(preCheckpointHeader.Extra)-extraVanity-extraSeal)/common.AddressLength) + for i := 0; i < len(masternodes); i++ { + copy(masternodes[i][:], preCheckpointHeader.Extra[extraVanity+i*common.AddressLength:]) + } + return masternodes +} + +// UpdateMasternodes updates the masternode list in the snapshot +func (c *XDPoS) UpdateMasternodes(chain consensus.ChainHeaderReader, header *types.Header, ms []Masternode) error { + number := header.Number.Uint64() + log.Trace("take snapshot", "number", number, "hash", header.Hash()) + + snap, err := c.snapshot(chain, number, header.Hash(), nil) + if err != nil { + return err + } + + newMasternodes := make(map[common.Address]struct{}) + for _, m := range ms { + newMasternodes[m.Address] = struct{}{} + } + snap.Signers = newMasternodes + + nm := []string{} + for _, n := range ms { + nm = append(nm, n.Address.String()) + } + c.recents.Add(snap.Hash, snap) + log.Info("New set of masternodes has been updated to snapshot", "number", snap.Number, "hash", snap.Hash, "new masternodes", nm) + return nil +} + +// RecoverSigner recovers the signer from a header +func (c *XDPoS) RecoverSigner(header *types.Header) (common.Address, error) { + return ecrecover(header, c.signatures) +} + +// RecoverValidator recovers the validator from a header's Validator field +func (c *XDPoS) RecoverValidator(header *types.Header) (common.Address, error) { + hash := header.Hash() + if address, known := c.validatorSignatures.Get(hash); known { + return address, nil + } + // TODO: Implement when header.Validator field exists + return ecrecover(header, c.signatures) +} + +// CacheData caches signing transactions from a block +func (c *XDPoS) CacheData(header *types.Header, txs []*types.Transaction, receipts []*types.Receipt) []*types.Transaction { + signTxs := []*types.Transaction{} + for _, tx := range txs { + if isSigningTransaction(tx) { + var b uint64 + 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 +} + +// CacheSigner caches signing transactions by hash +func (c *XDPoS) CacheSigner(hash common.Hash, txs []*types.Transaction) []*types.Transaction { + signTxs := []*types.Transaction{} + for _, tx := range txs { + if isSigningTransaction(tx) { + 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 +} + +// snapshot retrieves the authorization snapshot at a given point in time. +func (c *XDPoS) snapshot(chain consensus.ChainHeaderReader, number uint64, hash common.Hash, parents []*types.Header) (*Snapshot, error) { + var ( + headers []*types.Header + snap *Snapshot + ) + for snap == nil { + if s, ok := c.recents.Get(hash); ok { + snap = s + break + } + if (number+c.config.Gap)%c.config.Epoch == 0 { + if s, err := loadSnapshot(c.config, c.signatures, c.db, hash); err == nil { + log.Trace("Loaded voting snapshot from disk", "number", number, "hash", hash) + snap = s + break + } + } + if number == 0 { + genesis := chain.GetHeaderByNumber(0) + if err := c.VerifyHeader(chain, genesis); err != nil { + return nil, err + } + signers := make([]common.Address, (len(genesis.Extra)-extraVanity-extraSeal)/common.AddressLength) + for i := 0; i < len(signers); i++ { + copy(signers[i][:], genesis.Extra[extraVanity+i*common.AddressLength:]) + } + snap = newSnapshot(c.config, c.signatures, 0, genesis.Hash(), signers) + if err := snap.store(c.db); err != nil { + return nil, err + } + log.Trace("Stored genesis voting snapshot to disk") + break + } + var header *types.Header + if len(parents) > 0 { + header = parents[len(parents)-1] + if header.Hash() != hash || header.Number.Uint64() != number { + return nil, consensus.ErrUnknownAncestor + } + parents = parents[:len(parents)-1] + } else { + header = chain.GetHeader(hash, number) + if header == nil { + return nil, consensus.ErrUnknownAncestor + } + } + headers = append(headers, header) + number, hash = number-1, header.ParentHash + } + for i := 0; i < len(headers)/2; i++ { + headers[i], headers[len(headers)-1-i] = headers[len(headers)-1-i], headers[i] + } + snap, err := snap.apply(headers) + if err != nil { + return nil, err + } + c.recents.Add(snap.Hash, snap) + + if (snap.Number+c.config.Gap)%c.config.Epoch == 0 { + if err = snap.store(c.db); err != nil { + return nil, err + } + log.Trace("Stored voting snapshot to disk", "number", snap.Number, "hash", snap.Hash) + } + return snap, err +} + +func (c *XDPoS) removePenaltiesFromBlock(chain consensus.ChainHeaderReader, masternodes []common.Address, epochNumber uint64) []common.Address { + if epochNumber <= 0 { + return masternodes + } + header := chain.GetHeaderByNumber(epochNumber) + if header == nil { + return masternodes + } + return masternodes +} + +// Helper functions + +func whoIsCreator(snap *Snapshot, header *types.Header) (common.Address, error) { + if header.Number.Uint64() == 0 { + return common.Address{}, errors.New("Don't take block 0") + } + return ecrecover(header, snap.sigcache) +} + +func position(list []common.Address, x common.Address) int { + for i, item := range list { + if item == x { + return i + } + } + return -1 +} + +// Hop calculates the distance between two positions in the masternode list +func Hop(length, pre, cur int) int { + switch { + case pre < cur: + return cur - (pre + 1) + case pre > cur: + return (length - pre) + (cur - 1) + default: + return length - 1 + } +} + +func compareSignersLists(list1 []common.Address, list2 []common.Address) bool { + if len(list1) == 0 && len(list2) == 0 { + return true + } + if len(list1) != len(list2) { + return false + } + l1 := make([]common.Address, len(list1)) + l2 := make([]common.Address, len(list2)) + copy(l1, list1) + copy(l2, list2) + sort.Slice(l1, func(i, j int) bool { return l1[i].String() <= l1[j].String() }) + sort.Slice(l2, func(i, j int) bool { return l2[i].String() <= l2[j].String() }) + return reflect.DeepEqual(l1, l2) +} + +func removeItemFromArray(array []common.Address, toRemove []common.Address) []common.Address { + result := make([]common.Address, 0) + for _, item := range array { + found := false + for _, r := range toRemove { + if item == r { + found = true + break + } + } + if !found { + result = append(result, item) + } + } + return result +} + +func extractAddressFromBytes(byteAddresses []byte) []common.Address { + if len(byteAddresses)%common.AddressLength != 0 { + return []common.Address{} + } + addresses := make([]common.Address, len(byteAddresses)/common.AddressLength) + for i := 0; i < len(addresses); i++ { + copy(addresses[i][:], byteAddresses[i*common.AddressLength:]) + } + return addresses +} + +// ExtractValidatorsFromBytes extracts validator indices from bytes +func ExtractValidatorsFromBytes(byteValidators []byte) []int64 { + lenValidator := len(byteValidators) / M2ByteLength + var validators []int64 + for i := 0; i < lenValidator; i++ { + trimByte := bytes.Trim(byteValidators[i*M2ByteLength:(i+1)*M2ByteLength], "\x00") + intNumber, err := strconv.Atoi(string(trimByte)) + if err != nil { + log.Error("Can not convert string to integer", "error", err) + return []int64{} + } + validators = append(validators, int64(intNumber)) + } + return validators +} + +func isSigningTransaction(tx *types.Transaction) bool { + return false // Placeholder - needs XDC contract integration +} + +var wiggleTime = 500 * time.Millisecond + +var _ consensus.Engine = (*XDPoS)(nil) diff --git a/params/config.go b/params/config.go index c5dfad1cd3..56dfdc1139 100644 --- a/params/config.go +++ b/params/config.go @@ -490,6 +490,7 @@ type ChainConfig struct { // Various consensus engines Ethash *EthashConfig `json:"ethash,omitempty"` Clique *CliqueConfig `json:"clique,omitempty"` + XDPoS *XDPoSConfig `json:"xdpos,omitempty"` BlobScheduleConfig *BlobScheduleConfig `json:"blobSchedule,omitempty"` } @@ -512,6 +513,21 @@ func (c CliqueConfig) String() string { return fmt.Sprintf("clique(period: %d, epoch: %d)", c.Period, c.Epoch) } +// XDPoSConfig is the consensus engine configs for XDC Network's delegated-proof-of-stake based sealing. +type XDPoSConfig struct { + Period uint64 `json:"period"` // Number of seconds between blocks to enforce + Epoch uint64 `json:"epoch"` // Epoch length to reset votes and checkpoint + Reward uint64 `json:"reward"` // Block reward - unit Ether + RewardCheckpoint uint64 `json:"rewardCheckpoint"` // Checkpoint block for calculate rewards + Gap uint64 `json:"gap"` // Gap time preparing for the next epoch + FoudationWalletAddr common.Address `json:"foudationWalletAddr"` // Foundation Address Wallet +} + +// String implements the stringer interface, returning the consensus engine details. +func (c XDPoSConfig) String() string { + return fmt.Sprintf("xdpos(period: %d, epoch: %d, reward: %d)", c.Period, c.Epoch, c.Reward) +} + // String implements the fmt.Stringer interface, returning a string representation // of ChainConfig. func (c *ChainConfig) String() string {