From d1359cd762b7a5146a8c476f0c45c922abe8ace6 Mon Sep 17 00:00:00 2001 From: zhi Date: Wed, 10 Sep 2025 23:37:53 +0800 Subject: [PATCH 1/4] slash candidate unit test --- action/protocol/staking/protocol.go | 2 +- action/protocol/staking/protocol_test.go | 87 ++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index a192ce935d..e68cc16092 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -408,7 +408,6 @@ func (p *Protocol) SlashCandidate( if err != nil { return errors.Wrap(err, "failed to fetch bucket") } - prevWeightedVotes := p.calculateVoteWeight(bucket, true) if bucket.StakedAmount.Cmp(amount) < 0 { return errors.Errorf("amount %s is greater than staked amount %s", amount.String(), bucket.StakedAmount.String()) } @@ -416,6 +415,7 @@ func (p *Protocol) SlashCandidate( if err := csm.updateBucket(bucket.Index, bucket); err != nil { return errors.Wrapf(err, "failed to update bucket %d", bucket.Index) } + prevWeightedVotes := p.calculateVoteWeight(bucket, true) if err := candidate.SubVote(prevWeightedVotes); err != nil { return errors.Wrapf(err, "failed to sub candidate votes") } diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index 5f373b48a0..7bdf264d31 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -672,3 +672,90 @@ func TestIsSelfStakeBucket(t *testing.T) { r.False(selfStake) }) } + +func TestSlashCandidate(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + sm := testdb.NewMockStateManager(ctrl) + + owner := identityset.Address(1) + operator := identityset.Address(2) + reward := identityset.Address(3) + selfStake := big.NewInt(1000) + bucket := NewVoteBucket(owner, owner, new(big.Int).Set(selfStake), 10, time.Now(), true) + bucketIdx := uint64(0) + bucket.Index = bucketIdx + + cand := &Candidate{ + Owner: owner, + Operator: operator, + Reward: reward, + Name: "cand1", + Votes: big.NewInt(1000), + SelfStakeBucketIdx: bucketIdx, + SelfStake: new(big.Int).Set(selfStake), + } + cc, err := NewCandidateCenter(CandidateList{cand}) + require.NoError(err) + require.NoError(sm.WriteView(_protocolID, &viewData{ + candCenter: cc, + bucketPool: &BucketPool{ + enableSMStorage: true, + total: &totalAmount{ + amount: big.NewInt(0), + }, + }, + })) + csm, err := NewCandidateStateManager(sm) + require.NoError(err) + + p := &Protocol{ + config: Configuration{ + RegistrationConsts: RegistrationConsts{ + MinSelfStake: big.NewInt(1), + }, + MinSelfStakeToBeActive: big.NewInt(1), + }, + } + ctx := context.Background() + + t.Run("nil amount", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, nil) + require.ErrorContains(err, "nil or non-positive amount") + }) + + t.Run("zero amount", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, big.NewInt(0)) + require.ErrorContains(err, "nil or non-positive amount") + }) + + t.Run("candidate not exist", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, identityset.Address(9), big.NewInt(1)) + require.ErrorContains(err, "does not exist") + }) + + t.Run("bucket not exist", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, big.NewInt(1)) + require.ErrorContains(err, "failed to fetch bucket") + }) + + _, err = csm.putBucket(bucket) + require.NoError(err) + require.NoError(csm.DebitBucketPool(bucket.StakedAmount, true)) + t.Run("amount greater than staked", func(t *testing.T) { + err := p.SlashCandidate(ctx, sm, owner, big.NewInt(2000)) + require.ErrorContains(err, "is greater than staked amount") + }) + + t.Run("success", func(t *testing.T) { + amount := big.NewInt(400) + remaining := bucket.StakedAmount.Sub(bucket.StakedAmount, amount) + err := p.SlashCandidate(ctx, sm, owner, amount) + require.NoError(err) + bucket, err := csm.NativeBucket(bucketIdx) + require.NoError(err) + require.Equal(remaining.String(), bucket.StakedAmount.String()) + cand := csm.GetByIdentifier(owner) + require.Equal(remaining.String(), cand.SelfStake.String()) + }) +} From 82b00b29774b18b2e648ebe133e29b28c0c0bfcc Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 30 Sep 2025 13:42:18 +0800 Subject: [PATCH 2/4] remove from active candidate list after hardfork --- action/protocol/staking/protocol_test.go | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index 7bdf264d31..a9d963b6a6 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -712,12 +712,17 @@ func TestSlashCandidate(t *testing.T) { p := &Protocol{ config: Configuration{ RegistrationConsts: RegistrationConsts{ - MinSelfStake: big.NewInt(1), + MinSelfStake: big.NewInt(1000), }, - MinSelfStakeToBeActive: big.NewInt(1), + MinSelfStakeToBeActive: big.NewInt(590), }, } ctx := context.Background() + ctx = genesis.WithGenesisContext(ctx, genesis.TestDefault()) + ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: 100, + }) + ctx = protocol.WithFeatureCtx(ctx) t.Run("nil amount", func(t *testing.T) { err := p.SlashCandidate(ctx, sm, owner, nil) @@ -742,6 +747,10 @@ func TestSlashCandidate(t *testing.T) { _, err = csm.putBucket(bucket) require.NoError(err) require.NoError(csm.DebitBucketPool(bucket.StakedAmount, true)) + cl, err := p.ActiveCandidates(ctx, sm, 0) + require.NoError(err) + require.Equal(1, len(cl)) + t.Run("amount greater than staked", func(t *testing.T) { err := p.SlashCandidate(ctx, sm, owner, big.NewInt(2000)) require.ErrorContains(err, "is greater than staked amount") @@ -752,6 +761,18 @@ func TestSlashCandidate(t *testing.T) { remaining := bucket.StakedAmount.Sub(bucket.StakedAmount, amount) err := p.SlashCandidate(ctx, sm, owner, amount) require.NoError(err) + cl, err = p.ActiveCandidates(ctx, sm, 0) + require.NoError(err) + require.Equal(0, len(cl)) + cl, err = p.ActiveCandidates( + protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: genesis.Default.ToBeEnabledBlockHeight, + })), + sm, + 0, + ) + require.NoError(err) + require.Equal(1, len(cl)) bucket, err := csm.NativeBucket(bucketIdx) require.NoError(err) require.Equal(remaining.String(), bucket.StakedAmount.String()) From a80f3fc96c590282428b409a52fb68c9f781e265 Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 30 Sep 2025 14:18:48 +0800 Subject: [PATCH 3/4] credit bucket pool without deleting bucket --- action/protocol/staking/bucket_pool.go | 10 ++++--- action/protocol/staking/bucket_pool_test.go | 8 +++--- .../staking/candidate_statemanager.go | 6 ++--- .../protocol/staking/handler_stake_migrate.go | 2 +- action/protocol/staking/handlers.go | 2 +- action/protocol/staking/protocol.go | 2 +- action/protocol/staking/protocol_test.go | 27 +++++++++++++++---- 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/action/protocol/staking/bucket_pool.go b/action/protocol/staking/bucket_pool.go index 7616c0456d..7670ed031c 100644 --- a/action/protocol/staking/bucket_pool.go +++ b/action/protocol/staking/bucket_pool.go @@ -84,12 +84,14 @@ func (t *totalAmount) AddBalance(amount *big.Int, newBucket bool) { } } -func (t *totalAmount) SubBalance(amount *big.Int) error { +func (t *totalAmount) SubBalance(amount *big.Int, deleteBucket bool) error { if amount.Cmp(t.amount) == 1 || t.count == 0 { return state.ErrNotEnoughBalance } t.amount.Sub(t.amount, amount) - t.count-- + if deleteBucket { + t.count-- + } return nil } @@ -146,8 +148,8 @@ func (bp *BucketPool) Commit() error { } // CreditPool subtracts staked amount out of the pool -func (bp *BucketPool) CreditPool(sm protocol.StateManager, amount *big.Int) error { - if err := bp.total.SubBalance(amount); err != nil { +func (bp *BucketPool) CreditPool(sm protocol.StateManager, amount *big.Int, deleteBucket bool) error { + if err := bp.total.SubBalance(amount, deleteBucket); err != nil { return err } diff --git a/action/protocol/staking/bucket_pool_test.go b/action/protocol/staking/bucket_pool_test.go index 63f966e3dc..8858ca047f 100644 --- a/action/protocol/staking/bucket_pool_test.go +++ b/action/protocol/staking/bucket_pool_test.go @@ -41,11 +41,11 @@ func TestTotalAmount(t *testing.T) { r.Equal(a, b) // test sub balance - r.Equal(state.ErrNotEnoughBalance, a.SubBalance(big.NewInt(11))) - r.NoError(a.SubBalance(big.NewInt(4))) + r.Equal(state.ErrNotEnoughBalance, a.SubBalance(big.NewInt(11), true)) + r.NoError(a.SubBalance(big.NewInt(4), true)) r.Equal(big.NewInt(6), a.amount) r.EqualValues(0, a.count) - r.Equal(state.ErrNotEnoughBalance, a.SubBalance(big.NewInt(1))) + r.Equal(state.ErrNotEnoughBalance, a.SubBalance(big.NewInt(1), true)) // test add balance a.AddBalance(big.NewInt(1), true) @@ -128,7 +128,7 @@ func TestBucketPool(t *testing.T) { if v.debit { err = csm.DebitBucketPool(v.amount, v.newBucket) } else { - err = csm.CreditBucketPool(v.amount) + err = csm.CreditBucketPool(v.amount, true) } r.Equal(v.expected, err) diff --git a/action/protocol/staking/candidate_statemanager.go b/action/protocol/staking/candidate_statemanager.go index 25fc43c58f..c09deee582 100644 --- a/action/protocol/staking/candidate_statemanager.go +++ b/action/protocol/staking/candidate_statemanager.go @@ -52,7 +52,7 @@ type ( GetByOwner(address.Address) *Candidate GetByIdentifier(address.Address) *Candidate Upsert(*Candidate) error - CreditBucketPool(*big.Int) error + CreditBucketPool(*big.Int, bool) error DebitBucketPool(*big.Int, bool) error Commit(context.Context) error SM() protocol.StateManager @@ -157,8 +157,8 @@ func (csm *candSM) Upsert(d *Candidate) error { return csm.putCandidate(d) } -func (csm *candSM) CreditBucketPool(amount *big.Int) error { - return csm.bucketPool.CreditPool(csm.StateManager, amount) +func (csm *candSM) CreditBucketPool(amount *big.Int, deleteBucket bool) error { + return csm.bucketPool.CreditPool(csm.StateManager, amount, deleteBucket) } func (csm *candSM) DebitBucketPool(amount *big.Int, newBucket bool) error { diff --git a/action/protocol/staking/handler_stake_migrate.go b/action/protocol/staking/handler_stake_migrate.go index 248e6ce2d6..84de4074c6 100644 --- a/action/protocol/staking/handler_stake_migrate.go +++ b/action/protocol/staking/handler_stake_migrate.go @@ -125,7 +125,7 @@ func (p *Protocol) withdrawBucket(ctx context.Context, withdrawer *state.Account } // update bucket pool - if err := csm.CreditBucketPool(bucket.StakedAmount); err != nil { + if err := csm.CreditBucketPool(bucket.StakedAmount, true); err != nil { return nil, nil, errors.Wrapf(err, "failed to update staking bucket pool %s", err.Error()) } // update candidate vote diff --git a/action/protocol/staking/handlers.go b/action/protocol/staking/handlers.go index 95978d3e6b..b36853c4e6 100644 --- a/action/protocol/staking/handlers.go +++ b/action/protocol/staking/handlers.go @@ -269,7 +269,7 @@ func (p *Protocol) handleWithdrawStake(ctx context.Context, act *action.Withdraw } // update bucket pool - if err := csm.CreditBucketPool(bucket.StakedAmount); err != nil { + if err := csm.CreditBucketPool(bucket.StakedAmount, true); err != nil { return log, nil, &handleError{ err: errors.Wrapf(err, "failed to update staking bucket pool %s", err.Error()), failureStatus: iotextypes.ReceiptStatus_ErrWriteAccount, diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index e68cc16092..47de06724a 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -429,7 +429,7 @@ func (p *Protocol) SlashCandidate( if err := csm.Upsert(candidate); err != nil { return errors.Wrap(err, "failed to upsert candidate") } - return csm.CreditBucketPool(amount) + return csm.CreditBucketPool(amount, false) } // CreatePreStates updates state manager diff --git a/action/protocol/staking/protocol_test.go b/action/protocol/staking/protocol_test.go index a9d963b6a6..e09c0bc750 100644 --- a/action/protocol/staking/protocol_test.go +++ b/action/protocol/staking/protocol_test.go @@ -759,15 +759,15 @@ func TestSlashCandidate(t *testing.T) { t.Run("success", func(t *testing.T) { amount := big.NewInt(400) remaining := bucket.StakedAmount.Sub(bucket.StakedAmount, amount) - err := p.SlashCandidate(ctx, sm, owner, amount) - require.NoError(err) + require.NoError(p.SlashCandidate(ctx, sm, owner, amount)) cl, err = p.ActiveCandidates(ctx, sm, 0) require.NoError(err) require.Equal(0, len(cl)) + ctx = protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{ + BlockHeight: genesis.Default.ToBeEnabledBlockHeight, + })) cl, err = p.ActiveCandidates( - protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{ - BlockHeight: genesis.Default.ToBeEnabledBlockHeight, - })), + ctx, sm, 0, ) @@ -778,5 +778,22 @@ func TestSlashCandidate(t *testing.T) { require.Equal(remaining.String(), bucket.StakedAmount.String()) cand := csm.GetByIdentifier(owner) require.Equal(remaining.String(), cand.SelfStake.String()) + require.NoError(p.SlashCandidate(ctx, sm, owner, big.NewInt(11))) + cl, err = p.ActiveCandidates( + ctx, + sm, + 0, + ) + require.NoError(err) + require.Equal(0, len(cl)) + require.NoError(cand.AddSelfStake(big.NewInt(21))) + require.NoError(csm.Upsert(cand)) + cl, err = p.ActiveCandidates( + ctx, + sm, + 0, + ) + require.NoError(err) + require.Equal(1, len(cl)) }) } From 71f35aeee349c90131f74813236910cf7860251d Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 2 Oct 2025 22:55:03 +0800 Subject: [PATCH 4/4] address comment --- action/protocol/rewarding/reward.go | 10 +++++++--- action/protocol/staking/protocol.go | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/action/protocol/rewarding/reward.go b/action/protocol/rewarding/reward.go index 98ee8414ff..7ec3e9f45a 100644 --- a/action/protocol/rewarding/reward.go +++ b/action/protocol/rewarding/reward.go @@ -549,14 +549,18 @@ func (p *Protocol) slashUqd( } amount := big.NewInt(0).Mul(slashRate, big.NewInt(0).SetUint64(missed)) actLog, err := p.slashDelegate(ctx, sm, stakingProtocol, blockHeight, actionHash, candidate, amount) - if err != nil { + switch errors.Cause(err) { + case nil: + slashLogs = append(slashLogs, actLog) + totalSlashAmount.Add(totalSlashAmount, amount) + case staking.ErrNoSelfStakeBucket: + log.S().Errorf("Candidate %s doesn't have self-stake bucket, no slash", candidate.Address) + default: if err := view.Revert(snapshot); err != nil { return nil, nil, errors.Wrap(err, "failed to revert view") } return nil, nil, err } - slashLogs = append(slashLogs, actLog) - totalSlashAmount.Add(totalSlashAmount, amount) } } return totalSlashAmount, slashLogs, nil diff --git a/action/protocol/staking/protocol.go b/action/protocol/staking/protocol.go index 47de06724a..c36ba835d4 100644 --- a/action/protocol/staking/protocol.go +++ b/action/protocol/staking/protocol.go @@ -65,6 +65,7 @@ const ( var ( ErrWithdrawnBucket = errors.New("the bucket is already withdrawn") ErrEndorsementNotExist = errors.New("the endorsement does not exist") + ErrNoSelfStakeBucket = errors.New("no self-stake bucket") TotalBucketKey = append([]byte{_const}, []byte("totalBucket")...) ) @@ -404,10 +405,14 @@ func (p *Protocol) SlashCandidate( if candidate == nil { return errors.Wrapf(state.ErrStateNotExist, "candidate %s does not exist", owner.String()) } + if candidate.SelfStakeBucketIdx == candidateNoSelfStakeBucketIndex { + return errors.Wrap(ErrNoSelfStakeBucket, "failed to slash candidate") + } bucket, err := p.fetchBucket(csm, candidate.SelfStakeBucketIdx) if err != nil { return errors.Wrap(err, "failed to fetch bucket") } + prevWeightedVotes := p.calculateVoteWeight(bucket, true) if bucket.StakedAmount.Cmp(amount) < 0 { return errors.Errorf("amount %s is greater than staked amount %s", amount.String(), bucket.StakedAmount.String()) } @@ -415,7 +420,6 @@ func (p *Protocol) SlashCandidate( if err := csm.updateBucket(bucket.Index, bucket); err != nil { return errors.Wrapf(err, "failed to update bucket %d", bucket.Index) } - prevWeightedVotes := p.calculateVoteWeight(bucket, true) if err := candidate.SubVote(prevWeightedVotes); err != nil { return errors.Wrapf(err, "failed to sub candidate votes") }