diff --git a/action/protocol/staking/handler_candidate_endorsement_test.go b/action/protocol/staking/handler_candidate_endorsement_test.go index a9ac33e965..34938d61eb 100644 --- a/action/protocol/staking/handler_candidate_endorsement_test.go +++ b/action/protocol/staking/handler_candidate_endorsement_test.go @@ -285,6 +285,33 @@ func TestProtocol_HandleCandidateEndorsement(t *testing.T) { []expectCandidate{}, nil, }, + { + "once endorsed, bucket cannot be change candidate", + []uint64{0, 10}, + []uint64{0, 1, 2}, + 1300000, + identityset.Address(1), + 1, + uint64(1000000), + uint64(1000000), + big.NewInt(1000), + 1, + true, + true, + &appendAction{ + func() action.Action { + act, err := action.NewChangeCandidate(0, "test3", 1, []byte{}, uint64(1000000), big.NewInt(1000)) + require.NoError(err) + return act + }, + iotextypes.ReceiptStatus_ErrInvalidBucketType, //todo fix + nil, + }, + nil, + iotextypes.ReceiptStatus_Success, + []expectCandidate{}, + nil, + }, { "unendorse a valid bucket", []uint64{0, 9}, diff --git a/action/protocol/staking/handlers.go b/action/protocol/staking/handlers.go index 776b88db3f..1022298fd6 100644 --- a/action/protocol/staking/handlers.go +++ b/action/protocol/staking/handlers.go @@ -172,8 +172,10 @@ func (p *Protocol) handleUnstake(ctx context.Context, act *action.Unstake, csm C failureStatus: iotextypes.ReceiptStatus_ErrUnstakeBeforeMaturity, } } - if rErr := validateBucketEndorsement(NewEndorsementStateManager(csm.SM()), bucket, false, blkCtx.BlockHeight); rErr != nil { - return log, rErr + if !featureCtx.DisableDelegateEndorsement { + if rErr := validateBucketEndorsement(NewEndorsementStateManager(csm.SM()), bucket, false, blkCtx.BlockHeight); rErr != nil { + return log, rErr + } } // TODO: cannot unstake if selected as candidates in this or next epoch @@ -290,6 +292,7 @@ func (p *Protocol) handleChangeCandidate(ctx context.Context, act *action.Change ) (*receiptLog, error) { actionCtx := protocol.MustGetActionCtx(ctx) featureCtx := protocol.MustGetFeatureCtx(ctx) + blkCtx := protocol.MustGetBlockCtx(ctx) log := newReceiptLog(p.addr.String(), HandleChangeCandidate, featureCtx.NewStakingReceiptFormat) _, fetchErr := fetchCaller(ctx, csm, big.NewInt(0)) @@ -306,6 +309,11 @@ func (p *Protocol) handleChangeCandidate(ctx context.Context, act *action.Change if fetchErr != nil { return log, fetchErr } + if !featureCtx.DisableDelegateEndorsement { + if rErr := validateBucketEndorsement(NewEndorsementStateManager(csm.SM()), bucket, false, blkCtx.BlockHeight); rErr != nil { + return log, rErr + } + } log.AddTopics(byteutil.Uint64ToBytesBigEndian(bucket.Index), bucket.Candidate.Bytes(), candidate.Owner.Bytes()) prevCandidate := csm.GetByOwner(bucket.Candidate) @@ -349,6 +357,12 @@ func (p *Protocol) handleChangeCandidate(ctx context.Context, act *action.Change failureStatus: iotextypes.ReceiptStatus_ErrNotEnoughBalance, } } + // if the bucket equals to the previous candidate's self-stake bucket, it must be expired endorse bucket + // so we need to clear the self-stake of the previous candidate + if !featureCtx.DisableDelegateEndorsement && prevCandidate.SelfStakeBucketIdx == bucket.Index { + prevCandidate.SelfStake.SetInt64(0) + prevCandidate.SelfStakeBucketIdx = candidateNoSelfStakeBucketIndex + } if err := csm.Upsert(prevCandidate); err != nil { return log, csmErrorToHandleError(prevCandidate.Owner.String(), err) } diff --git a/action/protocol/staking/handlers_test.go b/action/protocol/staking/handlers_test.go index 40b194b1ee..57fa5b43b6 100644 --- a/action/protocol/staking/handlers_test.go +++ b/action/protocol/staking/handlers_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/golang/mock/gomock" + "github.com/mohae/deepcopy" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -1699,6 +1700,141 @@ func TestProtocol_HandleChangeCandidate(t *testing.T) { } } +func TestProtocol_HandleChangeCandidate_ClearPrevCandidateSelfStake(t *testing.T) { + r := require.New(t) + ctrl := gomock.NewController(t) + t.Run("clear if bucket is an expired endorse bucket", func(t *testing.T) { + sm, p, buckets, _ := initTestStateWithHeight(t, ctrl, + []*bucketConfig{ + {identityset.Address(1), identityset.Address(5), "100000000000000000000", 1, true, true, nil, 1}, + }, + []*candidateConfig{ + {identityset.Address(1), identityset.Address(11), identityset.Address(21), "test1"}, + {identityset.Address(2), identityset.Address(12), identityset.Address(22), "test2"}, + }, 1) + r.NoError(setupAccount(sm, identityset.Address(5), 10000)) + nonce := uint64(1) + act, err := action.NewChangeCandidate(nonce, "test2", buckets[0].Index, nil, 10000, big.NewInt(unit.Qev)) + r.NoError(err) + intrinsic, err := act.IntrinsicGas() + r.NoError(err) + ctx := context.Background() + g := deepcopy.Copy(genesis.Default).(genesis.Genesis) + g.ToBeEnabledBlockHeight = 0 + ctx = genesis.WithGenesisContext(ctx, g) + ctx = protocol.WithActionCtx(ctx, protocol.ActionCtx{ + Caller: identityset.Address(5), + GasPrice: big.NewInt(unit.Qev), + IntrinsicGas: intrinsic, + Nonce: nonce, + }) + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: 2, + BlockTimeStamp: time.Now(), + GasLimit: 1000000, + }) + ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) + recipt, err := p.Handle(ctx, act, sm) + r.NoError(err) + r.EqualValues(iotextypes.ReceiptStatus_Success, recipt.Status) + // test previous candidate self stake + csm, err := NewCandidateStateManager(sm, false) + r.NoError(err) + prevCand := csm.GetByOwner(identityset.Address(1)) + r.Equal("0", prevCand.SelfStake.String()) + r.EqualValues(uint64(candidateNoSelfStakeBucketIndex), prevCand.SelfStakeBucketIdx) + r.Equal("0", prevCand.Votes.String()) + }) + t.Run("not clear if bucket is a vote bucket", func(t *testing.T) { + sm, p, buckets, _ := initTestStateWithHeight(t, ctrl, + []*bucketConfig{ + {identityset.Address(1), identityset.Address(1), "120000000000000000000", 1, true, true, nil, 0}, + {identityset.Address(2), identityset.Address(2), "120000000000000000000", 1, true, true, nil, 0}, + {identityset.Address(1), identityset.Address(1), "100000000000000000000", 1, true, false, nil, 0}, + }, + []*candidateConfig{ + {identityset.Address(1), identityset.Address(11), identityset.Address(21), "test1"}, + {identityset.Address(2), identityset.Address(12), identityset.Address(22), "test2"}, + }, 1) + r.NoError(setupAccount(sm, identityset.Address(1), 10000)) + nonce := uint64(1) + act, err := action.NewChangeCandidate(nonce, "test2", buckets[2].Index, nil, 10000, big.NewInt(unit.Qev)) + r.NoError(err) + intrinsic, err := act.IntrinsicGas() + r.NoError(err) + ctx := context.Background() + g := deepcopy.Copy(genesis.Default).(genesis.Genesis) + g.ToBeEnabledBlockHeight = 0 + ctx = genesis.WithGenesisContext(ctx, g) + ctx = protocol.WithActionCtx(ctx, protocol.ActionCtx{ + Caller: identityset.Address(1), + GasPrice: big.NewInt(unit.Qev), + IntrinsicGas: intrinsic, + Nonce: nonce, + }) + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: 2, + BlockTimeStamp: time.Now(), + GasLimit: 1000000, + }) + ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) + recipt, err := p.Handle(ctx, act, sm) + r.NoError(err) + r.EqualValues(iotextypes.ReceiptStatus_Success, recipt.Status) + // test previous candidate self stake + csm, err := NewCandidateStateManager(sm, false) + r.NoError(err) + prevCand := csm.GetByOwner(buckets[2].Candidate) + r.Equal("120000000000000000000", prevCand.SelfStake.String()) + r.EqualValues(0, prevCand.SelfStakeBucketIdx) + r.Equal("124562140820308711042", prevCand.Votes.String()) + }) + t.Run("not clear if bucket is a vote bucket", func(t *testing.T) { + sm, p, buckets, _ := initTestStateWithHeight(t, ctrl, + []*bucketConfig{ + {identityset.Address(1), identityset.Address(1), "120000000000000000000", 1, true, true, nil, 0}, + {identityset.Address(2), identityset.Address(2), "120000000000000000000", 1, true, true, nil, 0}, + {identityset.Address(1), identityset.Address(1), "100000000000000000000", 1, true, false, nil, 0}, + }, + []*candidateConfig{ + {identityset.Address(1), identityset.Address(11), identityset.Address(21), "test1"}, + {identityset.Address(2), identityset.Address(12), identityset.Address(22), "test2"}, + }, 1) + r.NoError(setupAccount(sm, identityset.Address(1), 10000)) + nonce := uint64(1) + act, err := action.NewChangeCandidate(nonce, "test2", buckets[2].Index, nil, 10000, big.NewInt(unit.Qev)) + r.NoError(err) + intrinsic, err := act.IntrinsicGas() + r.NoError(err) + ctx := context.Background() + g := deepcopy.Copy(genesis.Default).(genesis.Genesis) + g.ToBeEnabledBlockHeight = 0 + ctx = genesis.WithGenesisContext(ctx, g) + ctx = protocol.WithActionCtx(ctx, protocol.ActionCtx{ + Caller: identityset.Address(1), + GasPrice: big.NewInt(unit.Qev), + IntrinsicGas: intrinsic, + Nonce: nonce, + }) + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: 2, + BlockTimeStamp: time.Now(), + GasLimit: 1000000, + }) + ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx)) + recipt, err := p.Handle(ctx, act, sm) + r.NoError(err) + r.EqualValues(iotextypes.ReceiptStatus_Success, recipt.Status) + // test previous candidate self stake + csm, err := NewCandidateStateManager(sm, false) + r.NoError(err) + prevCand := csm.GetByOwner(buckets[2].Candidate) + r.Equal("120000000000000000000", prevCand.SelfStake.String()) + r.EqualValues(0, prevCand.SelfStakeBucketIdx) + r.Equal("124562140820308711042", prevCand.Votes.String()) + }) +} + func TestProtocol_HandleTransferStake(t *testing.T) { require := require.New(t) ctrl := gomock.NewController(t)