From 84e3158af3de56ed640ec2761fa7294b8aed5ec6 Mon Sep 17 00:00:00 2001 From: envestcc Date: Wed, 3 Jan 2024 09:55:43 +0800 Subject: [PATCH 1/7] ActiveCandidate consider endorsement --- .../protocol/staking/candidate_statereader.go | 5 ++ action/protocol/staking/protocol.go | 55 ++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/action/protocol/staking/candidate_statereader.go b/action/protocol/staking/candidate_statereader.go index 593499d3ff..1173e3b23e 100644 --- a/action/protocol/staking/candidate_statereader.go +++ b/action/protocol/staking/candidate_statereader.go @@ -65,6 +65,7 @@ type ( AllCandidates() CandidateList TotalStakedAmount() *big.Int ActiveBucketsCount() uint64 + ContainsSelfStakingBucket(index uint64) bool } candSR struct { @@ -110,6 +111,10 @@ func (c *candSR) AllCandidates() CandidateList { return c.view.candCenter.All() } +func (c *candSR) ContainsSelfStakingBucket(index uint64) bool { + return c.view.candCenter.ContainsSelfStakingBucket(index) +} + func (c *candSR) TotalStakedAmount() *big.Int { return c.view.bucketPool.Total() } diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index b554deebb1..642697c31f 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -472,6 +472,55 @@ func (p *Protocol) Validate(ctx context.Context, act action.Action, sr protocol. return nil } +func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateReader, cand *Candidate) (bool, error) { + // at least min self stake + if cand.SelfStake.Cmp(p.config.RegistrationConsts.MinSelfStake) < 0 { + return false, nil + } + + // endorsement must not exipred if the self-stake bucket is an endorse bucket + featureCtx := protocol.MustGetFeatureCtx(ctx) + if featureCtx.DisableDelegateEndorsement { + return true, nil + } + srHeight, err := csr.SR().Height() + if err != nil { + return false, errors.Wrap(err, "failed to get StateReader height") + } + vb, err := csr.getBucket(cand.SelfStakeBucketIdx) + if err != nil { + if errors.Is(err, state.ErrStateNotExist) { + return false, nil + } + return false, err + } + // bucket is self-owned + if address.Equal(vb.Owner, cand.Owner) { + return true, nil + } + // bucket is not endorsed to the candidate + if !address.Equal(vb.Candidate, cand.Owner) { + return false, nil + } + esr := NewEndorsementStateReader(csr.SR()) + endorse, err := esr.Get(cand.SelfStakeBucketIdx) + switch { + case err == nil: + // endorsement exists and expired before end of next epoch + rp := rolldpos.MustGetProtocol(protocol.MustGetRegistry(ctx)) + currentEpochNum := rp.GetEpochNum(srHeight) + if endorse.Status(rp.GetEpochLastBlockHeight(currentEpochNum+1)) == EndorseExpired { + return false, nil + } + case !errors.Is(err, state.ErrStateNotExist): + // other error + return false, err + default: + // endorsement does not exist + } + return true, nil +} + // ActiveCandidates returns all active candidates in candidate center func (p *Protocol) ActiveCandidates(ctx context.Context, sr protocol.StateReader, height uint64) (state.CandidateList, error) { srHeight, err := sr.Height() @@ -496,7 +545,11 @@ func (p *Protocol) ActiveCandidates(ctx context.Context, sr protocol.StateReader } list[i].Votes.Add(list[i].Votes, contractVotes) } - if list[i].SelfStake.Cmp(p.config.RegistrationConsts.MinSelfStake) >= 0 { + active, err := p.isActiveCandidate(ctx, c, list[i]) + if err != nil { + return nil, err + } + if active { cand = append(cand, list[i]) } } From fdd2b62713a76ae7e019143cbf68dd83cc22032f Mon Sep 17 00:00:00 2001 From: envestcc Date: Mon, 22 Jan 2024 21:18:28 +0800 Subject: [PATCH 2/7] address comments --- action/protocol/staking/protocol.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 642697c31f..82f187baa3 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -498,10 +498,6 @@ func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateRead if address.Equal(vb.Owner, cand.Owner) { return true, nil } - // bucket is not endorsed to the candidate - if !address.Equal(vb.Candidate, cand.Owner) { - return false, nil - } esr := NewEndorsementStateReader(csr.SR()) endorse, err := esr.Get(cand.SelfStakeBucketIdx) switch { @@ -517,6 +513,7 @@ func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateRead return false, err default: // endorsement does not exist + return false, errors.New("endorsement does not exist") } return true, nil } From bd408d595e895ffc0c02284965120d4e6ed3aa2e Mon Sep 17 00:00:00 2001 From: envestcc Date: Thu, 25 Jan 2024 23:52:00 +0800 Subject: [PATCH 3/7] address comment --- action/protocol/staking/protocol.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 82f187baa3..fc03cfc018 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -56,8 +56,9 @@ const ( // Errors var ( - ErrWithdrawnBucket = errors.New("the bucket is already withdrawn") - TotalBucketKey = append([]byte{_const}, []byte("totalBucket")...) + ErrWithdrawnBucket = errors.New("the bucket is already withdrawn") + ErrEndorsementNotExist = errors.New("the endorsement does not exist") + TotalBucketKey = append([]byte{_const}, []byte("totalBucket")...) ) var ( @@ -489,10 +490,7 @@ func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateRead } vb, err := csr.getBucket(cand.SelfStakeBucketIdx) if err != nil { - if errors.Is(err, state.ErrStateNotExist) { - return false, nil - } - return false, err + return false, errors.Wrapf(err, "failed to get bucket %d", cand.SelfStakeBucketIdx) } // bucket is self-owned if address.Equal(vb.Owner, cand.Owner) { @@ -513,7 +511,7 @@ func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateRead return false, err default: // endorsement does not exist - return false, errors.New("endorsement does not exist") + return false, errors.Wrapf(ErrEndorsementNotExist, "bucket index %d", cand.SelfStakeBucketIdx) } return true, nil } From ae5e699e74e7c7a60f16adea1575d3203820b673 Mon Sep 17 00:00:00 2001 From: envestcc Date: Mon, 5 Feb 2024 10:25:45 +0800 Subject: [PATCH 4/7] refactor --- .../protocol/staking/candidate_statereader.go | 32 +++++++++++++++ action/protocol/staking/protocol.go | 40 ++++--------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/action/protocol/staking/candidate_statereader.go b/action/protocol/staking/candidate_statereader.go index 1173e3b23e..2d4bc56305 100644 --- a/action/protocol/staking/candidate_statereader.go +++ b/action/protocol/staking/candidate_statereader.go @@ -66,6 +66,7 @@ type ( TotalStakedAmount() *big.Int ActiveBucketsCount() uint64 ContainsSelfStakingBucket(index uint64) bool + IsActiveCandidateAt(cand *Candidate, height uint64) (bool, error) } candSR struct { @@ -111,6 +112,37 @@ func (c *candSR) AllCandidates() CandidateList { return c.view.candCenter.All() } +func (c *candSR) IsActiveCandidateAt(cand *Candidate, height uint64) (bool, error) { + // self-stake bucket should be either self-owned or endorsed + vb, err := c.getBucket(cand.SelfStakeBucketIdx) + if err != nil { + return false, errors.Wrapf(err, "failed to get bucket %d", cand.SelfStakeBucketIdx) + } + // self-owned bucket should not be unstaked + if address.Equal(vb.Owner, cand.Owner) { + return !vb.isUnstaked(), nil + } + // endorsed bucket should not be expired + if !address.Equal(vb.Candidate, cand.Owner) { + return false, nil + } + esr := NewEndorsementStateReader(c.SR()) + endorse, err := esr.Get(cand.SelfStakeBucketIdx) + switch { + case err == nil: + // endorsement should not be expired + if endorse.Status(height) == EndorseExpired { + return false, nil + } + return true, nil + case errors.Is(err, state.ErrStateNotExist): + // impossible to happen + return false, errors.Wrapf(ErrEndorsementNotExist, "bucket index %d", cand.SelfStakeBucketIdx) + default: + return false, err + } +} + func (c *candSR) ContainsSelfStakingBucket(index uint64) bool { return c.view.candCenter.ContainsSelfStakingBucket(index) } diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index fc03cfc018..268b46857d 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -474,46 +474,22 @@ func (p *Protocol) Validate(ctx context.Context, act action.Action, sr protocol. } func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateReader, cand *Candidate) (bool, error) { - // at least min self stake - if cand.SelfStake.Cmp(p.config.RegistrationConsts.MinSelfStake) < 0 { - return false, nil - } - - // endorsement must not exipred if the self-stake bucket is an endorse bucket featureCtx := protocol.MustGetFeatureCtx(ctx) + // before delegate endorsement enabled, candidate is always active unless it's self-stake bucket is unstaked if featureCtx.DisableDelegateEndorsement { + if cand.SelfStake.Cmp(p.config.RegistrationConsts.MinSelfStake) < 0 { + return false, nil + } return true, nil } + srHeight, err := csr.SR().Height() if err != nil { return false, errors.Wrap(err, "failed to get StateReader height") } - vb, err := csr.getBucket(cand.SelfStakeBucketIdx) - if err != nil { - return false, errors.Wrapf(err, "failed to get bucket %d", cand.SelfStakeBucketIdx) - } - // bucket is self-owned - if address.Equal(vb.Owner, cand.Owner) { - return true, nil - } - esr := NewEndorsementStateReader(csr.SR()) - endorse, err := esr.Get(cand.SelfStakeBucketIdx) - switch { - case err == nil: - // endorsement exists and expired before end of next epoch - rp := rolldpos.MustGetProtocol(protocol.MustGetRegistry(ctx)) - currentEpochNum := rp.GetEpochNum(srHeight) - if endorse.Status(rp.GetEpochLastBlockHeight(currentEpochNum+1)) == EndorseExpired { - return false, nil - } - case !errors.Is(err, state.ErrStateNotExist): - // other error - return false, err - default: - // endorsement does not exist - return false, errors.Wrapf(ErrEndorsementNotExist, "bucket index %d", cand.SelfStakeBucketIdx) - } - return true, nil + rp := rolldpos.MustGetProtocol(protocol.MustGetRegistry(ctx)) + currentEpochNum := rp.GetEpochNum(srHeight) + return csr.IsActiveCandidateAt(cand, rp.GetEpochLastBlockHeight(currentEpochNum+1)) } // ActiveCandidates returns all active candidates in candidate center From 55a48e1cb08cac06c8dd996bf9e16ac5c3da53ca Mon Sep 17 00:00:00 2001 From: envestcc Date: Thu, 7 Mar 2024 20:22:58 +0800 Subject: [PATCH 5/7] reuse isSelfStakeBucket --- action/protocol/poll/nativestakingV2.go | 4 +- .../protocol/staking/candidate_statereader.go | 32 ---------------- action/protocol/staking/protocol.go | 38 +++++++++++++------ 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/action/protocol/poll/nativestakingV2.go b/action/protocol/poll/nativestakingV2.go index 85b545c098..e548103f23 100644 --- a/action/protocol/poll/nativestakingV2.go +++ b/action/protocol/poll/nativestakingV2.go @@ -7,11 +7,12 @@ import ( "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/iotex-address/address" + "github.com/iotexproject/iotex-election/util" + "github.com/iotexproject/iotex-core/action" "github.com/iotexproject/iotex-core/action/protocol" "github.com/iotexproject/iotex-core/action/protocol/staking" "github.com/iotexproject/iotex-core/state" - "github.com/iotexproject/iotex-election/util" ) type nativeStakingV2 struct { @@ -83,6 +84,7 @@ func (ns *nativeStakingV2) CalculateCandidatesByHeight(ctx context.Context, sr p return cands, err } bcCtx := protocol.MustGetBlockchainCtx(ctx) + // TODO: put candidates which will expired during current or next epoch behind the active candidates return ns.filterAndSortCandidatesByVoteScore(cands, bcCtx.Tip.Timestamp), nil } diff --git a/action/protocol/staking/candidate_statereader.go b/action/protocol/staking/candidate_statereader.go index 2d4bc56305..1173e3b23e 100644 --- a/action/protocol/staking/candidate_statereader.go +++ b/action/protocol/staking/candidate_statereader.go @@ -66,7 +66,6 @@ type ( TotalStakedAmount() *big.Int ActiveBucketsCount() uint64 ContainsSelfStakingBucket(index uint64) bool - IsActiveCandidateAt(cand *Candidate, height uint64) (bool, error) } candSR struct { @@ -112,37 +111,6 @@ func (c *candSR) AllCandidates() CandidateList { return c.view.candCenter.All() } -func (c *candSR) IsActiveCandidateAt(cand *Candidate, height uint64) (bool, error) { - // self-stake bucket should be either self-owned or endorsed - vb, err := c.getBucket(cand.SelfStakeBucketIdx) - if err != nil { - return false, errors.Wrapf(err, "failed to get bucket %d", cand.SelfStakeBucketIdx) - } - // self-owned bucket should not be unstaked - if address.Equal(vb.Owner, cand.Owner) { - return !vb.isUnstaked(), nil - } - // endorsed bucket should not be expired - if !address.Equal(vb.Candidate, cand.Owner) { - return false, nil - } - esr := NewEndorsementStateReader(c.SR()) - endorse, err := esr.Get(cand.SelfStakeBucketIdx) - switch { - case err == nil: - // endorsement should not be expired - if endorse.Status(height) == EndorseExpired { - return false, nil - } - return true, nil - case errors.Is(err, state.ErrStateNotExist): - // impossible to happen - return false, errors.Wrapf(ErrEndorsementNotExist, "bucket index %d", cand.SelfStakeBucketIdx) - default: - return false, err - } -} - func (c *candSR) ContainsSelfStakingBucket(index uint64) bool { return c.view.candCenter.ContainsSelfStakingBucket(index) } diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 268b46857d..aeb519bc8d 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -474,22 +474,28 @@ func (p *Protocol) Validate(ctx context.Context, act action.Action, sr protocol. } func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateReader, cand *Candidate) (bool, error) { + if cand.SelfStake.Cmp(p.config.RegistrationConsts.MinSelfStake) < 0 { + return false, nil + } featureCtx := protocol.MustGetFeatureCtx(ctx) - // before delegate endorsement enabled, candidate is always active unless it's self-stake bucket is unstaked if featureCtx.DisableDelegateEndorsement { - if cand.SelfStake.Cmp(p.config.RegistrationConsts.MinSelfStake) < 0 { - return false, nil - } + // before endorsement feature, candidates with enough amount must be active return true, nil } - - srHeight, err := csr.SR().Height() + bucket, err := csr.getBucket(cand.SelfStakeBucketIdx) + switch { + case errors.Cause(err) == state.ErrStateNotExist: + // endorse bucket has been withdrawn + return false, nil + case err != nil: + return false, errors.Wrapf(err, "failed to get bucket %d", cand.SelfStakeBucketIdx) + default: + } + selfStake, err := isSelfStakeBucketCommon(featureCtx, csr.ContainsSelfStakingBucket, csr.SR(), bucket) if err != nil { - return false, errors.Wrap(err, "failed to get StateReader height") + return false, errors.Wrapf(err, "failed to check self-stake bucket %d", cand.SelfStakeBucketIdx) } - rp := rolldpos.MustGetProtocol(protocol.MustGetRegistry(ctx)) - currentEpochNum := rp.GetEpochNum(srHeight) - return csr.IsActiveCandidateAt(cand, rp.GetEpochLastBlockHeight(currentEpochNum+1)) + return selfStake, nil } // ActiveCandidates returns all active candidates in candidate center @@ -711,8 +717,16 @@ func writeCandCenterStateToStateDB(sm protocol.StateManager, name, op, owners Ca // isSelfStakeBucket returns true if the bucket is self-stake bucket and not expired func isSelfStakeBucket(featureCtx protocol.FeatureCtx, csm CandidateStateManager, bucket *VoteBucket) (bool, error) { + return isSelfStakeBucketCommon(featureCtx, csm.ContainsSelfStakingBucket, csm.SM(), bucket) +} + +func isSelfStakeBucketCommon( + featureCtx protocol.FeatureCtx, + containsSelfStakingBucket func(uint64) bool, + sr protocol.StateReader, + bucket *VoteBucket) (bool, error) { // bucket index should be settled in one of candidates - selfStake := csm.ContainsSelfStakingBucket(bucket.Index) + selfStake := containsSelfStakingBucket(bucket.Index) if featureCtx.DisableDelegateEndorsement || !selfStake { return selfStake, nil } @@ -722,7 +736,7 @@ func isSelfStakeBucket(featureCtx protocol.FeatureCtx, csm CandidateStateManager return !bucket.isUnstaked(), nil } // otherwise bucket should be an endorse bucket which is not expired - esm := NewEndorsementStateManager(csm.SM()) + esm := NewEndorsementStateReader(sr) height, err := esm.Height() if err != nil { return false, err From 77bbf07b42567692e900939bdd12cc09bed687e7 Mon Sep 17 00:00:00 2001 From: envestcc Date: Fri, 8 Mar 2024 11:39:06 +0800 Subject: [PATCH 6/7] introduce CandidateStateCommon --- .../protocol/staking/candidate_statemanager.go | 11 +++++++++++ action/protocol/staking/protocol.go | 16 ++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/action/protocol/staking/candidate_statemanager.go b/action/protocol/staking/candidate_statemanager.go index e59742ea23..9cffd002a0 100644 --- a/action/protocol/staking/candidate_statemanager.go +++ b/action/protocol/staking/candidate_statemanager.go @@ -58,6 +58,13 @@ type ( DebitBucketPool(*big.Int, bool) error Commit(context.Context) error SM() protocol.StateManager + SR() protocol.StateReader + } + + // CandidiateStateCommon is the common interface for candidate state manager and reader + CandidiateStateCommon interface { + ContainsSelfStakingBucket(uint64) bool + SR() protocol.StateReader } candSM struct { @@ -106,6 +113,10 @@ func (csm *candSM) SM() protocol.StateManager { return csm.StateManager } +func (csm *candSM) SR() protocol.StateReader { + return csm.StateManager +} + // DirtyView is csm's current state, which reflects base view + applying delta saved in csm's dock func (csm *candSM) DirtyView() *ViewData { return &ViewData{ diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index aeb519bc8d..ca0808b0f6 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -491,7 +491,7 @@ func (p *Protocol) isActiveCandidate(ctx context.Context, csr CandidateStateRead return false, errors.Wrapf(err, "failed to get bucket %d", cand.SelfStakeBucketIdx) default: } - selfStake, err := isSelfStakeBucketCommon(featureCtx, csr.ContainsSelfStakingBucket, csr.SR(), bucket) + selfStake, err := isSelfStakeBucket(featureCtx, csr, bucket) if err != nil { return false, errors.Wrapf(err, "failed to check self-stake bucket %d", cand.SelfStakeBucketIdx) } @@ -716,17 +716,9 @@ func writeCandCenterStateToStateDB(sm protocol.StateManager, name, op, owners Ca } // isSelfStakeBucket returns true if the bucket is self-stake bucket and not expired -func isSelfStakeBucket(featureCtx protocol.FeatureCtx, csm CandidateStateManager, bucket *VoteBucket) (bool, error) { - return isSelfStakeBucketCommon(featureCtx, csm.ContainsSelfStakingBucket, csm.SM(), bucket) -} - -func isSelfStakeBucketCommon( - featureCtx protocol.FeatureCtx, - containsSelfStakingBucket func(uint64) bool, - sr protocol.StateReader, - bucket *VoteBucket) (bool, error) { +func isSelfStakeBucket(featureCtx protocol.FeatureCtx, csc CandidiateStateCommon, bucket *VoteBucket) (bool, error) { // bucket index should be settled in one of candidates - selfStake := containsSelfStakingBucket(bucket.Index) + selfStake := csc.ContainsSelfStakingBucket(bucket.Index) if featureCtx.DisableDelegateEndorsement || !selfStake { return selfStake, nil } @@ -736,7 +728,7 @@ func isSelfStakeBucketCommon( return !bucket.isUnstaked(), nil } // otherwise bucket should be an endorse bucket which is not expired - esm := NewEndorsementStateReader(sr) + esm := NewEndorsementStateReader(csc.SR()) height, err := esm.Height() if err != nil { return false, err From 96663e0e08b8cff534cefb59b421eb4fe7c6b438 Mon Sep 17 00:00:00 2001 From: envestcc Date: Fri, 8 Mar 2024 20:52:26 +0800 Subject: [PATCH 7/7] fix test --- action/protocol/staking/handlers_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action/protocol/staking/handlers_test.go b/action/protocol/staking/handlers_test.go index c7250757e1..4746fb5019 100644 --- a/action/protocol/staking/handlers_test.go +++ b/action/protocol/staking/handlers_test.go @@ -2810,7 +2810,7 @@ func TestProtocol_FetchBucketAndValidate(t *testing.T) { }) isSelfStake := true isSelfStakeErr := error(nil) - patches.ApplyFunc(isSelfStakeBucket, func(featureCtx protocol.FeatureCtx, csm CandidateStateManager, bucket *VoteBucket) (bool, error) { + patches.ApplyFunc(isSelfStakeBucket, func(featureCtx protocol.FeatureCtx, csm CandidiateStateCommon, bucket *VoteBucket) (bool, error) { return isSelfStake, isSelfStakeErr }) _, err = p.fetchBucketAndValidate(protocol.FeatureCtx{}, csm, identityset.Address(1), 1, false, false) @@ -2831,7 +2831,7 @@ func TestProtocol_FetchBucketAndValidate(t *testing.T) { Owner: identityset.Address(1), }, nil }) - patches.ApplyFunc(isSelfStakeBucket, func(featureCtx protocol.FeatureCtx, csm CandidateStateManager, bucket *VoteBucket) (bool, error) { + patches.ApplyFunc(isSelfStakeBucket, func(featureCtx protocol.FeatureCtx, csm CandidiateStateCommon, bucket *VoteBucket) (bool, error) { return false, nil }) defer patches.Reset()