github.com/iotexproject/iotex-core@v1.14.1-rc1/action/protocol/staking/staking_statereader_test.go (about) 1 // Copyright (c) 2023 IoTeX Foundation 2 // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability 3 // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. 4 // This source code is governed by Apache License 2.0 that can be found in the LICENSE file. 5 6 package staking 7 8 import ( 9 "context" 10 "math/big" 11 "testing" 12 "time" 13 14 "github.com/golang/mock/gomock" 15 "github.com/stretchr/testify/require" 16 "golang.org/x/exp/slices" 17 18 "github.com/iotexproject/iotex-address/address" 19 "github.com/iotexproject/iotex-proto/golang/iotexapi" 20 21 "github.com/iotexproject/iotex-core/action/protocol" 22 "github.com/iotexproject/iotex-core/action/protocol/rolldpos" 23 "github.com/iotexproject/iotex-core/blockchain/genesis" 24 "github.com/iotexproject/iotex-core/pkg/util/byteutil" 25 "github.com/iotexproject/iotex-core/state" 26 "github.com/iotexproject/iotex-core/test/identityset" 27 "github.com/iotexproject/iotex-core/test/mock/mock_factory" 28 ) 29 30 func TestStakingStateReader(t *testing.T) { 31 r := require.New(t) 32 contractAddress := "io10a298zmzvrt4guq79a9f4x7qedj59y7ery84he" 33 testCandidates := CandidateList{ 34 { 35 Owner: identityset.Address(1), 36 Operator: identityset.Address(1), 37 Reward: identityset.Address(1), 38 Name: "cand1", 39 Votes: big.NewInt(10), 40 SelfStakeBucketIdx: 0, 41 SelfStake: big.NewInt(10), 42 }, 43 { 44 Owner: identityset.Address(2), 45 Operator: identityset.Address(2), 46 Reward: identityset.Address(2), 47 Name: "cand2", 48 Votes: big.NewInt(0), 49 SelfStakeBucketIdx: 0, 50 SelfStake: big.NewInt(0), 51 }, 52 } 53 testContractBuckets := []*VoteBucket{ 54 { 55 Index: 1, 56 Candidate: identityset.Address(1), 57 Owner: identityset.Address(1), 58 StakedAmount: big.NewInt(100), 59 StakedDuration: time.Hour * 24, 60 CreateTime: time.Now(), 61 StakeStartTime: time.Now(), 62 AutoStake: true, 63 ContractAddress: contractAddress, 64 }, 65 { 66 Index: 2, 67 Candidate: identityset.Address(2), 68 Owner: identityset.Address(2), 69 StakedAmount: big.NewInt(100), 70 StakedDuration: time.Hour * 24, 71 CreateTime: time.Now(), 72 StakeStartTime: time.Now(), 73 AutoStake: true, 74 ContractAddress: contractAddress, 75 }, 76 } 77 testNativeBuckets := []*VoteBucket{ 78 { 79 Index: 1, 80 Candidate: identityset.Address(1), 81 Owner: identityset.Address(1), 82 StakedAmount: big.NewInt(10), 83 StakedDuration: time.Hour * 24, 84 CreateTime: time.Now(), 85 StakeStartTime: time.Now(), 86 AutoStake: true, 87 }, 88 } 89 testNativeTotalAmount := &totalAmount{ 90 amount: big.NewInt(0), 91 } 92 for _, b := range testNativeBuckets { 93 testNativeTotalAmount.amount.Add(testNativeTotalAmount.amount, b.StakedAmount) 94 testNativeTotalAmount.count++ 95 } 96 var err error 97 states := make([][]byte, len(testNativeBuckets)) 98 for i := range states { 99 states[i], err = state.Serialize(testNativeBuckets[i]) 100 r.NoError(err) 101 } 102 prepare := func(t *testing.T) (*mock_factory.MockFactory, *MockContractStakingIndexer, ReadState, context.Context, *require.Assertions) { 103 r := require.New(t) 104 ctrl := gomock.NewController(t) 105 sf := mock_factory.NewMockFactory(ctrl) 106 sf.EXPECT().Height().Return(uint64(1), nil).AnyTimes() 107 candCenter, err := NewCandidateCenter(testCandidates) 108 r.NoError(err) 109 testNativeData := &ViewData{ 110 candCenter: candCenter, 111 bucketPool: &BucketPool{ 112 total: &totalAmount{ 113 amount: big.NewInt(100), 114 count: 1, 115 }, 116 }, 117 } 118 states := make([][]byte, len(testNativeBuckets)) 119 for i := range states { 120 states[i], err = state.Serialize(testNativeBuckets[i]) 121 r.NoError(err) 122 } 123 sf.EXPECT().ReadView(gomock.Any()).Return(testNativeData, nil).Times(1) 124 125 contractIndexer := NewMockContractStakingIndexer(ctrl) 126 contractIndexer.EXPECT().Buckets(gomock.Any()).Return(testContractBuckets, nil).AnyTimes() 127 128 stakeSR, err := newCompositeStakingStateReader(contractIndexer, nil, sf) 129 r.NoError(err) 130 r.NotNil(stakeSR) 131 132 reg := protocol.NewRegistry() 133 rolldposProto := rolldpos.NewProtocol(10, 10, 10) 134 rolldposProto.Register(reg) 135 g := genesis.Default 136 g.QuebecBlockHeight = 1 137 ctx := genesis.WithGenesisContext(context.Background(), g) 138 ctx = protocol.WithRegistry(ctx, reg) 139 ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{BlockHeight: 1000}) 140 ctx = protocol.WithFeatureCtx(ctx) 141 142 return sf, contractIndexer, stakeSR, ctx, r 143 } 144 addContractVotes := func(expectCand *Candidate) { 145 for _, b := range testContractBuckets { 146 if b.Candidate.String() == expectCand.Owner.String() { 147 expectCand.Votes.Add(expectCand.Votes, b.StakedAmount) 148 break 149 } 150 } 151 } 152 t.Run("readStateBuckets", func(t *testing.T) { 153 sf, _, stakeSR, ctx, r := prepare(t) 154 sf.EXPECT().States(gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 ...protocol.StateOption) (uint64, state.Iterator, error) { 155 iter := state.NewIterator(states) 156 return uint64(1), iter, nil 157 }).Times(1) 158 sf.EXPECT().State(gomock.Any(), gomock.Any()).Return(uint64(0), state.ErrStateNotExist).Times(1) 159 160 req := &iotexapi.ReadStakingDataRequest_VoteBuckets{ 161 Pagination: &iotexapi.PaginationParam{ 162 Offset: 0, 163 Limit: 100, 164 }, 165 } 166 buckets, height, err := stakeSR.readStateBuckets(ctx, req) 167 r.NoError(err) 168 r.EqualValues(1, height) 169 r.Len(buckets.Buckets, len(testNativeBuckets)+len(testContractBuckets)) 170 for i := range testNativeBuckets { 171 iotexBucket, err := testNativeBuckets[i].toIoTeXTypes() 172 r.NoError(err) 173 r.Equal(iotexBucket, buckets.Buckets[i]) 174 } 175 for i := range testContractBuckets { 176 iotexBucket, err := testContractBuckets[i].toIoTeXTypes() 177 r.NoError(err) 178 r.Equal(iotexBucket, buckets.Buckets[i+len(testNativeBuckets)]) 179 } 180 }) 181 t.Run("readStateBucketsWithEndorsement", func(t *testing.T) { 182 sf, _, stakeSR, ctx, r := prepare(t) 183 sf.EXPECT().States(gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 ...protocol.StateOption) (uint64, state.Iterator, error) { 184 iter := state.NewIterator(states) 185 return uint64(1), iter, nil 186 }).Times(1) 187 sf.EXPECT().State(gomock.AssignableToTypeOf(&Endorsement{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 188 arg0R := arg0.(*Endorsement) 189 *arg0R = Endorsement{ExpireHeight: 100} 190 return uint64(1), nil 191 }).AnyTimes() 192 193 req := &iotexapi.ReadStakingDataRequest_VoteBuckets{ 194 Pagination: &iotexapi.PaginationParam{ 195 Offset: 0, 196 Limit: 100, 197 }, 198 } 199 buckets, height, err := stakeSR.readStateBuckets(ctx, req) 200 r.NoError(err) 201 r.EqualValues(1, height) 202 r.Len(buckets.Buckets, len(testNativeBuckets)+len(testContractBuckets)) 203 iotexBuckets, err := toIoTeXTypesVoteBucketList(sf, testNativeBuckets) 204 r.NoError(err) 205 for i := range testNativeBuckets { 206 r.Equal(iotexBuckets.Buckets[i], buckets.Buckets[i]) 207 } 208 iotexBuckets, err = toIoTeXTypesVoteBucketList(sf, testContractBuckets) 209 r.NoError(err) 210 for i := range testContractBuckets { 211 r.Equal(iotexBuckets.Buckets[i], buckets.Buckets[i+len(testNativeBuckets)]) 212 } 213 }) 214 215 t.Run("readStateBucketsByVoter", func(t *testing.T) { 216 sf, _, stakeSR, ctx, r := prepare(t) 217 sf.EXPECT().State(gomock.AssignableToTypeOf(&BucketIndices{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 218 arg0R := arg0.(*BucketIndices) 219 *arg0R = []uint64{0} 220 return uint64(1), nil 221 }).Times(1) 222 sf.EXPECT().State(gomock.AssignableToTypeOf(&VoteBucket{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 223 arg0R := arg0.(*VoteBucket) 224 cfg := &protocol.StateConfig{} 225 if err := arg1[1](cfg); err != nil { 226 return uint64(0), err 227 } 228 idx := byteutil.BytesToUint64BigEndian(cfg.Key[1:]) 229 *arg0R = *testNativeBuckets[idx] 230 return uint64(1), nil 231 }).Times(1) 232 sf.EXPECT().State(gomock.AssignableToTypeOf(&totalBucketCount{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 233 arg0R := arg0.(*totalBucketCount) 234 *arg0R = totalBucketCount{count: 1} 235 return uint64(1), nil 236 }).Times(1) 237 sf.EXPECT().State(gomock.AssignableToTypeOf(&Endorsement{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 238 return uint64(0), state.ErrStateNotExist 239 }).Times(1) 240 241 req := &iotexapi.ReadStakingDataRequest_VoteBucketsByVoter{ 242 Pagination: &iotexapi.PaginationParam{ 243 Offset: 0, 244 Limit: 100, 245 }, 246 VoterAddress: identityset.Address(1).String(), 247 } 248 buckets, height, err := stakeSR.readStateBucketsByVoter(ctx, req) 249 r.NoError(err) 250 r.EqualValues(1, height) 251 r.Len(buckets.Buckets, 2) 252 iotexBucket, err := testNativeBuckets[0].toIoTeXTypes() 253 r.NoError(err) 254 r.Equal(iotexBucket, buckets.Buckets[0]) 255 iotexBucket, err = testContractBuckets[0].toIoTeXTypes() 256 r.NoError(err) 257 r.Equal(iotexBucket, buckets.Buckets[1]) 258 }) 259 t.Run("readStateBucketsByCandidate", func(t *testing.T) { 260 sf, contractIndexer, stakeSR, ctx, r := prepare(t) 261 sf.EXPECT().State(gomock.AssignableToTypeOf(&VoteBucket{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 262 arg0R := arg0.(*VoteBucket) 263 cfg := &protocol.StateConfig{} 264 if err := arg1[1](cfg); err != nil { 265 return uint64(0), err 266 } 267 idx := byteutil.BytesToUint64BigEndian(cfg.Key[1:]) 268 *arg0R = *testNativeBuckets[idx] 269 return uint64(1), nil 270 }).Times(1) 271 sf.EXPECT().State(gomock.AssignableToTypeOf(&BucketIndices{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 272 arg0R := arg0.(*BucketIndices) 273 *arg0R = []uint64{0} 274 return uint64(1), nil 275 }).Times(1) 276 sf.EXPECT().State(gomock.AssignableToTypeOf(&totalBucketCount{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 277 arg0R := arg0.(*totalBucketCount) 278 *arg0R = totalBucketCount{count: 1} 279 return uint64(1), nil 280 }).Times(1) 281 sf.EXPECT().State(gomock.AssignableToTypeOf(&Endorsement{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 282 return uint64(0), state.ErrStateNotExist 283 }).Times(1) 284 contractIndexer.EXPECT().BucketsByCandidate(gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 address.Address, arg1 uint64) ([]*VoteBucket, error) { 285 buckets := []*VoteBucket{} 286 for i := range testContractBuckets { 287 if testContractBuckets[i].Candidate.String() == arg0.String() { 288 buckets = append(buckets, testContractBuckets[i]) 289 } 290 } 291 return buckets, nil 292 }).Times(1) 293 req := &iotexapi.ReadStakingDataRequest_VoteBucketsByCandidate{ 294 Pagination: &iotexapi.PaginationParam{ 295 Offset: 0, 296 Limit: 100, 297 }, 298 CandName: "cand1", 299 } 300 buckets, height, err := stakeSR.readStateBucketsByCandidate(ctx, req) 301 r.NoError(err) 302 r.EqualValues(1, height) 303 r.Len(buckets.Buckets, 2) 304 iotexBucket, err := testNativeBuckets[0].toIoTeXTypes() 305 r.NoError(err) 306 r.Equal(iotexBucket, buckets.Buckets[0]) 307 iotexBucket, err = testContractBuckets[0].toIoTeXTypes() 308 r.NoError(err) 309 r.Equal(iotexBucket, buckets.Buckets[1]) 310 }) 311 t.Run("readStateBucketByIndices", func(t *testing.T) { 312 sf, contractIndexer, stakeSR, ctx, r := prepare(t) 313 sf.EXPECT().State(gomock.AssignableToTypeOf(&VoteBucket{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 314 arg0R := arg0.(*VoteBucket) 315 cfg := &protocol.StateConfig{} 316 if err := arg1[1](cfg); err != nil { 317 return uint64(0), err 318 } 319 idx := byteutil.BytesToUint64BigEndian(cfg.Key[1:]) 320 *arg0R = *testNativeBuckets[idx] 321 return uint64(1), nil 322 }).Times(1) 323 sf.EXPECT().State(gomock.AssignableToTypeOf(&totalBucketCount{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 324 arg0R := arg0.(*totalBucketCount) 325 *arg0R = totalBucketCount{count: 1} 326 return uint64(1), nil 327 }).Times(1) 328 sf.EXPECT().State(gomock.AssignableToTypeOf(&Endorsement{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 329 return uint64(0), state.ErrStateNotExist 330 }).Times(1) 331 contractIndexer.EXPECT().BucketsByIndices(gomock.Any(), gomock.Any()).DoAndReturn(func(arg0 []uint64, arg1 uint64) ([]*VoteBucket, error) { 332 buckets := []*VoteBucket{} 333 for i := range arg0 { 334 buckets = append(buckets, testContractBuckets[arg0[i]]) 335 } 336 return buckets, nil 337 }).Times(1) 338 req := &iotexapi.ReadStakingDataRequest_VoteBucketsByIndexes{ 339 Index: []uint64{0}, 340 } 341 buckets, height, err := stakeSR.readStateBucketByIndices(ctx, req) 342 r.NoError(err) 343 r.EqualValues(1, height) 344 r.Len(buckets.Buckets, 2) 345 iotexBucket, err := testNativeBuckets[0].toIoTeXTypes() 346 r.NoError(err) 347 r.Equal(iotexBucket, buckets.Buckets[0]) 348 iotexBucket, err = testContractBuckets[0].toIoTeXTypes() 349 r.NoError(err) 350 r.Equal(iotexBucket, buckets.Buckets[1]) 351 }) 352 t.Run("readStateBucketCount", func(t *testing.T) { 353 sf, contractIndexer, stakeSR, ctx, r := prepare(t) 354 sf.EXPECT().State(gomock.AssignableToTypeOf(&totalAmount{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 355 arg0R := arg0.(*totalAmount) 356 *arg0R = *testNativeTotalAmount 357 return uint64(1), nil 358 }).Times(1) 359 sf.EXPECT().State(gomock.AssignableToTypeOf(&totalBucketCount{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 360 arg0R := arg0.(*totalBucketCount) 361 *arg0R = totalBucketCount{count: 1} 362 return uint64(1), nil 363 }).Times(1) 364 contractIndexer.EXPECT().TotalBucketCount(gomock.Any()).Return(uint64(len(testContractBuckets)), nil).Times(1) 365 cfg := genesis.Default 366 cfg.GreenlandBlockHeight = 0 367 ctx = genesis.WithGenesisContext(ctx, cfg) 368 ctx = protocol.WithFeatureWithHeightCtx(ctx) 369 req := &iotexapi.ReadStakingDataRequest_BucketsCount{} 370 bucketCount, height, err := stakeSR.readStateBucketCount(ctx, req) 371 r.NoError(err) 372 r.EqualValues(1, height) 373 r.EqualValues(3, bucketCount.Total) 374 r.EqualValues(3, bucketCount.Active) 375 }) 376 t.Run("readStateCandidates", func(t *testing.T) { 377 _, contractIndexer, stakeSR, ctx, r := prepare(t) 378 contractIndexer.EXPECT().CandidateVotes(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, ownerAddr address.Address, height uint64) (*big.Int, error) { 379 for _, b := range testContractBuckets { 380 if b.Owner.String() == ownerAddr.String() { 381 return b.StakedAmount, nil 382 } 383 } 384 return big.NewInt(0), nil 385 }).MinTimes(1) 386 req := &iotexapi.ReadStakingDataRequest_Candidates{ 387 Pagination: &iotexapi.PaginationParam{ 388 Offset: 0, 389 Limit: 100, 390 }, 391 } 392 candidates, height, err := stakeSR.readStateCandidates(ctx, req) 393 r.NoError(err) 394 r.EqualValues(1, height) 395 r.Len(candidates.Candidates, 2) 396 for i := range candidates.Candidates { 397 idx := slices.IndexFunc(testCandidates, func(c *Candidate) bool { 398 return c.Owner.String() == candidates.Candidates[i].OwnerAddress 399 }) 400 r.True(idx >= 0) 401 expectCand := *testCandidates[idx] 402 addContractVotes(&expectCand) 403 r.EqualValues(expectCand.toIoTeXTypes(), candidates.Candidates[i]) 404 } 405 }) 406 t.Run("readStateCandidateByName", func(t *testing.T) { 407 _, contractIndexer, stakeSR, ctx, r := prepare(t) 408 contractIndexer.EXPECT().CandidateVotes(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, ownerAddr address.Address, height uint64) (*big.Int, error) { 409 for _, b := range testContractBuckets { 410 if b.Owner.String() == ownerAddr.String() { 411 return b.StakedAmount, nil 412 } 413 } 414 return big.NewInt(0), nil 415 }).MinTimes(1) 416 req := &iotexapi.ReadStakingDataRequest_CandidateByName{ 417 CandName: "cand1", 418 } 419 candidate, _, err := stakeSR.readStateCandidateByName(ctx, req) 420 r.NoError(err) 421 idx := slices.IndexFunc(testCandidates, func(c *Candidate) bool { 422 return c.Owner.String() == candidate.OwnerAddress 423 }) 424 r.True(idx >= 0) 425 expectCand := *testCandidates[idx] 426 addContractVotes(&expectCand) 427 r.EqualValues(expectCand.toIoTeXTypes(), candidate) 428 }) 429 t.Run("readStateCandidateByAddress", func(t *testing.T) { 430 _, contractIndexer, stakeSR, ctx, r := prepare(t) 431 contractIndexer.EXPECT().CandidateVotes(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, ownerAddr address.Address, height uint64) (*big.Int, error) { 432 for _, b := range testContractBuckets { 433 if b.Owner.String() == ownerAddr.String() { 434 return b.StakedAmount, nil 435 } 436 } 437 return big.NewInt(0), nil 438 }).MinTimes(1) 439 req := &iotexapi.ReadStakingDataRequest_CandidateByAddress{ 440 OwnerAddr: identityset.Address(1).String(), 441 } 442 candidate, _, err := stakeSR.readStateCandidateByAddress(ctx, req) 443 r.NoError(err) 444 idx := slices.IndexFunc(testCandidates, func(c *Candidate) bool { 445 return c.Owner.String() == candidate.OwnerAddress 446 }) 447 r.True(idx >= 0) 448 expectCand := *testCandidates[idx] 449 addContractVotes(&expectCand) 450 r.EqualValues(expectCand.toIoTeXTypes(), candidate) 451 }) 452 t.Run("readStateTotalStakingAmount", func(t *testing.T) { 453 sf, _, stakeSR, ctx, r := prepare(t) 454 sf.EXPECT().State(gomock.AssignableToTypeOf(&totalAmount{}), gomock.Any()).DoAndReturn(func(arg0 any, arg1 ...protocol.StateOption) (uint64, error) { 455 arg0R := arg0.(*totalAmount) 456 *arg0R = *testNativeTotalAmount 457 return uint64(1), nil 458 }).Times(1) 459 cfg := genesis.Default 460 cfg.GreenlandBlockHeight = 0 461 ctx = genesis.WithGenesisContext(ctx, cfg) 462 ctx = protocol.WithFeatureWithHeightCtx(ctx) 463 req := &iotexapi.ReadStakingDataRequest_TotalStakingAmount{} 464 total, height, err := stakeSR.readStateTotalStakingAmount(ctx, req) 465 r.NoError(err) 466 r.EqualValues(1, height) 467 r.EqualValues("210", total.Balance) 468 }) 469 }