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 }