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 := ×tamppb.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()