github.com/iotexproject/iotex-core@v1.14.1-rc1/blockindex/contractstaking/cache_test.go (about)

     1  package contractstaking
     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/pkg/errors"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/iotexproject/iotex-core/action/protocol"
    15  	"github.com/iotexproject/iotex-core/action/protocol/staking"
    16  	"github.com/iotexproject/iotex-core/blockchain/genesis"
    17  	"github.com/iotexproject/iotex-core/config"
    18  	"github.com/iotexproject/iotex-core/db"
    19  	"github.com/iotexproject/iotex-core/pkg/util/byteutil"
    20  	"github.com/iotexproject/iotex-core/test/identityset"
    21  	"github.com/iotexproject/iotex-core/testutil"
    22  )
    23  
    24  func _checkCacheCandidateVotes(ctx context.Context, r *require.Assertions, cache *contractStakingCache, height uint64, addr address.Address, expectVotes int64) {
    25  	votes, err := cache.CandidateVotes(ctx, addr, height)
    26  	r.NoError(err)
    27  	r.EqualValues(expectVotes, votes.Int64())
    28  }
    29  
    30  func calculateVoteWeightGen(c genesis.VoteWeightCalConsts) calculateVoteWeightFunc {
    31  	return func(v *Bucket) *big.Int {
    32  		return staking.CalculateVoteWeight(c, v, false)
    33  	}
    34  }
    35  
    36  func TestContractStakingCache_CandidateVotes(t *testing.T) {
    37  	checkCacheCandidateVotesGen := func(ctx context.Context) func(r *require.Assertions, cache *contractStakingCache, height uint64, addr address.Address, expectVotes int64) {
    38  		return func(r *require.Assertions, cache *contractStakingCache, height uint64, addr address.Address, expectVotes int64) {
    39  			_checkCacheCandidateVotes(ctx, r, cache, height, addr, expectVotes)
    40  		}
    41  	}
    42  	require := require.New(t)
    43  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(1).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
    44  	checkCacheCandidateVotes := checkCacheCandidateVotesGen(protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: 1})))
    45  	checkCacheCandidateVotesAfterRedsea := checkCacheCandidateVotesGen(protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: genesis.Default.RedseaBlockHeight})))
    46  	// no bucket
    47  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 0)
    48  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 0)
    49  
    50  	// one bucket
    51  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
    52  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
    53  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 100)
    54  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 103)
    55  
    56  	// two buckets
    57  	cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
    58  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 200)
    59  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 206)
    60  
    61  	// add one bucket with different delegate
    62  	cache.PutBucketInfo(3, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)})
    63  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 200)
    64  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(3), 100)
    65  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 206)
    66  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(3), 103)
    67  
    68  	// add one bucket with different owner
    69  	cache.PutBucketInfo(4, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(4)})
    70  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 300)
    71  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 309)
    72  
    73  	// add one bucket with different amount
    74  	cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 100, ActivatedAt: 1})
    75  	cache.PutBucketInfo(5, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
    76  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 500)
    77  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 516)
    78  
    79  	// add one bucket with different duration
    80  	cache.PutBucketType(3, &BucketType{Amount: big.NewInt(300), Duration: 200, ActivatedAt: 1})
    81  	cache.PutBucketInfo(6, &bucketInfo{TypeIndex: 3, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
    82  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 800)
    83  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 827)
    84  
    85  	// add one bucket that is unstaked
    86  	cache.PutBucketInfo(7, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: 1, UnstakedAt: 1, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
    87  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 800)
    88  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 827)
    89  
    90  	// add one bucket that is unlocked and staked
    91  	cache.PutBucketInfo(8, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: 100, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
    92  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 900)
    93  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 927)
    94  
    95  	// change delegate of bucket 1
    96  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)})
    97  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 800)
    98  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(3), 200)
    99  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 824)
   100  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(3), 206)
   101  
   102  	// change owner of bucket 1
   103  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)})
   104  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 800)
   105  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(3), 200)
   106  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 824)
   107  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(3), 206)
   108  
   109  	// change amount of bucket 1
   110  	cache.putBucketInfo(1, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)})
   111  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(1), 800)
   112  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(3), 300)
   113  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(1), 824)
   114  	checkCacheCandidateVotesAfterRedsea(require, cache, 0, identityset.Address(3), 310)
   115  
   116  	// put bucket info and make it disabled
   117  	bi := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)}
   118  	cache.PutBucketInfo(9, bi)
   119  	cache.candidateBucketMap[bi.Delegate.String()][9] = false
   120  	checkCacheCandidateVotes(require, cache, 0, identityset.Address(3), 300)
   121  }
   122  
   123  func TestContractStakingCache_Buckets(t *testing.T) {
   124  	require := require.New(t)
   125  	contractAddr := identityset.Address(27).String()
   126  	cache := newContractStakingCache(Config{ContractAddress: contractAddr, CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   127  
   128  	height := uint64(0)
   129  	// no bucket
   130  	bts, err := cache.Buckets(height)
   131  	require.NoError(err)
   132  	require.Empty(bts)
   133  
   134  	// add one bucket
   135  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   136  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
   137  	buckets, err := cache.Buckets(height)
   138  	require.NoError(err)
   139  	require.Len(buckets, 1)
   140  	checkVoteBucket(require, buckets[0], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   141  	bucket, ok, err := cache.Bucket(1, height)
   142  	require.NoError(err)
   143  	require.True(ok)
   144  	checkVoteBucket(require, bucket, 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   145  
   146  	// add one bucket with different index
   147  	cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 1})
   148  	cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(4)})
   149  	bts, err = cache.Buckets(height)
   150  	require.NoError(err)
   151  	bucketMaps := bucketsToMap(bts)
   152  	require.Len(bucketMaps, 2)
   153  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   154  	checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(3).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr)
   155  	bucket, ok, err = cache.Bucket(1, height)
   156  	require.NoError(err)
   157  	require.True(ok)
   158  	checkVoteBucket(require, bucket, 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   159  	bucket, ok, err = cache.Bucket(2, height)
   160  	require.NoError(err)
   161  	require.True(ok)
   162  	checkVoteBucket(require, bucket, 2, identityset.Address(3).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr)
   163  
   164  	// update delegate of bucket 2
   165  	cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(5), Owner: identityset.Address(4)})
   166  	bts, err = cache.Buckets(height)
   167  	require.NoError(err)
   168  	bucketMaps = bucketsToMap(bts)
   169  	require.Len(bucketMaps, 2)
   170  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   171  	checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr)
   172  	bucket, ok, err = cache.Bucket(1, height)
   173  	require.NoError(err)
   174  	require.True(ok)
   175  	checkVoteBucket(require, bucket, 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   176  	bucket, ok, err = cache.Bucket(2, height)
   177  	require.NoError(err)
   178  	require.True(ok)
   179  	checkVoteBucket(require, bucket, 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr)
   180  
   181  	// delete bucket 1
   182  	cache.DeleteBucketInfo(1)
   183  	bts, err = cache.Buckets(height)
   184  	require.NoError(err)
   185  	bucketMaps = bucketsToMap(bts)
   186  	require.Len(bucketMaps, 1)
   187  	checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr)
   188  	_, ok, err = cache.Bucket(1, height)
   189  	require.NoError(err)
   190  	require.False(ok)
   191  	bucket, ok, err = cache.Bucket(2, height)
   192  	require.NoError(err)
   193  	require.True(ok)
   194  	checkVoteBucket(require, bucket, 2, identityset.Address(5).String(), identityset.Address(4).String(), 200, 200, 1, 1, maxBlockNumber, true, contractAddr)
   195  }
   196  
   197  func TestContractStakingCache_BucketsByCandidate(t *testing.T) {
   198  	require := require.New(t)
   199  	contractAddr := identityset.Address(27).String()
   200  	cache := newContractStakingCache(Config{ContractAddress: contractAddr, CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   201  
   202  	height := uint64(0)
   203  	// no bucket
   204  	buckets, err := cache.BucketsByCandidate(identityset.Address(1), height)
   205  	require.NoError(err)
   206  	require.Len(buckets, 0)
   207  
   208  	// one bucket
   209  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   210  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
   211  	bts, err := cache.BucketsByCandidate(identityset.Address(1), height)
   212  	require.NoError(err)
   213  	bucketMaps := bucketsToMap(bts)
   214  	require.Len(bucketMaps, 1)
   215  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   216  
   217  	// two buckets
   218  	cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
   219  	bts, err = cache.BucketsByCandidate(identityset.Address(1), height)
   220  	require.NoError(err)
   221  	bucketMaps = bucketsToMap(bts)
   222  	require.Len(bucketMaps, 2)
   223  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   224  	checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   225  
   226  	// add one bucket with different delegate
   227  	cache.PutBucketInfo(3, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)})
   228  	bts, err = cache.BucketsByCandidate(identityset.Address(1), height)
   229  	require.NoError(err)
   230  	bucketMaps = bucketsToMap(bts)
   231  	require.Len(bucketMaps, 2)
   232  	require.Nil(bucketMaps[3])
   233  	bts, err = cache.BucketsByCandidate(identityset.Address(3), height)
   234  	require.NoError(err)
   235  	bucketMaps = bucketsToMap(bts)
   236  	require.Len(bucketMaps, 1)
   237  	checkVoteBucket(require, bucketMaps[3], 3, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   238  
   239  	// change delegate of bucket 1
   240  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)})
   241  	bts, err = cache.BucketsByCandidate(identityset.Address(1), height)
   242  	require.NoError(err)
   243  	bucketMaps = bucketsToMap(bts)
   244  	require.Len(bucketMaps, 1)
   245  	require.Nil(bucketMaps[1])
   246  	checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   247  	bts, err = cache.BucketsByCandidate(identityset.Address(3), height)
   248  	require.NoError(err)
   249  	bucketMaps = bucketsToMap(bts)
   250  	require.Len(bucketMaps, 2)
   251  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   252  	checkVoteBucket(require, bucketMaps[3], 3, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   253  
   254  	// delete bucket 2
   255  	cache.DeleteBucketInfo(2)
   256  	bts, err = cache.BucketsByCandidate(identityset.Address(1), height)
   257  	require.NoError(err)
   258  	bucketMaps = bucketsToMap(bts)
   259  	require.Len(bucketMaps, 0)
   260  	bts, err = cache.BucketsByCandidate(identityset.Address(3), height)
   261  	require.NoError(err)
   262  	bucketMaps = bucketsToMap(bts)
   263  	require.Len(bucketMaps, 2)
   264  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   265  	checkVoteBucket(require, bucketMaps[3], 3, identityset.Address(3).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   266  
   267  }
   268  
   269  func TestContractStakingCache_BucketsByIndices(t *testing.T) {
   270  	require := require.New(t)
   271  	contractAddr := identityset.Address(27).String()
   272  	cache := newContractStakingCache(Config{ContractAddress: contractAddr, CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   273  
   274  	height := uint64(0)
   275  	// no bucket
   276  	buckets, err := cache.BucketsByIndices([]uint64{1}, height)
   277  	require.NoError(err)
   278  	require.Len(buckets, 0)
   279  
   280  	// one bucket
   281  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   282  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
   283  	buckets, err = cache.BucketsByIndices([]uint64{1}, height)
   284  	require.NoError(err)
   285  	require.Len(buckets, 1)
   286  	bucketMaps := bucketsToMap(buckets)
   287  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   288  
   289  	// two buckets
   290  	cache.PutBucketInfo(2, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
   291  	buckets, err = cache.BucketsByIndices([]uint64{1, 2}, height)
   292  	require.NoError(err)
   293  	require.Len(buckets, 2)
   294  	bucketMaps = bucketsToMap(buckets)
   295  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   296  	checkVoteBucket(require, bucketMaps[2], 2, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   297  
   298  	// one bucket not found
   299  	buckets, err = cache.BucketsByIndices([]uint64{3}, height)
   300  	require.NoError(err)
   301  	require.Len(buckets, 0)
   302  
   303  	// one bucket found, one not found
   304  	buckets, err = cache.BucketsByIndices([]uint64{1, 3}, height)
   305  	require.NoError(err)
   306  	require.Len(buckets, 1)
   307  	bucketMaps = bucketsToMap(buckets)
   308  	checkVoteBucket(require, bucketMaps[1], 1, identityset.Address(1).String(), identityset.Address(2).String(), 100, 100, 1, 1, maxBlockNumber, true, contractAddr)
   309  
   310  	// delete bucket 1
   311  	cache.DeleteBucketInfo(1)
   312  	buckets, err = cache.BucketsByIndices([]uint64{1}, height)
   313  	require.NoError(err)
   314  	require.Len(buckets, 0)
   315  }
   316  
   317  func TestContractStakingCache_TotalBucketCount(t *testing.T) {
   318  	require := require.New(t)
   319  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(27).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   320  
   321  	height := uint64(0)
   322  	// no bucket
   323  	tbc, err := cache.TotalBucketCount(height)
   324  	require.NoError(err)
   325  	require.EqualValues(0, tbc)
   326  
   327  	// one bucket
   328  	cache.putTotalBucketCount(1)
   329  	tbc, err = cache.TotalBucketCount(height)
   330  	require.NoError(err)
   331  	require.EqualValues(1, tbc)
   332  
   333  	// two buckets
   334  	cache.putTotalBucketCount(2)
   335  	tbc, err = cache.TotalBucketCount(height)
   336  	require.NoError(err)
   337  	require.EqualValues(2, tbc)
   338  
   339  	// delete bucket 1
   340  	cache.DeleteBucketInfo(1)
   341  	tbc, err = cache.TotalBucketCount(height)
   342  	require.NoError(err)
   343  	require.EqualValues(2, tbc)
   344  }
   345  
   346  func TestContractStakingCache_ActiveBucketTypes(t *testing.T) {
   347  	require := require.New(t)
   348  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(27).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   349  
   350  	height := uint64(0)
   351  	// no bucket type
   352  	abt, err := cache.ActiveBucketTypes(height)
   353  	require.NoError(err)
   354  	require.Empty(abt)
   355  
   356  	// one bucket type
   357  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   358  	activeBucketTypes, err := cache.ActiveBucketTypes(height)
   359  	require.NoError(err)
   360  	require.Len(activeBucketTypes, 1)
   361  	require.EqualValues(100, activeBucketTypes[1].Amount.Int64())
   362  	require.EqualValues(100, activeBucketTypes[1].Duration)
   363  	require.EqualValues(1, activeBucketTypes[1].ActivatedAt)
   364  
   365  	// two bucket types
   366  	cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2})
   367  	activeBucketTypes, err = cache.ActiveBucketTypes(height)
   368  	require.NoError(err)
   369  	require.Len(activeBucketTypes, 2)
   370  	require.EqualValues(100, activeBucketTypes[1].Amount.Int64())
   371  	require.EqualValues(100, activeBucketTypes[1].Duration)
   372  	require.EqualValues(1, activeBucketTypes[1].ActivatedAt)
   373  	require.EqualValues(200, activeBucketTypes[2].Amount.Int64())
   374  	require.EqualValues(200, activeBucketTypes[2].Duration)
   375  	require.EqualValues(2, activeBucketTypes[2].ActivatedAt)
   376  
   377  	// add one inactive bucket type
   378  	cache.PutBucketType(3, &BucketType{Amount: big.NewInt(300), Duration: 300, ActivatedAt: maxBlockNumber})
   379  	activeBucketTypes, err = cache.ActiveBucketTypes(height)
   380  	require.NoError(err)
   381  	require.Len(activeBucketTypes, 2)
   382  	require.EqualValues(100, activeBucketTypes[1].Amount.Int64())
   383  	require.EqualValues(100, activeBucketTypes[1].Duration)
   384  	require.EqualValues(1, activeBucketTypes[1].ActivatedAt)
   385  	require.EqualValues(200, activeBucketTypes[2].Amount.Int64())
   386  	require.EqualValues(200, activeBucketTypes[2].Duration)
   387  	require.EqualValues(2, activeBucketTypes[2].ActivatedAt)
   388  
   389  	// deactivate bucket type 1
   390  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: maxBlockNumber})
   391  	activeBucketTypes, err = cache.ActiveBucketTypes(height)
   392  	require.NoError(err)
   393  	require.Len(activeBucketTypes, 1)
   394  	require.EqualValues(200, activeBucketTypes[2].Amount.Int64())
   395  	require.EqualValues(200, activeBucketTypes[2].Duration)
   396  	require.EqualValues(2, activeBucketTypes[2].ActivatedAt)
   397  
   398  	// reactivate bucket type 1
   399  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   400  	activeBucketTypes, err = cache.ActiveBucketTypes(height)
   401  	require.NoError(err)
   402  	require.Len(activeBucketTypes, 2)
   403  	require.EqualValues(100, activeBucketTypes[1].Amount.Int64())
   404  	require.EqualValues(100, activeBucketTypes[1].Duration)
   405  	require.EqualValues(1, activeBucketTypes[1].ActivatedAt)
   406  	require.EqualValues(200, activeBucketTypes[2].Amount.Int64())
   407  	require.EqualValues(200, activeBucketTypes[2].Duration)
   408  	require.EqualValues(2, activeBucketTypes[2].ActivatedAt)
   409  }
   410  
   411  func TestContractStakingCache_Merge(t *testing.T) {
   412  	require := require.New(t)
   413  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(27).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   414  	height := uint64(1)
   415  	ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: height}))
   416  
   417  	// create delta with one bucket type
   418  	delta := newContractStakingDelta()
   419  	delta.AddBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   420  	// merge delta into cache
   421  	err := cache.Merge(delta, height)
   422  	require.NoError(err)
   423  	// check that bucket type was added to cache
   424  	activeBucketTypes, err := cache.ActiveBucketTypes(height)
   425  	require.NoError(err)
   426  	require.Len(activeBucketTypes, 1)
   427  	require.EqualValues(100, activeBucketTypes[1].Amount.Int64())
   428  	require.EqualValues(100, activeBucketTypes[1].Duration)
   429  	require.EqualValues(1, activeBucketTypes[1].ActivatedAt)
   430  	require.EqualValues(height, cache.Height())
   431  
   432  	// create delta with one bucket
   433  	delta = newContractStakingDelta()
   434  	delta.AddBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
   435  	// merge delta into cache
   436  	err = cache.Merge(delta, height)
   437  	require.NoError(err)
   438  	// check that bucket was added to cache and vote count is correct
   439  	votes, err := cache.CandidateVotes(ctx, identityset.Address(1), height)
   440  	require.NoError(err)
   441  	require.EqualValues(100, votes.Int64())
   442  
   443  	// create delta with updated bucket delegate
   444  	delta = newContractStakingDelta()
   445  	delta.UpdateBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(3), Owner: identityset.Address(2)})
   446  	// merge delta into cache
   447  	err = cache.Merge(delta, height)
   448  	require.NoError(err)
   449  	// check that bucket delegate was updated and vote count is correct
   450  	votes, err = cache.CandidateVotes(ctx, identityset.Address(1), height)
   451  	require.NoError(err)
   452  	require.EqualValues(0, votes.Int64())
   453  	votes, err = cache.CandidateVotes(ctx, identityset.Address(3), height)
   454  	require.NoError(err)
   455  	require.EqualValues(100, votes.Int64())
   456  
   457  	// create delta with deleted bucket
   458  	delta = newContractStakingDelta()
   459  	delta.DeleteBucketInfo(1)
   460  	// merge delta into cache
   461  	err = cache.Merge(delta, height)
   462  	require.NoError(err)
   463  	// check that bucket was deleted from cache and vote count is 0
   464  	votes, err = cache.CandidateVotes(ctx, identityset.Address(3), height)
   465  	require.NoError(err)
   466  	require.EqualValues(0, votes.Int64())
   467  
   468  	// invalid delta
   469  	err = cache.Merge(nil, height)
   470  	require.Error(err)
   471  	require.Equal(err.Error(), "invalid contract staking delta")
   472  }
   473  
   474  func TestContractStakingCache_MatchBucketType(t *testing.T) {
   475  	require := require.New(t)
   476  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(27).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   477  
   478  	// no bucket types
   479  	_, bucketType, ok := cache.MatchBucketType(big.NewInt(100), 100)
   480  	require.False(ok)
   481  	require.Nil(bucketType)
   482  
   483  	// one bucket type
   484  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   485  	// match exact bucket type
   486  	id, bucketType, ok := cache.MatchBucketType(big.NewInt(100), 100)
   487  	require.True(ok)
   488  	require.EqualValues(1, id)
   489  	require.EqualValues(100, bucketType.Amount.Int64())
   490  	require.EqualValues(100, bucketType.Duration)
   491  	require.EqualValues(1, bucketType.ActivatedAt)
   492  
   493  	// match bucket type with different amount
   494  	_, bucketType, ok = cache.MatchBucketType(big.NewInt(200), 100)
   495  	require.False(ok)
   496  	require.Nil(bucketType)
   497  
   498  	// match bucket type with different duration
   499  	_, bucketType, ok = cache.MatchBucketType(big.NewInt(100), 200)
   500  	require.False(ok)
   501  	require.Nil(bucketType)
   502  
   503  	// no match
   504  	_, bucketType, ok = cache.MatchBucketType(big.NewInt(200), 200)
   505  	require.False(ok)
   506  	require.Nil(bucketType)
   507  }
   508  
   509  func TestContractStakingCache_BucketTypeCount(t *testing.T) {
   510  	require := require.New(t)
   511  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(27).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   512  
   513  	height := uint64(0)
   514  	// no bucket type
   515  	btc, err := cache.BucketTypeCount(height)
   516  	require.NoError(err)
   517  	require.EqualValues(0, btc)
   518  
   519  	// one bucket type
   520  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   521  	btc, err = cache.BucketTypeCount(height)
   522  	require.NoError(err)
   523  	require.EqualValues(1, btc)
   524  
   525  	// two bucket types
   526  	cache.PutBucketType(2, &BucketType{Amount: big.NewInt(200), Duration: 200, ActivatedAt: 2})
   527  	btc, err = cache.BucketTypeCount(height)
   528  	require.NoError(err)
   529  	require.EqualValues(2, btc)
   530  
   531  	// deactivate bucket type 1
   532  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: maxBlockNumber})
   533  	btc, err = cache.BucketTypeCount(height)
   534  	require.NoError(err)
   535  	require.EqualValues(2, btc)
   536  
   537  	btc, err = cache.BucketTypeCount(1)
   538  	require.Error(err)
   539  	require.Contains(err.Error(), "invalid height")
   540  }
   541  
   542  func TestContractStakingCache_LoadFromDB(t *testing.T) {
   543  	require := require.New(t)
   544  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(27).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   545  
   546  	// mock kvstore exception
   547  	ctrl := gomock.NewController(t)
   548  	mockKvStore := db.NewMockKVStore(ctrl)
   549  
   550  	// kvstore exception at load height
   551  	mockKvStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, errors.New("err1")).Times(1)
   552  	err := cache.LoadFromDB(mockKvStore)
   553  	require.Error(err)
   554  	require.Equal(err.Error(), "err1")
   555  
   556  	// kvstore exception at load total bucket count
   557  	mockKvStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, db.ErrNotExist).Times(1)
   558  	mockKvStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, errors.New("err2")).Times(1)
   559  	err = cache.LoadFromDB(mockKvStore)
   560  	require.Error(err)
   561  	require.Equal(err.Error(), "err2")
   562  
   563  	// kvstore exception at load bucket info
   564  	mockKvStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, db.ErrNotExist).Times(2)
   565  	mockKvStore.EXPECT().Filter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, errors.New("err3")).Times(1)
   566  	err = cache.LoadFromDB(mockKvStore)
   567  	require.Error(err)
   568  	require.Equal(err.Error(), "err3")
   569  
   570  	// mock bucketInfo Deserialize failed
   571  	mockKvStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, db.ErrNotExist).Times(2)
   572  	mockKvStore.EXPECT().Filter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, [][]byte{nil}, nil).Times(1)
   573  	err = cache.LoadFromDB(mockKvStore)
   574  	require.Error(err)
   575  
   576  	// kvstore exception at load bucket type
   577  	mockKvStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, db.ErrNotExist).Times(2)
   578  	mockKvStore.EXPECT().Filter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, db.ErrBucketNotExist).Times(1)
   579  	mockKvStore.EXPECT().Filter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, errors.New("err4")).Times(1)
   580  	err = cache.LoadFromDB(mockKvStore)
   581  	require.Error(err)
   582  	require.Equal(err.Error(), "err4")
   583  
   584  	mockKvStore.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, db.ErrNotExist).Times(2)
   585  	mockKvStore.EXPECT().Filter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, db.ErrBucketNotExist).Times(1)
   586  	mockKvStore.EXPECT().Filter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, [][]byte{nil}, nil).Times(1)
   587  	err = cache.LoadFromDB(mockKvStore)
   588  	require.Error(err)
   589  
   590  	// load from empty db
   591  	path, err := testutil.PathOfTempFile("staking.db")
   592  	require.NoError(err)
   593  	defer testutil.CleanupPath(path)
   594  	cfg := config.Default.DB
   595  	cfg.DbPath = path
   596  	kvstore := db.NewBoltDB(cfg)
   597  	require.NoError(kvstore.Start(context.Background()))
   598  	defer kvstore.Stop(context.Background())
   599  
   600  	height := uint64(0)
   601  	err = cache.LoadFromDB(kvstore)
   602  	require.NoError(err)
   603  	tbc, err := cache.TotalBucketCount(height)
   604  	require.NoError(err)
   605  	require.Equal(uint64(0), tbc)
   606  	bts, err := cache.Buckets(height)
   607  	require.NoError(err)
   608  	require.Equal(0, len(bts))
   609  	btc, err := cache.BucketTypeCount(height)
   610  	require.NoError(err)
   611  	require.EqualValues(0, btc)
   612  
   613  	// load from db with height and total bucket count
   614  	height = 12345
   615  	kvstore.Put(_StakingNS, _stakingHeightKey, byteutil.Uint64ToBytesBigEndian(height))
   616  	kvstore.Put(_StakingNS, _stakingTotalBucketCountKey, byteutil.Uint64ToBytesBigEndian(10))
   617  	err = cache.LoadFromDB(kvstore)
   618  	require.NoError(err)
   619  	tbc, err = cache.TotalBucketCount(height)
   620  	require.NoError(err)
   621  	require.Equal(uint64(10), tbc)
   622  	bts, err = cache.Buckets(height)
   623  	require.NoError(err)
   624  	require.Equal(0, len(bts))
   625  	btc, err = cache.BucketTypeCount(height)
   626  	require.NoError(err)
   627  	require.EqualValues(0, btc)
   628  
   629  	// load from db with bucket
   630  	bucketInfo := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}
   631  	kvstore.Put(_StakingBucketInfoNS, byteutil.Uint64ToBytesBigEndian(1), bucketInfo.Serialize())
   632  	bucketType := &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1}
   633  	kvstore.Put(_StakingBucketTypeNS, byteutil.Uint64ToBytesBigEndian(1), bucketType.Serialize())
   634  	err = cache.LoadFromDB(kvstore)
   635  	require.NoError(err)
   636  	tbc, err = cache.TotalBucketCount(height)
   637  	require.NoError(err)
   638  	require.Equal(uint64(10), tbc)
   639  	bi, ok := cache.BucketInfo(1)
   640  	require.True(ok)
   641  	bts, err = cache.Buckets(height)
   642  	require.NoError(err)
   643  	require.Equal(1, len(bts))
   644  	require.Equal(bucketInfo, bi)
   645  	btc, err = cache.BucketTypeCount(height)
   646  	require.NoError(err)
   647  	require.EqualValues(1, btc)
   648  	id, bt, ok := cache.MatchBucketType(big.NewInt(100), 100)
   649  	require.True(ok)
   650  	require.EqualValues(1, id)
   651  	require.EqualValues(100, bt.Amount.Int64())
   652  	require.EqualValues(100, bt.Duration)
   653  	require.EqualValues(1, bt.ActivatedAt)
   654  }
   655  
   656  func bucketsToMap(buckets []*staking.VoteBucket) map[uint64]*staking.VoteBucket {
   657  	m := make(map[uint64]*staking.VoteBucket)
   658  	for _, bucket := range buckets {
   659  		m[bucket.Index] = bucket
   660  	}
   661  	return m
   662  }
   663  
   664  func checkVoteBucket(r *require.Assertions, bucket *staking.VoteBucket, index uint64, candidate, owner string, amount, duration, createHeight, startHeight, unstakeHeight uint64, autoStake bool, contractAddr string) {
   665  	r.EqualValues(index, bucket.Index)
   666  	r.EqualValues(candidate, bucket.Candidate.String())
   667  	r.EqualValues(owner, bucket.Owner.String())
   668  	r.EqualValues(amount, bucket.StakedAmount.Int64())
   669  	r.EqualValues(duration, bucket.StakedDurationBlockNumber)
   670  	r.EqualValues(createHeight, bucket.CreateBlockHeight)
   671  	r.EqualValues(startHeight, bucket.StakeStartBlockHeight)
   672  	r.EqualValues(unstakeHeight, bucket.UnstakeStartBlockHeight)
   673  	r.EqualValues(autoStake, bucket.AutoStake)
   674  	r.EqualValues(contractAddr, bucket.ContractAddress)
   675  }
   676  
   677  func TestContractStakingCache_MustGetBucketInfo(t *testing.T) {
   678  	// build test condition to add a bucketInfo
   679  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(1).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   680  	cache.PutBucketInfo(1, &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)})
   681  
   682  	tryCatchMustGetBucketInfo := func(i uint64) (v *bucketInfo, err error) {
   683  		defer func() {
   684  			if r := recover(); r != nil {
   685  				err = errors.New(fmt.Sprint(r))
   686  			}
   687  		}()
   688  		v = cache.MustGetBucketInfo(i)
   689  		return
   690  	}
   691  
   692  	r := require.New(t)
   693  	v, err := tryCatchMustGetBucketInfo(1)
   694  	r.NotNil(v)
   695  	r.NoError(err)
   696  
   697  	v, err = tryCatchMustGetBucketInfo(2)
   698  	r.Nil(v)
   699  	r.Error(err)
   700  	r.Equal(err.Error(), "bucket info not found")
   701  }
   702  
   703  func TestContractStakingCache_MustGetBucketType(t *testing.T) {
   704  	// build test condition to add a bucketType
   705  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(1).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   706  	cache.PutBucketType(1, &BucketType{Amount: big.NewInt(100), Duration: 100, ActivatedAt: 1})
   707  
   708  	tryCatchMustGetBucketType := func(i uint64) (v *BucketType, err error) {
   709  		defer func() {
   710  			if r := recover(); r != nil {
   711  				err = errors.New(fmt.Sprint(r))
   712  			}
   713  		}()
   714  		v = cache.MustGetBucketType(i)
   715  		return
   716  	}
   717  
   718  	r := require.New(t)
   719  	v, err := tryCatchMustGetBucketType(1)
   720  	r.NotNil(v)
   721  	r.NoError(err)
   722  
   723  	v, err = tryCatchMustGetBucketType(2)
   724  	r.Nil(v)
   725  	r.Error(err)
   726  	r.Equal(err.Error(), "bucket type not found")
   727  }
   728  
   729  func TestContractStakingCache_DeleteBucketInfo(t *testing.T) {
   730  	// build test condition to add a bucketInfo
   731  	cache := newContractStakingCache(Config{ContractAddress: identityset.Address(1).String(), CalculateVoteWeight: calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts), BlockInterval: _blockInterval})
   732  	bi1 := &bucketInfo{TypeIndex: 1, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(1)}
   733  	bi2 := &bucketInfo{TypeIndex: 2, CreatedAt: 1, UnlockedAt: maxBlockNumber, UnstakedAt: maxBlockNumber, Delegate: identityset.Address(1), Owner: identityset.Address(2)}
   734  	cache.PutBucketInfo(1, bi1)
   735  	cache.PutBucketInfo(2, bi2)
   736  
   737  	cache.DeleteBucketInfo(1)
   738  
   739  	// remove candidate bucket before
   740  	delete(cache.candidateBucketMap, bi2.Delegate.String())
   741  	cache.DeleteBucketInfo(2)
   742  }