diff --git a/consensus/XDPoS/XDPoS.go b/consensus/XDPoS/XDPoS.go index d03f324d4c..26cadc3555 100644 --- a/consensus/XDPoS/XDPoS.go +++ b/consensus/XDPoS/XDPoS.go @@ -433,6 +433,15 @@ func (x *XDPoS) GetCurrentEpochSwitchBlock(chain consensus.ChainReader, blockNum } } +func (x *XDPoS) CalculateMissingRounds(chain consensus.ChainReader, header *types.Header) (*utils.PublicApiMissedRoundsMetadata, error) { + switch x.config.BlockConsensusVersion(header.Number, header.Extra, ExtraFieldCheck) { + case params.ConsensusEngineVersion2: + return x.EngineV2.CalculateMissingRounds(chain, header) + default: // Default "v1" + return nil, fmt.Errorf("Not supported in the v1 consensus") + } +} + // Same DB across all consensus engines func (x *XDPoS) GetDb() ethdb.Database { return x.db diff --git a/consensus/XDPoS/api.go b/consensus/XDPoS/api.go index 768e8b9a5c..a922e57f78 100644 --- a/consensus/XDPoS/api.go +++ b/consensus/XDPoS/api.go @@ -224,16 +224,7 @@ func (api *API) GetV2BlockByHeader(header *types.Header, uncle bool) *V2BlockInf } func (api *API) GetV2BlockByNumber(number *rpc.BlockNumber) *V2BlockInfo { - var header *types.Header - if number == nil || *number == rpc.LatestBlockNumber { - header = api.chain.CurrentHeader() - } else if *number == rpc.CommittedBlockNumber { - hash := api.XDPoS.EngineV2.GetLatestCommittedBlockInfo().Hash - header = api.chain.GetHeaderByHash(hash) - } else { - header = api.chain.GetHeaderByNumber(uint64(number.Int64())) - } - + header := api.getHeaderFromApiBlockNum(number) if header == nil { return &V2BlockInfo{ Number: big.NewInt(number.Int64()), @@ -285,6 +276,26 @@ func (api *API) NetworkInformation() NetworkInformation { return info } +/* +An API exclusively for V2 consensus, designed to assist in troubleshooting miners by identifying who mined during their allocated term. +*/ +func (api *API) GetMissedRoundsInEpochByBlockNum(number *rpc.BlockNumber) (*utils.PublicApiMissedRoundsMetadata, error) { + return api.XDPoS.CalculateMissingRounds(api.chain, api.getHeaderFromApiBlockNum(number)) +} + +func (api *API) getHeaderFromApiBlockNum(number *rpc.BlockNumber) *types.Header { + var header *types.Header + if number == nil || *number == rpc.LatestBlockNumber { + header = api.chain.CurrentHeader() + } else if *number == rpc.CommittedBlockNumber { + hash := api.XDPoS.EngineV2.GetLatestCommittedBlockInfo().Hash + header = api.chain.GetHeaderByHash(hash) + } else { + header = api.chain.GetHeaderByNumber(uint64(number.Int64())) + } + return header +} + func calculateSigners(message map[string]SignerTypes, pool map[string]map[common.Hash]utils.PoolObj, masternodes []common.Address) { for name, objs := range pool { var currentSigners []common.Address diff --git a/consensus/XDPoS/engines/engine_v2/engine.go b/consensus/XDPoS/engines/engine_v2/engine.go index 3e7eabd8df..cedeaa2f0b 100644 --- a/consensus/XDPoS/engines/engine_v2/engine.go +++ b/consensus/XDPoS/engines/engine_v2/engine.go @@ -360,7 +360,7 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er } if header.Coinbase != signer { - log.Error("[Prepare] The mined blocker header coinbase address mismatch with waller address", "headerCoinbase", header.Coinbase.Hex(), "WalletAddress", signer.Hex()) + log.Error("[Prepare] The mined blocker header coinbase address mismatch with wallet address", "headerCoinbase", header.Coinbase.Hex(), "WalletAddress", signer.Hex()) return consensus.ErrCoinbaseMismatch } diff --git a/consensus/XDPoS/engines/engine_v2/utils.go b/consensus/XDPoS/engines/engine_v2/utils.go index 1c3497ad60..e54b2b59b0 100644 --- a/consensus/XDPoS/engines/engine_v2/utils.go +++ b/consensus/XDPoS/engines/engine_v2/utils.go @@ -163,3 +163,54 @@ func (x *XDPoS_v2) GetSignersFromSnapshot(chain consensus.ChainReader, header *t snap, err := x.getSnapshot(chain, header.Number.Uint64(), false) return snap.NextEpochMasterNodes, err } + +func (x *XDPoS_v2) CalculateMissingRounds(chain consensus.ChainReader, header *types.Header) (*utils.PublicApiMissedRoundsMetadata, error) { + var missedRounds []utils.MissedRoundInfo + switchInfo, err := x.getEpochSwitchInfo(chain, header, header.Hash()) + if err != nil { + return nil, err + } + masternodes := switchInfo.Masternodes + + // Loop through from the epoch switch block to the current "header" block + nextHeader := header + for nextHeader.Number.Cmp(switchInfo.EpochSwitchBlockInfo.Number) > 0 { + parentHeader := chain.GetHeaderByHash(nextHeader.ParentHash) + parentRound, err := x.GetRoundNumber(parentHeader) + if err != nil { + return nil, err + } + currRound, err := x.GetRoundNumber(nextHeader) + if err != nil { + return nil, err + } + // This indicates that an increment in the round number is missing during the block production process. + if parentRound+1 != currRound { + // We need to iterate from the parentRound to the currRound to determine which miner did not perform mining. + for i := parentRound + 1; i < currRound; i++ { + leaderIndex := uint64(i) % x.config.Epoch % uint64(len(masternodes)) + whosTurn := masternodes[leaderIndex] + missedRounds = append( + missedRounds, + utils.MissedRoundInfo{ + Round: i, + Miner: whosTurn, + CurrentBlockHash: nextHeader.Hash(), + CurrentBlockNum: nextHeader.Number, + ParentBlockHash: parentHeader.Hash(), + ParentBlockNum: parentHeader.Number, + }, + ) + } + } + // Assign the pointer to the next one + nextHeader = parentHeader + } + missedRoundsMetadata := &utils.PublicApiMissedRoundsMetadata{ + EpochRound: switchInfo.EpochSwitchBlockInfo.Round, + EpochBlockNumber: switchInfo.EpochSwitchBlockInfo.Number, + MissedRounds: missedRounds, + } + + return missedRoundsMetadata, nil +} diff --git a/consensus/XDPoS/utils/types.go b/consensus/XDPoS/utils/types.go index 4f55973cfd..4073fb522b 100644 --- a/consensus/XDPoS/utils/types.go +++ b/consensus/XDPoS/utils/types.go @@ -57,3 +57,17 @@ type PublicApiSnapshot struct { Votes []*clique.Vote `json:"votes"` // List of votes cast in chronological order Tally map[common.Address]clique.Tally `json:"tally"` // Current vote tally to avoid recalculating } + +type MissedRoundInfo struct { + Round types.Round + Miner common.Address + CurrentBlockHash common.Hash + CurrentBlockNum *big.Int + ParentBlockHash common.Hash + ParentBlockNum *big.Int +} +type PublicApiMissedRoundsMetadata struct { + EpochRound types.Round + EpochBlockNumber *big.Int + MissedRounds []MissedRoundInfo +} diff --git a/consensus/tests/api_test.go b/consensus/tests/api_test.go index 3d6318151c..ed3e0897d0 100644 --- a/consensus/tests/api_test.go +++ b/consensus/tests/api_test.go @@ -18,7 +18,6 @@ var ( ) func TestConfigApi(t *testing.T) { - bc := backends.NewXDCSimulatedBackend(core.GenesisAlloc{ voterAddr: {Balance: new(big.Int).SetUint64(10000000000)}, }, 10000000, params.TestXDPoSMockChainConfig) diff --git a/consensus/tests/engine_v2_tests/api_test.go b/consensus/tests/engine_v2_tests/api_test.go new file mode 100644 index 0000000000..fb99f6f879 --- /dev/null +++ b/consensus/tests/engine_v2_tests/api_test.go @@ -0,0 +1,111 @@ +package engine_v2_tests + +import ( + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/XinFinOrg/XDPoSChain/rpc" + "github.com/stretchr/testify/assert" +) + +func TestGetMissedRoundsInEpochByBlockNumOnlyForV2Consensus(t *testing.T) { + _, bc, _, _, _ := PrepareXDCTestBlockChainWith128Candidates(t, 1802, params.TestXDPoSMockChainConfig) + + engine := bc.GetBlockChain().Engine().(*XDPoS.XDPoS) + blockNum := rpc.BlockNumber(123) + + data, err := engine.APIs(bc.GetBlockChain())[0].Service.(*XDPoS.API).GetMissedRoundsInEpochByBlockNum(&blockNum) + + assert.EqualError(t, err, "Not supported in the v1 consensus") + assert.Nil(t, data) +} + +func TestGetMissedRoundsInEpochByBlockNumReturnEmptyForV2(t *testing.T) { + _, bc, cb, _, _ := PrepareXDCTestBlockChainWith128Candidates(t, 1802, params.TestXDPoSMockChainConfig) + + engine := bc.GetBlockChain().Engine().(*XDPoS.XDPoS) + blockNum := rpc.BlockNumber(cb.NumberU64()) + + data, err := engine.APIs(bc.GetBlockChain())[0].Service.(*XDPoS.API).GetMissedRoundsInEpochByBlockNum(&blockNum) + + assert.Nil(t, err) + assert.Equal(t, types.Round(900), data.EpochRound) + assert.Equal(t, big.NewInt(1800), data.EpochBlockNumber) + assert.Equal(t, 0, len(data.MissedRounds)) + + blockNum = rpc.BlockNumber(1800) + + data, err = engine.APIs(bc.GetBlockChain())[0].Service.(*XDPoS.API).GetMissedRoundsInEpochByBlockNum(&blockNum) + + assert.Nil(t, err) + assert.Equal(t, types.Round(900), data.EpochRound) + assert.Equal(t, big.NewInt(1800), data.EpochBlockNumber) + assert.Equal(t, 0, len(data.MissedRounds)) + + blockNum = rpc.BlockNumber(1801) + + data, err = engine.APIs(bc.GetBlockChain())[0].Service.(*XDPoS.API).GetMissedRoundsInEpochByBlockNum(&blockNum) + + assert.Nil(t, err) + assert.Equal(t, types.Round(900), data.EpochRound) + assert.Equal(t, big.NewInt(1800), data.EpochBlockNumber) + assert.Equal(t, 0, len(data.MissedRounds)) +} + +func TestGetMissedRoundsInEpochByBlockNumReturnEmptyForV2FistEpoch(t *testing.T) { + _, bc, _, _, _ := PrepareXDCTestBlockChainWith128Candidates(t, 1802, params.TestXDPoSMockChainConfig) + + engine := bc.GetBlockChain().Engine().(*XDPoS.XDPoS) + blockNum := rpc.BlockNumber(901) + + data, err := engine.APIs(bc.GetBlockChain())[0].Service.(*XDPoS.API).GetMissedRoundsInEpochByBlockNum(&blockNum) + + assert.Nil(t, err) + assert.Equal(t, types.Round(1), data.EpochRound) + assert.Equal(t, big.NewInt(901), data.EpochBlockNumber) + assert.Equal(t, 0, len(data.MissedRounds)) +} + +func TestGetMissedRoundsInEpochByBlockNum(t *testing.T) { + blockchain, bc, currentBlock, signer, signFn := PrepareXDCTestBlockChainWith128Candidates(t, 1802, params.TestXDPoSMockChainConfig) + chainConfig := params.TestXDPoSMockChainConfig + engine := bc.GetBlockChain().Engine().(*XDPoS.XDPoS) + blockCoinBase := signer.Hex() + + startingBlockNum := currentBlock.Number().Int64() + 1 + // Skipped the round + roundNumber := startingBlockNum - chainConfig.XDPoS.V2.SwitchBlock.Int64() + 2 + block := CreateBlock(blockchain, chainConfig, currentBlock, int(startingBlockNum), roundNumber, blockCoinBase, signer, signFn, nil, nil, "b345a8560bd51926803dd17677c9f0751193914a851a4ec13063d6bf50220b53") + err := blockchain.InsertBlock(block) + if err != nil { + t.Fatal(err) + } + + // Update Signer as there is no previous signer assigned + err = UpdateSigner(blockchain) + if err != nil { + t.Fatal(err) + } + + blockNum := rpc.BlockNumber(1803) + + data, err := engine.APIs(bc.GetBlockChain())[0].Service.(*XDPoS.API).GetMissedRoundsInEpochByBlockNum(&blockNum) + + assert.Nil(t, err) + assert.Equal(t, types.Round(900), data.EpochRound) + assert.Equal(t, big.NewInt(1800), data.EpochBlockNumber) + assert.Equal(t, 2, len(data.MissedRounds)) + assert.NotEmpty(t, data.MissedRounds[0].Miner) + assert.Equal(t, data.MissedRounds[0].Round, types.Round(903)) + assert.Equal(t, data.MissedRounds[0].CurrentBlockNum, big.NewInt(1803)) + assert.Equal(t, data.MissedRounds[0].ParentBlockNum, big.NewInt(1802)) + assert.NotEmpty(t, data.MissedRounds[1].Miner) + assert.Equal(t, data.MissedRounds[1].Round, types.Round(904)) + assert.Equal(t, data.MissedRounds[0].CurrentBlockNum, big.NewInt(1803)) + assert.Equal(t, data.MissedRounds[0].ParentBlockNum, big.NewInt(1802)) + + assert.NotEqual(t, data.MissedRounds[0].Miner, data.MissedRounds[1].Miner) +} diff --git a/consensus/tests/engine_v2_tests/helper.go b/consensus/tests/engine_v2_tests/helper.go index 854eebd124..9b86fbcda4 100644 --- a/consensus/tests/engine_v2_tests/helper.go +++ b/consensus/tests/engine_v2_tests/helper.go @@ -545,6 +545,18 @@ func PrepareXDCTestBlockChainWith128Candidates(t *testing.T, numOfBlocks int, ch if err != nil { t.Fatal(err) } + + // First v2 block + if (int64(i) - chainConfig.XDPoS.V2.SwitchBlock.Int64()) == 1 { + lastv1BlockNumber := block.Header().Number.Uint64() - 1 + checkpointBlockNumber := lastv1BlockNumber - lastv1BlockNumber%chainConfig.XDPoS.Epoch + checkpointHeader := blockchain.GetHeaderByNumber(checkpointBlockNumber) + err := engine.EngineV2.Initial(blockchain, checkpointHeader) + if err != nil { + panic(err) + } + } + currentBlock = block } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index f80b2b6e0a..c07cf978c8 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -156,6 +156,12 @@ web3._extend({ name: 'getLatestPoolStatus', call: 'XDPoS_getLatestPoolStatus' }), + new web3._extend.Method({ + name: 'getMissedRoundsInEpochByBlockNum', + call: 'XDPoS_getMissedRoundsInEpochByBlockNum', + params: 1, + inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter] + }), ], properties: [ new web3._extend.Property({