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

     1  package staking
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/big"
     7  	"testing"
     8  
     9  	"github.com/golang/mock/gomock"
    10  	"github.com/iotexproject/iotex-address/address"
    11  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    12  	"github.com/mohae/deepcopy"
    13  	"github.com/pkg/errors"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/iotexproject/iotex-core/action"
    17  	"github.com/iotexproject/iotex-core/action/protocol"
    18  	accountutil "github.com/iotexproject/iotex-core/action/protocol/account/util"
    19  	"github.com/iotexproject/iotex-core/blockchain/genesis"
    20  	"github.com/iotexproject/iotex-core/pkg/unit"
    21  	"github.com/iotexproject/iotex-core/state"
    22  	"github.com/iotexproject/iotex-core/test/identityset"
    23  )
    24  
    25  type appendAction struct {
    26  	act       func() action.Action
    27  	status    iotextypes.ReceiptStatus
    28  	validator func(t *testing.T)
    29  }
    30  
    31  func TestProtocol_HandleCandidateEndorsement(t *testing.T) {
    32  	require := require.New(t)
    33  	ctrl := gomock.NewController(t)
    34  	initBucketCfgs := []*bucketConfig{
    35  		{identityset.Address(1), identityset.Address(1), "1", 1, true, false, nil, 0},
    36  		{identityset.Address(1), identityset.Address(1), "1200000000000000000000000", 30, true, false, nil, 0},
    37  		{identityset.Address(1), identityset.Address(1), "1200000000000000000000000", 30, true, false, &timeBeforeBlockII, 0},
    38  		{identityset.Address(2), identityset.Address(1), "1200000000000000000000000", 30, true, true, nil, endorsementNotExpireHeight},
    39  		{identityset.Address(1), identityset.Address(2), "1200000000000000000000000", 30, true, false, nil, 0},
    40  		{identityset.Address(2), identityset.Address(1), "1200000000000000000000000", 30, true, false, nil, 0},
    41  		{identityset.Address(2), identityset.Address(2), "1200000000000000000000000", 30, true, true, nil, 0},
    42  		{identityset.Address(1), identityset.Address(2), "1200000000000000000000000", 91, true, false, nil, endorsementNotExpireHeight},
    43  		{identityset.Address(1), identityset.Address(2), "1200000000000000000000000", 91, true, false, nil, 1},
    44  		{identityset.Address(2), identityset.Address(2), "1200000000000000000000000", 91, true, false, nil, 0},
    45  		{identityset.Address(1), identityset.Address(1), "1200000000000000000000000", 30, false, false, nil, 0},
    46  		{identityset.Address(2), identityset.Address(1), "1200000000000000000000000", 30, true, true, nil, endorsementNotExpireHeight},
    47  		{identityset.Address(2), identityset.Address(1), "1200000000000000000000000", 30, true, true, nil, 10},
    48  		{identityset.Address(2), identityset.Address(1), "1200000000000000000000000", 30, true, true, nil, 1},
    49  	}
    50  	initCandidateCfgs := []*candidateConfig{
    51  		{identityset.Address(1), identityset.Address(7), identityset.Address(1), "test1"},
    52  		{identityset.Address(2), identityset.Address(8), identityset.Address(1), "test2"},
    53  		{identityset.Address(3), identityset.Address(9), identityset.Address(11), "test3"},
    54  	}
    55  	initTestStateFromIds := func(bucketCfgIdx, candCfgIds []uint64) (protocol.StateManager, *Protocol, []*VoteBucket, []*Candidate) {
    56  		bucketCfgs := []*bucketConfig{}
    57  		for _, idx := range bucketCfgIdx {
    58  			bucketCfgs = append(bucketCfgs, initBucketCfgs[idx])
    59  		}
    60  		candCfgs := []*candidateConfig{}
    61  		for _, idx := range candCfgIds {
    62  			candCfgs = append(candCfgs, initCandidateCfgs[idx])
    63  		}
    64  		return initTestState(t, ctrl, bucketCfgs, candCfgs)
    65  	}
    66  	sm, p, _, _ := initTestState(t, ctrl, initBucketCfgs, initCandidateCfgs)
    67  
    68  	tests := []struct {
    69  		name string
    70  		// params
    71  		initBucketCfgIds    []uint64
    72  		initCandidateCfgIds []uint64
    73  		initBalance         int64
    74  		caller              address.Address
    75  		nonce               uint64
    76  		gasLimit            uint64
    77  		blkGasLimit         uint64
    78  		gasPrice            *big.Int
    79  		bucketID            uint64
    80  		endorse             bool
    81  		newProtocol         bool
    82  		append              *appendAction
    83  		// expect
    84  		err              error
    85  		status           iotextypes.ReceiptStatus
    86  		expectCandidates []expectCandidate
    87  		expectBuckets    []expectBucket
    88  	}{
    89  		{
    90  			"endorse candidate with invalid bucket index",
    91  			[]uint64{0, 1},
    92  			[]uint64{0, 1},
    93  			1300000,
    94  			identityset.Address(1),
    95  			1,
    96  			uint64(1000000),
    97  			uint64(1000000),
    98  			big.NewInt(1000),
    99  			2,
   100  			true,
   101  			true,
   102  			nil,
   103  			nil,
   104  			iotextypes.ReceiptStatus_ErrInvalidBucketIndex,
   105  			[]expectCandidate{},
   106  			nil,
   107  		},
   108  		{
   109  			"endorse candidate with invalid bucket owner",
   110  			[]uint64{0, 1},
   111  			[]uint64{0, 1},
   112  			1300000,
   113  			identityset.Address(2),
   114  			1,
   115  			uint64(1000000),
   116  			uint64(1000000),
   117  			big.NewInt(1000),
   118  			1,
   119  			true,
   120  			true,
   121  			nil,
   122  			nil,
   123  			iotextypes.ReceiptStatus_ErrUnauthorizedOperator,
   124  			[]expectCandidate{},
   125  			nil,
   126  		},
   127  		{
   128  			"endorse candidate with invalid bucket amount",
   129  			[]uint64{0, 1},
   130  			[]uint64{0, 1},
   131  			1000,
   132  			identityset.Address(1),
   133  			1,
   134  			uint64(1000000),
   135  			uint64(1000000),
   136  			big.NewInt(1000),
   137  			0,
   138  			true,
   139  			true,
   140  			nil,
   141  			nil,
   142  			iotextypes.ReceiptStatus_ErrInvalidBucketAmount,
   143  			[]expectCandidate{},
   144  			nil,
   145  		},
   146  		{
   147  			"endorse candidate with self-staked bucket",
   148  			[]uint64{0, 3},
   149  			[]uint64{0, 1},
   150  			1300000,
   151  			identityset.Address(1),
   152  			1,
   153  			uint64(1000000),
   154  			uint64(1000000),
   155  			big.NewInt(1000),
   156  			1,
   157  			true,
   158  			true,
   159  			nil,
   160  			nil,
   161  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   162  			[]expectCandidate{},
   163  			nil,
   164  		},
   165  		{
   166  			"endorse candidate with invalid bucket candidate",
   167  			[]uint64{0, 4},
   168  			[]uint64{0, 1},
   169  			1300000,
   170  			identityset.Address(1),
   171  			1,
   172  			uint64(1000000),
   173  			uint64(1000000),
   174  			big.NewInt(1000),
   175  			1,
   176  			true,
   177  			true,
   178  			nil,
   179  			nil,
   180  			iotextypes.ReceiptStatus_ErrUnauthorizedOperator,
   181  			[]expectCandidate{},
   182  			nil,
   183  		},
   184  		{
   185  			"endorse candidate with endorsed bucket",
   186  			[]uint64{0, 7},
   187  			[]uint64{0, 1},
   188  			1300000,
   189  			identityset.Address(2),
   190  			1,
   191  			uint64(1000000),
   192  			uint64(1000000),
   193  			big.NewInt(1000),
   194  			1,
   195  			true,
   196  			true,
   197  			nil,
   198  			nil,
   199  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   200  			[]expectCandidate{},
   201  			nil,
   202  		},
   203  		{
   204  			"endorse candidate with unstaked bucket",
   205  			[]uint64{0, 2},
   206  			[]uint64{0, 1},
   207  			1300000,
   208  			identityset.Address(1),
   209  			1,
   210  			uint64(1000000),
   211  			uint64(1000000),
   212  			big.NewInt(1000),
   213  			1,
   214  			true,
   215  			true,
   216  			nil,
   217  			nil,
   218  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   219  			[]expectCandidate{},
   220  			nil,
   221  		},
   222  		{
   223  			"endorse candidate with expired endorsement",
   224  			[]uint64{0, 8},
   225  			[]uint64{0, 1},
   226  			1300000,
   227  			identityset.Address(2),
   228  			1,
   229  			uint64(1000000),
   230  			uint64(1000000),
   231  			big.NewInt(1000),
   232  			1,
   233  			true,
   234  			true,
   235  			nil,
   236  			nil,
   237  			iotextypes.ReceiptStatus_Success,
   238  			[]expectCandidate{
   239  				{identityset.Address(1), candidateNoSelfStakeBucketIndex, "0", "1542516163985454635820817"},
   240  			},
   241  			[]expectBucket{
   242  				{0, identityset.Address(1), false, 0},
   243  				{1, identityset.Address(1), true, endorsementNotExpireHeight},
   244  			},
   245  		},
   246  		{
   247  			"endorse candidate with valid bucket",
   248  			[]uint64{0, 1},
   249  			[]uint64{0, 1},
   250  			1300000,
   251  			identityset.Address(1),
   252  			1,
   253  			uint64(1000000),
   254  			uint64(1000000),
   255  			big.NewInt(1000),
   256  			1,
   257  			true,
   258  			true,
   259  			nil,
   260  			nil,
   261  			iotextypes.ReceiptStatus_Success,
   262  			[]expectCandidate{},
   263  			nil,
   264  		},
   265  		{
   266  			"once endorsed, bucket cannot be unstaked",
   267  			[]uint64{0, 10},
   268  			[]uint64{0, 1},
   269  			1300000,
   270  			identityset.Address(1),
   271  			1,
   272  			uint64(1000000),
   273  			uint64(1000000),
   274  			big.NewInt(1000),
   275  			1,
   276  			true,
   277  			true,
   278  			&appendAction{
   279  				func() action.Action {
   280  					act, err := action.NewUnstake(0, 1, []byte{}, uint64(1000000), big.NewInt(1000))
   281  					require.NoError(err)
   282  					return act
   283  				},
   284  				iotextypes.ReceiptStatus_ErrInvalidBucketType,
   285  				nil,
   286  			},
   287  			nil,
   288  			iotextypes.ReceiptStatus_Success,
   289  			[]expectCandidate{},
   290  			nil,
   291  		},
   292  		{
   293  			"once endorsed, bucket cannot be change candidate",
   294  			[]uint64{0, 10},
   295  			[]uint64{0, 1, 2},
   296  			1300000,
   297  			identityset.Address(1),
   298  			1,
   299  			uint64(1000000),
   300  			uint64(1000000),
   301  			big.NewInt(1000),
   302  			1,
   303  			true,
   304  			true,
   305  			&appendAction{
   306  				func() action.Action {
   307  					act, err := action.NewChangeCandidate(0, "test3", 1, []byte{}, uint64(1000000), big.NewInt(1000))
   308  					require.NoError(err)
   309  					return act
   310  				},
   311  				iotextypes.ReceiptStatus_ErrInvalidBucketType, //todo fix
   312  				nil,
   313  			},
   314  			nil,
   315  			iotextypes.ReceiptStatus_Success,
   316  			[]expectCandidate{},
   317  			nil,
   318  		},
   319  		{
   320  			"unendorse a valid bucket",
   321  			[]uint64{0, 9},
   322  			[]uint64{0, 1},
   323  			1300000,
   324  			identityset.Address(2),
   325  			1,
   326  			uint64(1000000),
   327  			uint64(1000000),
   328  			big.NewInt(1000),
   329  			1,
   330  			true,
   331  			true,
   332  			&appendAction{
   333  				func() action.Action {
   334  					act := action.NewCandidateEndorsement(0, uint64(1000000), big.NewInt(1000), 1, false)
   335  					return act
   336  				},
   337  				iotextypes.ReceiptStatus_Success,
   338  				func(t *testing.T) {
   339  					csm, err := NewCandidateStateManager(sm, false)
   340  					require.NoError(err)
   341  					esm := NewEndorsementStateManager(csm.SM())
   342  					bucket, err := csm.getBucket(1)
   343  					require.NoError(err)
   344  					endorsement, err := esm.Get(bucket.Index)
   345  					require.NoError(err)
   346  					require.NotNil(endorsement)
   347  					require.Equal(uint64(1), endorsement.ExpireHeight)
   348  				},
   349  			},
   350  			nil,
   351  			iotextypes.ReceiptStatus_Success,
   352  			[]expectCandidate{},
   353  			nil,
   354  		},
   355  		{
   356  			"unendorse a self-staked bucket",
   357  			[]uint64{0, 10},
   358  			[]uint64{0, 1},
   359  			1300000,
   360  			identityset.Address(1),
   361  			1,
   362  			uint64(1000000),
   363  			uint64(1000000),
   364  			big.NewInt(1000),
   365  			1,
   366  			true,
   367  			true,
   368  			&appendAction{
   369  				func() action.Action {
   370  					act := action.NewCandidateEndorsement(0, uint64(1000000), big.NewInt(1000), 1, false)
   371  					return act
   372  				},
   373  				iotextypes.ReceiptStatus_Success,
   374  				func(t *testing.T) {
   375  					csm, err := NewCandidateStateManager(sm, false)
   376  					require.NoError(err)
   377  					esm := NewEndorsementStateManager(csm.SM())
   378  					bucket, err := csm.getBucket(1)
   379  					require.NoError(err)
   380  					endorsement, err := esm.Get(bucket.Index)
   381  					require.NoError(err)
   382  					require.NotNil(endorsement)
   383  					require.Equal(uint64(1), endorsement.ExpireHeight)
   384  				},
   385  			},
   386  			nil,
   387  			iotextypes.ReceiptStatus_Success,
   388  			[]expectCandidate{},
   389  			nil,
   390  		},
   391  		{
   392  			"endorsement withdraw if bucket is self-staked but without endorsement",
   393  			[]uint64{6},
   394  			[]uint64{1},
   395  			1300000,
   396  			identityset.Address(2),
   397  			1,
   398  			uint64(1000000),
   399  			uint64(1000000),
   400  			big.NewInt(1000),
   401  			0,
   402  			false,
   403  			true,
   404  			nil,
   405  			nil,
   406  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   407  			[]expectCandidate{},
   408  			nil,
   409  		},
   410  		{
   411  			"endorsement withdraw if bucket is self-staked by endorsement",
   412  			[]uint64{11},
   413  			[]uint64{0, 1},
   414  			1300000,
   415  			identityset.Address(1),
   416  			1,
   417  			uint64(1000000),
   418  			uint64(1000000),
   419  			big.NewInt(1000),
   420  			0,
   421  			false,
   422  			true,
   423  			nil,
   424  			nil,
   425  			iotextypes.ReceiptStatus_Success,
   426  			[]expectCandidate{
   427  				{identityset.Address(2), 0, "1200000000000000000000000", "1469480667073232815766914"},
   428  			},
   429  			[]expectBucket{
   430  				{0, identityset.Address(2), true, 17281},
   431  			},
   432  		},
   433  		{
   434  			"endorsement withdraw if bucket is endorsement withdrawing",
   435  			[]uint64{12},
   436  			[]uint64{0, 1},
   437  			1300000,
   438  			identityset.Address(1),
   439  			1,
   440  			uint64(1000000),
   441  			uint64(1000000),
   442  			big.NewInt(1000),
   443  			0,
   444  			false,
   445  			true,
   446  			nil,
   447  			nil,
   448  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   449  			nil,
   450  			nil,
   451  		},
   452  		{
   453  			"endorsement withdraw if bucket is endorsement expired",
   454  			[]uint64{13},
   455  			[]uint64{0, 1},
   456  			1300000,
   457  			identityset.Address(1),
   458  			1,
   459  			uint64(1000000),
   460  			uint64(1000000),
   461  			big.NewInt(1000),
   462  			0,
   463  			false,
   464  			true,
   465  			nil,
   466  			nil,
   467  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   468  			nil,
   469  			nil,
   470  		},
   471  		{
   472  			"endorsement withdraw if bucket without endorsement",
   473  			[]uint64{0},
   474  			[]uint64{0, 1},
   475  			1300000,
   476  			identityset.Address(1),
   477  			1,
   478  			uint64(1000000),
   479  			uint64(1000000),
   480  			big.NewInt(1000),
   481  			0,
   482  			false,
   483  			true,
   484  			nil,
   485  			nil,
   486  			iotextypes.ReceiptStatus_ErrInvalidBucketType,
   487  			nil,
   488  			nil,
   489  		},
   490  	}
   491  
   492  	for _, test := range tests {
   493  		t.Run(test.name, func(t *testing.T) {
   494  			nonce := test.nonce
   495  			if test.newProtocol {
   496  				sm, p, _, _ = initTestStateFromIds(test.initBucketCfgIds, test.initCandidateCfgIds)
   497  			}
   498  			require.NoError(setupAccount(sm, test.caller, test.initBalance))
   499  			act := action.NewCandidateEndorsement(nonce, test.gasLimit, test.gasPrice, test.bucketID, test.endorse)
   500  			IntrinsicGas, _ := act.IntrinsicGas()
   501  			ctx := protocol.WithActionCtx(context.Background(), protocol.ActionCtx{
   502  				Caller:       test.caller,
   503  				GasPrice:     test.gasPrice,
   504  				IntrinsicGas: IntrinsicGas,
   505  				Nonce:        nonce,
   506  			})
   507  			ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{
   508  				BlockHeight:    1,
   509  				BlockTimeStamp: timeBlock,
   510  				GasLimit:       test.blkGasLimit,
   511  			})
   512  			cfg := deepcopy.Copy(genesis.Default).(genesis.Genesis)
   513  			cfg.TsunamiBlockHeight = 1
   514  			ctx = genesis.WithGenesisContext(ctx, cfg)
   515  			ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx))
   516  			require.Equal(test.err, errors.Cause(p.Validate(ctx, act, sm)))
   517  			if test.err != nil {
   518  				return
   519  			}
   520  			r, err := p.Handle(ctx, act, sm)
   521  			require.NoError(err)
   522  			if r != nil {
   523  				require.Equal(uint64(test.status), r.Status)
   524  			} else {
   525  				require.Equal(test.status, iotextypes.ReceiptStatus_Failure)
   526  			}
   527  			var appendIntrinsicGas uint64
   528  			if test.append != nil {
   529  				nonce = nonce + 1
   530  				appendIntrinsicGas, _ = act.IntrinsicGas()
   531  				ctx := protocol.WithActionCtx(context.Background(), protocol.ActionCtx{
   532  					Caller:       test.caller,
   533  					GasPrice:     test.gasPrice,
   534  					IntrinsicGas: IntrinsicGas,
   535  					Nonce:        nonce,
   536  				})
   537  				ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{
   538  					BlockHeight:    1,
   539  					BlockTimeStamp: timeBlock,
   540  					GasLimit:       test.blkGasLimit,
   541  				})
   542  				ctx = genesis.WithGenesisContext(ctx, cfg)
   543  				ctx = protocol.WithFeatureCtx(protocol.WithFeatureWithHeightCtx(ctx))
   544  				r, err = p.Handle(ctx, test.append.act(), sm)
   545  				require.NoError(err)
   546  				if r != nil {
   547  					require.Equal(uint64(test.append.status), r.Status, fmt.Sprintf("except :%d, actual:%d", test.append.status, r.Status))
   548  					if test.append.validator != nil {
   549  						test.append.validator(t)
   550  					}
   551  				} else {
   552  					require.Equal(test.status, iotextypes.ReceiptStatus_Failure)
   553  				}
   554  			}
   555  
   556  			if test.err == nil && test.status == iotextypes.ReceiptStatus_Success {
   557  				// check candidate
   558  				csm, err := NewCandidateStateManager(sm, false)
   559  				require.NoError(err)
   560  				for _, expectCand := range test.expectCandidates {
   561  					candidate := csm.GetByOwner(expectCand.owner)
   562  					require.NotNil(candidate)
   563  					require.Equal(expectCand.candSelfStakeIndex, candidate.SelfStakeBucketIdx)
   564  					require.Equal(expectCand.candSelfStakeAmountStr, candidate.SelfStake.String())
   565  					require.Equal(expectCand.candVoteStr, candidate.Votes.String())
   566  				}
   567  				// check buckets
   568  				esm := NewEndorsementStateManager(csm.SM())
   569  				for _, expectBkt := range test.expectBuckets {
   570  					bkt, err := csm.getBucket(expectBkt.id)
   571  					require.NoError(err)
   572  					require.Equal(expectBkt.candidate, bkt.Candidate)
   573  					endorse, err := esm.Get(expectBkt.id)
   574  					if expectBkt.hasEndorsement {
   575  						require.NoError(err)
   576  						require.EqualValues(expectBkt.endorsementExpireHeight, endorse.ExpireHeight)
   577  					} else {
   578  						require.ErrorIs(err, state.ErrStateNotExist)
   579  					}
   580  				}
   581  
   582  				// test staker's account
   583  				caller, err := accountutil.LoadAccount(sm, test.caller)
   584  				require.NoError(err)
   585  				actCost, err := act.Cost()
   586  				actCost.Add(actCost, big.NewInt(0).Mul(test.gasPrice, big.NewInt(0).SetUint64(appendIntrinsicGas)))
   587  				require.NoError(err)
   588  				total := big.NewInt(0)
   589  				require.Equal(unit.ConvertIotxToRau(test.initBalance), total.Add(total, caller.Balance).Add(total, actCost))
   590  				require.Equal(nonce+1, caller.PendingNonce())
   591  			}
   592  		})
   593  	}
   594  }