From 71b9005f3434488c9a3330f74ce91bdaed29d6fe Mon Sep 17 00:00:00 2001 From: wgr523 Date: Mon, 28 Oct 2024 15:14:30 +0800 Subject: [PATCH] feat: add api xdpos_getBlockInfoByEpochNum (#674) * feat: add api xdpos_getBlockInfoByEpochNum * feat: add cache round2epochBlockInfo * fix: round2epochBlockInfo contains round now * feat: binary search in GetBlockByEpochNumber * fix: change some code back, refine style --- consensus/XDPoS/api.go | 16 +++ consensus/XDPoS/engines/engine_v2/engine.go | 7 + .../XDPoS/engines/engine_v2/epochSwitch.go | 13 +- consensus/XDPoS/engines/engine_v2/utils.go | 122 ++++++++++++++++++ consensus/XDPoS/utils/constants.go | 2 + consensus/XDPoS/utils/types.go | 7 + consensus/tests/engine_v2_tests/api_test.go | 59 +++++++++ internal/web3ext/web3ext.go | 5 + 8 files changed, 230 insertions(+), 1 deletion(-) diff --git a/consensus/XDPoS/api.go b/consensus/XDPoS/api.go index fd06703128..287e37b7f4 100644 --- a/consensus/XDPoS/api.go +++ b/consensus/XDPoS/api.go @@ -344,3 +344,19 @@ func (api *API) GetEpochNumbersBetween(begin, end *rpc.BlockNumber) ([]uint64, e } return epochSwitchNumbers, nil } + +/* +An API exclusively for V2 consensus, designed to assist in getting rewards of the epoch number. +Given the epoch number, search the epoch switch block. +*/ +func (api *API) GetBlockInfoByEpochNum(epochNumber uint64) (*utils.EpochNumInfo, error) { + result, err := api.XDPoS.EngineV2.GetBlockByEpochNumber(api.chain, epochNumber) + if err != nil { + return nil, err + } + return &utils.EpochNumInfo{ + EpochBlockHash: result.Hash, + EpochRound: result.Round, + EpochBlockNumber: result.Number, + }, nil +} diff --git a/consensus/XDPoS/engines/engine_v2/engine.go b/consensus/XDPoS/engines/engine_v2/engine.go index f2e4a5e4d1..dafc87fbd8 100644 --- a/consensus/XDPoS/engines/engine_v2/engine.go +++ b/consensus/XDPoS/engines/engine_v2/engine.go @@ -37,6 +37,10 @@ type XDPoS_v2 struct { epochSwitches *lru.ARCCache // infos of epoch: master nodes, epoch switch block info, parent of that info verifiedHeaders *lru.ARCCache + // only contains epoch switch block info + // input: round, output: infos of epoch switch block and next epoch switch block info + round2epochBlockInfo *lru.ARCCache + 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 @@ -77,6 +81,7 @@ func New(chainConfig *params.ChainConfig, db ethdb.Database, minePeriodCh chan i signatures, _ := lru.NewARC(utils.InmemorySnapshots) epochSwitches, _ := lru.NewARC(int(utils.InmemoryEpochs)) verifiedHeaders, _ := lru.NewARC(utils.InmemorySnapshots) + round2epochBlockInfo, _ := lru.NewARC(utils.InmemoryRound2Epochs) timeoutPool := utils.NewPool() votePool := utils.NewPool() @@ -96,6 +101,8 @@ func New(chainConfig *params.ChainConfig, db ethdb.Database, minePeriodCh chan i BroadcastCh: make(chan interface{}), minePeriodCh: minePeriodCh, + round2epochBlockInfo: round2epochBlockInfo, + timeoutPool: timeoutPool, votePool: votePool, diff --git a/consensus/XDPoS/engines/engine_v2/epochSwitch.go b/consensus/XDPoS/engines/engine_v2/epochSwitch.go index abb35bde09..e9d76daa62 100644 --- a/consensus/XDPoS/engines/engine_v2/epochSwitch.go +++ b/consensus/XDPoS/engines/engine_v2/epochSwitch.go @@ -56,7 +56,6 @@ func (x *XDPoS_v2) getEpochSwitchInfo(chain consensus.ChainReader, header *types log.Error("[getEpochSwitchInfo] get extra field", "err", err, "number", h.Number.Uint64()) return nil, err } - snap, err := x.getSnapshot(chain, h.Number.Uint64(), false) if err != nil { log.Error("[getEpochSwitchInfo] Adaptor v2 getSnapshot has error", "err", err) @@ -155,6 +154,14 @@ func (x *XDPoS_v2) IsEpochSwitch(header *types.Header) (bool, uint64, error) { return true, epochNum, nil } log.Debug("[IsEpochSwitch]", "is", parentRound < epochStartRound, "parentRound", parentRound, "round", round, "number", header.Number.Uint64(), "epochNum", epochNum, "hash", header.Hash()) + // if isEpochSwitch, add to cache + if parentRound < epochStartRound { + x.round2epochBlockInfo.Add(round, &types.BlockInfo{ + Hash: header.Hash(), + Number: header.Number, + Round: round, + }) + } return parentRound < epochStartRound, epochNum, nil } @@ -175,6 +182,10 @@ func (x *XDPoS_v2) GetEpochSwitchInfoBetween(chain consensus.ChainReader, begin, return nil, err } iteratorHeader = nil + // V2 switch epoch switch info has nil parent + if epochSwitchInfo.EpochSwitchParentBlockInfo == nil { + break + } iteratorHash = epochSwitchInfo.EpochSwitchParentBlockInfo.Hash iteratorNum = epochSwitchInfo.EpochSwitchBlockInfo.Number if iteratorNum.Cmp(begin.Number) >= 0 { diff --git a/consensus/XDPoS/engines/engine_v2/utils.go b/consensus/XDPoS/engines/engine_v2/utils.go index 92da8d0f7d..2e3486156d 100644 --- a/consensus/XDPoS/engines/engine_v2/utils.go +++ b/consensus/XDPoS/engines/engine_v2/utils.go @@ -3,6 +3,7 @@ package engine_v2 import ( "errors" "fmt" + "math/big" "github.com/XinFinOrg/XDPoSChain/accounts" "github.com/XinFinOrg/XDPoSChain/common" @@ -218,3 +219,124 @@ func (x *XDPoS_v2) CalculateMissingRounds(chain consensus.ChainReader, header *t return missedRoundsMetadata, nil } + +func (x *XDPoS_v2) getBlockByEpochNumberInCache(chain consensus.ChainReader, estRound types.Round) *types.BlockInfo { + epochSwitchInCache := make([]*types.BlockInfo, 0) + for r := estRound; r < estRound+types.Round(x.config.Epoch); r++ { + info, ok := x.round2epochBlockInfo.Get(r) + if ok { + blockInfo := info.(*types.BlockInfo) + epochSwitchInCache = append(epochSwitchInCache, blockInfo) + } + } + if len(epochSwitchInCache) == 1 { + return epochSwitchInCache[0] + } else if len(epochSwitchInCache) == 0 { + return nil + } + // when multiple cache hits, need to find the one in main chain + for _, blockInfo := range epochSwitchInCache { + header := chain.GetHeaderByNumber(blockInfo.Number.Uint64()) + if header == nil { + continue + } + if header.Hash() == blockInfo.Hash { + return blockInfo + } + } + return nil +} + +func (x *XDPoS_v2) binarySearchBlockByEpochNumber(chain consensus.ChainReader, targetEpochNum uint64, start, end uint64) (*types.BlockInfo, error) { + // `end` must be larger than the target and `start` could be the target + for start < end { + header := chain.GetHeaderByNumber((start + end) / 2) + if header == nil { + return nil, errors.New("header nil in binary search") + } + isEpochSwitch, epochNum, err := x.IsEpochSwitch(header) + if err != nil { + return nil, err + } + if epochNum == targetEpochNum { + _, round, _, err := x.getExtraFields(header) + if err != nil { + return nil, err + } + if isEpochSwitch { + return &types.BlockInfo{ + Hash: header.Hash(), + Round: round, + Number: header.Number, + }, nil + } else { + end = header.Number.Uint64() + // trick to shorten the search + estStart := end - uint64(round)%x.config.Epoch + if start < estStart { + start = estStart + } + } + } else if epochNum > targetEpochNum { + end = header.Number.Uint64() + } else if epochNum < targetEpochNum { + // if start keeps the same, means no result and the search is over + nextStart := header.Number.Uint64() + if nextStart == start { + break + } + start = nextStart + } + } + return nil, errors.New("no epoch switch header in binary search (all rounds in this epoch are missed, which is very rare)") +} + +func (x *XDPoS_v2) GetBlockByEpochNumber(chain consensus.ChainReader, targetEpochNum uint64) (*types.BlockInfo, error) { + currentHeader := chain.CurrentHeader() + epochSwitchInfo, err := x.getEpochSwitchInfo(chain, currentHeader, currentHeader.Hash()) + if err != nil { + return nil, err + } + epochNum := x.config.V2.SwitchBlock.Uint64()/x.config.Epoch + uint64(epochSwitchInfo.EpochSwitchBlockInfo.Round)/x.config.Epoch + // if current epoch is this epoch, we early return the result + if targetEpochNum == epochNum { + return epochSwitchInfo.EpochSwitchBlockInfo, nil + } + if targetEpochNum > epochNum { + return nil, errors.New("input epoch number > current epoch number") + } + if targetEpochNum < x.config.V2.SwitchBlock.Uint64()/x.config.Epoch { + return nil, errors.New("input epoch number < v2 begin epoch number") + } + // the block's round should be in [estRound,estRound+Epoch-1] + estRound := types.Round((targetEpochNum - x.config.V2.SwitchBlock.Uint64()/x.config.Epoch) * x.config.Epoch) + // check the round2epochBlockInfo cache + blockInfo := x.getBlockByEpochNumberInCache(chain, estRound) + if blockInfo != nil { + return blockInfo, nil + } + // if cache miss, we do search + epoch := big.NewInt(int64(x.config.Epoch)) + estblockNumDiff := new(big.Int).Mul(epoch, big.NewInt(int64(epochNum-targetEpochNum))) + estBlockNum := new(big.Int).Sub(epochSwitchInfo.EpochSwitchBlockInfo.Number, estblockNumDiff) + if estBlockNum.Cmp(x.config.V2.SwitchBlock) == -1 { + estBlockNum.Set(x.config.V2.SwitchBlock) + } + // if the targrt is close, we search brute-forcily + closeEpochNum := uint64(2) + if closeEpochNum >= epochNum-targetEpochNum { + estBlockHeader := chain.GetHeaderByNumber(estBlockNum.Uint64()) + epochSwitchInfos, err := x.GetEpochSwitchInfoBetween(chain, estBlockHeader, currentHeader) + if err != nil { + return nil, err + } + for _, info := range epochSwitchInfos { + epochNum := x.config.V2.SwitchBlock.Uint64()/x.config.Epoch + uint64(info.EpochSwitchBlockInfo.Round)/x.config.Epoch + if epochNum == targetEpochNum { + return info.EpochSwitchBlockInfo, nil + } + } + } + // else, we use binary search + return x.binarySearchBlockByEpochNumber(chain, targetEpochNum, estBlockNum.Uint64(), epochSwitchInfo.EpochSwitchBlockInfo.Number.Uint64()) +} diff --git a/consensus/XDPoS/utils/constants.go b/consensus/XDPoS/utils/constants.go index c8df06cf46..6cef0034a0 100644 --- a/consensus/XDPoS/utils/constants.go +++ b/consensus/XDPoS/utils/constants.go @@ -17,6 +17,8 @@ var ( 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 + + InmemoryRound2Epochs = 65536 // Number of mapping of epoch switch blocks for quickly locating epoch switch block. One epoch ~ 0.5hours, so 65536 epochs ~ 3.7 years. And it uses ~ 10MB memory. ) const ( diff --git a/consensus/XDPoS/utils/types.go b/consensus/XDPoS/utils/types.go index 897e984b48..96817c294e 100644 --- a/consensus/XDPoS/utils/types.go +++ b/consensus/XDPoS/utils/types.go @@ -71,3 +71,10 @@ type PublicApiMissedRoundsMetadata struct { EpochBlockNumber *big.Int MissedRounds []MissedRoundInfo } + +// Given an epoch number, this struct records the epoch switch block (first block in epoch) infos such as block number +type EpochNumInfo struct { + EpochBlockHash common.Hash `json:"hash"` + EpochRound types.Round `json:"round"` + EpochBlockNumber *big.Int `json:"number"` +} diff --git a/consensus/tests/engine_v2_tests/api_test.go b/consensus/tests/engine_v2_tests/api_test.go index aa482c5e73..0b1258d2a4 100644 --- a/consensus/tests/engine_v2_tests/api_test.go +++ b/consensus/tests/engine_v2_tests/api_test.go @@ -168,3 +168,62 @@ func TestGetEpochNumbersBetween(t *testing.T) { assert.Nil(t, numbers) assert.EqualError(t, err, "illegal begin block number") } +func TestGetBlockByEpochNumber(t *testing.T) { + blockchain, _, currentBlock, signer, signFn := PrepareXDCTestBlockChainWithPenaltyForV2Engine(t, 1802, params.TestXDPoSMockChainConfig) + + blockCoinBase := "0x111000000000000000000000000000000123" + largeRound := int64(1802) + newBlock := CreateBlock(blockchain, params.TestXDPoSMockChainConfig, currentBlock, int(currentBlock.NumberU64())+1, largeRound, blockCoinBase, signer, signFn, nil, nil, currentBlock.Header().Root.Hex()) + err := blockchain.InsertBlock(newBlock) + assert.Nil(t, err) + largeRound2 := int64(3603) + newBlock2 := CreateBlock(blockchain, params.TestXDPoSMockChainConfig, newBlock, int(newBlock.NumberU64())+1, largeRound2, blockCoinBase, signer, signFn, nil, nil, newBlock.Header().Root.Hex()) + err = blockchain.InsertBlock(newBlock2) + assert.Nil(t, err) + + // block num, round, epoch is as follows + // 900,0,1 (v2 switch block, not v2 epoch switch block) + // 901,1,1 (1st epoch switch block) + // 902,2,1 + // ... + // 1800,900,2 (2nd epoch switch block) + // 1801,901,2 + // 1802,902,2 + // 1803,1802,3 (epoch switch) + // epoch 4 has no block + // 1804,3603,5 (epoch switch) + engine := blockchain.Engine().(*XDPoS.XDPoS) + + // init the snapshot, otherwise getEpochSwitchInfo would return error + checkpointHeader := blockchain.GetHeaderByNumber(blockchain.Config().XDPoS.V2.SwitchBlock.Uint64() + 1) + err = engine.Initial(blockchain, checkpointHeader) + assert.Nil(t, err) + + info, err := engine.APIs(blockchain)[0].Service.(*XDPoS.API).GetBlockInfoByEpochNum(0) + assert.NotNil(t, err) + assert.Nil(t, info) + + info, err = engine.APIs(blockchain)[0].Service.(*XDPoS.API).GetBlockInfoByEpochNum(1) + assert.Equal(t, info.EpochRound, types.Round(1)) + assert.Nil(t, err) + + info, err = engine.APIs(blockchain)[0].Service.(*XDPoS.API).GetBlockInfoByEpochNum(2) + assert.Equal(t, info.EpochRound, types.Round(900)) + assert.Nil(t, err) + + info, err = engine.APIs(blockchain)[0].Service.(*XDPoS.API).GetBlockInfoByEpochNum(3) + assert.Equal(t, info.EpochRound, types.Round(largeRound)) + assert.Nil(t, err) + + info, err = engine.APIs(blockchain)[0].Service.(*XDPoS.API).GetBlockInfoByEpochNum(4) + assert.NotNil(t, err) + assert.Nil(t, info) + + info, err = engine.APIs(blockchain)[0].Service.(*XDPoS.API).GetBlockInfoByEpochNum(5) + assert.Equal(t, info.EpochRound, types.Round(largeRound2)) + assert.Nil(t, err) + + info, err = engine.APIs(blockchain)[0].Service.(*XDPoS.API).GetBlockInfoByEpochNum(6) + assert.NotNil(t, err) + assert.Nil(t, info) +} diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 688f8df33a..1921ad6099 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -168,6 +168,11 @@ web3._extend({ params: 2, inputFormatter: [web3._extend.formatters.inputBlockNumberFormatter, web3._extend.formatters.inputBlockNumberFormatter] }), + new web3._extend.Method({ + name: 'getBlockInfoByEpochNum', + call: 'XDPoS_getBlockInfoByEpochNum', + params: 1, + }), ], properties: [ new web3._extend.Property({