From 02f8190fa7125e032e59725bb2b594a4351e8949 Mon Sep 17 00:00:00 2001 From: Dustin Xie Date: Sat, 8 Oct 2022 13:56:22 -0500 Subject: [PATCH 1/4] add PersistStakingPatchBlock into blockchain.Config --- action/protocol/staking/builder.go | 12 ++++++++ action/protocol/staking/handlers_test.go | 11 +++++-- action/protocol/staking/protocol.go | 30 +++++++++++--------- action/protocol/staking/protocol_test.go | 21 +++++++++++--- action/protocol/staking/validations_test.go | 6 +++- action/protocol/staking/vote_reviser_test.go | 6 +++- blockchain/config.go | 3 ++ chainservice/builder.go | 5 +++- 8 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 action/protocol/staking/builder.go diff --git a/action/protocol/staking/builder.go b/action/protocol/staking/builder.go new file mode 100644 index 0000000000..8dcd7316a6 --- /dev/null +++ b/action/protocol/staking/builder.go @@ -0,0 +1,12 @@ +package staking + +import "github.com/iotexproject/iotex-core/blockchain/genesis" + +type ( + + // BuilderConfig returns the configuration of the builder + BuilderConfig struct { + Staking genesis.Staking + PersistStakingPatchBlock uint64 + } +) diff --git a/action/protocol/staking/handlers_test.go b/action/protocol/staking/handlers_test.go index 51bb33dffa..3c5133d105 100644 --- a/action/protocol/staking/handlers_test.go +++ b/action/protocol/staking/handlers_test.go @@ -10,6 +10,7 @@ import ( "context" "encoding/hex" "encoding/json" + "math" "math/big" "testing" "time" @@ -71,7 +72,10 @@ func TestProtocol_HandleCreateStake(t *testing.T) { require.NoError(err) // create protocol - p, err := NewProtocol(depositGas, genesis.Default.Staking, nil, genesis.Default.GreenlandBlockHeight) + p, err := NewProtocol(depositGas, &BuilderConfig{ + Staking: genesis.Default.Staking, + PersistStakingPatchBlock: math.MaxUint64, + }, nil, genesis.Default.GreenlandBlockHeight) require.NoError(err) // set up candidate @@ -2664,7 +2668,10 @@ func initAll(t *testing.T, ctrl *gomock.Controller) (protocol.StateManager, *Pro require.NoError(err) // create protocol - p, err := NewProtocol(depositGas, genesis.Default.Staking, nil, genesis.Default.GreenlandBlockHeight) + p, err := NewProtocol(depositGas, &BuilderConfig{ + Staking: genesis.Default.Staking, + PersistStakingPatchBlock: math.MaxUint64, + }, nil, genesis.Default.GreenlandBlockHeight) require.NoError(err) // set up candidate diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 49ee758c1e..496aa1b710 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -75,11 +75,12 @@ type ( // Configuration is the staking protocol configuration. Configuration struct { - VoteWeightCalConsts genesis.VoteWeightCalConsts - RegistrationConsts RegistrationConsts - WithdrawWaitingPeriod time.Duration - MinStakeAmount *big.Int - BootstrapCandidates []genesis.BootstrapCandidate + VoteWeightCalConsts genesis.VoteWeightCalConsts + RegistrationConsts RegistrationConsts + WithdrawWaitingPeriod time.Duration + MinStakeAmount *big.Int + BootstrapCandidates []genesis.BootstrapCandidate + PersistStakingPatchBlock uint64 } // DepositGas deposits gas to some pool @@ -103,42 +104,43 @@ func FindProtocol(registry *protocol.Registry) *Protocol { } // NewProtocol instantiates the protocol of staking -func NewProtocol(depositGas DepositGas, cfg genesis.Staking, candBucketsIndexer *CandidatesBucketsIndexer, reviseHeights ...uint64) (*Protocol, error) { +func NewProtocol(depositGas DepositGas, cfg *BuilderConfig, candBucketsIndexer *CandidatesBucketsIndexer, reviseHeights ...uint64) (*Protocol, error) { h := hash.Hash160b([]byte(_protocolID)) addr, err := address.FromBytes(h[:]) if err != nil { return nil, err } - minStakeAmount, ok := new(big.Int).SetString(cfg.MinStakeAmount, 10) + minStakeAmount, ok := new(big.Int).SetString(cfg.Staking.MinStakeAmount, 10) if !ok { return nil, ErrInvalidAmount } - regFee, ok := new(big.Int).SetString(cfg.RegistrationConsts.Fee, 10) + regFee, ok := new(big.Int).SetString(cfg.Staking.RegistrationConsts.Fee, 10) if !ok { return nil, ErrInvalidAmount } - minSelfStake, ok := new(big.Int).SetString(cfg.RegistrationConsts.MinSelfStake, 10) + minSelfStake, ok := new(big.Int).SetString(cfg.Staking.RegistrationConsts.MinSelfStake, 10) if !ok { return nil, ErrInvalidAmount } // new vote reviser, revise ate greenland - voteReviser := NewVoteReviser(cfg.VoteWeightCalConsts, reviseHeights...) + voteReviser := NewVoteReviser(cfg.Staking.VoteWeightCalConsts, reviseHeights...) return &Protocol{ addr: addr, config: Configuration{ - VoteWeightCalConsts: cfg.VoteWeightCalConsts, + VoteWeightCalConsts: cfg.Staking.VoteWeightCalConsts, RegistrationConsts: RegistrationConsts{ Fee: regFee, MinSelfStake: minSelfStake, }, - WithdrawWaitingPeriod: cfg.WithdrawWaitingPeriod, - MinStakeAmount: minStakeAmount, - BootstrapCandidates: cfg.BootstrapCandidates, + WithdrawWaitingPeriod: cfg.Staking.WithdrawWaitingPeriod, + MinStakeAmount: minStakeAmount, + BootstrapCandidates: cfg.Staking.BootstrapCandidates, + PersistStakingPatchBlock: cfg.PersistStakingPatchBlock, }, depositGas: depositGas, candBucketsIndexer: candBucketsIndexer, diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index 365e6a9165..a83489d4e4 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -8,6 +8,7 @@ package staking import ( "context" + "math" "math/big" "testing" "time" @@ -86,7 +87,10 @@ func TestProtocol(t *testing.T) { } // test loading with no candidate in stateDB - stk, err := NewProtocol(nil, genesis.Default.Staking, nil, genesis.Default.GreenlandBlockHeight) + stk, err := NewProtocol(nil, &BuilderConfig{ + Staking: genesis.Default.Staking, + PersistStakingPatchBlock: math.MaxUint64, + }, nil, genesis.Default.GreenlandBlockHeight) r.NotNil(stk) r.NoError(err) buckets, _, err := csr.getAllBuckets() @@ -189,7 +193,10 @@ func TestCreatePreStates(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t) sm := testdb.NewMockStateManager(ctrl) - p, err := NewProtocol(nil, genesis.Default.Staking, nil, genesis.Default.GreenlandBlockHeight) + p, err := NewProtocol(nil, &BuilderConfig{ + Staking: genesis.Default.Staking, + PersistStakingPatchBlock: math.MaxUint64, + }, nil, genesis.Default.GreenlandBlockHeight) require.NoError(err) ctx := protocol.WithBlockCtx( genesis.WithGenesisContext(context.Background(), genesis.Default), @@ -249,7 +256,10 @@ func Test_CreatePreStatesWithRegisterProtocol(t *testing.T) { ctx := context.Background() require.NoError(cbi.Start(ctx)) - p, err := NewProtocol(nil, genesis.Default.Staking, cbi, genesis.Default.GreenlandBlockHeight) + p, err := NewProtocol(nil, &BuilderConfig{ + Staking: genesis.Default.Staking, + PersistStakingPatchBlock: math.MaxUint64, + }, cbi, genesis.Default.GreenlandBlockHeight) require.NoError(err) rol := rolldpos.NewProtocol(23, 4, 3) @@ -362,7 +372,10 @@ func Test_CreateGenesisStates(t *testing.T) { ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) for _, test := range testBootstrapCandidates { cfg.BootstrapCandidates = test.BootstrapCandidate - p, err := NewProtocol(nil, cfg, nil, genesis.Default.GreenlandBlockHeight) + p, err := NewProtocol(nil, &BuilderConfig{ + Staking: cfg, + PersistStakingPatchBlock: math.MaxUint64, + }, nil, genesis.Default.GreenlandBlockHeight) require.NoError(err) v, err := p.Start(ctx, sm) diff --git a/action/protocol/staking/validations_test.go b/action/protocol/staking/validations_test.go index 7b1a40964f..c9747a7c64 100644 --- a/action/protocol/staking/validations_test.go +++ b/action/protocol/staking/validations_test.go @@ -7,6 +7,7 @@ package staking import ( + "math" "math/big" "testing" @@ -64,7 +65,10 @@ func TestIsValidCandidateName(t *testing.T) { func initTestProtocol(t *testing.T) (*Protocol, []*Candidate) { require := require.New(t) - p, err := NewProtocol(nil, genesis.Default.Staking, nil, genesis.Default.GreenlandBlockHeight) + p, err := NewProtocol(nil, &BuilderConfig{ + Staking: genesis.Default.Staking, + PersistStakingPatchBlock: math.MaxUint64, + }, nil, genesis.Default.GreenlandBlockHeight) require.NoError(err) var cans []*Candidate diff --git a/action/protocol/staking/vote_reviser_test.go b/action/protocol/staking/vote_reviser_test.go index c7e2bf55e3..ca3476a7d7 100644 --- a/action/protocol/staking/vote_reviser_test.go +++ b/action/protocol/staking/vote_reviser_test.go @@ -2,6 +2,7 @@ package staking import ( "context" + "math" "math/big" "testing" "time" @@ -113,7 +114,10 @@ func TestVoteReviser(t *testing.T) { // test loading with no candidate in stateDB stk, err := NewProtocol( nil, - genesis.Default.Staking, + &BuilderConfig{ + Staking: genesis.Default.Staking, + PersistStakingPatchBlock: math.MaxUint64, + }, nil, genesis.Default.GreenlandBlockHeight, genesis.Default.HawaiiBlockHeight, diff --git a/blockchain/config.go b/blockchain/config.go index 192e0c528c..19c24ce6e1 100644 --- a/blockchain/config.go +++ b/blockchain/config.go @@ -67,6 +67,8 @@ type ( WorkingSetCacheSize uint64 `yaml:"workingSetCacheSize"` // StreamingBlockBufferSize StreamingBlockBufferSize uint64 `yaml:"streamingBlockBufferSize"` + // PersistStakingPatchBlock is the block to persist staking patch + PersistStakingPatchBlock uint64 `yaml:"persistStakingPatchBlock"` } ) @@ -103,6 +105,7 @@ var ( StateDBCacheSize: 1000, WorkingSetCacheSize: 20, StreamingBlockBufferSize: 200, + PersistStakingPatchBlock: 19778037, } // ErrConfig config error diff --git a/chainservice/builder.go b/chainservice/builder.go index 37ab52198b..b3aeb56590 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -448,7 +448,10 @@ func (builder *Builder) registerStakingProtocol() error { } stakingProtocol, err := staking.NewProtocol( rewarding.DepositGas, - builder.cfg.Genesis.Staking, + &staking.BuilderConfig{ + Staking: builder.cfg.Genesis.Staking, + PersistStakingPatchBlock: builder.cfg.Chain.PersistStakingPatchBlock, + }, builder.cs.candBucketsIndexer, builder.cfg.Genesis.GreenlandBlockHeight, builder.cfg.Genesis.HawaiiBlockHeight, From ebf4e0c5f3d5e1edbb361292eed37459c63f42d7 Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 11 Oct 2022 09:44:35 -0700 Subject: [PATCH 2/4] [protocol] add PreCommit() interface --- action/protocol/protocol.go | 5 +++++ state/factory/util.go | 14 ++++++++++++++ state/factory/workingset.go | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/action/protocol/protocol.go b/action/protocol/protocol.go index 1a14a2dba1..66e452729b 100644 --- a/action/protocol/protocol.go +++ b/action/protocol/protocol.go @@ -52,6 +52,11 @@ type PreStatesCreator interface { CreatePreStates(context.Context, StateManager) error } +// PreCommitter performs pre-commit action of the protocol +type PreCommitter interface { + PreCommit(context.Context, StateManager) error +} + // Committer performs commit action of the protocol type Committer interface { Commit(context.Context, StateManager) error diff --git a/state/factory/util.go b/state/factory/util.go index ab416ad696..d524c24f23 100644 --- a/state/factory/util.go +++ b/state/factory/util.go @@ -81,6 +81,20 @@ func generateWorkingSetCacheKey(blkHeader block.Header, producerAddr string) has return hash.Hash256b(sum) } +func protocolPreCommit(ctx context.Context, sr protocol.StateManager) error { + if reg, ok := protocol.GetRegistry(ctx); ok { + for _, p := range reg.All() { + post, ok := p.(protocol.PreCommitter) + if ok && sr.ProtocolDirty(p.Name()) { + if err := post.PreCommit(ctx, sr); err != nil { + return err + } + } + } + } + return nil +} + func protocolCommit(ctx context.Context, sr protocol.StateManager) error { if reg, ok := protocol.GetRegistry(ctx); ok { for _, p := range reg.All() { diff --git a/state/factory/workingset.go b/state/factory/workingset.go index 048c84d532..cc80966dad 100644 --- a/state/factory/workingset.go +++ b/state/factory/workingset.go @@ -216,10 +216,14 @@ func (ws *workingSet) ResetSnapshots() { // Commit persists all changes in RunActions() into the DB func (ws *workingSet) Commit(ctx context.Context) error { + if err := protocolPreCommit(ctx, ws); err != nil { + return err + } if err := ws.store.Commit(); err != nil { return err } if err := protocolCommit(ctx, ws); err != nil { + // TODO (zhi): wrap the error and eventually panic it in caller side return err } ws.Reset() From af8a83b72e07581d415e2e2f0418aef369da5a85 Mon Sep 17 00:00:00 2001 From: Dustin Xie Date: Thu, 6 Oct 2022 22:37:24 -0500 Subject: [PATCH 3/4] [staking] persist name/operator map and owner list to stateDB --- action/protocol/staking/candidate_center.go | 17 ++-- .../staking/candidate_center_extra.go | 99 +++++++++++++++++++ .../staking/candidate_statemanager.go | 19 ++-- action/protocol/staking/handlers.go | 2 + action/protocol/staking/protocol.go | 84 ++++++++++++++++ state/factory/factory.go | 3 +- state/factory/statedb.go | 3 +- 7 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 action/protocol/staking/candidate_center_extra.go diff --git a/action/protocol/staking/candidate_center.go b/action/protocol/staking/candidate_center.go index f991cfd296..6b110b4c3e 100644 --- a/action/protocol/staking/candidate_center.go +++ b/action/protocol/staking/candidate_center.go @@ -27,6 +27,7 @@ type ( ownerMap map[string]*Candidate operatorMap map[string]*Candidate selfStkBucketMap map[uint64]*Candidate + owners CandidateList } // CandidateCenter is a struct to manage the candidates @@ -58,12 +59,7 @@ func NewCandidateCenter(all CandidateList) (*CandidateCenter, error) { } c := CandidateCenter{ - base: &candBase{ - nameMap: make(map[string]*Candidate), - ownerMap: make(map[string]*Candidate), - operatorMap: make(map[string]*Candidate), - selfStkBucketMap: make(map[uint64]*Candidate), - }, + base: newCandBase(), change: delta, } @@ -415,6 +411,15 @@ func (cc *candChange) delete(owner address.Address) { // candBase funcs //====================================== +func newCandBase() *candBase { + return &candBase{ + nameMap: make(map[string]*Candidate), + ownerMap: make(map[string]*Candidate), + operatorMap: make(map[string]*Candidate), + selfStkBucketMap: make(map[uint64]*Candidate), + } +} + func (cb *candBase) size() int { cb.lock.RLock() defer cb.lock.RUnlock() diff --git a/action/protocol/staking/candidate_center_extra.go b/action/protocol/staking/candidate_center_extra.go new file mode 100644 index 0000000000..30e9554dd1 --- /dev/null +++ b/action/protocol/staking/candidate_center_extra.go @@ -0,0 +1,99 @@ +// Copyright (c) 2022 IoTeX Foundation +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package staking + +func (cb *candBase) clone() *candBase { + cb.lock.RLock() + defer cb.lock.RUnlock() + clone := newCandBase() + for name, cand := range cb.nameMap { + clone.nameMap[name] = cand.Clone() + } + for owner, cand := range cb.ownerMap { + clone.ownerMap[owner] = cand.Clone() + } + for operator, cand := range cb.operatorMap { + clone.operatorMap[operator] = cand.Clone() + } + for bucket, cand := range cb.selfStkBucketMap { + clone.selfStkBucketMap[bucket] = cand.Clone() + } + if len(cb.owners) > 0 { + for _, cand := range cb.owners { + clone.owners = append(clone.owners, cand.Clone()) + } + } + return clone +} + +func (cb *candBase) candsInNameMap() CandidateList { + cb.lock.RLock() + defer cb.lock.RUnlock() + if len(cb.nameMap) == 0 { + return nil + } + + list := make(CandidateList, 0, len(cb.nameMap)) + for _, d := range cb.nameMap { + list = append(list, d.Clone()) + } + return list +} + +func (cb *candBase) candsInOperatorMap() CandidateList { + cb.lock.RLock() + defer cb.lock.RUnlock() + if len(cb.operatorMap) == 0 { + return nil + } + + list := make(CandidateList, 0, len(cb.operatorMap)) + for _, d := range cb.operatorMap { + list = append(list, d.Clone()) + } + return list +} + +func (cb *candBase) ownersList() CandidateList { + cb.lock.RLock() + defer cb.lock.RUnlock() + return cb.owners +} + +func (cb *candBase) recordOwner(c *Candidate) { + cb.lock.Lock() + defer cb.lock.Unlock() + for i, d := range cb.owners { + if d.Owner.String() == c.Owner.String() { + cb.owners[i] = c.Clone() + return + } + } + // this is a new candidate + cb.owners = append(cb.owners, c.Clone()) +} + +func (cb *candBase) loadNameOperatorMapOwnerList(name, op, owners CandidateList) error { + cb.lock.Lock() + defer cb.lock.Unlock() + cb.nameMap = make(map[string]*Candidate) + for _, d := range name { + if err := d.Validate(); err != nil { + return err + } + cb.nameMap[d.Name] = d + } + cb.operatorMap = make(map[string]*Candidate) + for _, d := range op { + if err := d.Validate(); err != nil { + return err + } + cb.operatorMap[d.Operator.String()] = d + } + cb.owners = owners + return nil +} diff --git a/action/protocol/staking/candidate_statemanager.go b/action/protocol/staking/candidate_statemanager.go index 950e680a18..7ce38605f4 100644 --- a/action/protocol/staking/candidate_statemanager.go +++ b/action/protocol/staking/candidate_statemanager.go @@ -30,17 +30,15 @@ type ( delBucket(index uint64) error putBucketAndIndex(bucket *VoteBucket) (uint64, error) delBucketAndIndex(owner, cand address.Address, index uint64) error - putBucketIndex(addr address.Address, prefix byte, index uint64) error - delBucketIndex(addr address.Address, prefix byte, index uint64) error } // CandidateSet related to setting candidates CandidateSet interface { - putCandidate(d *Candidate) error - delCandidate(name address.Address) error - putVoterBucketIndex(addr address.Address, index uint64) error - delVoterBucketIndex(addr address.Address, index uint64) error - putCandBucketIndex(addr address.Address, index uint64) error - delCandBucketIndex(addr address.Address, index uint64) error + putCandidate(*Candidate) error + delCandidate(address.Address) error + putVoterBucketIndex(address.Address, uint64) error + delVoterBucketIndex(address.Address, uint64) error + putCandBucketIndex(address.Address, uint64) error + delCandBucketIndex(address.Address, uint64) error } // CandidateStateManager is candidate state manager on top of StateManager CandidateStateManager interface { @@ -55,7 +53,6 @@ type ( ContainsSelfStakingBucket(uint64) bool GetByName(string) *Candidate GetByOwner(address.Address) *Candidate - GetBySelfStakingIndex(uint64) *Candidate Upsert(*Candidate) error CreditBucketPool(*big.Int) error DebitBucketPool(*big.Int, bool) error @@ -141,10 +138,6 @@ func (csm *candSM) GetByOwner(addr address.Address) *Candidate { return csm.candCenter.GetByOwner(addr) } -func (csm *candSM) GetBySelfStakingIndex(index uint64) *Candidate { - return csm.candCenter.GetBySelfStakingIndex(index) -} - // Upsert writes the candidate into state manager and cand center func (csm *candSM) Upsert(d *Candidate) error { if err := csm.candCenter.Upsert(d); err != nil { diff --git a/action/protocol/staking/handlers.go b/action/protocol/staking/handlers.go index 9a9ad805b0..f7861fc390 100644 --- a/action/protocol/staking/handlers.go +++ b/action/protocol/staking/handlers.go @@ -684,6 +684,7 @@ func (p *Protocol) handleCandidateRegister(ctx context.Context, act *action.Cand if err := csm.Upsert(c); err != nil { return log, nil, csmErrorToHandleError(owner.String(), err) } + csm.DirtyView().candCenter.base.recordOwner(c) // update bucket pool if err := csm.DebitBucketPool(act.Amount(), true); err != nil { @@ -763,6 +764,7 @@ func (p *Protocol) handleCandidateUpdate(ctx context.Context, act *action.Candid if err := csm.Upsert(c); err != nil { return log, csmErrorToHandleError(c.Owner.String(), err) } + csm.DirtyView().candCenter.base.recordOwner(c) log.AddAddress(actCtx.Caller) return log, nil diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 496aa1b710..363fdd8c28 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -39,6 +39,9 @@ const ( // _candidateNameSpace is the bucket name for candidate state _candidateNameSpace = "Candidate" + + // CandsMapNS is the bucket name to store candidate map + CandsMapNS = "CandsMap" ) const ( @@ -57,6 +60,12 @@ var ( TotalBucketKey = append([]byte{_const}, []byte("totalBucket")...) ) +var ( + _nameKey = []byte("name") + _operatorKey = []byte("operator") + _ownerKey = []byte("owner") +) + type ( // ReceiptError indicates a non-critical error with corresponding receipt status ReceiptError interface { @@ -166,6 +175,16 @@ func (p *Protocol) Start(ctx context.Context, sr protocol.StateReader) (interfac if err != nil { return nil, errors.Wrap(err, "failed to start staking protocol") } + + if p.needToReadCandsMap(height) { + name, operator, owners, err := readCandCenterStateFromStateDB(sr, height) + if err != nil { + return nil, errors.Wrap(err, "failed to read name/operator map") + } + if err = c.candCenter.base.loadNameOperatorMapOwnerList(name, operator, owners); err != nil { + return nil, errors.Wrap(err, "failed to load name/operator map to cand center") + } + } return c, nil } @@ -293,6 +312,36 @@ func (p *Protocol) handleStakingIndexer(epochStartHeight uint64, sm protocol.Sta return p.candBucketsIndexer.PutCandidates(epochStartHeight, candidateList) } +// PreCommit preforms pre-commit +func (p *Protocol) PreCommit(ctx context.Context, sm protocol.StateManager) error { + height, err := sm.Height() + if err != nil { + return err + } + if !p.needToWriteCandsMap(height) { + return nil + } + + featureWithHeightCtx := protocol.MustGetFeatureWithHeightCtx(ctx) + csm, err := NewCandidateStateManager(sm, featureWithHeightCtx.ReadStateFromDB(height)) + if err != nil { + return err + } + cc := csm.DirtyView().candCenter + base := cc.base.clone() + if _, err = base.commit(cc.change); err != nil { + return errors.Wrap(err, "failed to apply candidate change in pre-commit") + } + // persist nameMap/operatorMap and ownerList to stateDB + name := base.candsInNameMap() + op := base.candsInOperatorMap() + owners := base.ownersList() + if len(name) == 0 || len(op) == 0 { + return ErrNilParameters + } + return errors.Wrap(p.writeCandCenterStateToStateDB(sm, name, op, owners), "failed to write name/operator map to stateDB") +} + // Commit commits the last change func (p *Protocol) Commit(ctx context.Context, sm protocol.StateManager) error { featureWithHeightCtx := protocol.MustGetFeatureWithHeightCtx(ctx) @@ -543,3 +592,38 @@ func (p *Protocol) settleAction( r.AddLogs(logs...).AddTransactionLogs(depositLog).AddTransactionLogs(tLogs...) return &r, nil } + +func (p *Protocol) needToReadCandsMap(height uint64) bool { + return height > p.config.PersistStakingPatchBlock +} + +func (p *Protocol) needToWriteCandsMap(height uint64) bool { + return height >= p.config.PersistStakingPatchBlock +} + +func readCandCenterStateFromStateDB(sr protocol.StateReader, height uint64) (CandidateList, CandidateList, CandidateList, error) { + var ( + name, operator, owner CandidateList + ) + if _, err := sr.State(&name, protocol.NamespaceOption(CandsMapNS), protocol.KeyOption(_nameKey)); err != nil { + return nil, nil, nil, err + } + if _, err := sr.State(&operator, protocol.NamespaceOption(CandsMapNS), protocol.KeyOption(_operatorKey)); err != nil { + return nil, nil, nil, err + } + if _, err := sr.State(&owner, protocol.NamespaceOption(CandsMapNS), protocol.KeyOption(_ownerKey)); err != nil { + return nil, nil, nil, err + } + return name, operator, owner, nil +} + +func (p *Protocol) writeCandCenterStateToStateDB(sm protocol.StateManager, name, op, owners CandidateList) error { + if _, err := sm.PutState(name, protocol.NamespaceOption(CandsMapNS), protocol.KeyOption(_nameKey)); err != nil { + return err + } + if _, err := sm.PutState(op, protocol.NamespaceOption(CandsMapNS), protocol.KeyOption(_operatorKey)); err != nil { + return err + } + _, err := sm.PutState(owners, protocol.NamespaceOption(CandsMapNS), protocol.KeyOption(_ownerKey)) + return err +} diff --git a/state/factory/factory.go b/state/factory/factory.go index 738be7d14e..7dd01f2a31 100644 --- a/state/factory/factory.go +++ b/state/factory/factory.go @@ -24,6 +24,7 @@ import ( "github.com/iotexproject/iotex-core/action" "github.com/iotexproject/iotex-core/action/protocol" "github.com/iotexproject/iotex-core/action/protocol/execution/evm" + "github.com/iotexproject/iotex-core/action/protocol/staking" "github.com/iotexproject/iotex-core/actpool" "github.com/iotexproject/iotex-core/blockchain" "github.com/iotexproject/iotex-core/blockchain/block" @@ -296,7 +297,7 @@ func (sf *factory) flusherOptions(preEaster bool) []db.KVStoreFlusherOption { if wi.Namespace() == ArchiveTrieNamespace { return true } - if wi.Namespace() != evm.CodeKVNameSpace { + if wi.Namespace() != evm.CodeKVNameSpace && wi.Namespace() != staking.CandsMapNS { return false } return preEaster diff --git a/state/factory/statedb.go b/state/factory/statedb.go index 3693cac57d..12f57cb864 100644 --- a/state/factory/statedb.go +++ b/state/factory/statedb.go @@ -23,6 +23,7 @@ import ( "github.com/iotexproject/iotex-core/action" "github.com/iotexproject/iotex-core/action/protocol" "github.com/iotexproject/iotex-core/action/protocol/execution/evm" + "github.com/iotexproject/iotex-core/action/protocol/staking" "github.com/iotexproject/iotex-core/actpool" "github.com/iotexproject/iotex-core/blockchain/block" "github.com/iotexproject/iotex-core/blockchain/genesis" @@ -430,7 +431,7 @@ func (sdb *stateDB) flusherOptions(preEaster bool) []db.KVStoreFlusherOption { return append( opts, db.SerializeFilterOption(func(wi *batch.WriteInfo) bool { - return wi.Namespace() == evm.CodeKVNameSpace + return wi.Namespace() == evm.CodeKVNameSpace || wi.Namespace() == staking.CandsMapNS }), ) } From cfc602ef7cae43b4b654e9ce77e6e5e37de3dc6b Mon Sep 17 00:00:00 2001 From: zhi Date: Mon, 10 Oct 2022 01:16:17 -0700 Subject: [PATCH 4/4] [staking] patch for staking protocol (#3661) --- action/protocol/staking/builder.go | 1 + action/protocol/staking/candidate_test.go | 9 +++ action/protocol/staking/patchstore.go | 80 ++++++++++++++++++++++ action/protocol/staking/patchstore_test.go | 38 ++++++++++ action/protocol/staking/protocol.go | 12 +++- blockchain/config.go | 2 + chainservice/builder.go | 1 + db/db_bolt_test.go | 3 + 8 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 action/protocol/staking/patchstore.go create mode 100644 action/protocol/staking/patchstore_test.go diff --git a/action/protocol/staking/builder.go b/action/protocol/staking/builder.go index 8dcd7316a6..163715d2ee 100644 --- a/action/protocol/staking/builder.go +++ b/action/protocol/staking/builder.go @@ -8,5 +8,6 @@ type ( BuilderConfig struct { Staking genesis.Staking PersistStakingPatchBlock uint64 + StakingPatchDir string } ) diff --git a/action/protocol/staking/candidate_test.go b/action/protocol/staking/candidate_test.go index 891e2c2970..9ba9e7c2fd 100644 --- a/action/protocol/staking/candidate_test.go +++ b/action/protocol/staking/candidate_test.go @@ -58,6 +58,15 @@ func TestSer(t *testing.T) { l1 := &CandidateList{} r.NoError(l1.Deserialize(ser)) r.Equal(l, l1) + + // empty CandidateList can successfully Serialize/Deserialize + var m CandidateList + ser, err = m.Serialize() + r.NoError(err) + r.Equal([]byte{}, ser) + var m1 CandidateList + r.NoError(m1.Deserialize(ser)) + r.Nil(m1) } func TestClone(t *testing.T) { diff --git a/action/protocol/staking/patchstore.go b/action/protocol/staking/patchstore.go new file mode 100644 index 0000000000..c260a0f387 --- /dev/null +++ b/action/protocol/staking/patchstore.go @@ -0,0 +1,80 @@ +// Copyright (c) 2020 IoTeX Foundation +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package staking + +import ( + "encoding/csv" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +const ( + _name = "name" + _operator = "operator" +) + +// PatchStore is the patch store of staking protocol +type PatchStore struct { + dir string +} + +// NewPatchStore creates a new staking patch store +func NewPatchStore(dir string) *PatchStore { + return &PatchStore{dir: dir} +} + +func (store *PatchStore) pathOf(height uint64) string { + return filepath.Join(store.dir, fmt.Sprintf("%d.patch", height)) +} + +func (store *PatchStore) read(reader *csv.Reader) (CandidateList, error) { + record, err := reader.Read() + if err != nil { + return nil, err + } + if len(record) != 1 { + return nil, errors.Errorf("invalid record %+v", record) + } + data, err := hex.DecodeString(record[0]) + if err != nil { + return nil, err + } + var list CandidateList + if err := list.Deserialize(data); err != nil { + return nil, err + } + return list, nil +} + +// Read reads CandidateList by name and CandidateList by operator of given height +func (store *PatchStore) Read(height uint64) (CandidateList, CandidateList, CandidateList, error) { + file, err := os.Open(store.pathOf(height)) + if err != nil { + return nil, nil, nil, err + } + reader := csv.NewReader(file) + reader.FieldsPerRecord = -1 + listByName, err := store.read(reader) + if err != nil { + return nil, nil, nil, err + } + listByOperator, err := store.read(reader) + if err != nil { + return nil, nil, nil, err + } + listByOwner, err := store.read(reader) + if err != nil && err != io.EOF { + // io.EOF indicates an empty owner list + return nil, nil, nil, err + } + return listByName, listByOperator, listByOwner, nil +} diff --git a/action/protocol/staking/patchstore_test.go b/action/protocol/staking/patchstore_test.go new file mode 100644 index 0000000000..f1fc555c93 --- /dev/null +++ b/action/protocol/staking/patchstore_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2019 IoTeX Foundation +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package staking + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInvalidDirectory(t *testing.T) { + require := require.New(t) + dir := filepath.Join(t.TempDir(), "invalid") + _, err := os.Create(dir) + require.NoError(err) + _, _, _, err = NewPatchStore(dir).Read(0) + require.ErrorContains(err, "not a directory") +} + +func TestInvalidDirectory2(t *testing.T) { + require := require.New(t) + dir := t.TempDir() + require.NoError(os.Remove(dir)) + _, err := os.Stat(dir) + require.ErrorIs(err, os.ErrNotExist) + _, _, _, err = NewPatchStore(dir).Read(0) + require.ErrorContains(err, "no such file or directory") +} + +func TestCorruptedData(t *testing.T) { + // TODO: add test for corrupted data +} diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 363fdd8c28..8836066000 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -80,6 +80,7 @@ type ( config Configuration candBucketsIndexer *CandidatesBucketsIndexer voteReviser *VoteReviser + patch *PatchStore } // Configuration is the staking protocol configuration. @@ -154,6 +155,7 @@ func NewProtocol(depositGas DepositGas, cfg *BuilderConfig, candBucketsIndexer * depositGas: depositGas, candBucketsIndexer: candBucketsIndexer, voteReviser: voteReviser, + patch: NewPatchStore(cfg.StakingPatchDir), }, nil } @@ -179,7 +181,10 @@ func (p *Protocol) Start(ctx context.Context, sr protocol.StateReader) (interfac if p.needToReadCandsMap(height) { name, operator, owners, err := readCandCenterStateFromStateDB(sr, height) if err != nil { - return nil, errors.Wrap(err, "failed to read name/operator map") + // stateDB does not have name/operator map yet + if name, operator, owners, err = p.patch.Read(height); err != nil { + return nil, errors.Wrap(err, "failed to read name/operator map") + } } if err = c.candCenter.base.loadNameOperatorMapOwnerList(name, operator, owners); err != nil { return nil, errors.Wrap(err, "failed to load name/operator map to cand center") @@ -339,7 +344,10 @@ func (p *Protocol) PreCommit(ctx context.Context, sm protocol.StateManager) erro if len(name) == 0 || len(op) == 0 { return ErrNilParameters } - return errors.Wrap(p.writeCandCenterStateToStateDB(sm, name, op, owners), "failed to write name/operator map to stateDB") + if err := p.writeCandCenterStateToStateDB(sm, name, op, owners); err != nil { + return errors.Wrap(err, "failed to write name/operator map to stateDB") + } + return nil } // Commit commits the last change diff --git a/blockchain/config.go b/blockchain/config.go index 19c24ce6e1..3e454d91fc 100644 --- a/blockchain/config.go +++ b/blockchain/config.go @@ -28,6 +28,7 @@ type ( ChainDBPath string `yaml:"chainDBPath"` TrieDBPatchFile string `yaml:"trieDBPatchFile"` TrieDBPath string `yaml:"trieDBPath"` + StakingPatchDir string `yaml:"stakingPatchDir"` IndexDBPath string `yaml:"indexDBPath"` BloomfilterIndexDBPath string `yaml:"bloomfilterIndexDBPath"` CandidateIndexDBPath string `yaml:"candidateIndexDBPath"` @@ -78,6 +79,7 @@ var ( ChainDBPath: "/var/data/chain.db", TrieDBPatchFile: "/var/data/trie.db.patch", TrieDBPath: "/var/data/trie.db", + StakingPatchDir: "/var/data", IndexDBPath: "/var/data/index.db", BloomfilterIndexDBPath: "/var/data/bloomfilter.index.db", CandidateIndexDBPath: "/var/data/candidate.index.db", diff --git a/chainservice/builder.go b/chainservice/builder.go index b3aeb56590..3b5fee2bf5 100644 --- a/chainservice/builder.go +++ b/chainservice/builder.go @@ -451,6 +451,7 @@ func (builder *Builder) registerStakingProtocol() error { &staking.BuilderConfig{ Staking: builder.cfg.Genesis.Staking, PersistStakingPatchBlock: builder.cfg.Chain.PersistStakingPatchBlock, + StakingPatchDir: builder.cfg.Chain.StakingPatchDir, }, builder.cs.candBucketsIndexer, builder.cfg.Genesis.GreenlandBlockHeight, diff --git a/db/db_bolt_test.go b/db/db_bolt_test.go index f44ff970ff..90fb5bbe99 100644 --- a/db/db_bolt_test.go +++ b/db/db_bolt_test.go @@ -74,6 +74,9 @@ func TestBucketExists(t *testing.T) { r.False(kv.BucketExists("name")) r.NoError(kv.Put("name", []byte("key"), []byte{})) r.True(kv.BucketExists("name")) + v, err := kv.Get("name", []byte("key")) + r.NoError(err) + r.Equal([]byte{}, v) } func BenchmarkBoltDB_Get(b *testing.B) {