Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions action/protocol/rewarding/reward.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions action/protocol/staking/bucket_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 4 additions & 4 deletions action/protocol/staking/bucket_pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions action/protocol/staking/candidate_statemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion action/protocol/staking/handler_stake_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion action/protocol/staking/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion action/protocol/staking/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")...)
)

Expand Down Expand Up @@ -404,6 +405,9 @@ 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the slashed candidate may be already unstaked

return errors.Wrap(err, "failed to fetch bucket")
Expand All @@ -429,7 +433,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
Expand Down
125 changes: 125 additions & 0 deletions action/protocol/staking/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,128 @@ 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(1000),
},
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)
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))
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")
})

t.Run("success", func(t *testing.T) {
amount := big.NewInt(400)
remaining := bucket.StakedAmount.Sub(bucket.StakedAmount, amount)
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(
ctx,
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())
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))
})
}