github.com/iotexproject/iotex-core@v1.14.1-rc1/action/protocol/staking/handler_candidate_selfstake_test.go (about)

     1  package staking
     2  
     3  import (
     4  	"context"
     5  	"math"
     6  	"math/big"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/golang/mock/gomock"
    11  	"github.com/iotexproject/iotex-address/address"
    12  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    13  	"github.com/mohae/deepcopy"
    14  	"github.com/pkg/errors"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/iotexproject/iotex-core/action"
    18  	"github.com/iotexproject/iotex-core/action/protocol"
    19  	accountutil "github.com/iotexproject/iotex-core/action/protocol/account/util"
    20  	"github.com/iotexproject/iotex-core/blockchain/genesis"
    21  	"github.com/iotexproject/iotex-core/pkg/unit"
    22  	"github.com/iotexproject/iotex-core/test/identityset"
    23  	"github.com/iotexproject/iotex-core/testutil/testdb"
    24  )
    25  
    26  var (
    27  	timeBeforeBlockI  = time.Unix(1502044560, 0)
    28  	timeBeforeBlockII = time.Unix(1602044560, 0)
    29  	timeBlock         = time.Unix(1702044560, 0)
    30  )
    31  
    32  type (
    33  	bucketConfig struct {
    34  		Candidate       address.Address
    35  		Owner           address.Address
    36  		StakedAmountStr string
    37  		StakedDuration  uint32
    38  		AutoStake       bool
    39  		SelfStake       bool
    40  		UnstakeTime     *time.Time
    41  		EndorseExpire   uint64
    42  	}
    43  	candidateConfig struct {
    44  		Owner    address.Address
    45  		Operator address.Address
    46  		Reward   address.Address
    47  		Name     string
    48  	}
    49  	expectCandidate struct {
    50  		owner                  address.Address
    51  		candSelfStakeIndex     uint64
    52  		candSelfStakeAmountStr string
    53  		candVoteStr            string
    54  	}
    55  	expectBucket struct {
    56  		id                      uint64
    57  		candidate               address.Address
    58  		hasEndorsement          bool
    59  		endorsementExpireHeight uint64
    60  	}
    61  )
    62  
    63  func initTestState(t *testing.T, ctrl *gomock.Controller, bucketCfgs []*bucketConfig, candidateCfgs []*candidateConfig) (protocol.StateManager, *Protocol, []*VoteBucket, []*Candidate) {
    64  	return initTestStateWithHeight(t, ctrl, bucketCfgs, candidateCfgs, 0)
    65  }
    66  
    67  func initTestStateWithHeight(t *testing.T, ctrl *gomock.Controller, bucketCfgs []*bucketConfig, candidateCfgs []*candidateConfig, height uint64) (protocol.StateManager, *Protocol, []*VoteBucket, []*Candidate) {
    68  	require := require.New(t)
    69  	sm := testdb.NewMockStateManagerWithoutHeightFunc(ctrl)
    70  	sm.EXPECT().Height().Return(height, nil).AnyTimes()
    71  	csm := newCandidateStateManager(sm)
    72  	esm := NewEndorsementStateManager(sm)
    73  	_, err := sm.PutState(
    74  		&totalBucketCount{count: 0},
    75  		protocol.NamespaceOption(_stakingNameSpace),
    76  		protocol.KeyOption(TotalBucketKey),
    77  	)
    78  	require.NoError(err)
    79  
    80  	// create protocol
    81  	p, err := NewProtocol(depositGas, &BuilderConfig{
    82  		Staking:                  genesis.Default.Staking,
    83  		PersistStakingPatchBlock: math.MaxUint64,
    84  	}, nil, nil, genesis.Default.GreenlandBlockHeight)
    85  	require.NoError(err)
    86  
    87  	// set up bucket
    88  	buckets := []*VoteBucket{}
    89  	candVotesMap := make(map[string]*big.Int)
    90  	selfStakeMap := make(map[string]uint64)
    91  	for _, bktCfg := range bucketCfgs {
    92  		amount, _ := big.NewInt(0).SetString(bktCfg.StakedAmountStr, 10)
    93  		// bkt := NewVoteBucket(bktCfg.Candidate, bktCfg.Owner, amount, bktCfg.StakedDuration, time.Now(), bktCfg.AutoStake)
    94  		bkt := &VoteBucket{
    95  			Candidate:        bktCfg.Candidate,
    96  			Owner:            bktCfg.Owner,
    97  			StakedAmount:     amount,
    98  			StakedDuration:   time.Duration(bktCfg.StakedDuration) * 24 * time.Hour,
    99  			CreateTime:       timeBeforeBlockI,
   100  			StakeStartTime:   timeBeforeBlockI,
   101  			UnstakeStartTime: time.Unix(0, 0).UTC(),
   102  			AutoStake:        bktCfg.AutoStake,
   103  		}
   104  		if bktCfg.UnstakeTime != nil {
   105  			bkt.UnstakeStartTime = bktCfg.UnstakeTime.UTC()
   106  		}
   107  		_, err = csm.putBucketAndIndex(bkt)
   108  		require.NoError(err)
   109  		buckets = append(buckets, bkt)
   110  		if _, ok := candVotesMap[bkt.Candidate.String()]; !ok {
   111  			candVotesMap[bkt.Candidate.String()] = big.NewInt(0)
   112  		}
   113  		candVotesMap[bkt.Candidate.String()].Add(candVotesMap[bkt.Candidate.String()], p.calculateVoteWeight(bkt, bktCfg.SelfStake))
   114  		if bktCfg.SelfStake {
   115  			selfStakeMap[bkt.Candidate.String()] = bkt.Index
   116  		}
   117  		if bktCfg.EndorseExpire != 0 {
   118  			require.NoError(esm.Put(bkt.Index, &Endorsement{ExpireHeight: bktCfg.EndorseExpire}))
   119  		}
   120  	}
   121  
   122  	// set up candidate
   123  	candidates := []*Candidate{}
   124  	for _, candCfg := range candidateCfgs {
   125  		selfStakeAmount := big.NewInt(0)
   126  		selfStakeBucketID := uint64(candidateNoSelfStakeBucketIndex)
   127  		if _, ok := selfStakeMap[candCfg.Owner.String()]; ok {
   128  			selfStakeAmount = selfStakeAmount.SetBytes(buckets[selfStakeMap[candCfg.Owner.String()]].StakedAmount.Bytes())
   129  			selfStakeBucketID = selfStakeMap[candCfg.Owner.String()]
   130  		}
   131  		votes := big.NewInt(0)
   132  		if candVotesMap[candCfg.Owner.String()] != nil {
   133  			votes = votes.Add(votes, candVotesMap[candCfg.Owner.String()])
   134  		}
   135  		cand := &Candidate{
   136  			Owner:              candCfg.Owner,
   137  			Operator:           candCfg.Operator,
   138  			Reward:             candCfg.Reward,
   139  			Name:               candCfg.Name,
   140  			Votes:              votes,
   141  			SelfStakeBucketIdx: selfStakeBucketID,
   142  			SelfStake:          selfStakeAmount,
   143  		}
   144  		require.NoError(csm.putCandidate(cand))
   145  		candidates = append(candidates, cand)
   146  	}
   147  	cfg := deepcopy.Copy(genesis.Default).(genesis.Genesis)
   148  	cfg.TsunamiBlockHeight = 1
   149  	ctx := genesis.WithGenesisContext(context.Background(), cfg)
   150  	ctx = protocol.WithFeatureWithHeightCtx(ctx)
   151  	v, err := p.Start(ctx, sm)
   152  	require.NoError(err)
   153  	cc, ok := v.(*ViewData)
   154  	require.True(ok)
   155  	require.NoError(sm.WriteView(_protocolID, cc))
   156  
   157  	return sm, p, buckets, candidates
   158  }
   159  
   160  func TestProtocol_HandleCandidateSelfStake(t *testing.T) {
   161  	require := require.New(t)
   162  	ctrl := gomock.NewController(t)
   163  	// NOT change existed items in initBucketCfgs and initCandidateCfgs
   164  	// only append new items to the end of the list if needed
   165  	initBucketCfgs := []*bucketConfig{
   166  		{identityset.Address(1), identityset.Address(1), "1", 1, true, false, nil, 0},
   167  		{identityset.Address(1), identityset.Address(1), "1200000000000000000000000", 30, true, false, nil, 0},
   168  		{identityset.Address(1), identityset.Address(1), "1200000000000000000000000", 30, true, false, &timeBeforeBlockII, 0},
   169  		{identityset.Address(2), identityset.Address(2), "1200000000000000000000000", 30, true, true, nil, 0},
   170  		{identityset.Address(1), identityset.Address(2), "1200000000000000000000000", 30, true, false, nil, 0},
   171  		{identityset.Address(2), identityset.Address(1), "1200000000000000000000000", 30, true, false, nil, 0},
   172  		{identityset.Address(2), identityset.Address(2), "1200000000000000000000000", 30, true, true, nil, 0},
   173  		{identityset.Address(1), identityset.Address(2), "1200000000000000000000000", 91, true, false, nil, endorsementNotExpireHeight},
   174  		{identityset.Address(1), identityset.Address(2), "1200000000000000000000000", 91, true, false, nil, 1},
   175  		{identityset.Address(2), identityset.Address(2), "1200000000000000000000000", 91, true, false, nil, 0},
   176  		{identityset.Address(1), identityset.Address(1), "1200000000000000000000000", 30, true, true, nil, 0},
   177  		{identityset.Address(2), identityset.Address(1), "1200000000000000000000000", 30, true, true, nil, endorsementNotExpireHeight},
   178  	}
   179  	initCandidateCfgs := []*candidateConfig{
   180  		{identityset.Address(1), identityset.Address(7), identityset.Address(1), "test1"},
   181  		{identityset.Address(2), identityset.Address(8), identityset.Address(1), "test2"},
   182  	}
   183  	initTestStateFromIds := func(bucketCfgIdx, candCfgIds []uint64) (protocol.StateManager, *Protocol, []*VoteBucket, []*Candidate) {
   184  		bucketCfgs := []*bucketConfig{}
   185  		for _, idx := range bucketCfgIdx {
   186  			bucketCfgs = append(bucketCfgs, initBucketCfgs[idx])
   187  		}
   188  		candCfgs := []*candidateConfig{}
   189  		for _, idx := range candCfgIds {
   190  			candCfgs = append(candCfgs, initCandidateCfgs[idx])
   191  		}
   192  		return initTestState(t, ctrl, bucketCfgs, candCfgs)
   193  	}
   194  	sm, p, _, _ := initTestState(t, ctrl, initBucketCfgs, initCandidateCfgs)
   195  
   196  	tests := []struct {
   197  		name string
   198  		// params
   199  		initBucketCfgIds    []uint64
   200  		initCandidateCfgIds []uint64
   201  		initBalance         int64
   202  		caller              address.Address
   203  		nonce               uint64
   204  		gasLimit            uint64
   205  		blkGasLimit         uint64
   206  		gasPrice            *big.Int
   207  		bucketID            uint64
   208  		newProtocol         bool
   209  		// expect
   210  		err              error
   211  		status           iotextypes.ReceiptStatus
   212  		expectCandidates []expectCandidate
   213  		expectBuckets    []expectBucket
   214  	}{
   215  		{
   216  			"selfstake for unstaked candidate",
   217  			[]uint64{0, 1},
   218  			[]uint64{0, 1},
   219  			1300000,
   220  			identityset.Address(1),
   221  			1,
   222  			uint64(1000000),
   223  			uint64(1000000),
   224  			big.NewInt(1000),
   225  			1,
   226  			true,
   227  			nil,
   228  			iotextypes.ReceiptStatus_Success,
   229  			[]expectCandidate{
   230  				{identityset.Address(1), 1, "1200000000000000000000000", "1469480667073232815766915"},
   231  			},
   232  			nil,
   233  		},
   234  		{
   235  			"selfstake bucket amount is unsufficient",
   236  			[]uint64{0, 1},
   237  			[]uint64{0, 1},
   238  			1300000,
   239  			identityset.Address(1),
   240  			1,
   241  			uint64(1000000),
   242  			uint64(1000000),
   243  			big.NewInt(1000),
   244  			0,
   245  			true,
   246  			nil,
   247  			iotextypes.ReceiptStatus_ErrInvalidBucketAmount,
   248  			nil,
   249  			nil,
   250  		},
   251  		{
   252  			"selfstake bucket is unstaked",
   253  			[]uint64{0, 2},
   254  			[]uint64{0, 1},
   255  			1300000,
   256  			identityset.Address(1),
   257  			1,
   258  			uint64(1000000),
   259  			uint64(1000000),
   260  			big.NewInt(1000),
   261  			1,
   262  			true,
   263  			nil,
   264  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   265  			nil,
   266  			nil,
   267  		},
   268  		{
   269  			"bucket is already selfstaked",
   270  			[]uint64{0, 10},
   271  			[]uint64{0, 1},
   272  			1300000,
   273  			identityset.Address(1),
   274  			1,
   275  			uint64(1000000),
   276  			uint64(1000000),
   277  			big.NewInt(1000),
   278  			1,
   279  			true,
   280  			nil,
   281  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   282  			nil,
   283  			nil,
   284  		},
   285  		{
   286  			"other candidate's bucket is unauthorized",
   287  			[]uint64{0, 4},
   288  			[]uint64{0, 1},
   289  			1300000,
   290  			identityset.Address(1),
   291  			1,
   292  			uint64(1000000),
   293  			uint64(1000000),
   294  			big.NewInt(1000),
   295  			1,
   296  			true,
   297  			nil,
   298  			iotextypes.ReceiptStatus_ErrUnauthorizedOperator,
   299  			nil,
   300  			nil,
   301  		},
   302  		{
   303  			"bucket has been voted to other candidate",
   304  			[]uint64{0, 5, 6},
   305  			[]uint64{0, 1},
   306  			1300000,
   307  			identityset.Address(1),
   308  			1,
   309  			uint64(1000000),
   310  			uint64(1000000),
   311  			big.NewInt(1000),
   312  			1,
   313  			true,
   314  			nil,
   315  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   316  			nil,
   317  			nil,
   318  		},
   319  		{
   320  			"bucket is endorsed to candidate",
   321  			[]uint64{0, 7},
   322  			[]uint64{0},
   323  			1300000,
   324  			identityset.Address(1),
   325  			1,
   326  			uint64(1000000),
   327  			uint64(1000000),
   328  			big.NewInt(1000),
   329  			1,
   330  			true,
   331  			nil,
   332  			iotextypes.ReceiptStatus_Success,
   333  			[]expectCandidate{
   334  				{identityset.Address(1), 1, "1200000000000000000000000", "1635067133824581908640995"},
   335  			},
   336  			[]expectBucket{
   337  				{1, identityset.Address(1), false, 0},
   338  			},
   339  		},
   340  		{
   341  			"bucket endorsement is expired",
   342  			[]uint64{0, 8},
   343  			[]uint64{0},
   344  			1300000,
   345  			identityset.Address(1),
   346  			1,
   347  			uint64(1000000),
   348  			uint64(1000000),
   349  			big.NewInt(1000),
   350  			1,
   351  			true,
   352  			nil,
   353  			iotextypes.ReceiptStatus_ErrUnauthorizedOperator,
   354  			nil,
   355  			nil,
   356  		},
   357  		{
   358  			"candidate has already been selfstaked",
   359  			[]uint64{3, 9},
   360  			[]uint64{1},
   361  			1300000,
   362  			identityset.Address(2),
   363  			1,
   364  			uint64(1000000),
   365  			uint64(1000000),
   366  			big.NewInt(1000),
   367  			1,
   368  			true,
   369  			nil,
   370  			iotextypes.ReceiptStatus_Success,
   371  			[]expectCandidate{
   372  				{identityset.Address(2), 1, "1200000000000000000000000", "3104547800897814724407908"},
   373  			},
   374  			[]expectBucket{
   375  				{1, identityset.Address(2), false, 0},
   376  			},
   377  		},
   378  		{
   379  			"bucket is already selfstaked by endorsement",
   380  			[]uint64{0, 11},
   381  			[]uint64{0, 1},
   382  			1300000,
   383  			identityset.Address(2),
   384  			1,
   385  			uint64(1000000),
   386  			uint64(1000000),
   387  			big.NewInt(1000),
   388  			1,
   389  			true,
   390  			nil,
   391  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   392  			nil,
   393  			nil,
   394  		},
   395  		{
   396  			"bucket has no endorsement",
   397  			[]uint64{0, 5},
   398  			[]uint64{0, 1},
   399  			1300000,
   400  			identityset.Address(2),
   401  			1,
   402  			uint64(1000000),
   403  			uint64(1000000),
   404  			big.NewInt(1000),
   405  			1,
   406  			true,
   407  			nil,
   408  			iotextypes.ReceiptStatus_ErrUnauthorizedOperator,
   409  			nil,
   410  			nil,
   411  		},
   412  	}
   413  
   414  	for _, test := range tests {
   415  		t.Run(test.name, func(t *testing.T) {
   416  			nonce := test.nonce
   417  			if test.newProtocol {
   418  				sm, p, _, _ = initTestStateFromIds(test.initBucketCfgIds, test.initCandidateCfgIds)
   419  			}
   420  			require.NoError(setupAccount(sm, test.caller, test.initBalance))
   421  			act := action.NewCandidateActivate(nonce, test.gasLimit, test.gasPrice, test.bucketID)
   422  			IntrinsicGas, _ := act.IntrinsicGas()
   423  			ctx := protocol.WithActionCtx(context.Background(), protocol.ActionCtx{
   424  				Caller:       test.caller,
   425  				GasPrice:     test.gasPrice,
   426  				IntrinsicGas: IntrinsicGas,
   427  				Nonce:        nonce,
   428  			})
   429  			ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{
   430  				BlockHeight:    1,
   431  				BlockTimeStamp: timeBlock,
   432  				GasLimit:       test.blkGasLimit,
   433  			})
   434  			cfg := deepcopy.Copy(genesis.Default).(genesis.Genesis)
   435  			cfg.TsunamiBlockHeight = 1
   436  			ctx = genesis.WithGenesisContext(ctx, cfg)
   437  			ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx))
   438  			require.Equal(test.err, errors.Cause(p.Validate(ctx, act, sm)))
   439  			if test.err != nil {
   440  				return
   441  			}
   442  			r, err := p.Handle(ctx, act, sm)
   443  			require.NoError(err)
   444  			if r != nil {
   445  				require.Equal(uint64(test.status), r.Status)
   446  			} else {
   447  				require.Equal(test.status, iotextypes.ReceiptStatus_Failure)
   448  			}
   449  
   450  			if test.err == nil && test.status == iotextypes.ReceiptStatus_Success {
   451  				// check candidate
   452  				csm, err := NewCandidateStateManager(sm, false)
   453  				require.NoError(err)
   454  				for _, expectCand := range test.expectCandidates {
   455  					candidate := csm.GetByOwner(expectCand.owner)
   456  					require.NotNil(candidate)
   457  					require.Equal(expectCand.candSelfStakeIndex, candidate.SelfStakeBucketIdx)
   458  					require.Equal(expectCand.candSelfStakeAmountStr, candidate.SelfStake.String())
   459  					require.Equal(expectCand.candVoteStr, candidate.Votes.String())
   460  				}
   461  				// check buckets
   462  				for _, expectBkt := range test.expectBuckets {
   463  					bkt, err := csm.getBucket(expectBkt.id)
   464  					require.NoError(err)
   465  					require.Equal(expectBkt.candidate, bkt.Candidate)
   466  				}
   467  
   468  				// test staker's account
   469  				caller, err := accountutil.LoadAccount(sm, test.caller)
   470  				require.NoError(err)
   471  				actCost, err := act.Cost()
   472  				require.NoError(err)
   473  				total := big.NewInt(0)
   474  				require.Equal(unit.ConvertIotxToRau(test.initBalance), total.Add(total, caller.Balance).Add(total, actCost))
   475  				require.Equal(nonce+1, caller.PendingNonce())
   476  			}
   477  		})
   478  	}
   479  }