Repository: iotexproject/iotex-core
Pull request #4593: Forked ChainManager
Author: login: envestcc
Labels: name: 3s block
Created: 2025-03-26T03:35:21Z | Merged: 2025-04-15T07:42:57Z

# Description

Changes for ChainManager:
- implemented its own `MintNewBlock` method and caches the minted blocks.
- can fork a chain at specifying the block hash.

base #4590 and #4581

## Type of change
Please delete options that are not relevant.
- [] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [] Code refactor or improvement
- [] Breaking change (fix or feature that would cause a new or changed behavior of existing functionality)
- [] This change requires a documentation update

# How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [] make test
- [] fullsync
- [] Other test (please specify)

**Test Configuration**:
- Firmware version:
- Hardware:
- Toolchain:
- SDK:

# Checklist:
- [] My code follows the style guidelines of this project
- [] I have performed a self-review of my code
- [] I have commented my code, particularly in hard-to-understand areas
- [] I have made corresponding changes to the documentation
- [] My changes generate no new warnings
- [] I have added tests that prove my fix is effective or that my feature works
- [] New and existing unit tests pass locally with my changes
- [] Any dependent changes have been merged and published in downstream modules



Diff:
diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go
index e431c6b61b..065bd32bb6 100644
--- a/blockchain/blockchain.go
+++ b/blockchain/blockchain.go
@@ -454,10 +454,7 @@ func (bc *blockchain) MintNewBlock(timestamp time.Time, opts ...MintOption) (*bl
 	ctx = bc.contextWithBlock(ctx, minterAddress, newblockHeight, timestamp, protocol.CalcBaseFee(genesis.MustExtractGenesisContext(ctx).Blockchain, &tip), protocol.CalcExcessBlobGas(tip.ExcessBlobGas, tip.BlobGasUsed))
 	ctx = protocol.WithFeatureCtx(ctx)
 	// run execution and update state trie root hash
-	blk, err := bc.bbf.Mint(
-		ctx,
-		producerPrivateKey,
-	)
+	blk, err := bc.bbf.Mint(ctx, producerPrivateKey)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to create block")
 	}
diff --git a/chainservice/builder.go b/chainservice/builder.go
index 262b355411..28783c0321 100644
--- a/chainservice/builder.go
+++ b/chainservice/builder.go
@@ -513,8 +513,8 @@ func (builder *Builder) createBlockchain(forSubChain, forTest bool) blockchain.B
 	if builder.cfg.Consensus.Scheme == config.RollDPoSScheme {
 		mintOpts = append(mintOpts, factory.WithTimeoutOption(builder.cfg.Chain.MintTimeout))
 	}
-	minter := factory.NewMinter(builder.cs.factory, builder.cs.actpool, mintOpts...)
-	return blockchain.NewBlockchain(builder.cfg.Chain, builder.cfg.Genesis, builder.cs.blockdao, minter, chainOpts...)
+	builder.cs.minter = factory.NewMinter(builder.cs.factory, builder.cs.actpool, mintOpts...)
+	return blockchain.NewBlockchain(builder.cfg.Chain, builder.cfg.Genesis, builder.cs.blockdao, builder.cs.minter, chainOpts...)
 }
 
 func (builder *Builder) buildNodeInfoManager() error {
@@ -786,6 +786,7 @@ func (builder *Builder) buildConsensusComponent() error {
 	}
 	if rDPoSProtocol := rolldpos.FindProtocol(builder.cs.registry); rDPoSProtocol != nil {
 		copts = append(copts, consensus.WithRollDPoSProtocol(rDPoSProtocol))
+		copts = append(copts, consensus.WithBlockBuilderFactory(builder.cs.minter))
 	}
 	if pollProtocol := poll.FindProtocol(builder.cs.registry); pollProtocol != nil {
 		copts = append(copts, consensus.WithPollProtocol(pollProtocol))
diff --git a/chainservice/chainservice.go b/chainservice/chainservice.go
index 989eb59d24..03b0ceb846 100644
--- a/chainservice/chainservice.go
+++ b/chainservice/chainservice.go
@@ -78,6 +78,7 @@ type ChainService struct {
 	nodeInfoManager          *nodeinfo.InfoManager
 	apiStats                 *nodestats.APILocalStats
 	actionsync               *actsync.ActionSync
+	minter                   *factory.Minter
 }
 
 // Start starts the server
diff --git a/consensus/consensus.go b/consensus/consensus.go
index b52e296fbf..b1505f260d 100644
--- a/consensus/consensus.go
+++ b/consensus/consensus.go
@@ -9,6 +9,7 @@ import (
 	"context"
 
 	"github.com/facebookgo/clock"
+	"github.com/iotexproject/go-pkgs/hash"
 	"github.com/iotexproject/iotex-proto/golang/iotextypes"
 	"github.com/pkg/errors"
 	"go.uber.org/zap"
@@ -24,7 +25,6 @@ import (
 	"github.com/iotexproject/iotex-core/v2/pkg/lifecycle"
 	"github.com/iotexproject/iotex-core/v2/pkg/log"
 	"github.com/iotexproject/iotex-core/v2/state"
-	"github.com/iotexproject/iotex-core/v2/state/factory"
 )
 
 // Consensus is the interface for handling IotxConsensus view change.
@@ -49,6 +49,7 @@ type optionParams struct {
 	broadcastHandler scheme.Broadcast
 	pp               poll.Protocol
 	rp               *rp.Protocol
+	bbf              rolldpos.BlockBuilderFactory
 }
 
 // Option sets Consensus construction parameter.
@@ -78,11 +79,19 @@ func WithPollProtocol(pp poll.Protocol) Option {
 	}
 }
 
+// WithBlockBuilderFactory is an option to set block builder factory
+func WithBlockBuilderFactory(bbf rolldpos.BlockBuilderFactory) Option {
+	return func(ops *optionParams) error {
+		ops.bbf = bbf
+		return nil
+	}
+}
+
 // NewConsensus creates a IotxConsensus struct.
 func NewConsensus(
 	cfg rolldpos.BuilderConfig,
 	bc blockchain.Blockchain,
-	sf factory.Factory,
+	sf rolldpos.StateReaderFactory,
 	opts ...Option,
 ) (Consensus, error) {
 	var ops optionParams
@@ -100,7 +109,19 @@ func NewConsensus(
 	var err error
 	switch cfg.Scheme {
 	case RollDPoSScheme:
-		delegatesByEpochFunc := func(epochNum uint64) ([]string, error) {
+		if ops.bbf == nil {
+			return nil, errors.New("block builder factory is not set")
+		}
+		chainMgr := rolldpos.NewChainManager(bc, sf, ops.bbf)
+		delegatesByEpochFunc := func(epochNum uint64, prevHash []byte) ([]string, error) {
+			fork, err := chainMgr.Fork(hash.Hash256(prevHash))
+			if err != nil {
+				return nil, err
+			}
+			forkSF, err := fork.StateReader()
+			if err != nil {
+				return nil, err
+			}
 			re := protocol.NewRegistry()
 			if err := ops.rp.Register(re); err != nil {
 				return nil, err
@@ -110,15 +131,14 @@ func NewConsensus(
 				cfg.Genesis,
 			)
 			ctx = protocol.WithFeatureWithHeightCtx(ctx)
-			tipHeight := bc.TipHeight()
+			tipHeight := fork.TipHeight()
 			tipEpochNum := ops.rp.GetEpochNum(tipHeight)
 			var candidatesList state.CandidateList
-			var err error
 			switch epochNum {
 			case tipEpochNum:
-				candidatesList, err = ops.pp.Delegates(ctx, sf)
+				candidatesList, err = ops.pp.Delegates(ctx, forkSF)
 			case tipEpochNum + 1:
-				candidatesList, err = ops.pp.NextDelegates(ctx, sf)
+				candidatesList, err = ops.pp.NextDelegates(ctx, forkSF)
 			default:
 				err = errors.Errorf("invalid epoch number %d compared to tip epoch number %d", epochNum, tipEpochNum)
 			}
@@ -135,7 +155,7 @@ func NewConsensus(
 		bd := rolldpos.NewRollDPoSBuilder().
 			SetPriKey(cfg.Chain.ProducerPrivateKeys()...).
 			SetConfig(cfg).
-			SetChainManager(rolldpos.NewChainManager(bc)).
+			SetChainManager(chainMgr).
 			SetBlockDeserializer(block.NewDeserializer(bc.EvmNetworkID())).
 			SetClock(clock).
 			SetBroadcast(ops.broadcastHandler).
diff --git a/consensus/scheme/rolldpos/chainmanager.go b/consensus/scheme/rolldpos/chainmanager.go
new file mode 100644
index 0000000000..11243d8860
--- /dev/null
+++ b/consensus/scheme/rolldpos/chainmanager.go
@@ -0,0 +1,425 @@
+package rolldpos
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/crypto"
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/iotexproject/iotex-address/address"
+	"github.com/pkg/errors"
+	"github.com/prometheus/client_golang/prometheus"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/action/protocol"
+	"github.com/iotexproject/iotex-core/v2/blockchain"
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/blockchain/genesis"
+	"github.com/iotexproject/iotex-core/v2/db"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+	"github.com/iotexproject/iotex-core/v2/pkg/prometheustimer"
+)
+
+var (
+	blockMtc = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "iotex_blockmint_metrics",
+			Help: "Block mint metrics.",
+		},
+		[]string{"type"},
+	)
+)
+
+type (
+	// ChainManager defines the blockchain interface
+	ChainManager interface {
+		ForkChain
+		// Start starts the chain manager
+		Start(ctx context.Context) error
+		// Fork creates a new chain manager with the given hash
+		Fork(hash hash.Hash256) (ForkChain, error)
+		// CommitBlock validates and appends a block to the chain
+		CommitBlock(blk *block.Block) error
+		// ValidateBlock validates a new block before adding it to the blockchain
+		ValidateBlock(blk *block.Block) error
+	}
+	// ForkChain defines the blockchain interface
+	ForkChain interface {
+		// BlockProposeTime return propose time by height
+		BlockProposeTime(uint64) (time.Time, error)
+		// BlockCommitTime return commit time by height
+		BlockCommitTime(uint64) (time.Time, error)
+		// TipHeight returns tip block's height
+		TipHeight() uint64
+		// TipHash returns tip block's hash
+		TipHash() hash.Hash256
+		// StateReader returns the state reader
+		StateReader() (protocol.StateReader, error)
+		// MintNewBlock creates a new block with given actions
+		// Note: the coinbase transfer will be added to the given transfers when minting a new block
+		MintNewBlock(time.Time, crypto.PrivateKey, hash.Hash256) (*block.Block, error)
+	}
+	// StateReaderFactory is the factory interface of state reader
+	StateReaderFactory interface {
+		StateReaderAt(uint64, hash.Hash256) (protocol.StateReader, error)
+	}
+
+	// BlockBuilderFactory is the factory interface of block builder
+	BlockBuilderFactory interface {
+		Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error)
+		ReceiveBlock(*block.Block) error
+	}
+
+	chainManager struct {
+		srf          StateReaderFactory
+		timerFactory *prometheustimer.TimerFactory
+		bc           blockchain.Blockchain
+		bbf          BlockBuilderFactory
+		pool         *proposalPool
+	}
+
+	forkChain struct {
+		cm   *chainManager
+		head *block.Header
+		sr   protocol.StateReader
+	}
+)
+
+func init() {
+	prometheus.MustRegister(blockMtc)
+}
+
+func newForkChain(cm *chainManager, head *block.Header, sr protocol.StateReader) *forkChain {
+	return &forkChain{
+		cm:   cm,
+		head: head,
+		sr:   sr,
+	}
+}
+
+// NewChainManager creates a chain manager
+func NewChainManager(bc blockchain.Blockchain, srf StateReaderFactory, bbf BlockBuilderFactory) ChainManager {
+	timerFactory, err := prometheustimer.New(
+		"iotex_blockchain_perf",
+		"Performance of blockchain module",
+		[]string{"topic", "chainID"},
+		[]string{"default", strconv.FormatUint(uint64(bc.ChainID()), 10)},
+	)
+	if err != nil {
+		log.L().Panic("Failed to generate prometheus timer factory.", zap.Error(err))
+	}
+
+	return &chainManager{
+		bc:           bc,
+		bbf:          bbf,
+		timerFactory: timerFactory,
+		srf:          srf,
+		pool:         newProposalPool(),
+	}
+}
+
+// BlockProposeTime return propose time by height
+func (fc *forkChain) BlockProposeTime(height uint64) (time.Time, error) {
+	t, err := fc.cm.BlockProposeTime(height)
+	switch errors.Cause(err) {
+	case nil:
+		return t, nil
+	case db.ErrNotExist:
+		header, err := fc.cm.header(height, fc.tipHash())
+		if err != nil {
+			return time.Time{}, err
+		}
+		return header.Timestamp(), nil
+	default:
+		return time.Time{}, err
+	}
+}
+
+func (cm *chainManager) BlockProposeTime(height uint64) (time.Time, error) {
+	if height == 0 {
+		return time.Unix(cm.bc.Genesis().Timestamp, 0), nil
+	}
+	head, err := cm.bc.BlockHeaderByHeight(height)
+	if err != nil {
+		return time.Time{}, errors.Wrapf(
+			err, "error when getting the block at height: %d",
+			height,
+		)
+	}
+	return head.Timestamp(), nil
+}
+
+// BlockCommitTime return commit time by height
+func (fc *forkChain) BlockCommitTime(height uint64) (time.Time, error) {
+	return fc.cm.BlockCommitTime(height)
+}
+
+func (cm *chainManager) BlockCommitTime(height uint64) (time.Time, error) {
+	footer, err := cm.bc.BlockFooterByHeight(height)
+	if err != nil {
+		return time.Time{}, errors.Wrapf(
+			err, "error when getting the block at height: %d",
+			height,
+		)
+	}
+	return footer.CommitTime(), nil
+}
+
+func (fc *forkChain) MintNewBlock(timestamp time.Time, pk crypto.PrivateKey, prevHash hash.Hash256) (*block.Block, error) {
+	return fc.cm.mintNewBlock(timestamp, pk, prevHash, fc.tipInfo())
+}
+
+func (cm *chainManager) MintNewBlock(timestamp time.Time, pk crypto.PrivateKey, prevHash hash.Hash256) (*block.Block, error) {
+	return cm.mintNewBlock(timestamp, pk, prevHash, cm.tipInfo())
+}
+
+func (cm *chainManager) mintNewBlock(timestamp time.Time, pk crypto.PrivateKey, prevHash hash.Hash256, tipInfo *protocol.TipInfo) (*block.Block, error) {
+	mintNewBlockTimer := cm.timerFactory.NewTimer("MintBlock")
+	defer mintNewBlockTimer.End()
+	var (
+		newblockHeight = tipInfo.Height + 1
+		producer       address.Address
+		err            error
+	)
+	// safety check
+	if prevHash != tipInfo.Hash {
+		return nil, errors.Errorf("invalid prev hash, expecting %x, got %x", prevHash, tipInfo.Hash)
+	}
+	producer = pk.PublicKey().Address()
+	ctx := cm.mintContext(context.Background(), timestamp, producer, tipInfo)
+	// create a new block
+	log.L().Debug("Produce a new block.", zap.Uint64("height", newblockHeight), zap.Time("timestamp", timestamp), log.Hex("prevHash", prevHash[:]))
+	// run execution and update state trie root hash
+	blk, err := cm.bbf.Mint(ctx, pk)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to create block")
+	}
+	if err = cm.pool.AddBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to add proposal", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	blockMtc.WithLabelValues("MintGas").Set(float64(blk.GasUsed()))
+	blockMtc.WithLabelValues("MintActions").Set(float64(len(blk.Actions)))
+	return blk, nil
+}
+
+// TipHeight returns tip block's height
+func (cm *chainManager) TipHeight() uint64 {
+	return cm.bc.TipHeight()
+}
+
+func (fc *forkChain) TipHeight() uint64 {
+	return fc.head.Height()
+}
+
+// TipHash returns tip block's hash
+func (fc *forkChain) TipHash() hash.Hash256 {
+	return fc.tipHash()
+}
+
+func (cm *chainManager) StateReader() (protocol.StateReader, error) {
+	return cm.srf.StateReaderAt(cm.bc.TipHeight(), cm.bc.TipHash())
+}
+
+// StateReader returns the state reader
+func (fc *forkChain) StateReader() (protocol.StateReader, error) {
+	return fc.sr, nil
+}
+
+// ValidateBlock validates a new block before adding it to the blockchain
+func (cm *chainManager) ValidateBlock(blk *block.Block) error {
+	err := cm.bc.ValidateBlock(blk)
+	if err != nil {
+		return err
+	}
+	if err = cm.pool.AddBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to add proposal", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	return nil
+}
+
+// CommitBlock validates and appends a block to the chain
+func (cm *chainManager) CommitBlock(blk *block.Block) error {
+	if err := cm.bc.CommitBlock(blk); err != nil {
+		return err
+	}
+	if err := cm.bbf.ReceiveBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to receive block", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	if err := cm.pool.ReceiveBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to receive block", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	return nil
+}
+
+func (cm *chainManager) Start(ctx context.Context) error {
+	head, err := cm.bc.BlockHeaderByHeight(cm.bc.TipHeight())
+	if err != nil {
+		return errors.Wrap(err, "failed to get the head block")
+	}
+	cm.pool.Init(head.HashBlock())
+	return nil
+}
+
+// Fork creates a new chain manager with the given hash
+func (cm *chainManager) Fork(hash hash.Hash256) (ForkChain, error) {
+	var (
+		head *block.Header
+		err  error
+		tip  = cm.tipInfo()
+	)
+	if hash != tip.Hash {
+		blk := cm.pool.BlockByHash(hash)
+		if blk == nil {
+			return nil, errors.Errorf("block %x not found when fork", hash)
+		}
+		head = &blk.Header
+	} else {
+		head, err = cm.bc.BlockHeaderByHeight(tip.Height)
+		if head == nil {
+			return nil, errors.Errorf("block %x not found when fork", hash)
+		}
+	}
+	sr, err := cm.srf.StateReaderAt(head.Height(), hash)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to create state reader at %d, hash %x", head.Height(), head.HashBlock())
+	}
+	return newForkChain(cm, head, sr), nil
+}
+
+func (cm *chainManager) draftBlockByHeight(height uint64, tipHash hash.Hash256) *block.Block {
+	for blk := cm.pool.BlockByHash(tipHash); blk != nil && blk.Height() >= height; blk = cm.pool.BlockByHash(blk.PrevHash()) {
+		if blk.Height() == height {
+			return blk
+		}
+	}
+	return nil
+}
+
+func (cm *chainManager) TipHash() hash.Hash256 {
+	if cm.bc.TipHeight() == 0 {
+		g := cm.bc.Genesis()
+		return g.Hash()
+	}
+	return cm.bc.TipHash()
+}
+
+func (fc *forkChain) tipHash() hash.Hash256 {
+	if fc.head.Height() == 0 {
+		g := fc.cm.bc.Genesis()
+		return g.Hash()
+	}
+	return fc.head.HashBlock()
+}
+
+func (cm *chainManager) tipInfo() *protocol.TipInfo {
+	height := cm.bc.TipHeight()
+	if height == 0 {
+		g := cm.bc.Genesis()
+		return &protocol.TipInfo{
+			Height:    0,
+			Hash:      g.Hash(),
+			Timestamp: time.Unix(g.Timestamp, 0),
+		}
+	}
+	head, err := cm.bc.BlockHeaderByHeight(height)
+	if err != nil {
+		log.L().Error("failed to get the head block", zap.Error(err))
+		return nil
+	}
+
+	return &protocol.TipInfo{
+		Height:        head.Height(),
+		GasUsed:       head.GasUsed(),
+		Hash:          head.HashBlock(),
+		Timestamp:     head.Timestamp(),
+		BaseFee:       head.BaseFee(),
+		BlobGasUsed:   head.BlobGasUsed(),
+		ExcessBlobGas: head.ExcessBlobGas(),
+	}
+}
+
+func (fc *forkChain) tipInfo() *protocol.TipInfo {
+	return &protocol.TipInfo{
+		Height:        fc.head.Height(),
+		GasUsed:       fc.head.GasUsed(),
+		Hash:          fc.head.HashBlock(),
+		Timestamp:     fc.head.Timestamp(),
+		BaseFee:       fc.head.BaseFee(),
+		BlobGasUsed:   fc.head.BlobGasUsed(),
+		ExcessBlobGas: fc.head.ExcessBlobGas(),
+	}
+}
+
+func (cm *chainManager) header(height uint64, tipHash hash.Hash256) (*block.Header, error) {
+	header, err := cm.bc.BlockHeaderByHeight(height)
+	switch errors.Cause(err) {
+	case nil:
+		return header, nil
+	case db.ErrNotExist:
+		if blk := cm.draftBlockByHeight(height, tipHash); blk != nil {
+			return &blk.Header, nil
+		}
+	}
+	return nil, err
+}
+
+func (cm *chainManager) mintContext(
+	ctx context.Context,
+	timestamp time.Time,
+	producer address.Address,
+	tip *protocol.TipInfo,
+) context.Context {
+	g := cm.bc.Genesis()
+	// blockchain context
+	ctx = genesis.WithGenesisContext(
+		protocol.WithBlockchainCtx(
+			ctx,
+			protocol.BlockchainCtx{
+				Tip:          *tip,
+				ChainID:      cm.bc.ChainID(),
+				EvmNetworkID: cm.bc.EvmNetworkID(),
+				GetBlockHash: func(u uint64) (hash.Hash256, error) {
+					header, err := cm.header(u, tip.Hash)
+					if err != nil {
+						return hash.ZeroHash256, err
+					}
+					return header.HashBlock(), nil
+				},
+				GetBlockTime: func(height uint64) (time.Time, error) {
+					return cm.getBlockTime(height, tip.Hash)
+				},
+			},
+		),
+		g,
+	)
+	ctx = protocol.WithFeatureWithHeightCtx(ctx)
+	// block context
+	height := tip.Height + 1
+	ctx = protocol.WithBlockCtx(
+		ctx,
+		protocol.BlockCtx{
+			BlockHeight:    height,
+			BlockTimeStamp: timestamp,
+			Producer:       producer,
+			GasLimit:       g.BlockGasLimitByHeight(height),
+			BaseFee:        protocol.CalcBaseFee(g.Blockchain, tip),
+			ExcessBlobGas:  protocol.CalcExcessBlobGas(tip.ExcessBlobGas, tip.BlobGasUsed),
+		})
+	return protocol.WithFeatureCtx(ctx)
+}
+
+func (cm *chainManager) getBlockTime(height uint64, tipHash hash.Hash256) (time.Time, error) {
+	if height == 0 {
+		return time.Unix(cm.bc.Genesis().Timestamp, 0), nil
+	}
+	header, err := cm.header(height, tipHash)
+	if err != nil {
+		return time.Time{}, err
+	}
+	return header.Timestamp(), nil
+}
diff --git a/consensus/scheme/rolldpos/proposalpool.go b/consensus/scheme/rolldpos/proposalpool.go
new file mode 100644
index 0000000000..e6e968d8d0
--- /dev/null
+++ b/consensus/scheme/rolldpos/proposalpool.go
@@ -0,0 +1,113 @@
+package rolldpos
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// nodes is a map of draft proposal blocks
+	// key is the hash of the block
+	nodes map[hash.Hash256]*block.Block
+	// leaves is a map of tip blocks of forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	leaves map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		nodes:  make(map[hash.Hash256]*block.Block),
+		leaves: make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Debug("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.nodes[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.leaves[prevHash]; ok {
+		delete(d.leaves, prevHash)
+	} else if prevHash != d.root && d.nodes[prevHash] == nil {
+		return errors.Errorf("block %x is not a tip of any fork", prevHash[:])
+	}
+	d.leaves[hash] = blk.Timestamp()
+	d.nodes[hash] = blk
+	log.L().Debug("added block to draft pool", log.Hex("hash", hash[:]), zap.Uint64("height", blk.Height()), zap.Time("timestamp", blk.Timestamp()))
+	return nil
+}
+
+// ReceiveBlock a block has been confirmed and remove all the blocks that is previous to it, or other forks
+func (d *proposalPool) ReceiveBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	contain := func(node, leaf hash.Hash256) (exist bool) {
+		for b := leaf; ; {
+			if b == node {
+				return true
+			}
+			blk := d.nodes[b]
+			if blk == nil {
+				return false
+			}
+			b = blk.PrevHash()
+		}
+	}
+
+	// remove blocks in other forks or older blocks in the same fork
+	leavesToDelete := make([]hash.Hash256, 0)
+	for leaf := range d.leaves {
+		start := d.nodes[leaf]
+		has := contain(blk.HashBlock(), leaf)
+		if has {
+			start = blk
+		}
+		for b := start; b != nil; b = d.nodes[b.PrevHash()] {
+			ha := b.HashBlock()
+			log.L().Debug("remove block from draft pool", log.Hex("hash", ha[:]), zap.Uint64("height", b.Height()), zap.Time("timestamp", b.Timestamp()))
+			delete(d.nodes, b.HashBlock())
+		}
+		if !has || blk.HashBlock() == leaf {
+			leavesToDelete = append(leavesToDelete, leaf)
+		}
+	}
+	// reset forks to only this one
+	for _, f := range leavesToDelete {
+		delete(d.leaves, f)
+	}
+	d.root = blk.HashBlock()
+	return nil
+}
+
+// BlockByHash returns the block by hash
+func (d *proposalPool) BlockByHash(hash hash.Hash256) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	return d.nodes[hash]
+}
diff --git a/consensus/scheme/rolldpos/proposalpool_test.go b/consensus/scheme/rolldpos/proposalpool_test.go
new file mode 100644
index 0000000000..91df6b9667
--- /dev/null
+++ b/consensus/scheme/rolldpos/proposalpool_test.go
@@ -0,0 +1,128 @@
+package rolldpos
+
+import (
+	"testing"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/stretchr/testify/require"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/test/identityset"
+)
+
+func TestProposalPool(t *testing.T) {
+	r := require.New(t)
+	t.Run("empty", func(t *testing.T) {
+		pool := newProposalPool()
+		// create a mock block
+		rootHash := hash.ZeroHash256
+		blk := makeBlock2(r, 1, rootHash)
+		// set pool root
+		pool.Init(rootHash)
+		err := pool.AddBlock(blk)
+		r.NoError(err)
+		r.NotNil(pool.BlockByHash(blk.HashBlock()))
+		r.Contains(pool.leaves, blk.HashBlock())
+	})
+	t.Run("already-exist", func(t *testing.T) {
+		pool := newProposalPool()
+		rootHash := hash.ZeroHash256
+		blk := makeBlock2(r, 1, rootHash)
+		pool.Init(rootHash)
+		err := pool.AddBlock(blk)
+		r.NoError(err)
+		// add again
+		err2 := pool.AddBlock(blk)
+		r.NoError(err2, "should not error when block already exists")
+	})
+	t.Run("new-tip-block", func(t *testing.T) {
+		pool := newProposalPool()
+		rootHash := hash.ZeroHash256
+		parentBlk := makeBlock2(r, 1, rootHash)
+		pool.Init(rootHash)
+		r.NoError(pool.AddBlock(parentBlk))
+
+		// add child
+		childBlk := makeBlock2(r, 2, parentBlk.HashBlock())
+		err := pool.AddBlock(childBlk)
+		r.NoError(err)
+		r.NotNil(pool.BlockByHash(childBlk.HashBlock()))
+		r.Contains(pool.leaves, childBlk.HashBlock())
+		r.NotContains(pool.leaves, parentBlk.HashBlock(), "parent should be removed from leaves")
+	})
+	t.Run("new-fork", func(t *testing.T) {
+		pool := newProposalPool()
+		rootHash := hash.ZeroHash256
+		blkA := makeBlock2(r, 1, rootHash)
+		pool.Init(rootHash)
+		r.NoError(pool.AddBlock(blkA))
+
+		// root -> blkA
+		//      -> blkB
+		blkB := makeBlock2(r, 1, rootHash)
+		r.NotEqual(blkA.HashBlock(), blkB.HashBlock())
+		r.NoError(pool.AddBlock(blkB))
+		r.NotNil(pool.BlockByHash(blkA.HashBlock()))
+		r.Contains(pool.leaves, blkA.HashBlock())
+		r.Contains(pool.leaves, blkB.HashBlock())
+
+		// root -> blkA
+		// 			-> blkAA
+		//          -> blkAB
+		//      -> blkB
+		//          -> blkBA
+		//          -> blkBB
+		blkAA := makeBlock2(r, 2, blkA.HashBlock())
+		r.NoError(pool.AddBlock(blkAA))
+		r.NotNil(pool.BlockByHash(blkAA.HashBlock()))
+		r.Contains(pool.leaves, blkAA.HashBlock())
+		r.NotContains(pool.leaves, blkA.HashBlock(), "blkA should be removed from leaves")
+		blkAB := makeBlock2(r, 2, blkA.HashBlock())
+		r.NoError(pool.AddBlock(blkAB))
+		r.NotNil(pool.BlockByHash(blkAB.HashBlock()))
+		r.Contains(pool.leaves, blkAB.HashBlock())
+		blkBA := makeBlock2(r, 2, blkB.HashBlock())
+		r.NoError(pool.AddBlock(blkBA))
+		r.NotNil(pool.BlockByHash(blkBA.HashBlock()))
+		r.Contains(pool.leaves, blkBA.HashBlock())
+		r.NotContains(pool.leaves, blkB.HashBlock(), "blkB should be removed from leaves")
+		blkBB := makeBlock2(r, 2, blkB.HashBlock())
+		r.NoError(pool.AddBlock(blkBB))
+		r.NotNil(pool.BlockByHash(blkBB.HashBlock()))
+		r.Contains(pool.leaves, blkBB.HashBlock())
+
+		// blkA confirmed
+		// root -> blkA -> blkAA
+		//          	-> blkAB
+		r.NoError(pool.ReceiveBlock(blkA))
+		r.NotNil(pool.BlockByHash(blkAA.HashBlock()))
+		r.NotNil(pool.BlockByHash(blkAB.HashBlock()))
+		r.Nil(pool.BlockByHash(blkA.HashBlock()))
+		r.Nil(pool.BlockByHash(blkB.HashBlock()))
+		r.Nil(pool.BlockByHash(blkBA.HashBlock()))
+		r.Nil(pool.BlockByHash(blkBB.HashBlock()))
+	})
+	t.Run("invalid-parent", func(t *testing.T) {
+		pool := newProposalPool()
+		rootHash := hash.ZeroHash256
+		pool.Init(rootHash)
+		parent := makeBlock2(r, 1, rootHash)
+		blk := makeBlock2(r, 2, parent.HashBlock())
+		err := pool.AddBlock(blk)
+		r.Error(err)
+		r.Contains(err.Error(), "is not a tip of any fork")
+	})
+}
+
+// makeBlock is a helper to create a block with the given height and prevHash
+func makeBlock2(r *require.Assertions, height uint64, prevHash hash.Hash256) *block.Block {
+	// minimal mock
+	bd := block.NewBuilder(block.NewRunnableActionsBuilder().Build()).
+		SetPrevBlockHash(prevHash).
+		SetHeight(height).
+		SetTimestamp(time.Now())
+	blk, err := bd.SignAndBuild(identityset.PrivateKey(1))
+	r.NoError(err)
+	return &blk
+}
diff --git a/consensus/scheme/rolldpos/rolldpos.go b/consensus/scheme/rolldpos/rolldpos.go
index 7f41569510..ba693f443e 100644
--- a/consensus/scheme/rolldpos/rolldpos.go
+++ b/consensus/scheme/rolldpos/rolldpos.go
@@ -44,29 +44,6 @@ type (
 		Delay             time.Duration                `yaml:"delay"`
 		ConsensusDBPath   string                       `yaml:"consensusDBPath"`
 	}
-
-	// ChainManager defines the blockchain interface
-	ChainManager interface {
-		// BlockProposeTime return propose time by height
-		BlockProposeTime(uint64) (time.Time, error)
-		// BlockCommitTime return commit time by height
-		BlockCommitTime(uint64) (time.Time, error)
-		// MintNewBlock creates a new block with given actions
-		// Note: the coinbase transfer will be added to the given transfers when minting a new block
-		MintNewBlock(time.Time, crypto.PrivateKey) (*block.Block, error)
-		// CommitBlock validates and appends a block to the chain
-		CommitBlock(blk *block.Block) error
-		// ValidateBlock validates a new block before adding it to the blockchain
-		ValidateBlock(blk *block.Block) error
-		// TipHeight returns tip block's height
-		TipHeight() uint64
-		// ChainAddress returns chain address on parent chain, the root chain return empty.
-		ChainAddress() string
-	}
-
-	chainManager struct {
-		bc blockchain.Blockchain
-	}
 )
 
 // DefaultConfig is the default config
@@ -85,65 +62,6 @@ var DefaultConfig = Config{
 	ConsensusDBPath:   "/var/data/consensus.db",
 }
 
-// NewChainManager creates a chain manager
-func NewChainManager(bc blockchain.Blockchain) ChainManager {
-	return &chainManager{
-		bc: bc,
-	}
-}
-
-// BlockProposeTime return propose time by height
-func (cm *chainManager) BlockProposeTime(height uint64) (time.Time, error) {
-	if height == 0 {
-		return time.Unix(cm.bc.Genesis().Timestamp, 0), nil
-	}
-	header, err := cm.bc.BlockHeaderByHeight(height)
-	if err != nil {
-		return time.Time{}, errors.Wrapf(
-			err, "error when getting the block at height: %d",
-			height,
-		)
-	}
-	return header.Timestamp(), nil
-}
-
-// BlockCommitTime return commit time by height
-func (cm *chainManager) BlockCommitTime(height uint64) (time.Time, error) {
-	footer, err := cm.bc.BlockFooterByHeight(height)
-	if err != nil {
-		return time.Time{}, errors.Wrapf(
-			err, "error when getting the block at height: %d",
-			height,
-		)
-	}
-	return footer.CommitTime(), nil
-}
-
-// MintNewBlock creates a new block with given actions
-func (cm *chainManager) MintNewBlock(timestamp time.Time, privKey crypto.PrivateKey) (*block.Block, error) {
-	return cm.bc.MintNewBlock(timestamp, blockchain.WithProducerPrivateKey(privKey))
-}
-
-// CommitBlock validates and appends a block to the chain
-func (cm *chainManager) CommitBlock(blk *block.Block) error {
-	return cm.bc.CommitBlock(blk)
-}
-
-// ValidateBlock validates a new block before adding it to the blockchain
-func (cm *chainManager) ValidateBlock(blk *block.Block) error {
-	return cm.bc.ValidateBlock(blk)
-}
-
-// TipHeight returns tip block's height
-func (cm *chainManager) TipHeight() uint64 {
-	return cm.bc.TipHeight()
-}
-
-// ChainAddress returns chain address on parent chain, the root chain return empty.
-func (cm *chainManager) ChainAddress() string {
-	return cm.bc.ChainAddress()
-}
-
 // RollDPoS is Roll-DPoS consensus main entrance
 type RollDPoS struct {
 	cfsm       *consensusfsm.ConsensusFSM
@@ -244,7 +162,8 @@ func (r *RollDPoS) Calibrate(height uint64) {
 // ValidateBlockFooter validates the signatures in the block footer
 func (r *RollDPoS) ValidateBlockFooter(blk *block.Block) error {
 	height := blk.Height()
-	round, err := r.ctx.RoundCalculator().NewRound(height, r.ctx.BlockInterval(height), blk.Timestamp(), nil)
+	roundCalc := r.ctx.RoundCalculator().Fork(r.ctx.Chain())
+	round, err := roundCalc.NewRound(height, r.ctx.BlockInterval(height), blk.Timestamp(), nil)
 	if err != nil {
 		return err
 	}
diff --git a/consensus/scheme/rolldpos/rolldpos_test.go b/consensus/scheme/rolldpos/rolldpos_test.go
index 4fca049633..b77ea3a14b 100644
--- a/consensus/scheme/rolldpos/rolldpos_test.go
+++ b/consensus/scheme/rolldpos/rolldpos_test.go
@@ -44,6 +44,7 @@ import (
 	"github.com/iotexproject/iotex-core/v2/state/factory"
 	"github.com/iotexproject/iotex-core/v2/test/identityset"
 	"github.com/iotexproject/iotex-core/v2/test/mock/mock_blockchain"
+	"github.com/iotexproject/iotex-core/v2/test/mock/mock_factory"
 	"github.com/iotexproject/iotex-core/v2/testutil"
 )
 
@@ -71,13 +72,15 @@ func TestNewRollDPoS(t *testing.T) {
 		g.NumDelegates,
 		g.NumSubEpochs,
 	)
-	delegatesByEpoch := func(uint64) ([]string, error) { return nil, nil }
+	delegatesByEpoch := func(uint64, []byte) ([]string, error) { return nil, nil }
 	t.Run("normal", func(t *testing.T) {
 		sk := identityset.PrivateKey(0)
+		chain := mock_blockchain.NewMockBlockchain(ctrl)
+		chain.EXPECT().ChainID().Return(uint32(1)).AnyTimes()
 		r, err := NewRollDPoSBuilder().
 			SetConfig(builderCfg).
 			SetPriKey(sk).
-			SetChainManager(NewChainManager(mock_blockchain.NewMockBlockchain(ctrl))).
+			SetChainManager(NewChainManager(chain, mock_factory.NewMockFactory(ctrl), &dummyBlockBuildFactory{})).
 			SetBroadcast(func(_ proto.Message) error {
 				return nil
 			}).
@@ -90,10 +93,12 @@ func TestNewRollDPoS(t *testing.T) {
 	})
 	t.Run("mock-clock", func(t *testing.T) {
 		sk := identityset.PrivateKey(0)
+		chain := mock_blockchain.NewMockBlockchain(ctrl)
+		chain.EXPECT().ChainID().Return(uint32(1)).AnyTimes()
 		r, err := NewRollDPoSBuilder().
 			SetConfig(builderCfg).
 			SetPriKey(sk).
-			SetChainManager(NewChainManager(mock_blockchain.NewMockBlockchain(ctrl))).
+			SetChainManager(NewChainManager(chain, mock_factory.NewMockFactory(ctrl), &dummyBlockBuildFactory{})).
 			SetBroadcast(func(_ proto.Message) error {
 				return nil
 			}).
@@ -110,10 +115,12 @@ func TestNewRollDPoS(t *testing.T) {
 
 	t.Run("root chain API", func(t *testing.T) {
 		sk := identityset.PrivateKey(0)
+		chain := mock_blockchain.NewMockBlockchain(ctrl)
+		chain.EXPECT().ChainID().Return(uint32(1)).AnyTimes()
 		r, err := NewRollDPoSBuilder().
 			SetConfig(builderCfg).
 			SetPriKey(sk).
-			SetChainManager(NewChainManager(mock_blockchain.NewMockBlockchain(ctrl))).
+			SetChainManager(NewChainManager(chain, mock_factory.NewMockFactory(ctrl), &dummyBlockBuildFactory{})).
 			SetBroadcast(func(_ proto.Message) error {
 				return nil
 			}).
@@ -197,8 +204,11 @@ func TestValidateBlockFooter(t *testing.T) {
 	blockHeight := uint64(8)
 	footer := &block.Footer{}
 	bc := mock_blockchain.NewMockBlockchain(ctrl)
-	bc.EXPECT().BlockFooterByHeight(blockHeight).Return(footer, nil).Times(5)
-
+	bc.EXPECT().BlockFooterByHeight(blockHeight).Return(footer, nil).AnyTimes()
+	bc.EXPECT().ChainID().Return(uint32(1)).AnyTimes()
+	bc.EXPECT().TipHeight().Return(blockHeight).AnyTimes()
+	bc.EXPECT().BlockHeaderByHeight(blockHeight).Return(&block.Header{}, nil).AnyTimes()
+	bc.EXPECT().TipHash().Return(hash.ZeroHash256).AnyTimes()
 	sk1 := identityset.PrivateKey(1)
 	g := genesis.TestDefault()
 	g.NumDelegates = 4
@@ -213,13 +223,16 @@ func TestValidateBlockFooter(t *testing.T) {
 		Genesis:            g,
 		SystemActive:       true,
 	}
-	bc.EXPECT().Genesis().Return(g).Times(5)
+	builderCfg.Consensus.ConsensusDBPath = ""
+	bc.EXPECT().Genesis().Return(g).AnyTimes()
+	sf := mock_factory.NewMockFactory(ctrl)
+	sf.EXPECT().StateReaderAt(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
 	rp := rolldpos.NewProtocol(
 		g.NumCandidateDelegates,
 		g.NumDelegates,
 		g.NumSubEpochs,
 	)
-	delegatesByEpoch := func(uint64) ([]string, error) {
+	delegatesByEpoch := func(uint64, []byte) ([]string, error) {
 		return []string{
 			candidates[0],
 			candidates[1],
@@ -230,7 +243,7 @@ func TestValidateBlockFooter(t *testing.T) {
 	r, err := NewRollDPoSBuilder().
 		SetConfig(builderCfg).
 		SetPriKey(sk1).
-		SetChainManager(NewChainManager(bc)).
+		SetChainManager(NewChainManager(bc, sf, &dummyBlockBuildFactory{})).
 		SetBroadcast(func(_ proto.Message) error {
 			return nil
 		}).
@@ -241,6 +254,7 @@ func TestValidateBlockFooter(t *testing.T) {
 		Build()
 	require.NoError(t, err)
 	require.NotNil(t, r)
+	require.NoError(t, r.Start(context.Background()))
 
 	// all right
 	blk := makeBlock(t, 1, 4, false, 9)
@@ -283,8 +297,11 @@ func TestRollDPoS_Metrics(t *testing.T) {
 	blockHeight := uint64(8)
 	footer := &block.Footer{}
 	bc := mock_blockchain.NewMockBlockchain(ctrl)
-	bc.EXPECT().TipHeight().Return(blockHeight).Times(1)
-	bc.EXPECT().BlockFooterByHeight(blockHeight).Return(footer, nil).Times(2)
+	bc.EXPECT().TipHeight().Return(blockHeight).AnyTimes()
+	bc.EXPECT().TipHash().Return(hash.ZeroHash256).AnyTimes()
+	bc.EXPECT().BlockFooterByHeight(blockHeight).Return(footer, nil).AnyTimes()
+	bc.EXPECT().ChainID().Return(uint32(1)).AnyTimes()
+	bc.EXPECT().BlockHeaderByHeight(blockHeight).Return(&block.Header{}, nil).AnyTimes()
 
 	sk1 := identityset.PrivateKey(1)
 	cfg := DefaultConfig
@@ -303,12 +320,13 @@ func TestRollDPoS_Metrics(t *testing.T) {
 		SystemActive:       true,
 	}
 	bc.EXPECT().Genesis().Return(g).Times(2)
+	sf := mock_factory.NewMockFactory(ctrl)
 	rp := rolldpos.NewProtocol(
 		g.NumCandidateDelegates,
 		g.NumDelegates,
 		g.NumSubEpochs,
 	)
-	delegatesByEpoch := func(uint64) ([]string, error) {
+	delegatesByEpoch := func(uint64, []byte) ([]string, error) {
 		return []string{
 			candidates[0],
 			candidates[1],
@@ -319,7 +337,7 @@ func TestRollDPoS_Metrics(t *testing.T) {
 	r, err := NewRollDPoSBuilder().
 		SetConfig(builderCfg).
 		SetPriKey(sk1).
-		SetChainManager(NewChainManager(bc)).
+		SetChainManager(NewChainManager(bc, sf, &dummyBlockBuildFactory{})).
 		SetBroadcast(func(_ proto.Message) error {
 			return nil
 		}).
@@ -426,7 +444,7 @@ func TestRollDPoSConsensus(t *testing.T) {
 			chainAddrs[i] = addressMap[rawAddress]
 		}
 
-		delegatesByEpochFunc := func(_ uint64) ([]string, error) {
+		delegatesByEpochFunc := func(_ uint64, _ []byte) ([]string, error) {
 			candidates := make([]string, 0, numNodes)
 			for _, addr := range chainAddrs {
 				candidates = append(candidates, addr.encodedAddr)
@@ -476,11 +494,11 @@ func TestRollDPoSConsensus(t *testing.T) {
 				peers: make(map[net.Addr]*RollDPoS),
 			}
 			p2ps = append(p2ps, p2p)
-
+			minter := factory.NewMinter(sf, actPool)
 			consensus, err := NewRollDPoSBuilder().
 				SetPriKey(chainAddrs[i].priKey).
 				SetConfig(builderCfg).
-				SetChainManager(NewChainManager(chain)).
+				SetChainManager(NewChainManager(chain, sf, minter)).
 				SetBroadcast(p2p.Broadcast).
 				SetDelegatesByEpochFunc(delegatesByEpochFunc).
 				SetProposersByEpochFunc(delegatesByEpochFunc).
@@ -729,3 +747,20 @@ func TestRollDPoSConsensus(t *testing.T) {
 		}
 	})
 }
+
+type dummyBlockBuildFactory struct{}
+
+func (d *dummyBlockBuildFactory) Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
+	return &block.Block{}, nil
+}
+
+func (d *dummyBlockBuildFactory) ReceiveBlock(*block.Block) error {
+	return nil
+}
+func (d *dummyBlockBuildFactory) Init(hash.Hash256) {}
+func (d *dummyBlockBuildFactory) AddProposal(*block.Block) error {
+	return nil
+}
+func (d *dummyBlockBuildFactory) Block(hash.Hash256) *block.Block {
+	return &block.Block{}
+}
diff --git a/consensus/scheme/rolldpos/rolldposctx.go b/consensus/scheme/rolldpos/rolldposctx.go
index dbf1667d80..8cf0eb60c7 100644
--- a/consensus/scheme/rolldpos/rolldposctx.go
+++ b/consensus/scheme/rolldpos/rolldposctx.go
@@ -72,7 +72,7 @@ func init() {
 
 type (
 	// NodesSelectionByEpochFunc defines a function to select nodes
-	NodesSelectionByEpochFunc func(uint64) ([]string, error)
+	NodesSelectionByEpochFunc func(uint64, []byte) ([]string, error)
 
 	// RDPoSCtx is the context of RollDPoS
 	RDPoSCtx interface {
@@ -179,6 +179,9 @@ func NewRollDPoSCtx(
 }
 
 func (ctx *rollDPoSCtx) Start(c context.Context) (err error) {
+	if err := ctx.chain.Start(c); err != nil {
+		return errors.Wrap(err, "Error when starting the chain")
+	}
 	var eManager *endorsementManager
 	if ctx.eManagerDB != nil {
 		if err := ctx.eManagerDB.Start(c); err != nil {
@@ -229,7 +232,12 @@ func (ctx *rollDPoSCtx) CheckVoteEndorser(
 	if endorserAddr == nil {
 		return errors.New("failed to get address")
 	}
-	if !ctx.roundCalc.IsDelegate(endorserAddr.String(), height) {
+	fork, err := ctx.chain.Fork(ctx.round.prevHash)
+	if err != nil {
+		return errors.Wrapf(err, "failed to get fork at block %d, hash %x", height, ctx.round.prevHash[:])
+	}
+	roundCalc := ctx.roundCalc.Fork(fork)
+	if !roundCalc.IsDelegate(endorserAddr.String(), height) {
 		return errors.Errorf("%s is not delegate of the corresponding round", endorserAddr)
 	}
 
@@ -255,7 +263,13 @@ func (ctx *rollDPoSCtx) CheckBlockProposer(
 	if endorserAddr == nil {
 		return errors.New("failed to get address")
 	}
-	if proposer := ctx.roundCalc.Proposer(height, ctx.BlockInterval(height), en.Timestamp()); proposer != endorserAddr.String() {
+	prevHash := proposal.block.PrevHash()
+	fork, err := ctx.chain.Fork(prevHash)
+	if err != nil {
+		return errors.Wrapf(err, "failed to get fork at block %d, hash %x", proposal.block.Height(), prevHash[:])
+	}
+	roundCalc := ctx.roundCalc.Fork(fork)
+	if proposer := roundCalc.Proposer(height, ctx.BlockInterval(height), en.Timestamp()); proposer != endorserAddr.String() {
 		return errors.Errorf(
 			"%s is not proposer of the corresponding round, %s expected",
 			endorserAddr.String(),
@@ -263,14 +277,14 @@ func (ctx *rollDPoSCtx) CheckBlockProposer(
 		)
 	}
 	proposerAddr := proposal.ProposerAddress()
-	if ctx.roundCalc.Proposer(height, ctx.BlockInterval(height), proposal.block.Timestamp()) != proposerAddr {
+	if roundCalc.Proposer(height, ctx.BlockInterval(height), proposal.block.Timestamp()) != proposerAddr {
 		return errors.Errorf("%s is not proposer of the corresponding round", proposerAddr)
 	}
 	if !proposal.block.VerifySignature() {
 		return errors.Errorf("invalid block signature")
 	}
 	if proposerAddr != endorserAddr.String() {
-		round, err := ctx.roundCalc.NewRound(height, ctx.BlockInterval(height), en.Timestamp(), nil)
+		round, err := roundCalc.NewRound(height, ctx.BlockInterval(height), en.Timestamp(), nil)
 		if err != nil {
 			return err
 		}
@@ -656,7 +670,7 @@ func (ctx *rollDPoSCtx) mintNewBlock(privateKey crypto.PrivateKey) (*EndorsedCon
 	blk := ctx.round.CachedMintedBlock()
 	if blk == nil {
 		// in case that there is no cached block in eManagerDB, it mints a new block.
-		blk, err = ctx.chain.MintNewBlock(ctx.round.StartTime(), privateKey)
+		blk, err = ctx.chain.MintNewBlock(ctx.round.StartTime(), privateKey, ctx.round.PrevHash())
 		if err != nil {
 			return nil, err
 		}
diff --git a/consensus/scheme/rolldpos/rolldposctx_test.go b/consensus/scheme/rolldpos/rolldposctx_test.go
index a0f59ebfbb..4d64dff736 100644
--- a/consensus/scheme/rolldpos/rolldposctx_test.go
+++ b/consensus/scheme/rolldpos/rolldposctx_test.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/facebookgo/clock"
 	"github.com/iotexproject/go-pkgs/crypto"
+	"github.com/iotexproject/go-pkgs/hash"
 	"github.com/iotexproject/iotex-proto/golang/iotextypes"
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/require"
@@ -25,10 +26,11 @@ import (
 	"github.com/iotexproject/iotex-core/v2/db"
 	"github.com/iotexproject/iotex-core/v2/endorsement"
 	"github.com/iotexproject/iotex-core/v2/state"
+	"github.com/iotexproject/iotex-core/v2/state/factory"
 	"github.com/iotexproject/iotex-core/v2/test/identityset"
 )
 
-var dummyCandidatesByHeightFunc = func(uint64) ([]string, error) { return nil, nil }
+var dummyCandidatesByHeightFunc = func(uint64, []byte) ([]string, error) { return nil, nil }
 
 func TestRollDPoSCtx(t *testing.T) {
 	require := require.New(t)
@@ -36,7 +38,7 @@ func TestRollDPoSCtx(t *testing.T) {
 	g := genesis.TestDefault()
 	dbConfig := db.DefaultConfig
 	dbConfig.DbPath = DefaultConfig.ConsensusDBPath
-	b, _, _, _, _ := makeChain(t)
+	b, sf, _, _, _ := makeChain(t)
 
 	t.Run("case 1:panic because of chain is nil", func(t *testing.T) {
 		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, nil, block.NewDeserializer(0), nil, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, nil, 0)
@@ -44,7 +46,7 @@ func TestRollDPoSCtx(t *testing.T) {
 	})
 
 	t.Run("case 2:panic because of rp is nil", func(t *testing.T) {
-		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b), block.NewDeserializer(0), nil, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, nil, 0)
+		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b, sf, &dummyBlockBuildFactory{}), block.NewDeserializer(0), nil, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, nil, 0)
 		require.Error(err)
 	})
 
@@ -54,7 +56,7 @@ func TestRollDPoSCtx(t *testing.T) {
 		g.NumSubEpochs,
 	)
 	t.Run("case 3:panic because of clock is nil", func(t *testing.T) {
-		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b), block.NewDeserializer(0), rp, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, nil, 0)
+		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b, sf, &dummyBlockBuildFactory{}), block.NewDeserializer(0), rp, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, nil, 0)
 		require.Error(err)
 	})
 
@@ -64,19 +66,19 @@ func TestRollDPoSCtx(t *testing.T) {
 	cfg.FSM.AcceptLockEndorsementTTL = time.Second
 	cfg.FSM.CommitTTL = time.Second
 	t.Run("case 4:panic because of fsm time bigger than block interval", func(t *testing.T) {
-		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b), block.NewDeserializer(0), rp, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, c, 0)
+		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b, sf, &dummyBlockBuildFactory{}), block.NewDeserializer(0), rp, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, c, 0)
 		require.Error(err)
 	})
 
 	g.Blockchain.BlockInterval = time.Second * 20
 	t.Run("case 5:panic because of nil CandidatesByHeight function", func(t *testing.T) {
-		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b), block.NewDeserializer(0), rp, nil, nil, nil, nil, c, 0)
+		_, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b, sf, &dummyBlockBuildFactory{}), block.NewDeserializer(0), rp, nil, nil, nil, nil, c, 0)
 		require.Error(err)
 	})
 
 	t.Run("case 6:normal", func(t *testing.T) {
 		bh := g.BeringBlockHeight
-		rctx, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b), block.NewDeserializer(0), rp, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, c, bh)
+		rctx, err := NewRollDPoSCtx(consensusfsm.NewConsensusConfig(cfg.FSM, consensusfsm.DefaultDardanellesUpgradeConfig, g, cfg.Delay), dbConfig, true, time.Second, true, NewChainManager(b, sf, &dummyBlockBuildFactory{}), block.NewDeserializer(0), rp, nil, dummyCandidatesByHeightFunc, dummyCandidatesByHeightFunc, nil, c, bh)
 		require.NoError(err)
 		require.Equal(bh, rctx.RoundCalculator().beringHeight)
 		require.NotNil(rctx)
@@ -89,7 +91,7 @@ func TestCheckVoteEndorser(t *testing.T) {
 	c := clock.New()
 	g := genesis.TestDefault()
 	g.Blockchain.BlockInterval = time.Second * 20
-	delegatesByEpochFunc := func(epochnum uint64) ([]string, error) {
+	delegatesByEpochFunc := func(epochnum uint64, _ []byte) ([]string, error) {
 		re := protocol.NewRegistry()
 		if err := rp.Register(re); err != nil {
 			return nil, err
@@ -130,7 +132,7 @@ func TestCheckVoteEndorser(t *testing.T) {
 		true,
 		time.Second,
 		true,
-		NewChainManager(b),
+		NewChainManager(b, sf, &dummyBlockBuildFactory{}),
 		block.NewDeserializer(0),
 		rp,
 		nil,
@@ -142,6 +144,7 @@ func TestCheckVoteEndorser(t *testing.T) {
 	)
 	require.NoError(err)
 	require.NotNil(rctx)
+	require.NoError(rctx.Start(context.Background()))
 
 	// case 1:endorser nil caused panic
 	require.Panics(func() { rctx.CheckVoteEndorser(0, nil, nil) }, "")
@@ -158,10 +161,10 @@ func TestCheckVoteEndorser(t *testing.T) {
 func TestCheckBlockProposer(t *testing.T) {
 	require := require.New(t)
 	g := genesis.TestDefault()
-	b, sf, _, rp, pp := makeChain(t)
+	b, sf, ap, rp, pp := makeChain(t)
 	c := clock.New()
 	g.Blockchain.BlockInterval = time.Second * 20
-	delegatesByEpochFunc := func(epochnum uint64) ([]string, error) {
+	delegatesByEpochFunc := func(epochnum uint64, _ []byte) ([]string, error) {
 		re := protocol.NewRegistry()
 		if err := rp.Register(re); err != nil {
 			return nil, err
@@ -202,7 +205,7 @@ func TestCheckBlockProposer(t *testing.T) {
 		true,
 		time.Second,
 		true,
-		NewChainManager(b),
+		NewChainManager(b, sf, factory.NewMinter(sf, ap)),
 		block.NewDeserializer(0),
 		rp,
 		nil,
@@ -214,7 +217,8 @@ func TestCheckBlockProposer(t *testing.T) {
 	)
 	require.NoError(err)
 	require.NotNil(rctx)
-	block := getBlockforctx(t, 0, false)
+	prevHash := b.TipHash()
+	block := getBlockforctx(t, 0, false, prevHash)
 	en := endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(10).PublicKey(), nil)
 	bp := newBlockProposal(&block, []*endorsement.Endorsement{en})
 
@@ -239,20 +243,20 @@ func TestCheckBlockProposer(t *testing.T) {
 	require.Error(rctx.CheckBlockProposer(51, bp, en))
 
 	// case 6:invalid block signature
-	block = getBlockforctx(t, 1, false)
+	block = getBlockforctx(t, 1, false, prevHash)
 	en = endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(1).PublicKey(), nil)
 	bp = newBlockProposal(&block, []*endorsement.Endorsement{en})
 	require.Error(rctx.CheckBlockProposer(51, bp, en))
 
 	// case 7:invalid endorsement for the vote when call AddVoteEndorsement
-	block = getBlockforctx(t, 1, true)
+	block = getBlockforctx(t, 1, true, prevHash)
 	en = endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(1).PublicKey(), nil)
 	en2 := endorsement.NewEndorsement(time.Unix(1596329600, 0), identityset.PrivateKey(7).PublicKey(), nil)
 	bp = newBlockProposal(&block, []*endorsement.Endorsement{en2, en})
 	require.Error(rctx.CheckBlockProposer(51, bp, en2))
 
 	// case 8:Insufficient endorsements
-	block = getBlockforctx(t, 1, true)
+	block = getBlockforctx(t, 1, true, prevHash)
 	hash := block.HashBlock()
 	vote := NewConsensusVote(hash[:], COMMIT)
 	ens, err := endorsement.Endorse(vote, time.Unix(1562382592, 0), identityset.PrivateKey(7))
@@ -262,7 +266,7 @@ func TestCheckBlockProposer(t *testing.T) {
 	require.Error(rctx.CheckBlockProposer(51, bp, ens[0]))
 
 	// case 9:normal
-	block = getBlockforctx(t, 1, true)
+	block = getBlockforctx(t, 1, true, prevHash)
 	bp = newBlockProposal(&block, []*endorsement.Endorsement{en})
 	require.NoError(rctx.CheckBlockProposer(51, bp, en))
 }
@@ -273,7 +277,7 @@ func TestNotProducingMultipleBlocks(t *testing.T) {
 	c := clock.New()
 	g := genesis.TestDefault()
 	g.Blockchain.BlockInterval = time.Second * 20
-	delegatesByEpoch := func(epochnum uint64) ([]string, error) {
+	delegatesByEpoch := func(epochnum uint64, _ []byte) ([]string, error) {
 		re := protocol.NewRegistry()
 		if err := rp.Register(re); err != nil {
 			return nil, err
@@ -314,7 +318,7 @@ func TestNotProducingMultipleBlocks(t *testing.T) {
 		true,
 		time.Second,
 		true,
-		NewChainManager(b),
+		NewChainManager(b, sf, &dummyBlockBuildFactory{}),
 		block.NewDeserializer(0),
 		rp,
 		nil,
@@ -348,14 +352,14 @@ func TestNotProducingMultipleBlocks(t *testing.T) {
 	require.Equal(height1, height2)
 }
 
-func getBlockforctx(t *testing.T, i int, sign bool) block.Block {
+func getBlockforctx(t *testing.T, i int, sign bool, prevHash hash.Hash256) block.Block {
 	require := require.New(t)
 	ts := &timestamppb.Timestamp{Seconds: 1596329600, Nanos: 10}
 	hcore := &iotextypes.BlockHeaderCore{
 		Version:          1,
 		Height:           51,
 		Timestamp:        ts,
-		PrevBlockHash:    []byte(""),
+		PrevBlockHash:    prevHash[:],
 		TxRoot:           []byte(""),
 		DeltaStateDigest: []byte(""),
 		ReceiptRoot:      []byte(""),
diff --git a/consensus/scheme/rolldpos/roundcalculator.go b/consensus/scheme/rolldpos/roundcalculator.go
index c012341396..42e4f9d482 100644
--- a/consensus/scheme/rolldpos/roundcalculator.go
+++ b/consensus/scheme/rolldpos/roundcalculator.go
@@ -9,15 +9,17 @@ import (
 	"time"
 
 	"github.com/pkg/errors"
+	"go.uber.org/zap"
 
 	"github.com/iotexproject/iotex-core/v2/action/protocol/rolldpos"
 	"github.com/iotexproject/iotex-core/v2/endorsement"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
 )
 
 var errInvalidCurrentTime = errors.New("invalid current time")
 
 type roundCalculator struct {
-	chain                ChainManager
+	chain                ForkChain
 	timeBasedRotation    bool
 	rp                   *rolldpos.Protocol
 	delegatesByEpochFunc NodesSelectionByEpochFunc
@@ -77,6 +79,7 @@ func (c *roundCalculator) UpdateRound(round *roundCtx, height uint64, blockInter
 			return nil, err
 		}
 	}
+	prevHash := c.chain.TipHash()
 	return &roundCtx{
 		epochNum:             epochNum,
 		epochStartHeight:     epochStartHeight,
@@ -87,6 +90,7 @@ func (c *roundCalculator) UpdateRound(round *roundCtx, height uint64, blockInter
 
 		height:             height,
 		roundNum:           roundNum,
+		prevHash:           prevHash,
 		proposer:           proposer,
 		roundStartTime:     roundStartTime,
 		nextRoundStartTime: roundStartTime.Add(blockInterval),
@@ -101,6 +105,7 @@ func (c *roundCalculator) UpdateRound(round *roundCtx, height uint64, blockInter
 func (c *roundCalculator) Proposer(height uint64, blockInterval time.Duration, roundStartTime time.Time) string {
 	round, err := c.newRound(height, blockInterval, roundStartTime, nil, 0)
 	if err != nil {
+		log.L().Warn("Failed to get proposer", zap.Error(err))
 		return ""
 	}
 
@@ -110,6 +115,7 @@ func (c *roundCalculator) Proposer(height uint64, blockInterval time.Duration, r
 func (c *roundCalculator) IsDelegate(addr string, height uint64) bool {
 	delegates, err := c.Delegates(height)
 	if err != nil {
+		log.L().Warn("Failed to get delegates", zap.Error(err))
 		return false
 	}
 	for _, d := range delegates {
@@ -181,13 +187,15 @@ func (c *roundCalculator) roundInfo(
 // Delegates returns list of delegates at given height
 func (c *roundCalculator) Delegates(height uint64) ([]string, error) {
 	epochNum := c.rp.GetEpochNum(height)
-	return c.delegatesByEpochFunc(epochNum)
+	prevHash := c.chain.TipHash()
+	return c.delegatesByEpochFunc(epochNum, prevHash[:])
 }
 
 // Proposers returns list of candidate proposers at given height
 func (c *roundCalculator) Proposers(height uint64) ([]string, error) {
 	epochNum := c.rp.GetEpochNum(height)
-	return c.proposersByEpochFunc(epochNum)
+	prevHash := c.chain.TipHash()
+	return c.proposersByEpochFunc(epochNum, prevHash[:])
 }
 
 // NewRoundWithToleration starts new round with tolerated over time
@@ -245,6 +253,7 @@ func (c *roundCalculator) newRound(
 			return nil, err
 		}
 	}
+	prevHash := c.chain.TipHash()
 	round = &roundCtx{
 		epochNum:             epochNum,
 		epochStartHeight:     epochStartHeight,
@@ -255,6 +264,7 @@ func (c *roundCalculator) newRound(
 
 		height:             height,
 		roundNum:           roundNum,
+		prevHash:           prevHash,
 		proposer:           proposer,
 		eManager:           eManager,
 		roundStartTime:     roundStartTime,
@@ -285,3 +295,14 @@ func (c *roundCalculator) calculateProposer(
 	proposer = proposers[idx%numProposers]
 	return
 }
+
+func (c *roundCalculator) Fork(fork ForkChain) *roundCalculator {
+	return &roundCalculator{
+		chain:                fork,
+		timeBasedRotation:    c.timeBasedRotation,
+		rp:                   c.rp,
+		delegatesByEpochFunc: c.delegatesByEpochFunc,
+		proposersByEpochFunc: c.proposersByEpochFunc,
+		beringHeight:         c.beringHeight,
+	}
+}
diff --git a/consensus/scheme/rolldpos/roundcalculator_test.go b/consensus/scheme/rolldpos/roundcalculator_test.go
index d97470f224..d2f47747d4 100644
--- a/consensus/scheme/rolldpos/roundcalculator_test.go
+++ b/consensus/scheme/rolldpos/roundcalculator_test.go
@@ -227,7 +227,7 @@ func makeChain(t *testing.T) (blockchain.Blockchain, factory.Factory, actpool.Ac
 
 func makeRoundCalculator(t *testing.T) *roundCalculator {
 	bc, sf, _, rp, pp := makeChain(t)
-	delegatesByEpoch := func(epochNum uint64) ([]string, error) {
+	delegatesByEpoch := func(epochNum uint64, _ []byte) ([]string, error) {
 		re := protocol.NewRegistry()
 		if err := rp.Register(re); err != nil {
 			return nil, err
@@ -265,7 +265,7 @@ func makeRoundCalculator(t *testing.T) *roundCalculator {
 		return addrs, nil
 	}
 	return &roundCalculator{
-		NewChainManager(bc),
+		NewChainManager(bc, sf, &dummyBlockBuildFactory{}),
 		true,
 		rp,
 		delegatesByEpoch,
diff --git a/consensus/scheme/rolldpos/roundctx.go b/consensus/scheme/rolldpos/roundctx.go
index d5ae15340e..463be111be 100644
--- a/consensus/scheme/rolldpos/roundctx.go
+++ b/consensus/scheme/rolldpos/roundctx.go
@@ -11,8 +11,11 @@ import (
 	"github.com/pkg/errors"
 	"go.uber.org/zap"
 
+	"github.com/iotexproject/go-pkgs/hash"
+
 	"github.com/iotexproject/iotex-core/v2/blockchain/block"
 	"github.com/iotexproject/iotex-core/v2/endorsement"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
 )
 
 // ErrInsufficientEndorsements represents the error that not enough endorsements
@@ -37,6 +40,7 @@ type roundCtx struct {
 
 	height             uint64
 	roundNum           uint32
+	prevHash           hash.Hash256
 	proposer           string
 	roundStartTime     time.Time
 	nextRoundStartTime time.Time
@@ -53,6 +57,7 @@ func (ctx *roundCtx) Log(l *zap.Logger) *zap.Logger {
 		zap.Uint64("epoch", ctx.epochNum),
 		zap.Uint32("round", ctx.roundNum),
 		zap.String("proposer", ctx.proposer),
+		log.Hex("prevHash", ctx.prevHash[:]),
 	)
 }
 
@@ -88,6 +93,10 @@ func (ctx *roundCtx) Number() uint32 {
 	return ctx.roundNum
 }
 
+func (ctx *roundCtx) PrevHash() hash.Hash256 {
+	return ctx.prevHash
+}
+
 func (ctx *roundCtx) Proposer() string {
 	return ctx.proposer
 }
diff --git a/e2etest/local_test.go b/e2etest/local_test.go
index d388f6ae08..9934cbfb8a 100644
--- a/e2etest/local_test.go
+++ b/e2etest/local_test.go
@@ -559,8 +559,8 @@ func TestStartExistingBlockchain(t *testing.T) {
 
 func newTestConfig() (config.Config, error) {
 	cfg := config.Default
-	cfg.Genesis = genesis.TestDefault()
 	cfg = deepcopy.Copy(cfg).(config.Config)
+	cfg.Genesis = genesis.TestDefault()
 	cfg.Chain.TrieDBPath = _triePath
 	cfg.Chain.ChainDBPath = _dBPath
 	cfg.Chain.BlobStoreDBPath = _blobPath
diff --git a/state/factory/blockpreparer.go b/state/factory/blockpreparer.go
new file mode 100644
index 0000000000..61399c40d0
--- /dev/null
+++ b/state/factory/blockpreparer.go
@@ -0,0 +1,85 @@
+package factory
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+type (
+	blockPreparer struct {
+		tasks   map[hash.Hash256]map[int64]chan struct{}
+		results map[hash.Hash256]map[int64]*mintResult
+		mu      sync.Mutex
+	}
+	mintResult struct {
+		blk *block.Block
+		err error
+	}
+)
+
+func newBlockPreparer() *blockPreparer {
+	return &blockPreparer{
+		tasks:   make(map[hash.Hash256]map[int64]chan struct{}),
+		results: make(map[hash.Hash256]map[int64]*mintResult),
+	}
+}
+
+func (d *blockPreparer) PrepareOrWait(ctx context.Context, prevHash []byte, timestamp time.Time, fn func() (*block.Block, error)) (*block.Block, error) {
+	d.mu.Lock()
+	task := d.prepare(prevHash, timestamp, fn)
+	d.mu.Unlock()
+
+	select {
+	case <-task:
+	case <-ctx.Done():
+		var null *block.Block
+		return null, errors.Wrapf(ctx.Err(), "wait for draft block timeout %v", timestamp)
+	}
+
+	d.mu.Lock()
+	res := d.results[hash.Hash256(prevHash)][timestamp.UnixNano()]
+	d.mu.Unlock()
+	return res.blk, res.err
+}
+
+func (d *blockPreparer) prepare(prevHash []byte, timestamp time.Time, mintFn func() (*block.Block, error)) chan struct{} {
+	if forks, ok := d.tasks[hash.BytesToHash256(prevHash)]; ok {
+		if ch, ok := forks[timestamp.UnixNano()]; ok {
+			log.L().Debug("draft block already exists", log.Hex("prevHash", prevHash))
+			return ch
+		}
+	} else {
+		d.tasks[hash.BytesToHash256(prevHash)] = make(map[int64]chan struct{})
+	}
+	task := make(chan struct{})
+	d.tasks[hash.BytesToHash256(prevHash)][timestamp.UnixNano()] = task
+
+	go func() {
+		blk, err := mintFn()
+		d.mu.Lock()
+		if _, ok := d.results[hash.BytesToHash256(prevHash)]; !ok {
+			d.results[hash.BytesToHash256(prevHash)] = make(map[int64]*mintResult)
+		}
+		d.results[hash.BytesToHash256(prevHash)][timestamp.UnixNano()] = &mintResult{blk: blk, err: err}
+		d.mu.Unlock()
+		close(task)
+		log.L().Debug("prepare mint returned", zap.Error(err))
+	}()
+
+	return task
+}
+
+func (d *blockPreparer) ReceiveBlock(blk *block.Block) error {
+	d.mu.Lock()
+	delete(d.tasks, blk.PrevHash())
+	d.mu.Unlock()
+	return nil
+}
diff --git a/state/factory/factory.go b/state/factory/factory.go
index 271ea72a44..d16feefbbc 100644
--- a/state/factory/factory.go
+++ b/state/factory/factory.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/iotexproject/go-pkgs/cache"
 	"github.com/iotexproject/go-pkgs/crypto"
+	"github.com/iotexproject/go-pkgs/hash"
 
 	"github.com/iotexproject/iotex-core/v2/action"
 	"github.com/iotexproject/iotex-core/v2/action/protocol"
@@ -76,6 +77,7 @@ type (
 		PutBlock(context.Context, *block.Block) error
 		WorkingSet(context.Context) (protocol.StateManager, error)
 		WorkingSetAtHeight(context.Context, uint64, ...*action.SealedEnvelope) (protocol.StateManager, error)
+		StateReaderAt(blkHeight uint64, blkHash hash.Hash256) (protocol.StateReader, error)
 	}
 
 	// factory implements StateFactory interface, tracks changes to account/contract and batch-commits to DB
diff --git a/state/factory/minter.go b/state/factory/minter.go
index 67dfe4443c..00ad707077 100644
--- a/state/factory/minter.go
+++ b/state/factory/minter.go
@@ -7,45 +7,76 @@ package factory
 
 import (
 	"context"
+	"sync"
 	"time"
 
 	"github.com/iotexproject/go-pkgs/crypto"
 
 	"github.com/iotexproject/iotex-core/v2/action/protocol"
 	"github.com/iotexproject/iotex-core/v2/actpool"
-	"github.com/iotexproject/iotex-core/v2/blockchain"
 	"github.com/iotexproject/iotex-core/v2/blockchain/block"
 )
 
-type MintOption func(*minter)
+type MintOption func(*Minter)
 
 // WithTimeoutOption sets the timeout for NewBlockBuilder
 func WithTimeoutOption(timeout time.Duration) MintOption {
-	return func(m *minter) {
+	return func(m *Minter) {
 		m.timeout = timeout
 	}
 }
 
-type minter struct {
-	f       Factory
-	ap      actpool.ActPool
-	timeout time.Duration
+// Minter is a wrapper of Factory to mint blocks
+type Minter struct {
+	f             Factory
+	ap            actpool.ActPool
+	timeout       time.Duration
+	blockPreparer *blockPreparer
+	mu            sync.Mutex
 }
 
 // NewMinter creates a wrapper instance
-func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) blockchain.BlockMinter {
-	m := &minter{f: f, ap: ap}
+func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) *Minter {
+	m := &Minter{
+		f:             f,
+		ap:            ap,
+		blockPreparer: newBlockPreparer(),
+	}
 	for _, opt := range opts {
 		opt(m)
 	}
 	return m
 }
 
-// NewBlockBuilder implements the BlockMinter interface
-func (m *minter) Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
+// Mint creates a block with the given private key
+func (m *Minter) Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
+	bcCtx := protocol.MustGetBlockchainCtx(ctx)
+	blkCtx := protocol.MustGetBlockCtx(ctx)
+
+	return m.blockPreparer.PrepareOrWait(ctx, bcCtx.Tip.Hash[:], blkCtx.BlockTimeStamp, func() (*block.Block, error) {
+		return m.mint(ctx, pk)
+	})
+}
+
+// ReceiveBlock receives a confirmed block
+func (m *Minter) ReceiveBlock(blk *block.Block) error {
+	return m.blockPreparer.ReceiveBlock(blk)
+}
+
+func (m *Minter) mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
 	if m.timeout > 0 {
-		var cancel context.CancelFunc
-		ctx, cancel = context.WithDeadline(ctx, protocol.MustGetBlockCtx(ctx).BlockTimeStamp.Add(m.timeout))
+		// set deadline for NewBlockBuilder
+		// ensure that minting finishes before `block start time + timeout` and that its duration does not exceed the timeout.
+		var (
+			cancel context.CancelFunc
+			now    = time.Now()
+			blkTs  = protocol.MustGetBlockCtx(ctx).BlockTimeStamp
+			ddl    = blkTs.Add(m.timeout)
+		)
+		if now.Before(blkTs) {
+			ddl = now.Add(m.timeout)
+		}
+		ctx, cancel = context.WithDeadline(ctx, ddl)
 		defer cancel()
 	}
 	return m.f.Mint(ctx, m.ap, pk)
diff --git a/state/factory/statedb.go b/state/factory/statedb.go
index afbe6fae31..9a15440fa0 100644
--- a/state/factory/statedb.go
+++ b/state/factory/statedb.go
@@ -422,6 +422,25 @@ func (sdb *stateDB) ReadView(name string) (protocol.View, error) {
 	return sdb.protocolView.Read(name)
 }
 
+// StateReaderAt returns a state reader at a specific height
+func (sdb *stateDB) StateReaderAt(blkHeight uint64, blkHash hash.Hash256) (protocol.StateReader, error) {
+	sdb.mutex.RLock()
+	curHeight := sdb.currentChainHeight
+	sdb.mutex.RUnlock()
+	if blkHeight == curHeight {
+		return sdb, nil
+	} else if blkHeight < curHeight {
+		return nil, errors.Errorf("cannot read state at height %d, current height is %d", blkHeight, curHeight)
+	}
+	if data, ok := sdb.workingsets.Get(blkHash); ok {
+		if ws, ok := data.(*workingSet); ok {
+			return ws, nil
+		}
+		return nil, errors.New("type assertion failed to be WorkingSet")
+	}
+	return nil, errors.Errorf("failed to get workingset at %x", blkHash)
+}
+
 //======================================
 // private trie constructor functions
 //======================================
diff --git a/test/mock/mock_factory/mock_factory.go b/test/mock/mock_factory/mock_factory.go
index e5fb5c73ee..1e73d588c9 100644
--- a/test/mock/mock_factory/mock_factory.go
+++ b/test/mock/mock_factory/mock_factory.go
@@ -10,6 +10,7 @@ import (
 
 	gomock "github.com/golang/mock/gomock"
 	crypto "github.com/iotexproject/go-pkgs/crypto"
+	hash "github.com/iotexproject/go-pkgs/hash"
 	action "github.com/iotexproject/iotex-core/v2/action"
 	protocol "github.com/iotexproject/iotex-core/v2/action/protocol"
 	actpool "github.com/iotexproject/iotex-core/v2/actpool"
@@ -147,6 +148,21 @@ func (mr *MockFactoryMockRecorder) State(arg0 interface{}, arg1 ...interface{})
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockFactory)(nil).State), varargs...)
 }
 
+// StateReaderAt mocks base method.
+func (m *MockFactory) StateReaderAt(blkHeight uint64, blkHash hash.Hash256) (protocol.StateReader, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "StateReaderAt", blkHeight, blkHash)
+	ret0, _ := ret[0].(protocol.StateReader)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// StateReaderAt indicates an expected call of StateReaderAt.
+func (mr *MockFactoryMockRecorder) StateReaderAt(blkHeight, blkHash interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateReaderAt", reflect.TypeOf((*MockFactory)(nil).StateReaderAt), blkHeight, blkHash)
+}
+
 // States mocks base method.
 func (m *MockFactory) States(arg0 ...protocol.StateOption) (uint64, state.Iterator, error) {
 	m.ctrl.T.Helper()


Reviews:
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T01:12:08Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T01:12:18Z
user: login: CoderZhi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T00:55:39Z
user: login: CoderZhi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-05T09:34:11Z
user: login: CoderZhi
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T14:17:11Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:25:03Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:25:31Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:39:52Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:44:00Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:48:54Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:53:41Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:57:24Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T00:13:32Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T00:14:57Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T00:15:15Z
user: login: envestcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T01:48:09Z
user: login: envestcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T01:56:32Z
user: login: envestcc
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T02:09:39Z
user: login: dustinxie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T00:47:31Z
user: login: dustinxie
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T02:57:29Z

Review comments:
diff_hunk: @@ -423,19 +425,40 @@ func getChainConfig(g genesis.Blockchain, height uint64, id uint32, getBlockTime
 	sumatraTime, err := getBlockTime(g.SumatraBlockHeight)
 	if err != nil {
 		return nil, err
+	} else if sumatraTime != nil {
+		sumatraTimestamp := (uint64)(sumatraTime.Unix())
+		chainConfig.ShanghaiTime = &sumatraTimestamp
path: action/protocol/execution/evm/evm.go
user: login: CoderZhi
body: revert
created_at: 2025-03-26T13:14:36Z
author_association: COLLABORATOR
diff_hunk: @@ -423,19 +425,40 @@ func getChainConfig(g genesis.Blockchain, height uint64, id uint32, getBlockTime
 	sumatraTime, err := getBlockTime(g.SumatraBlockHeight)
 	if err != nil {
 		return nil, err
+	} else if sumatraTime != nil {
+		sumatraTimestamp := (uint64)(sumatraTime.Unix())
+		chainConfig.ShanghaiTime = &sumatraTimestamp
 	}
-	sumatraTimestamp := (uint64)(sumatraTime.Unix())
-	chainConfig.ShanghaiTime = &sumatraTimestamp
 	// enable Cancun at Vanuatu
 	cancunTime, err := getBlockTime(g.VanuatuBlockHeight)
 	if err != nil {
 		return nil, err
+	} else if cancunTime != nil {
+		cancunTimestamp := (uint64)(cancunTime.Unix())
+		chainConfig.CancunTime = &cancunTimestamp
path: action/protocol/execution/evm/evm.go
user: login: CoderZhi
body: revert
created_at: 2025-03-26T13:14:44Z
author_association: COLLABORATOR
diff_hunk: @@ -253,7 +253,17 @@ func (bc *blockchain) Start(ctx context.Context) error {
 				GetBlockTime: bc.getBlockTime,
 			},
 		), bc.genesis))
-	return bc.lifecycle.OnStart(ctx)
+	err := bc.lifecycle.OnStart(ctx)
+	if err != nil {
path: blockchain/blockchain.go
user: login: dustinxie
body: combine 2 lines
created_at: 2025-04-02T01:12:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -253,7 +253,17 @@ func (bc *blockchain) Start(ctx context.Context) error {
 				GetBlockTime: bc.getBlockTime,
 			},
 		), bc.genesis))
-	return bc.lifecycle.OnStart(ctx)
+	err := bc.lifecycle.OnStart(ctx)
+	if err != nil {
+		return err
+	}
+	// init block builder factory
+	if tip, err := bc.tipInfo(bc.TipHeight()); err != nil {
+		return errors.Wrap(err, "failed to get tip info")
+	} else {
path: blockchain/blockchain.go
user: login: dustinxie
body: remove else
created_at: 2025-04-02T01:12:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,44 +7,130 @@ package factory
 
 import (
 	"context"
+	"sync"
 	"time"
 
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/go-pkgs/crypto"
+	"github.com/iotexproject/go-pkgs/hash"
+
 	"github.com/iotexproject/iotex-core/v2/action"
 	"github.com/iotexproject/iotex-core/v2/action/protocol"
 	"github.com/iotexproject/iotex-core/v2/actpool"
-	"github.com/iotexproject/iotex-core/v2/blockchain"
 	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
 )
 
-type MintOption func(*minter)
+type MintOption func(*Minter)
 
 // WithTimeoutOption sets the timeout for NewBlockBuilder
 func WithTimeoutOption(timeout time.Duration) MintOption {
-	return func(m *minter) {
+	return func(m *Minter) {
 		m.timeout = timeout
 	}
 }
 
-type minter struct {
-	f       Factory
-	ap      actpool.ActPool
-	timeout time.Duration
+type Minter struct {
+	f             Factory
+	ap            actpool.ActPool
+	timeout       time.Duration
+	blockPreparer *blockPreparer
+	proposalPool  *proposalPool
+	mu            sync.Mutex
 }
 
 // NewMinter creates a wrapper instance
-func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) blockchain.BlockBuilderFactory {
-	m := &minter{f: f, ap: ap}
+func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) *Minter {
+	m := &Minter{
+		f:             f,
+		ap:            ap,
+		blockPreparer: newBlockPreparer(),
+		proposalPool:  newProposalPool(),
+	}
 	for _, opt := range opts {
 		opt(m)
 	}
 	return m
 }
 
-// NewBlockBuilder implements the BlockMinter interface
-func (m *minter) NewBlockBuilder(ctx context.Context, sign func(action.Envelope) (*action.SealedEnvelope, error)) (*block.Builder, error) {
+func (m *Minter) Init(root hash.Hash256) {
+	m.proposalPool.Init(root)
+}
+
+func (m *Minter) Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
+	bcCtx := protocol.MustGetBlockchainCtx(ctx)
+	blkCtx := protocol.MustGetBlockCtx(ctx)
+
+	// create a new block
+	blk, err := m.blockPreparer.PrepareOrWait(ctx, bcCtx.Tip.Hash[:], blkCtx.BlockTimeStamp, func() (*block.Block, error) {
+		blk, err := m.mint(ctx, pk)
+		if err != nil {
+			return nil, err
+		}
+		if err = m.proposalPool.AddBlock(blk); err != nil {
+			log.L().Error("failed to add block to proposal pool", zap.Error(err))
+		}
+		return blk, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	return blk, nil
+}
+
+func (m *Minter) AddProposal(blk *block.Block) error {
+	return m.proposalPool.AddBlock(blk)
+}
+
+func (m *Minter) ReceiveBlock(blk *block.Block) error {
+	prevHash := blk.PrevHash()
+	l := log.L().With(zap.Uint64("height", blk.Height()), log.Hex("prevHash", prevHash[:]), zap.Time("timestamp", blk.Timestamp()))
+	if err := m.blockPreparer.ReceiveBlock(blk); err != nil {
+		l.Error("failed to receive block", zap.Error(err))
+	}
+	if err := m.proposalPool.ReceiveBlock(blk); err != nil {
+		l.Error("failed to receive block", zap.Error(err))
path: state/factory/minter.go
user: login: CoderZhi
body: different error message from L90
created_at: 2025-04-03T00:39:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,148 @@
+package factory
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// blocks is a map of draft blocks
+	// key is the hash of the block
+	blocks map[hash.Hash256]*block.Block
+	// forks is a map of draft blocks that are forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	forks map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		blocks: make(map[hash.Hash256]*block.Block),
+		forks:  make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Info("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.blocks[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.forks[prevHash]; ok {
+		delete(d.forks, prevHash)
+	} else if prevHash != d.root {
+		return errors.Errorf("block %x is not a tip of any fork", prevHash[:])
+	}
+	d.forks[hash] = blk.Timestamp()
+	d.blocks[hash] = blk
+	log.L().Info("added block to draft pool", log.Hex("hash", hash[:]), zap.Uint64("height", blk.Height()), zap.Time("timestamp", blk.Timestamp()))
+	return nil
+}
+
+// ReceiveBlock a block has been confirmed and remove all the blocks that is previous to it, or other forks
+func (d *proposalPool) ReceiveBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	// find the fork that contains this block
+	fork, err := d.forkAt(blk)
+	if err != nil {
+		return err
+	}
+
+	// remove blocks in other forks or older blocks in the same fork
+	for f := range d.forks {
+		start := d.blocks[f]
+		if f == fork {
+			start = blk
+		}
+		for b := start; b != nil; b = d.blocks[b.PrevHash()] {
+			ha := b.HashBlock()
+			log.L().Info("remove block from draft pool", log.Hex("hash", ha[:]), zap.Uint64("height", b.Height()), zap.Time("timestamp", b.Timestamp()))
path: state/factory/proposalpool.go
user: login: CoderZhi
body: Debug
created_at: 2025-04-03T00:50:13Z
author_association: COLLABORATOR
diff_hunk: @@ -7,44 +7,130 @@ package factory
 
 import (
 	"context"
+	"sync"
 	"time"
 
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/go-pkgs/crypto"
+	"github.com/iotexproject/go-pkgs/hash"
+
 	"github.com/iotexproject/iotex-core/v2/action"
 	"github.com/iotexproject/iotex-core/v2/action/protocol"
 	"github.com/iotexproject/iotex-core/v2/actpool"
-	"github.com/iotexproject/iotex-core/v2/blockchain"
 	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
 )
 
-type MintOption func(*minter)
+type MintOption func(*Minter)
 
 // WithTimeoutOption sets the timeout for NewBlockBuilder
 func WithTimeoutOption(timeout time.Duration) MintOption {
-	return func(m *minter) {
+	return func(m *Minter) {
 		m.timeout = timeout
 	}
 }
 
-type minter struct {
-	f       Factory
-	ap      actpool.ActPool
-	timeout time.Duration
+type Minter struct {
+	f             Factory
+	ap            actpool.ActPool
+	timeout       time.Duration
+	blockPreparer *blockPreparer
+	proposalPool  *proposalPool
+	mu            sync.Mutex
 }
 
 // NewMinter creates a wrapper instance
-func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) blockchain.BlockBuilderFactory {
-	m := &minter{f: f, ap: ap}
+func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) *Minter {
+	m := &Minter{
+		f:             f,
+		ap:            ap,
+		blockPreparer: newBlockPreparer(),
+		proposalPool:  newProposalPool(),
+	}
 	for _, opt := range opts {
 		opt(m)
 	}
 	return m
 }
 
-// NewBlockBuilder implements the BlockMinter interface
-func (m *minter) NewBlockBuilder(ctx context.Context, sign func(action.Envelope) (*action.SealedEnvelope, error)) (*block.Builder, error) {
+func (m *Minter) Init(root hash.Hash256) {
+	m.proposalPool.Init(root)
+}
+
+func (m *Minter) Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
+	bcCtx := protocol.MustGetBlockchainCtx(ctx)
+	blkCtx := protocol.MustGetBlockCtx(ctx)
+
+	// create a new block
+	blk, err := m.blockPreparer.PrepareOrWait(ctx, bcCtx.Tip.Hash[:], blkCtx.BlockTimeStamp, func() (*block.Block, error) {
+		blk, err := m.mint(ctx, pk)
+		if err != nil {
+			return nil, err
+		}
+		if err = m.proposalPool.AddBlock(blk); err != nil {
+			log.L().Error("failed to add block to proposal pool", zap.Error(err))
+		}
+		return blk, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	return blk, nil
+}
+
+func (m *Minter) AddProposal(blk *block.Block) error {
+	return m.proposalPool.AddBlock(blk)
+}
+
+func (m *Minter) ReceiveBlock(blk *block.Block) error {
+	prevHash := blk.PrevHash()
+	l := log.L().With(zap.Uint64("height", blk.Height()), log.Hex("prevHash", prevHash[:]), zap.Time("timestamp", blk.Timestamp()))
+	if err := m.blockPreparer.ReceiveBlock(blk); err != nil {
+		l.Error("failed to receive block", zap.Error(err))
+	}
+	if err := m.proposalPool.ReceiveBlock(blk); err != nil {
path: state/factory/minter.go
user: login: CoderZhi
body: the error is ignored? will it cause a problem?
created_at: 2025-04-03T00:53:44Z
author_association: COLLABORATOR
diff_hunk: @@ -253,7 +252,11 @@ func (bc *blockchain) Start(ctx context.Context) error {
 				GetBlockTime: bc.getBlockTime,
 			},
 		), bc.genesis))
-	return bc.lifecycle.OnStart(ctx)
+	err := bc.lifecycle.OnStart(ctx)
path: blockchain/blockchain.go
user: login: CoderZhi
body: return
created_at: 2025-04-05T09:30:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,367 @@
+package rolldpos
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/crypto"
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/iotexproject/iotex-address/address"
+	"github.com/pkg/errors"
+	"github.com/prometheus/client_golang/prometheus"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/action/protocol"
+	"github.com/iotexproject/iotex-core/v2/blockchain"
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/blockchain/genesis"
+	"github.com/iotexproject/iotex-core/v2/db"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+	"github.com/iotexproject/iotex-core/v2/pkg/prometheustimer"
+)
+
+var (
+	blockMtc = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "iotex_blockmint_metrics",
+			Help: "Block mint metrics.",
+		},
+		[]string{"type"},
+	)
+)
+
+type (
+	// ChainManager defines the blockchain interface
+	ChainManager interface {
+		Start(ctx context.Context) error
+		// Final returns the finalized blockchain
+		Final() ForkChain
+		// Fork creates a new chain manager with the given hash
+		Fork(hash hash.Hash256) (ForkChain, error)
+	}
+	// ForkChain defines the blockchain interface
+	ForkChain interface {
+		// BlockProposeTime return propose time by height
+		BlockProposeTime(uint64) (time.Time, error)
+		// BlockCommitTime return commit time by height
+		BlockCommitTime(uint64) (time.Time, error)
+		// TipHeight returns tip block's height
+		TipHeight() uint64
+		// TipHash returns tip block's hash
+		TipHash() hash.Hash256
+		// ChainAddress returns chain address on parent chain, the root chain return empty.
+		ChainAddress() string
+		// StateReader returns the state reader
+		StateReader() protocol.StateReader
+		// MintNewBlock creates a new block with given actions
+		// Note: the coinbase transfer will be added to the given transfers when minting a new block
+		MintNewBlock(time.Time, crypto.PrivateKey, hash.Hash256) (*block.Block, error)
+		// ValidateBlock validates a new block before adding it to the blockchain
+		ValidateBlock(blk *block.Block) error
+		// CommitBlock validates and appends a block to the chain
+		CommitBlock(blk *block.Block) error
+	}
+	// StateReaderFactory is the factory interface of state reader
+	StateReaderFactory interface {
+		StateReaderAt(*block.Header) (protocol.StateReader, error)
+	}
+
+	// BlockBuilderFactory is the factory interface of block builder
+	BlockBuilderFactory interface {
+		Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error)
+		ReceiveBlock(*block.Block) error
+	}
+
+	chainManager struct {
+		*forkChain
+		srf StateReaderFactory
+	}
+
+	forkChain struct {
+		bc           blockchain.Blockchain
+		head         *block.Header
+		sr           protocol.StateReader
+		timerFactory *prometheustimer.TimerFactory
+		bbf          BlockBuilderFactory
+		pool         *proposalPool
+	}
+)
+
+func init() {
+	prometheus.MustRegister(blockMtc)
+}
+
+func newForkChain(bc blockchain.Blockchain, head *block.Header, sr protocol.StateReader, bbf BlockBuilderFactory, pool *proposalPool) *forkChain {
+	timerFactory, err := prometheustimer.New(
+		"iotex_blockchain_perf",
+		"Performance of blockchain module",
+		[]string{"topic", "chainID"},
+		[]string{"default", strconv.FormatUint(uint64(bc.ChainID()), 10)},
+	)
+	if err != nil {
+		log.L().Panic("Failed to generate prometheus timer factory.", zap.Error(err))
+	}
+	return &forkChain{
+		bc:           bc,
+		head:         head,
+		sr:           sr,
+		bbf:          bbf,
+		timerFactory: timerFactory,
+		pool:         pool,
+	}
+}
+
+// NewChainManager creates a chain manager
+func NewChainManager(bc blockchain.Blockchain, srf StateReaderFactory, bbf BlockBuilderFactory) ChainManager {
+	return &chainManager{
+		forkChain: newForkChain(bc, nil, nil, bbf, newProposalPool()),
+		srf:       srf,
+	}
+}
+
+// BlockProposeTime return propose time by height
+func (cm *forkChain) BlockProposeTime(height uint64) (time.Time, error) {
+	if height == 0 {
+		return time.Unix(cm.bc.Genesis().Timestamp, 0), nil
+	}
+	header, err := cm.header(height)
+	if err != nil {
+		return time.Time{}, err
+	}
+	return header.Timestamp(), nil
+}
+
+// BlockCommitTime return commit time by height
+func (cm *forkChain) BlockCommitTime(height uint64) (time.Time, error) {
+	footer, err := cm.bc.BlockFooterByHeight(height)
+	if err != nil {
+		return time.Time{}, errors.Wrapf(
+			err, "error when getting the block at height: %d",
+			height,
+		)
+	}
+	return footer.CommitTime(), nil
+}
+
+// MintNewBlock creates a new block with given actions
+func (fc *forkChain) MintNewBlock(timestamp time.Time, pk crypto.PrivateKey, prevHash hash.Hash256) (*block.Block, error) {
+	mintNewBlockTimer := fc.timerFactory.NewTimer("MintBlock")
+	defer mintNewBlockTimer.End()
+	var (
+		tipHeight      = fc.head.Height()
+		tipHash        = fc.tipHash()
+		newblockHeight = tipHeight + 1
+		producer       address.Address
+		err            error
+	)
+	// safety check
+	if prevHash != tipHash {
+		return nil, errors.Errorf("invalid prev hash, expecting %x, got %x", tipHash, tipHash)
+	}
+	producer = pk.PublicKey().Address()
+	ctx := fc.mintContext(context.Background(), timestamp, producer)
+	// create a new block
+	log.L().Debug("Produce a new block.", zap.Uint64("height", newblockHeight), zap.Time("timestamp", timestamp), log.Hex("prevHash", tipHash[:]))
+	// run execution and update state trie root hash
+	blk, err := fc.bbf.Mint(ctx, pk)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to create block")
+	}
+	if err = fc.pool.AddBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to add proposal", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	blockMtc.WithLabelValues("MintGas").Set(float64(blk.GasUsed()))
+	blockMtc.WithLabelValues("MintActions").Set(float64(len(blk.Actions)))
+	return blk, nil
+}
+
+// TipHeight returns tip block's height
+func (cm *forkChain) TipHeight() uint64 {
+	return cm.head.Height()
+}
+
+// TipHash returns tip block's hash
+func (cm *forkChain) TipHash() hash.Hash256 {
+	return cm.tipHash()
+}
+
+// ChainAddress returns chain address on parent chain, the root chain return empty.
+func (cm *forkChain) ChainAddress() string {
+	return cm.bc.ChainAddress()
+}
+
+// StateReader returns the state reader
+func (cm *forkChain) StateReader() protocol.StateReader {
+	return cm.sr
+}
+
+// ValidateBlock validates a new block before adding it to the blockchain
+func (cm *forkChain) ValidateBlock(blk *block.Block) error {
+	err := cm.bc.ValidateBlock(blk)
+	if err != nil {
+		return err
+	}
+	if err = cm.pool.AddBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to add proposal", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	return nil
+}
+
+// CommitBlock validates and appends a block to the chain
+func (cm *forkChain) CommitBlock(blk *block.Block) error {
+	if err := cm.bc.CommitBlock(blk); err != nil {
+		return err
+	}
+	if err := cm.bbf.ReceiveBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to receive block", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	if err := cm.pool.ReceiveBlock(blk); err != nil {
+		blkHash := blk.HashBlock()
+		log.L().Error("failed to receive block", zap.Error(err), zap.Uint64("height", blk.Height()), log.Hex("hash", blkHash[:]))
+	}
+	return nil
+}
+
+func (cm *chainManager) Start(ctx context.Context) error {
+	head, err := cm.bc.BlockHeaderByHeight(cm.bc.TipHeight())
+	if err != nil {
+		return errors.Wrap(err, "failed to get the head block")
+	}
+	sr, err := cm.srf.StateReaderAt(head)
+	if err != nil {
+		return errors.Wrapf(err, "failed to create state reader at %d, hash %x", head.Height(), head.HashBlock())
+	}
+	cm.forkChain.head = head
+	cm.forkChain.sr = sr
+	cm.pool.Init(cm.forkChain.TipHash())
+	return nil
+}
+
+func (cm *chainManager) Final() ForkChain {
+	return cm.forkChain
+}
+
+// Fork creates a new chain manager with the given hash
+func (cm *chainManager) Fork(hash hash.Hash256) (ForkChain, error) {
path: consensus/scheme/rolldpos/chainmanager.go
user: login: CoderZhi
body: `Fork` is only implemented in `chainManager`, therefore, we may have an instance of `ChainManager` in `forkChain`. then, we don't need to pass in `bc`, `timeFactory`, `bbf`, and `pool` in `forkChain`. 
created_at: 2025-04-05T09:33:31Z
author_association: COLLABORATOR
diff_hunk: @@ -7,45 +7,78 @@ package factory
 
 import (
 	"context"
+	"sync"
 	"time"
 
 	"github.com/iotexproject/go-pkgs/crypto"
 
 	"github.com/iotexproject/iotex-core/v2/action/protocol"
 	"github.com/iotexproject/iotex-core/v2/actpool"
-	"github.com/iotexproject/iotex-core/v2/blockchain"
 	"github.com/iotexproject/iotex-core/v2/blockchain/block"
 )
 
-type MintOption func(*minter)
+type MintOption func(*Minter)
 
 // WithTimeoutOption sets the timeout for NewBlockBuilder
 func WithTimeoutOption(timeout time.Duration) MintOption {
-	return func(m *minter) {
+	return func(m *Minter) {
 		m.timeout = timeout
 	}
 }
 
-type minter struct {
-	f       Factory
-	ap      actpool.ActPool
-	timeout time.Duration
+type Minter struct {
path: state/factory/minter.go
user: login: CoderZhi
body: comment
created_at: 2025-04-09T14:16:08Z
author_association: COLLABORATOR
diff_hunk: @@ -7,45 +7,78 @@ package factory
 
 import (
 	"context"
+	"sync"
 	"time"
 
 	"github.com/iotexproject/go-pkgs/crypto"
 
 	"github.com/iotexproject/iotex-core/v2/action/protocol"
 	"github.com/iotexproject/iotex-core/v2/actpool"
-	"github.com/iotexproject/iotex-core/v2/blockchain"
 	"github.com/iotexproject/iotex-core/v2/blockchain/block"
 )
 
-type MintOption func(*minter)
+type MintOption func(*Minter)
 
 // WithTimeoutOption sets the timeout for NewBlockBuilder
 func WithTimeoutOption(timeout time.Duration) MintOption {
-	return func(m *minter) {
+	return func(m *Minter) {
 		m.timeout = timeout
 	}
 }
 
-type minter struct {
-	f       Factory
-	ap      actpool.ActPool
-	timeout time.Duration
+type Minter struct {
+	f             Factory
+	ap            actpool.ActPool
+	timeout       time.Duration
+	blockPreparer *blockPreparer
+	mu            sync.Mutex
 }
 
 // NewMinter creates a wrapper instance
-func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) blockchain.BlockMinter {
-	m := &minter{f: f, ap: ap}
+func NewMinter(f Factory, ap actpool.ActPool, opts ...MintOption) *Minter {
+	m := &Minter{
+		f:             f,
+		ap:            ap,
+		blockPreparer: newBlockPreparer(),
+	}
 	for _, opt := range opts {
 		opt(m)
 	}
 	return m
 }
 
-// NewBlockBuilder implements the BlockMinter interface
-func (m *minter) Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
+func (m *Minter) Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error) {
+	bcCtx := protocol.MustGetBlockchainCtx(ctx)
+	blkCtx := protocol.MustGetBlockCtx(ctx)
+
+	// create a new block
+	blk, err := m.blockPreparer.PrepareOrWait(ctx, bcCtx.Tip.Hash[:], blkCtx.BlockTimeStamp, func() (*block.Block, error) {
path: state/factory/minter.go
user: login: CoderZhi
body: return
created_at: 2025-04-09T14:16:45Z
author_association: COLLABORATOR
diff_hunk: @@ -100,7 +109,19 @@ func NewConsensus(
 	var err error
 	switch cfg.Scheme {
 	case RollDPoSScheme:
-		delegatesByEpochFunc := func(epochNum uint64) ([]string, error) {
+		if ops.bbf == nil {
+			return nil, errors.New("block builder factory is not set")
+		}
+		chanMgr := rolldpos.NewChainManager(bc, sf, ops.bbf)
+		delegatesByEpochFunc := func(epochNum uint64, prevHash []byte) ([]string, error) {
+			fork, serr := chanMgr.Fork(hash.Hash256(prevHash))
+			if serr != nil {
+				return nil, serr
+			}
+			forkSF, serr := fork.StateReader()
+			if err != nil {
path: consensus/consensus.go
user: login: dustinxie
body: here `err` will always be `nil`, do you mean to check `serr` above?
created_at: 2025-04-09T23:25:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,7 +109,19 @@ func NewConsensus(
 	var err error
 	switch cfg.Scheme {
 	case RollDPoSScheme:
-		delegatesByEpochFunc := func(epochNum uint64) ([]string, error) {
+		if ops.bbf == nil {
+			return nil, errors.New("block builder factory is not set")
+		}
+		chanMgr := rolldpos.NewChainManager(bc, sf, ops.bbf)
path: consensus/consensus.go
user: login: dustinxie
body: chainMgr
created_at: 2025-04-09T23:25:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+package rolldpos
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// nodes is a map of draft proposal blocks
+	// key is the hash of the block
+	nodes map[hash.Hash256]*block.Block
+	// leaves is a map of tip blocks of forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	leaves map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		nodes:  make(map[hash.Hash256]*block.Block),
+		leaves: make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Debug("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.nodes[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.leaves[prevHash]; ok {
+		delete(d.leaves, prevHash)
+	} else if prevHash != d.root {
+		return errors.Errorf("block %x is not a tip of any fork", prevHash[:])
+	}
+	d.leaves[hash] = blk.Timestamp()
+	d.nodes[hash] = blk
+	log.L().Debug("added block to draft pool", log.Hex("hash", hash[:]), zap.Uint64("height", blk.Height()), zap.Time("timestamp", blk.Timestamp()))
+	return nil
+}
+
+// ReceiveBlock a block has been confirmed and remove all the blocks that is previous to it, or other forks
+func (d *proposalPool) ReceiveBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	// find the fork that contains this block
+	fork, err := d.forkAt(blk)
+	if err != nil {
+		return err
+	}
+
+	// remove blocks in other forks or older blocks in the same fork
+	for f := range d.leaves {
+		start := d.nodes[f]
+		if f == fork {
+			start = blk
+		}
+		for b := start; b != nil; b = d.nodes[b.PrevHash()] {
+			ha := b.HashBlock()
+			log.L().Debug("remove block from draft pool", log.Hex("hash", ha[:]), zap.Uint64("height", b.Height()), zap.Time("timestamp", b.Timestamp()))
+			delete(d.nodes, b.HashBlock())
+		}
+	}
+	// reset forks to only this one
+	if forkTime, ok := d.leaves[fork]; ok && d.nodes[fork] != nil {
+		d.leaves = map[hash.Hash256]time.Time{fork: forkTime}
+	} else {
+		d.leaves = map[hash.Hash256]time.Time{}
+	}
+	d.root = blk.HashBlock()
+	return nil
+}
+
+// Block returns the latest block at the given height
+func (d *proposalPool) Block(height uint64) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	var blk *block.Block
+	for _, b := range d.nodes {
+		if b.Height() != height {
+			continue
+		} else if blk == nil {
path: consensus/scheme/rolldpos/proposalpool.go
user: login: dustinxie
body: remove else
```
    continue
}
if blk == nil
```
created_at: 2025-04-09T23:39:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+package rolldpos
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// nodes is a map of draft proposal blocks
+	// key is the hash of the block
+	nodes map[hash.Hash256]*block.Block
+	// leaves is a map of tip blocks of forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	leaves map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		nodes:  make(map[hash.Hash256]*block.Block),
+		leaves: make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Debug("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.nodes[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.leaves[prevHash]; ok {
+		delete(d.leaves, prevHash)
+	} else if prevHash != d.root {
+		return errors.Errorf("block %x is not a tip of any fork", prevHash[:])
+	}
+	d.leaves[hash] = blk.Timestamp()
+	d.nodes[hash] = blk
+	log.L().Debug("added block to draft pool", log.Hex("hash", hash[:]), zap.Uint64("height", blk.Height()), zap.Time("timestamp", blk.Timestamp()))
+	return nil
+}
+
+// ReceiveBlock a block has been confirmed and remove all the blocks that is previous to it, or other forks
+func (d *proposalPool) ReceiveBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	// find the fork that contains this block
+	fork, err := d.forkAt(blk)
+	if err != nil {
+		return err
+	}
+
+	// remove blocks in other forks or older blocks in the same fork
+	for f := range d.leaves {
+		start := d.nodes[f]
+		if f == fork {
+			start = blk
+		}
+		for b := start; b != nil; b = d.nodes[b.PrevHash()] {
+			ha := b.HashBlock()
+			log.L().Debug("remove block from draft pool", log.Hex("hash", ha[:]), zap.Uint64("height", b.Height()), zap.Time("timestamp", b.Timestamp()))
+			delete(d.nodes, b.HashBlock())
+		}
+	}
+	// reset forks to only this one
+	if forkTime, ok := d.leaves[fork]; ok && d.nodes[fork] != nil {
+		d.leaves = map[hash.Hash256]time.Time{fork: forkTime}
+	} else {
+		d.leaves = map[hash.Hash256]time.Time{}
+	}
+	d.root = blk.HashBlock()
+	return nil
+}
+
+// Block returns the latest block at the given height
+func (d *proposalPool) Block(height uint64) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	var blk *block.Block
+	for _, b := range d.nodes {
+		if b.Height() != height {
+			continue
+		} else if blk == nil {
+			blk = b
+		} else if b.Timestamp().After(blk.Timestamp()) {
+			blk = b
+		}
+	}
+	return blk
+}
+
+// BlockByHash returns the block by hash
+func (d *proposalPool) BlockByHash(hash hash.Hash256) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	return d.nodes[hash]
+}
+
+func (d *proposalPool) BlockByTime(prevHash hash.Hash256, timestamp time.Time) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	for _, b := range d.nodes {
+		if b.PrevHash() == prevHash && b.Timestamp().Equal(timestamp) {
path: consensus/scheme/rolldpos/proposalpool.go
user: login: dustinxie
body: from the logic here, it's possible to have 2 blocks with same prevHash and diff time, is that right?
```
b1.PrevHash() = 0x1234, b1.Time() = 17:56:32
b2.PrevHash() = 0x1234, b2.Time() = 17.56.35 (3 seconds later)
```
see comments at L55
created_at: 2025-04-09T23:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+package rolldpos
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// nodes is a map of draft proposal blocks
+	// key is the hash of the block
+	nodes map[hash.Hash256]*block.Block
+	// leaves is a map of tip blocks of forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	leaves map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		nodes:  make(map[hash.Hash256]*block.Block),
+		leaves: make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Debug("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.nodes[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.leaves[prevHash]; ok {
+		delete(d.leaves, prevHash)
path: consensus/scheme/rolldpos/proposalpool.go
user: login: dustinxie
body: if trying to add 2 blocks with same prevHash, then only the first will be added successfully, say
```
b1.PrevHash() = 0x1234, b1.Time() = 17:56:32
b2.PrevHash() = 0x1234, b2.Time() = 17.56.35 (3 seconds later)
```
only b1 will be added (which deletes `d.leaves[0x1234]`, b2 won't be added into the pool
then situation at L126 won't happend? see comments at L126
created_at: 2025-04-09T23:48:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -566,6 +580,7 @@ func (ctx *rollDPoSCtx) Commit(msg interface{}) (bool, error) {
 
 	_consensusDurationMtc.WithLabelValues().Set(float64(time.Since(ctx.round.roundStartTime)))
 	if pendingBlock.Height() > 1 {
+		// prevBlkProposeTime, err := ctx.chain.Final().BlockProposeTime(pendingBlock.Height() - 1)
path: consensus/scheme/rolldpos/rolldposctx.go
user: login: dustinxie
body: remove if not needed
created_at: 2025-04-09T23:53:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+package rolldpos
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// nodes is a map of draft proposal blocks
+	// key is the hash of the block
+	nodes map[hash.Hash256]*block.Block
+	// leaves is a map of tip blocks of forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	leaves map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		nodes:  make(map[hash.Hash256]*block.Block),
+		leaves: make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Debug("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.nodes[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.leaves[prevHash]; ok {
+		delete(d.leaves, prevHash)
+	} else if prevHash != d.root {
+		return errors.Errorf("block %x is not a tip of any fork", prevHash[:])
+	}
+	d.leaves[hash] = blk.Timestamp()
+	d.nodes[hash] = blk
+	log.L().Debug("added block to draft pool", log.Hex("hash", hash[:]), zap.Uint64("height", blk.Height()), zap.Time("timestamp", blk.Timestamp()))
+	return nil
+}
+
+// ReceiveBlock a block has been confirmed and remove all the blocks that is previous to it, or other forks
+func (d *proposalPool) ReceiveBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	// find the fork that contains this block
+	fork, err := d.forkAt(blk)
+	if err != nil {
+		return err
+	}
+
+	// remove blocks in other forks or older blocks in the same fork
+	for f := range d.leaves {
+		start := d.nodes[f]
+		if f == fork {
+			start = blk
+		}
+		for b := start; b != nil; b = d.nodes[b.PrevHash()] {
+			ha := b.HashBlock()
+			log.L().Debug("remove block from draft pool", log.Hex("hash", ha[:]), zap.Uint64("height", b.Height()), zap.Time("timestamp", b.Timestamp()))
+			delete(d.nodes, b.HashBlock())
+		}
+	}
path: consensus/scheme/rolldpos/proposalpool.go
user: login: dustinxie
body: let's sync offline for this part of logic
created_at: 2025-04-09T23:57:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,426 @@
+package rolldpos
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/crypto"
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/iotexproject/iotex-address/address"
+	"github.com/pkg/errors"
+	"github.com/prometheus/client_golang/prometheus"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/action/protocol"
+	"github.com/iotexproject/iotex-core/v2/blockchain"
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/blockchain/genesis"
+	"github.com/iotexproject/iotex-core/v2/db"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+	"github.com/iotexproject/iotex-core/v2/pkg/prometheustimer"
+)
+
+var (
+	blockMtc = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "iotex_blockmint_metrics",
+			Help: "Block mint metrics.",
+		},
+		[]string{"type"},
+	)
+)
+
+type (
+	// ChainManager defines the blockchain interface
+	ChainManager interface {
+		ForkChain
+		// Start starts the chain manager
+		Start(ctx context.Context) error
+		// Fork creates a new chain manager with the given hash
+		Fork(hash hash.Hash256) (ForkChain, error)
+		// CommitBlock validates and appends a block to the chain
+		CommitBlock(blk *block.Block) error
+		// ValidateBlock validates a new block before adding it to the blockchain
+		ValidateBlock(blk *block.Block) error
+	}
+	// ForkChain defines the blockchain interface
+	ForkChain interface {
+		// BlockProposeTime return propose time by height
+		BlockProposeTime(uint64) (time.Time, error)
+		// BlockCommitTime return commit time by height
+		BlockCommitTime(uint64) (time.Time, error)
+		// TipHeight returns tip block's height
+		TipHeight() uint64
+		// TipHash returns tip block's hash
+		TipHash() hash.Hash256
+		// StateReader returns the state reader
+		StateReader() (protocol.StateReader, error)
+		// MintNewBlock creates a new block with given actions
+		// Note: the coinbase transfer will be added to the given transfers when minting a new block
+		MintNewBlock(time.Time, crypto.PrivateKey, hash.Hash256) (*block.Block, error)
+	}
+	// StateReaderFactory is the factory interface of state reader
+	StateReaderFactory interface {
+		StateReaderAt(uint64, hash.Hash256) (protocol.StateReader, error)
+	}
+
+	// BlockBuilderFactory is the factory interface of block builder
+	BlockBuilderFactory interface {
+		Mint(ctx context.Context, pk crypto.PrivateKey) (*block.Block, error)
+		ReceiveBlock(*block.Block) error
+	}
+
+	chainManager struct {
+		ForkChain
path: consensus/scheme/rolldpos/chainmanager.go
user: login: dustinxie
body: think use `fc ForkChain` is better
since `chainManager` implements all 6 methods of the `ForkChain` interface on its own
created_at: 2025-04-10T00:13:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,6 +92,7 @@ func (c *roundCalculator) UpdateRound(round *roundCtx, height uint64, blockInter
 
 		height:             height,
 		roundNum:           roundNum,
+		prevHash:           hash.Hash256(prevHash),
path: consensus/scheme/rolldpos/roundcalculator.go
user: login: dustinxie
body: the data type is [32]byte, so can directly do
`prevHash: prevHash`
created_at: 2025-04-10T00:14:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -255,6 +266,7 @@ func (c *roundCalculator) newRound(
 
 		height:             height,
 		roundNum:           roundNum,
+		prevHash:           hash.Hash256(prevHash),
path: consensus/scheme/rolldpos/roundcalculator.go
user: login: dustinxie
body: same here
created_at: 2025-04-10T00:15:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,7 +109,19 @@ func NewConsensus(
 	var err error
 	switch cfg.Scheme {
 	case RollDPoSScheme:
-		delegatesByEpochFunc := func(epochNum uint64) ([]string, error) {
+		if ops.bbf == nil {
+			return nil, errors.New("block builder factory is not set")
+		}
+		chanMgr := rolldpos.NewChainManager(bc, sf, ops.bbf)
+		delegatesByEpochFunc := func(epochNum uint64, prevHash []byte) ([]string, error) {
+			fork, serr := chanMgr.Fork(hash.Hash256(prevHash))
+			if serr != nil {
+				return nil, serr
+			}
+			forkSF, serr := fork.StateReader()
+			if err != nil {
path: consensus/consensus.go
user: login: envestcc
body: good catch
created_at: 2025-04-10T01:48:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,148 @@
+package rolldpos
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// nodes is a map of draft proposal blocks
+	// key is the hash of the block
+	nodes map[hash.Hash256]*block.Block
+	// leaves is a map of tip blocks of forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	leaves map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		nodes:  make(map[hash.Hash256]*block.Block),
+		leaves: make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Debug("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.nodes[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.leaves[prevHash]; ok {
+		delete(d.leaves, prevHash)
+	} else if prevHash != d.root {
+		return errors.Errorf("block %x is not a tip of any fork", prevHash[:])
+	}
+	d.leaves[hash] = blk.Timestamp()
+	d.nodes[hash] = blk
+	log.L().Debug("added block to draft pool", log.Hex("hash", hash[:]), zap.Uint64("height", blk.Height()), zap.Time("timestamp", blk.Timestamp()))
+	return nil
+}
+
+// ReceiveBlock a block has been confirmed and remove all the blocks that is previous to it, or other forks
+func (d *proposalPool) ReceiveBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+
+	// find the fork that contains this block
+	fork, err := d.forkAt(blk)
+	if err != nil {
+		return err
+	}
+
+	// remove blocks in other forks or older blocks in the same fork
+	for f := range d.leaves {
+		start := d.nodes[f]
+		if f == fork {
+			start = blk
+		}
+		for b := start; b != nil; b = d.nodes[b.PrevHash()] {
+			ha := b.HashBlock()
+			log.L().Debug("remove block from draft pool", log.Hex("hash", ha[:]), zap.Uint64("height", b.Height()), zap.Time("timestamp", b.Timestamp()))
+			delete(d.nodes, b.HashBlock())
+		}
+	}
+	// reset forks to only this one
+	if forkTime, ok := d.leaves[fork]; ok && d.nodes[fork] != nil {
+		d.leaves = map[hash.Hash256]time.Time{fork: forkTime}
+	} else {
+		d.leaves = map[hash.Hash256]time.Time{}
+	}
+	d.root = blk.HashBlock()
+	return nil
+}
+
+// Block returns the latest block at the given height
+func (d *proposalPool) Block(height uint64) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	var blk *block.Block
+	for _, b := range d.nodes {
+		if b.Height() != height {
+			continue
+		} else if blk == nil {
+			blk = b
+		} else if b.Timestamp().After(blk.Timestamp()) {
+			blk = b
+		}
+	}
+	return blk
+}
+
+// BlockByHash returns the block by hash
+func (d *proposalPool) BlockByHash(hash hash.Hash256) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	return d.nodes[hash]
+}
+
+func (d *proposalPool) BlockByTime(prevHash hash.Hash256, timestamp time.Time) *block.Block {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	for _, b := range d.nodes {
+		if b.PrevHash() == prevHash && b.Timestamp().Equal(timestamp) {
path: consensus/scheme/rolldpos/proposalpool.go
user: login: envestcc
body: yes, but in practice it should be relatively rare, which requires all 24 delegates to fail.
created_at: 2025-04-10T01:56:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,148 @@
+package rolldpos
+
+import (
+	"sync"
+	"time"
+
+	"github.com/iotexproject/go-pkgs/hash"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+
+	"github.com/iotexproject/iotex-core/v2/blockchain/block"
+	"github.com/iotexproject/iotex-core/v2/pkg/log"
+)
+
+// proposalPool is a pool of draft blocks
+type proposalPool struct {
+	// nodes is a map of draft proposal blocks
+	// key is the hash of the block
+	nodes map[hash.Hash256]*block.Block
+	// leaves is a map of tip blocks of forks
+	// key is the hash of the tip block of the fork
+	// value is the timestamp of the block
+	leaves map[hash.Hash256]time.Time
+	// root is the hash of the tip block of the blockchain
+	root hash.Hash256
+	mu   sync.Mutex
+}
+
+func newProposalPool() *proposalPool {
+	return &proposalPool{
+		nodes:  make(map[hash.Hash256]*block.Block),
+		leaves: make(map[hash.Hash256]time.Time),
+	}
+}
+
+func (d *proposalPool) Init(root hash.Hash256) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	d.root = root
+	log.L().Debug("proposal pool initialized", log.Hex("root", root[:]))
+}
+
+// AddBlock adds a block to the draft pool
+func (d *proposalPool) AddBlock(blk *block.Block) error {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	// nothing to do if the block already exists
+	hash := blk.HashBlock()
+	if _, ok := d.nodes[hash]; ok {
+		return nil
+	}
+	// it must be a new tip of any fork, or make a new fork
+	prevHash := blk.PrevHash()
+	if _, ok := d.leaves[prevHash]; ok {
+		delete(d.leaves, prevHash)
path: consensus/scheme/rolldpos/proposalpool.go
user: login: envestcc
body: No, b1 and b2 are both included as two forks in your case. It won't go L55, b/c the tip is b1 instead of b1.prev.
created_at: 2025-04-10T02:09:39Z
author_association: MEMBER
diff_hunk: @@ -67,82 +67,47 @@ func (d *proposalPool) ReceiveBlock(blk *block.Block) error {
 	d.mu.Lock()
 	defer d.mu.Unlock()
 
-	// find the fork that contains this block
-	fork, err := d.forkAt(blk)
-	if err != nil {
-		return err
+	contain := func(node, leaf hash.Hash256) (exist bool) {
+		for b := leaf; ; {
+			if b == node {
+				return true
+			}
+			blk := d.nodes[b]
+			if blk == nil {
+				return false
+			}
+			b = blk.PrevHash()
+		}
 	}
 
 	// remove blocks in other forks or older blocks in the same fork
-	for f := range d.leaves {
-		start := d.nodes[f]
-		if f == fork {
+	leavesToDelete := make([]hash.Hash256, 0)
+	for leaf := range d.leaves {
+		start := d.nodes[leaf]
+		has := contain(blk.HashBlock(), leaf)
+		if has {
 			start = blk
 		}
 		for b := start; b != nil; b = d.nodes[b.PrevHash()] {
 			ha := b.HashBlock()
 			log.L().Debug("remove block from draft pool", log.Hex("hash", ha[:]), zap.Uint64("height", b.Height()), zap.Time("timestamp", b.Timestamp()))
 			delete(d.nodes, b.HashBlock())
 		}
+		if !has || blk.HashBlock() == leaf {
+			leavesToDelete = append(leavesToDelete, leaf)
path: consensus/scheme/rolldpos/proposalpool.go
user: login: dustinxie
body: so just to confirm, for the case we discussed, this will delete A, B, 3, 4, and 1, 2 remain as 2 leaves?
created_at: 2025-04-15T00:47:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-15T05:02:17Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=iotexproject_iotex-core&pullRequest=4593) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=iotexproject_iotex-core&pullRequest=4593&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=iotexproject_iotex-core&pullRequest=4593&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=iotexproject_iotex-core&pullRequest=4593&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=iotexproject_iotex-core&pullRequest=4593&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1.7% Duplication on New Code](https://sonarcloud.io/component_measures?id=iotexproject_iotex-core&pullRequest=4593&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=iotexproject_iotex-core&pullRequest=4593)


author_association: NONE