diff --git a/consensus/XDPoS/XDPoS.go b/consensus/XDPoS/XDPoS.go index e8878734e5..e41b1135f3 100644 --- a/consensus/XDPoS/XDPoS.go +++ b/consensus/XDPoS/XDPoS.go @@ -305,12 +305,27 @@ func (x *XDPoS) RecoverValidator(header *types.Header) (common.Address, error) { func (x *XDPoS) GetMasternodesFromCheckpointHeader(preCheckpointHeader *types.Header, n, e uint64) []common.Address { switch x.config.BlockConsensusVersion(preCheckpointHeader.Number) { case params.ConsensusEngineVersion2: - return []common.Address{} + return x.EngineV2.GetMasternodesFromEpochSwitchHeader(preCheckpointHeader) default: // Default "v1" return x.EngineV1.GetMasternodesFromCheckpointHeader(preCheckpointHeader, n, e) } } +// Check is epoch switch (checkpoint) block +func (x *XDPoS) IsEpochSwitch(header *types.Header) bool { + switch x.config.BlockConsensusVersion(header.Number) { + case params.ConsensusEngineVersion2: + b, _, err := x.EngineV2.IsEpochSwitch(header) + if err != nil { + log.Error("[IsEpochSwitch] Adaptor v2 IsEpochSwitch has error", "err", err) + return false + } + return b + default: // Default "v1" + return x.EngineV1.IsEpochSwitch(header) + } +} + // Same DB across all consensus engines func (x *XDPoS) GetDb() ethdb.Database { return x.db diff --git a/consensus/XDPoS/engines/engine_v1/engine.go b/consensus/XDPoS/engines/engine_v1/engine.go index 0ba47d36db..c2351de31b 100644 --- a/consensus/XDPoS/engines/engine_v1/engine.go +++ b/consensus/XDPoS/engines/engine_v1/engine.go @@ -988,3 +988,8 @@ func NewFaker(db ethdb.Database, config *params.XDPoSConfig) *XDPoS_v1 { } return fakeEngine } + +// Epoch Switch is also known as checkpoint in v1 +func (x *XDPoS_v1) IsEpochSwitch(header *types.Header) bool { + return (header.Number.Uint64() % x.config.Epoch) == 0 +} diff --git a/consensus/XDPoS/engines/engine_v2/engine.go b/consensus/XDPoS/engines/engine_v2/engine.go index f69c01878d..fc91eb75e6 100644 --- a/consensus/XDPoS/engines/engine_v2/engine.go +++ b/consensus/XDPoS/engines/engine_v2/engine.go @@ -29,8 +29,9 @@ type XDPoS_v2 struct { config *params.XDPoSConfig // Consensus engine configuration parameters 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 + recents *lru.ARCCache // Snapshots for recent block to speed up reorgs + signatures *lru.ARCCache // Signatures of recent blocks to speed up mining + epochSwitches *lru.ARCCache // infos of epoch: master nodes, epoch switch block info, parent of that info signer common.Address // Ethereum address of the signing key signFn clique.SignerFn // Signer function to authorize hashes with @@ -61,6 +62,7 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS_v2 { recents, _ := lru.NewARC(utils.InmemorySnapshots) signatures, _ := lru.NewARC(utils.InmemorySnapshots) + epochSwitches, _ := lru.NewARC(int(utils.InmemoryEpochs)) votePool := utils.NewPool(config.V2.CertThreshold) engine := &XDPoS_v2{ @@ -69,6 +71,7 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS_v2 { signatures: signatures, recents: recents, + epochSwitches: epochSwitches, timeoutWorker: timer, BroadcastCh: make(chan interface{}), timeoutPool: timeoutPool, @@ -359,21 +362,6 @@ func whoIsCreator(snap *SnapshotV2, header *types.Header) (common.Address, error return m, nil } -// Copy from v1 -func (x *XDPoS_v2) GetMasternodes(chain consensus.ChainReader, header *types.Header) []common.Address { - n := header.Number.Uint64() - e := x.config.Epoch - switch { - case n%e == 0: - return utils.GetMasternodesFromCheckpointHeader(header) - case n%e != 0: - h := chain.GetHeaderByNumber(n - (n % e)) - return utils.GetMasternodesFromCheckpointHeader(h) - default: - return []common.Address{} - } -} - // Copy from v1 func (x *XDPoS_v2) GetSnapshot(chain consensus.ChainReader, header *types.Header) (*SnapshotV2, error) { number := header.Number.Uint64() @@ -1053,3 +1041,95 @@ func (x *XDPoS_v2) GetProperties() (utils.Round, *utils.QuorumCert, *utils.Quoru defer x.lock.Unlock() return x.currentRound, x.lockQuorumCert, x.highestQuorumCert, x.highestVotedRound, x.highestCommitBlock } + +// Get master nodes over extra data of epoch switch block. +func (x *XDPoS_v2) GetMasternodesFromEpochSwitchHeader(epochSwitchHeader *types.Header) []common.Address { + if epochSwitchHeader == nil { + log.Error("[GetMasternodesFromEpochSwitchHeader] use nil epoch switch block to get master nodes") + return []common.Address{} + } + masternodes := make([]common.Address, len(epochSwitchHeader.Validators)/common.AddressLength) + for i := 0; i < len(masternodes); i++ { + copy(masternodes[i][:], epochSwitchHeader.Validators[i*common.AddressLength:]) + } + + return masternodes +} + +func (x *XDPoS_v2) IsEpochSwitch(header *types.Header) (bool, uint64, error) { + var decodedExtraField utils.ExtraFields_v2 + err := utils.DecodeBytesExtraFields(header.Extra, &decodedExtraField) + if err != nil { + log.Error("[IsEpochSwitch] decode header error", "err", err, "header", header, "extra", common.Bytes2Hex(header.Extra)) + return false, 0, err + } + parentRound := decodedExtraField.QuorumCert.ProposedBlockInfo.Round + round := decodedExtraField.Round + epochStart := round - round%utils.Round(x.config.Epoch) + // if parent is last v1 block and this is first v2 block, this is treated as epoch switch + if decodedExtraField.QuorumCert.ProposedBlockInfo.Number.Cmp(x.config.XDPoSV2Block) == 0 { + log.Info("[IsEpochSwitch] true, parent equals XDPoSV2Block", "round", round, "number", header.Number.Uint64(), "hash", header.Hash()) + return true, x.config.XDPoSV2Block.Uint64()/x.config.Epoch + uint64(round)/x.config.Epoch, nil + } + log.Info("[IsEpochSwitch]", "parent round", parentRound, "round", round, "number", header.Number.Uint64(), "hash", header.Hash()) + return parentRound < epochStart, x.config.XDPoSV2Block.Uint64()/x.config.Epoch + uint64(round)/x.config.Epoch, nil +} + +// Given header and its hash, get epoch switch info from the epoch switch block of that epoch, +// header is allow to be nil. +func (x *XDPoS_v2) getEpochSwitchInfo(chain consensus.ChainReader, header *types.Header, hash common.Hash) (*utils.EpochSwitchInfo, error) { + e, ok := x.epochSwitches.Get(hash) + if ok { + log.Debug("[getEpochSwitchInfo] cache hit", "hash", hash.Hex()) + epochSwitchInfo := e.(*utils.EpochSwitchInfo) + return epochSwitchInfo, nil + } + h := header + if h == nil { + log.Debug("[getEpochSwitchInfo] header missing, get header", "hash", hash.Hex()) + h = chain.GetHeaderByHash(hash) + } + isEpochSwitch, _, err := x.IsEpochSwitch(h) + if err != nil { + return nil, err + } + if isEpochSwitch { + log.Debug("[getEpochSwitchInfo] header is epoch switch", "hash", hash.Hex(), "number", h.Number.Uint64()) + masternodes := x.GetMasternodesFromEpochSwitchHeader(h) + // create the epoch switch info and cache it + var decodedExtraField utils.ExtraFields_v2 + err = utils.DecodeBytesExtraFields(h.Extra, &decodedExtraField) + if err != nil { + return nil, err + } + epochSwitchInfo := &utils.EpochSwitchInfo{ + Masternodes: masternodes, + EpochSwitchBlockInfo: &utils.BlockInfo{ + Hash: hash, + Number: h.Number, + Round: decodedExtraField.Round, + }, + EpochSwitchParentBlockInfo: decodedExtraField.QuorumCert.ProposedBlockInfo, + } + x.epochSwitches.Add(hash, epochSwitchInfo) + return epochSwitchInfo, nil + } + epochSwitchInfo, err := x.getEpochSwitchInfo(chain, nil, h.ParentHash) + if err != nil { + log.Error("[getEpochSwitchInfo] recursive error", "err", err, "hash", hash.Hex(), "number", h.Number.Uint64()) + return nil, err + } + log.Debug("[getEpochSwitchInfo] get epoch switch info recursively", "hash", hash.Hex(), "number", h.Number.Uint64()) + x.epochSwitches.Add(hash, epochSwitchInfo) + return epochSwitchInfo, nil +} + +// Given header, get master node from the epoch switch block of that epoch +func (x *XDPoS_v2) GetMasternodes(chain consensus.ChainReader, header *types.Header) []common.Address { + epochSwitchInfo, err := x.getEpochSwitchInfo(chain, header, header.Hash()) + if err != nil { + log.Error("[GetMasternodes] Adaptor v2 getEpochSwitchInfo has error, potentially bug", "err", err) + return []common.Address{} + } + return epochSwitchInfo.Masternodes +} diff --git a/consensus/XDPoS/utils/constants.go b/consensus/XDPoS/utils/constants.go index f2c7490ccc..6b378c41ab 100644 --- a/consensus/XDPoS/utils/constants.go +++ b/consensus/XDPoS/utils/constants.go @@ -15,7 +15,8 @@ var ( 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. + UncleHash = types.CalcUncleHash(nil) // Always Keccak256(RLP([])) as uncles are meaningless outside of PoW. + InmemoryEpochs = 5 * EpochLength // Number of mapping from block to epoch switch infos to keep in memory ) const ( diff --git a/consensus/XDPoS/utils/types.go b/consensus/XDPoS/utils/types.go index 722f4cf8d7..8b56459f20 100644 --- a/consensus/XDPoS/utils/types.go +++ b/consensus/XDPoS/utils/types.go @@ -108,6 +108,12 @@ type ExtraFields_v2 struct { QuorumCert *QuorumCert } +type EpochSwitchInfo struct { + Masternodes []common.Address + EpochSwitchBlockInfo *BlockInfo + EpochSwitchParentBlockInfo *BlockInfo +} + // Encode XDPoS 2.0 extra fields into bytes func (e *ExtraFields_v2) EncodeToBytes() ([]byte, error) { bytes, err := rlp.EncodeToBytes(e) diff --git a/consensus/tests/adaptor_test.go b/consensus/tests/adaptor_test.go index f6ee182151..63891f8340 100644 --- a/consensus/tests/adaptor_test.go +++ b/consensus/tests/adaptor_test.go @@ -3,10 +3,12 @@ package tests import ( "fmt" "math/big" + "reflect" "testing" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/params" "github.com/stretchr/testify/assert" @@ -58,3 +60,126 @@ func TestAdaptorShouldGetAuthorForDifferentConsensusVersion(t *testing.T) { // Make sure the value is exactly the same as from V2 engine assert.Equal(t, addressFromAdaptor, addressFromV2Engine) } + +func TestAdaptorGetMasternodesFromCheckpointHeader(t *testing.T) { + blockchain, _, currentBlock, _, _, _ := PrepareXDCTestBlockChainForV2Engine(t, 1, params.TestXDPoSMockChainConfigWithV2Engine, 0) + adaptor := blockchain.Engine().(*XDPoS.XDPoS) + headerV1 := currentBlock.Header() + headerV1.Extra = common.Hex2Bytes("d7830100018358444388676f312e31352e38856c696e757800000000000000000278c350152e15fa6ffc712a5a73d704ce73e2e103d9e17ae3ff2c6712e44e25b09ac5ee91f6c9ff065551f0dcac6f00cae11192d462db709be3758ccef312ee5eea8d7bad5374c6a652150515d744508b61c1a4deb4e4e7bf057e4e3824c11fd2569bcb77a52905cda63b5a58507910bed335e4c9d87ae0ecdfafd400") + masternodesV1 := adaptor.GetMasternodesFromCheckpointHeader(headerV1, 0, 0) + headerV2 := currentBlock.Header() + headerV2.Number.Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(1)) + headerV2.Validators = common.Hex2Bytes("0278c350152e15fa6ffc712a5a73d704ce73e2e103d9e17ae3ff2c6712e44e25b09ac5ee91f6c9ff065551f0dcac6f00cae11192d462db709be3758c") + masternodesV2 := adaptor.GetMasternodesFromCheckpointHeader(headerV2, 0, 0) + assert.True(t, reflect.DeepEqual(masternodesV1, masternodesV2), "GetMasternodesFromCheckpointHeader in adaptor for v1 v2 not equal", "v1", masternodesV1, "v2", masternodesV2) +} +func TestAdaptorIsEpochSwitch(t *testing.T) { + blockchain, _, currentBlock, _, _, _ := PrepareXDCTestBlockChainForV2Engine(t, 1, params.TestXDPoSMockChainConfigWithV2Engine, 0) + adaptor := blockchain.Engine().(*XDPoS.XDPoS) + header := currentBlock.Header() + // v1 + header.Number.SetUint64(0) + assert.True(t, adaptor.IsEpochSwitch(header), "header should be epoch switch", header) + header.Number.SetUint64(1) + assert.False(t, adaptor.IsEpochSwitch(header), "header should not be epoch switch", header) + // v2 + parentBlockInfo := &utils.BlockInfo{ + Hash: header.ParentHash, + Round: utils.Round(0), + Number: big.NewInt(0).Set(blockchain.Config().XDPoS.XDPoSV2Block), + } + quorumCert := &utils.QuorumCert{ + ProposedBlockInfo: parentBlockInfo, + Signatures: nil, + } + extra := utils.ExtraFields_v2{ + Round: 1, + QuorumCert: quorumCert, + } + extraBytes, err := extra.EncodeToBytes() + assert.Nil(t, err) + header.Extra = extraBytes + header.Number.Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(1)) + assert.True(t, adaptor.IsEpochSwitch(header), "header should be epoch switch", header) + parentBlockInfo = &utils.BlockInfo{ + Hash: header.ParentHash, + Round: utils.Round(1), + Number: big.NewInt(0).Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(1)), + } + quorumCert = &utils.QuorumCert{ + ProposedBlockInfo: parentBlockInfo, + Signatures: nil, + } + extra = utils.ExtraFields_v2{ + Round: 2, + QuorumCert: quorumCert, + } + extraBytes, err = extra.EncodeToBytes() + assert.Nil(t, err) + header.Extra = extraBytes + header.Number.Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(2)) + assert.False(t, adaptor.IsEpochSwitch(header), "header should not be epoch switch", header) + parentBlockInfo = &utils.BlockInfo{ + Hash: header.ParentHash, + Round: utils.Round(blockchain.Config().XDPoS.Epoch) - 1, + Number: big.NewInt(0).Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(100)), + } + quorumCert = &utils.QuorumCert{ + ProposedBlockInfo: parentBlockInfo, + Signatures: nil, + } + extra = utils.ExtraFields_v2{ + Round: utils.Round(blockchain.Config().XDPoS.Epoch) + 1, + QuorumCert: quorumCert, + } + extraBytes, err = extra.EncodeToBytes() + assert.Nil(t, err) + header.Extra = extraBytes + header.Number.Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(101)) + assert.True(t, adaptor.IsEpochSwitch(header), "header should be epoch switch", header) + parentBlockInfo = &utils.BlockInfo{ + Hash: header.ParentHash, + Round: utils.Round(blockchain.Config().XDPoS.Epoch) + 1, + Number: big.NewInt(0).Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(100)), + } + quorumCert = &utils.QuorumCert{ + ProposedBlockInfo: parentBlockInfo, + Signatures: nil, + } + extra = utils.ExtraFields_v2{ + Round: utils.Round(blockchain.Config().XDPoS.Epoch) + 2, + QuorumCert: quorumCert, + } + extraBytes, err = extra.EncodeToBytes() + assert.Nil(t, err) + header.Extra = extraBytes + header.Number.Add(blockchain.Config().XDPoS.XDPoSV2Block, big.NewInt(101)) + assert.False(t, adaptor.IsEpochSwitch(header), "header should not be epoch switch", header) +} + +func TestAdaptorGetMasternodesV2(t *testing.T) { + // we skip test for v1 since it's hard to make a real genesis block + blockchain, _, currentBlock, signer, signFn, _ := PrepareXDCTestBlockChainForV2Engine(t, 10, params.TestXDPoSMockChainConfigWithV2Engine, 0) + adaptor := blockchain.Engine().(*XDPoS.XDPoS) + blockNum := 11 + blockCoinBase := "0x111000000000000000000000000000000123" + blockHeader := createBlock(params.TestXDPoSMockChainConfigWithV2Engine, currentBlock, blockNum, 1, blockCoinBase, signer, signFn) + // it contains 3 master nodes + blockHeader.Validators = common.Hex2Bytes("0278c350152e15fa6ffc712a5a73d704ce73e2e103d9e17ae3ff2c6712e44e25b09ac5ee91f6c9ff065551f0dcac6f00cae11192d462db709be3758c") + // block 11 is the first v2 block, and is treated as epoch switch block + currentBlock, err := insertBlock(blockchain, blockHeader) + if err != nil { + t.Fatal(err) + } + masternodes1 := adaptor.GetMasternodes(blockchain, currentBlock.Header()) + assert.Equal(t, 3, len(masternodes1)) + for blockNum = 12; blockNum < 15; blockNum++ { + blockHeader = createBlock(params.TestXDPoSMockChainConfigWithV2Engine, currentBlock, blockNum, int64(blockNum-10), blockCoinBase, signer, signFn) + currentBlock, err = insertBlock(blockchain, blockHeader) + if err != nil { + t.Fatal(err) + } + masternodes2 := adaptor.GetMasternodes(blockchain, currentBlock.Header()) + assert.True(t, reflect.DeepEqual(masternodes1, masternodes2), "at block number", blockNum) + } +} diff --git a/consensus/tests/test_helper.go b/consensus/tests/test_helper.go index c711502131..746c603ef5 100644 --- a/consensus/tests/test_helper.go +++ b/consensus/tests/test_helper.go @@ -488,6 +488,7 @@ func createXDPoSTestBlock(bc *BlockChain, customHeader *types.Header, txs []*typ Time: big.NewInt(customHeader.Number.Int64() * 10), Extra: customHeader.Extra, Validator: customHeader.Validator, + Validators: customHeader.Validators, } var block *types.Block if len(txs) == 0 { diff --git a/contracts/utils.go b/contracts/utils.go index d7a244b3f2..66ec359163 100644 --- a/contracts/utils.go +++ b/contracts/utils.go @@ -352,6 +352,7 @@ func GetRewardForCheckpoint(c *XDPoS.XDPoS, chain consensus.ChainReader, header } } header = chain.GetHeader(header.ParentHash, prevCheckpoint) + //TODO: i think this should be c.GetMasternodesFrom... masternodes := utils.GetMasternodesFromCheckpointHeader(header) for i := startBlockNumber; i <= endBlockNumber; i++ {