github.com/iotexproject/iotex-core@v1.14.1-rc1/blockindex/contractstaking/indexer_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 contractstaking
     7  
     8  import (
     9  	"context"
    10  	"math/big"
    11  	"strconv"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/ethereum/go-ethereum/common"
    17  	"github.com/iotexproject/iotex-address/address"
    18  	"github.com/stretchr/testify/require"
    19  	"golang.org/x/exp/slices"
    20  
    21  	"github.com/iotexproject/iotex-core/action/protocol"
    22  	"github.com/iotexproject/iotex-core/action/protocol/staking"
    23  	"github.com/iotexproject/iotex-core/blockchain/block"
    24  	"github.com/iotexproject/iotex-core/blockchain/genesis"
    25  	"github.com/iotexproject/iotex-core/config"
    26  	"github.com/iotexproject/iotex-core/consensus/consensusfsm"
    27  	"github.com/iotexproject/iotex-core/db"
    28  	"github.com/iotexproject/iotex-core/test/identityset"
    29  	"github.com/iotexproject/iotex-core/testutil"
    30  )
    31  
    32  const (
    33  	_testStakingContractAddress = "io19ys8f4uhwms6lq6ulexr5fwht9gsjes8mvuugd"
    34  )
    35  
    36  var (
    37  	_blockInterval = consensusfsm.DefaultDardanellesUpgradeConfig.BlockInterval
    38  )
    39  
    40  func TestNewContractStakingIndexer(t *testing.T) {
    41  	r := require.New(t)
    42  
    43  	t.Run("kvStore is nil", func(t *testing.T) {
    44  		_, err := NewContractStakingIndexer(nil, Config{
    45  			ContractAddress:      "io19ys8f4uhwms6lq6ulexr5fwht9gsjes8mvuugd",
    46  			ContractDeployHeight: 0,
    47  			CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
    48  			BlockInterval:        _blockInterval,
    49  		})
    50  		r.Error(err)
    51  		r.Contains(err.Error(), "kv store is nil")
    52  	})
    53  
    54  	t.Run("invalid contract address", func(t *testing.T) {
    55  		kvStore := db.NewMemKVStore()
    56  		_, err := NewContractStakingIndexer(kvStore, Config{
    57  			ContractAddress:      "invalid address",
    58  			ContractDeployHeight: 0,
    59  			CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
    60  			BlockInterval:        _blockInterval,
    61  		})
    62  		r.Error(err)
    63  		r.Contains(err.Error(), "invalid contract address")
    64  	})
    65  
    66  	t.Run("valid input", func(t *testing.T) {
    67  		contractAddr, err := address.FromString("io19ys8f4uhwms6lq6ulexr5fwht9gsjes8mvuugd")
    68  		r.NoError(err)
    69  		indexer, err := NewContractStakingIndexer(db.NewMemKVStore(), Config{
    70  			ContractAddress:      contractAddr.String(),
    71  			ContractDeployHeight: 0,
    72  			CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
    73  			BlockInterval:        _blockInterval,
    74  		})
    75  		r.NoError(err)
    76  		r.NotNil(indexer)
    77  	})
    78  }
    79  
    80  func TestContractStakingIndexerLoadCache(t *testing.T) {
    81  	r := require.New(t)
    82  	testDBPath, err := testutil.PathOfTempFile("staking.db")
    83  	r.NoError(err)
    84  	defer testutil.CleanupPath(testDBPath)
    85  	cfg := db.DefaultConfig
    86  	cfg.DbPath = testDBPath
    87  	kvStore := db.NewBoltDB(cfg)
    88  	indexer, err := NewContractStakingIndexer(kvStore, Config{
    89  		ContractAddress:      _testStakingContractAddress,
    90  		ContractDeployHeight: 0,
    91  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
    92  		BlockInterval:        _blockInterval,
    93  	})
    94  	r.NoError(err)
    95  	r.NoError(indexer.Start(context.Background()))
    96  
    97  	// create a stake
    98  	height := uint64(1)
    99  	startHeight := uint64(1)
   100  	handler := newContractStakingEventHandler(indexer.cache)
   101  	activateBucketType(r, handler, 10, 100, height)
   102  	owner := identityset.Address(0)
   103  	delegate := identityset.Address(1)
   104  	stake(r, handler, owner, delegate, 1, 10, 100, height)
   105  	err = indexer.commit(handler, height)
   106  	r.NoError(err)
   107  	buckets, err := indexer.Buckets(height)
   108  	r.NoError(err)
   109  	r.EqualValues(1, len(buckets))
   110  	tbc, err := indexer.TotalBucketCount(height)
   111  	r.EqualValues(1, tbc)
   112  	r.NoError(err)
   113  	h, err := indexer.Height()
   114  	r.NoError(err)
   115  	r.EqualValues(height, h)
   116  
   117  	r.NoError(indexer.Stop(context.Background()))
   118  
   119  	// load cache from db
   120  	newIndexer, err := NewContractStakingIndexer(db.NewBoltDB(cfg), Config{
   121  		ContractAddress:      _testStakingContractAddress,
   122  		ContractDeployHeight: startHeight,
   123  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   124  		BlockInterval:        _blockInterval,
   125  	})
   126  	r.NoError(err)
   127  	r.NoError(newIndexer.Start(context.Background()))
   128  
   129  	// check cache
   130  	newBuckets, err := newIndexer.Buckets(height)
   131  	r.NoError(err)
   132  	r.Equal(len(buckets), len(newBuckets))
   133  	for i := range buckets {
   134  		r.EqualValues(buckets[i], newBuckets[i])
   135  	}
   136  	newHeight, err := newIndexer.Height()
   137  	r.NoError(err)
   138  	r.Equal(height, newHeight)
   139  	r.Equal(startHeight, newIndexer.StartHeight())
   140  	tbc, err = newIndexer.TotalBucketCount(height)
   141  	r.EqualValues(1, tbc)
   142  	r.NoError(err)
   143  	r.NoError(newIndexer.Stop(context.Background()))
   144  }
   145  
   146  func TestContractStakingIndexerDirty(t *testing.T) {
   147  	r := require.New(t)
   148  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   149  	r.NoError(err)
   150  	defer testutil.CleanupPath(testDBPath)
   151  	cfg := db.DefaultConfig
   152  	cfg.DbPath = testDBPath
   153  	kvStore := db.NewBoltDB(cfg)
   154  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   155  		ContractAddress:      _testStakingContractAddress,
   156  		ContractDeployHeight: 0,
   157  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   158  		BlockInterval:        _blockInterval,
   159  	})
   160  	r.NoError(err)
   161  	r.NoError(indexer.Start(context.Background()))
   162  
   163  	// before commit dirty, the cache should be empty
   164  	height := uint64(1)
   165  	handler := newContractStakingEventHandler(indexer.cache)
   166  	gotHeight, err := indexer.Height()
   167  	r.NoError(err)
   168  	r.EqualValues(0, gotHeight)
   169  	// after commit dirty, the cache should be updated
   170  	err = indexer.commit(handler, height)
   171  	r.NoError(err)
   172  	gotHeight, err = indexer.Height()
   173  	r.NoError(err)
   174  	r.EqualValues(height, gotHeight)
   175  
   176  	r.NoError(indexer.Stop(context.Background()))
   177  }
   178  
   179  func TestContractStakingIndexerThreadSafe(t *testing.T) {
   180  	r := require.New(t)
   181  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   182  	r.NoError(err)
   183  	defer testutil.CleanupPath(testDBPath)
   184  	cfg := db.DefaultConfig
   185  	cfg.DbPath = testDBPath
   186  	kvStore := db.NewBoltDB(cfg)
   187  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   188  		ContractAddress:      _testStakingContractAddress,
   189  		ContractDeployHeight: 0,
   190  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   191  		BlockInterval:        _blockInterval,
   192  	})
   193  	r.NoError(err)
   194  	r.NoError(indexer.Start(context.Background()))
   195  
   196  	wait := sync.WaitGroup{}
   197  	wait.Add(6)
   198  	owner := identityset.Address(0)
   199  	delegate := identityset.Address(1)
   200  	ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: 1}))
   201  	// read concurrently
   202  	for i := 0; i < 5; i++ {
   203  		go func() {
   204  			defer wait.Done()
   205  			for i := 0; i < 1000; i++ {
   206  				_, err := indexer.Buckets(0)
   207  				r.NoError(err)
   208  				_, err = indexer.BucketTypes(0)
   209  				r.NoError(err)
   210  				_, err = indexer.BucketsByCandidate(delegate, 0)
   211  				r.NoError(err)
   212  				indexer.CandidateVotes(ctx, delegate, 0)
   213  				_, err = indexer.Height()
   214  				r.NoError(err)
   215  				indexer.TotalBucketCount(0)
   216  			}
   217  		}()
   218  	}
   219  	// write
   220  	go func() {
   221  		defer wait.Done()
   222  		// activate bucket type
   223  		handler := newContractStakingEventHandler(indexer.cache)
   224  		activateBucketType(r, handler, 10, 100, 1)
   225  		r.NoError(indexer.commit(handler, 1))
   226  		for i := 2; i < 1000; i++ {
   227  			height := uint64(i)
   228  			handler := newContractStakingEventHandler(indexer.cache)
   229  			stake(r, handler, owner, delegate, int64(i), 10, 100, height)
   230  			err := indexer.commit(handler, height)
   231  			r.NoError(err)
   232  		}
   233  	}()
   234  	wait.Wait()
   235  	r.NoError(indexer.Stop(context.Background()))
   236  	// no panic means thread safe
   237  }
   238  
   239  func TestContractStakingIndexerBucketType(t *testing.T) {
   240  	r := require.New(t)
   241  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   242  	r.NoError(err)
   243  	defer testutil.CleanupPath(testDBPath)
   244  	cfg := db.DefaultConfig
   245  	cfg.DbPath = testDBPath
   246  	kvStore := db.NewBoltDB(cfg)
   247  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   248  		ContractAddress:      _testStakingContractAddress,
   249  		ContractDeployHeight: 0,
   250  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   251  		BlockInterval:        _blockInterval,
   252  	})
   253  	r.NoError(err)
   254  	r.NoError(indexer.Start(context.Background()))
   255  
   256  	// activate
   257  	bucketTypeData := [][2]int64{
   258  		{10, 10},
   259  		{20, 10},
   260  		{10, 100},
   261  		{20, 100},
   262  	}
   263  	existInBucketTypeData := func(bt *BucketType) int {
   264  		idx := slices.IndexFunc(bucketTypeData, func(data [2]int64) bool {
   265  			if bt.Amount.Int64() == data[0] && int64(bt.Duration) == data[1] {
   266  				return true
   267  			}
   268  			return false
   269  		})
   270  		return idx
   271  	}
   272  
   273  	height := uint64(1)
   274  	handler := newContractStakingEventHandler(indexer.cache)
   275  	for _, data := range bucketTypeData {
   276  		activateBucketType(r, handler, data[0], data[1], height)
   277  	}
   278  	err = indexer.commit(handler, height)
   279  	r.NoError(err)
   280  	bucketTypes, err := indexer.BucketTypes(height)
   281  	r.NoError(err)
   282  	r.Equal(len(bucketTypeData), len(bucketTypes))
   283  	for _, bt := range bucketTypes {
   284  		r.True(existInBucketTypeData(bt) >= 0)
   285  		r.EqualValues(height, bt.ActivatedAt)
   286  	}
   287  	// deactivate
   288  	height++
   289  	handler = newContractStakingEventHandler(indexer.cache)
   290  	for i := 0; i < 2; i++ {
   291  		data := bucketTypeData[i]
   292  		deactivateBucketType(r, handler, data[0], data[1], height)
   293  	}
   294  	err = indexer.commit(handler, height)
   295  	r.NoError(err)
   296  	bucketTypes, err = indexer.BucketTypes(height)
   297  	r.NoError(err)
   298  	r.Equal(len(bucketTypeData)-2, len(bucketTypes))
   299  	for _, bt := range bucketTypes {
   300  		r.True(existInBucketTypeData(bt) >= 0)
   301  		r.EqualValues(1, bt.ActivatedAt)
   302  	}
   303  	// reactivate
   304  	height++
   305  	handler = newContractStakingEventHandler(indexer.cache)
   306  	for i := 0; i < 2; i++ {
   307  		data := bucketTypeData[i]
   308  		activateBucketType(r, handler, data[0], data[1], height)
   309  	}
   310  	err = indexer.commit(handler, height)
   311  	r.NoError(err)
   312  	bucketTypes, err = indexer.BucketTypes(height)
   313  	r.NoError(err)
   314  	r.Equal(len(bucketTypeData), len(bucketTypes))
   315  	for _, bt := range bucketTypes {
   316  		idx := existInBucketTypeData(bt)
   317  		r.True(idx >= 0)
   318  		if idx < 2 {
   319  			r.EqualValues(height, bt.ActivatedAt)
   320  		} else {
   321  			r.EqualValues(1, bt.ActivatedAt)
   322  		}
   323  	}
   324  	r.NoError(indexer.Stop(context.Background()))
   325  }
   326  
   327  func TestContractStakingIndexerBucketInfo(t *testing.T) {
   328  	r := require.New(t)
   329  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   330  	r.NoError(err)
   331  	defer testutil.CleanupPath(testDBPath)
   332  	cfg := db.DefaultConfig
   333  	cfg.DbPath = testDBPath
   334  	kvStore := db.NewBoltDB(cfg)
   335  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   336  		ContractAddress:      _testStakingContractAddress,
   337  		ContractDeployHeight: 0,
   338  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   339  		BlockInterval:        _blockInterval,
   340  	})
   341  	r.NoError(err)
   342  	r.NoError(indexer.Start(context.Background()))
   343  
   344  	// init bucket type
   345  	bucketTypeData := [][2]int64{
   346  		{10, 10},
   347  		{20, 10},
   348  		{10, 100},
   349  		{20, 100},
   350  	}
   351  	height := uint64(1)
   352  	handler := newContractStakingEventHandler(indexer.cache)
   353  	for _, data := range bucketTypeData {
   354  		activateBucketType(r, handler, data[0], data[1], height)
   355  	}
   356  	err = indexer.commit(handler, height)
   357  	r.NoError(err)
   358  	ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: 1}))
   359  
   360  	// stake
   361  	owner := identityset.Address(0)
   362  	delegate := identityset.Address(1)
   363  	height++
   364  	createHeight := height
   365  	handler = newContractStakingEventHandler(indexer.cache)
   366  	stake(r, handler, owner, delegate, 1, 10, 100, height)
   367  	r.NoError(err)
   368  	r.NoError(indexer.commit(handler, height))
   369  	bucket, ok, err := indexer.Bucket(1, height)
   370  	r.NoError(err)
   371  	r.True(ok)
   372  	r.EqualValues(1, bucket.Index)
   373  	r.EqualValues(owner, bucket.Owner)
   374  	r.EqualValues(delegate, bucket.Candidate)
   375  	r.EqualValues(10, bucket.StakedAmount.Int64())
   376  	r.EqualValues(100, bucket.StakedDurationBlockNumber)
   377  	r.EqualValues(height, bucket.StakeStartBlockHeight)
   378  	r.True(bucket.AutoStake)
   379  	r.EqualValues(height, bucket.CreateBlockHeight)
   380  	r.EqualValues(maxBlockNumber, bucket.UnstakeStartBlockHeight)
   381  	r.EqualValues(_testStakingContractAddress, bucket.ContractAddress)
   382  	votes, err := indexer.CandidateVotes(ctx, delegate, height)
   383  	r.NoError(err)
   384  	r.EqualValues(10, votes.Uint64())
   385  	tbc, err := indexer.TotalBucketCount(height)
   386  	r.NoError(err)
   387  	r.EqualValues(1, tbc)
   388  
   389  	// transfer
   390  	newOwner := identityset.Address(2)
   391  	height++
   392  	handler = newContractStakingEventHandler(indexer.cache)
   393  	transfer(r, handler, newOwner, int64(bucket.Index))
   394  	r.NoError(indexer.commit(handler, height))
   395  	bucket, ok, err = indexer.Bucket(bucket.Index, height)
   396  	r.NoError(err)
   397  	r.True(ok)
   398  	r.EqualValues(newOwner, bucket.Owner)
   399  
   400  	// unlock
   401  	height++
   402  	handler = newContractStakingEventHandler(indexer.cache)
   403  	unlock(r, handler, int64(bucket.Index), height)
   404  	r.NoError(indexer.commit(handler, height))
   405  	bucket, ok, err = indexer.Bucket(bucket.Index, height)
   406  	r.NoError(err)
   407  	r.True(ok)
   408  	r.EqualValues(1, bucket.Index)
   409  	r.EqualValues(newOwner, bucket.Owner)
   410  	r.EqualValues(delegate, bucket.Candidate)
   411  	r.EqualValues(10, bucket.StakedAmount.Int64())
   412  	r.EqualValues(100, bucket.StakedDurationBlockNumber)
   413  	r.EqualValues(height, bucket.StakeStartBlockHeight)
   414  	r.False(bucket.AutoStake)
   415  	r.EqualValues(createHeight, bucket.CreateBlockHeight)
   416  	r.EqualValues(maxBlockNumber, bucket.UnstakeStartBlockHeight)
   417  	r.EqualValues(_testStakingContractAddress, bucket.ContractAddress)
   418  	votes, err = indexer.CandidateVotes(ctx, delegate, height)
   419  	r.NoError(err)
   420  	r.EqualValues(10, votes.Uint64())
   421  	tbc, err = indexer.TotalBucketCount(height)
   422  	r.NoError(err)
   423  	r.EqualValues(1, tbc)
   424  
   425  	// lock again
   426  	height++
   427  	handler = newContractStakingEventHandler(indexer.cache)
   428  	lock(r, handler, int64(bucket.Index), int64(10))
   429  	r.NoError(indexer.commit(handler, height))
   430  	bucket, ok, err = indexer.Bucket(bucket.Index, height)
   431  	r.NoError(err)
   432  	r.True(ok)
   433  	r.EqualValues(1, bucket.Index)
   434  	r.EqualValues(newOwner, bucket.Owner)
   435  	r.EqualValues(delegate, bucket.Candidate)
   436  	r.EqualValues(10, bucket.StakedAmount.Int64())
   437  	r.EqualValues(10, bucket.StakedDurationBlockNumber)
   438  	r.EqualValues(createHeight, bucket.StakeStartBlockHeight)
   439  	r.True(bucket.AutoStake)
   440  	r.EqualValues(createHeight, bucket.CreateBlockHeight)
   441  	r.EqualValues(maxBlockNumber, bucket.UnstakeStartBlockHeight)
   442  	r.EqualValues(_testStakingContractAddress, bucket.ContractAddress)
   443  	votes, err = indexer.CandidateVotes(ctx, delegate, height)
   444  	r.NoError(err)
   445  	r.EqualValues(10, votes.Uint64())
   446  	tbc, err = indexer.TotalBucketCount(height)
   447  	r.NoError(err)
   448  	r.EqualValues(1, tbc)
   449  
   450  	// unstake
   451  	height++
   452  	handler = newContractStakingEventHandler(indexer.cache)
   453  	unlock(r, handler, int64(bucket.Index), height)
   454  	unstake(r, handler, int64(bucket.Index), height)
   455  	r.NoError(indexer.commit(handler, height))
   456  	bucket, ok, err = indexer.Bucket(bucket.Index, height)
   457  	r.NoError(err)
   458  	r.True(ok)
   459  	r.EqualValues(1, bucket.Index)
   460  	r.EqualValues(newOwner, bucket.Owner)
   461  	r.EqualValues(delegate, bucket.Candidate)
   462  	r.EqualValues(10, bucket.StakedAmount.Int64())
   463  	r.EqualValues(10, bucket.StakedDurationBlockNumber)
   464  	r.EqualValues(height, bucket.StakeStartBlockHeight)
   465  	r.False(bucket.AutoStake)
   466  	r.EqualValues(createHeight, bucket.CreateBlockHeight)
   467  	r.EqualValues(height, bucket.UnstakeStartBlockHeight)
   468  	r.EqualValues(_testStakingContractAddress, bucket.ContractAddress)
   469  	votes, err = indexer.CandidateVotes(ctx, delegate, height)
   470  	r.NoError(err)
   471  	r.EqualValues(0, votes.Uint64())
   472  	tbc, err = indexer.TotalBucketCount(height)
   473  	r.NoError(err)
   474  	r.EqualValues(1, tbc)
   475  
   476  	// withdraw
   477  	height++
   478  	handler = newContractStakingEventHandler(indexer.cache)
   479  	withdraw(r, handler, int64(bucket.Index))
   480  	r.NoError(indexer.commit(handler, height))
   481  	bucket, ok, err = indexer.Bucket(bucket.Index, height)
   482  	r.NoError(err)
   483  	r.False(ok)
   484  	votes, err = indexer.CandidateVotes(ctx, delegate, height)
   485  	r.NoError(err)
   486  	r.EqualValues(0, votes.Uint64())
   487  	tbc, err = indexer.TotalBucketCount(height)
   488  	r.NoError(err)
   489  	r.EqualValues(1, tbc)
   490  }
   491  
   492  func TestContractStakingIndexerChangeBucketType(t *testing.T) {
   493  	r := require.New(t)
   494  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   495  	r.NoError(err)
   496  	defer testutil.CleanupPath(testDBPath)
   497  	cfg := db.DefaultConfig
   498  	cfg.DbPath = testDBPath
   499  	kvStore := db.NewBoltDB(cfg)
   500  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   501  		ContractAddress:      _testStakingContractAddress,
   502  		ContractDeployHeight: 0,
   503  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   504  		BlockInterval:        _blockInterval,
   505  	})
   506  	r.NoError(err)
   507  	r.NoError(indexer.Start(context.Background()))
   508  
   509  	// init bucket type
   510  	bucketTypeData := [][2]int64{
   511  		{10, 10},
   512  		{20, 10},
   513  		{10, 100},
   514  		{20, 100},
   515  	}
   516  	height := uint64(1)
   517  	handler := newContractStakingEventHandler(indexer.cache)
   518  	for _, data := range bucketTypeData {
   519  		activateBucketType(r, handler, data[0], data[1], height)
   520  	}
   521  	err = indexer.commit(handler, height)
   522  	r.NoError(err)
   523  
   524  	t.Run("expand bucket type", func(t *testing.T) {
   525  		owner := identityset.Address(0)
   526  		delegate := identityset.Address(1)
   527  		height++
   528  		handler = newContractStakingEventHandler(indexer.cache)
   529  		stake(r, handler, owner, delegate, 1, 10, 100, height)
   530  		r.NoError(err)
   531  		r.NoError(indexer.commit(handler, height))
   532  		bucket, ok, err := indexer.Bucket(1, height)
   533  		r.NoError(err)
   534  		r.True(ok)
   535  
   536  		expandBucketType(r, handler, int64(bucket.Index), 20, 100)
   537  		r.NoError(indexer.commit(handler, height))
   538  		bucket, ok, err = indexer.Bucket(bucket.Index, height)
   539  		r.NoError(err)
   540  		r.True(ok)
   541  		r.EqualValues(20, bucket.StakedAmount.Int64())
   542  		r.EqualValues(100, bucket.StakedDurationBlockNumber)
   543  	})
   544  }
   545  
   546  func TestContractStakingIndexerReadBuckets(t *testing.T) {
   547  	r := require.New(t)
   548  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   549  	r.NoError(err)
   550  	defer testutil.CleanupPath(testDBPath)
   551  	cfg := db.DefaultConfig
   552  	cfg.DbPath = testDBPath
   553  	kvStore := db.NewBoltDB(cfg)
   554  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   555  		ContractAddress:      _testStakingContractAddress,
   556  		ContractDeployHeight: 0,
   557  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   558  		BlockInterval:        _blockInterval,
   559  	})
   560  	r.NoError(err)
   561  	r.NoError(indexer.Start(context.Background()))
   562  
   563  	// init bucket type
   564  	bucketTypeData := [][2]int64{
   565  		{10, 10},
   566  		{20, 10},
   567  		{10, 100},
   568  		{20, 100},
   569  	}
   570  	height := uint64(1)
   571  	handler := newContractStakingEventHandler(indexer.cache)
   572  	for _, data := range bucketTypeData {
   573  		activateBucketType(r, handler, data[0], data[1], height)
   574  	}
   575  	err = indexer.commit(handler, height)
   576  	r.NoError(err)
   577  
   578  	// stake
   579  	stakeData := []struct {
   580  		owner, delegate  int
   581  		amount, duration uint64
   582  	}{
   583  		{1, 2, 10, 10},
   584  		{1, 2, 20, 10},
   585  		{1, 2, 10, 100},
   586  		{1, 2, 20, 100},
   587  		{1, 3, 10, 100},
   588  		{1, 3, 20, 100},
   589  	}
   590  	height++
   591  	handler = newContractStakingEventHandler(indexer.cache)
   592  	for i, data := range stakeData {
   593  		stake(r, handler, identityset.Address(data.owner), identityset.Address(data.delegate), int64(i), int64(data.amount), int64(data.duration), height)
   594  	}
   595  	r.NoError(err)
   596  	r.NoError(indexer.commit(handler, height))
   597  
   598  	t.Run("Buckets", func(t *testing.T) {
   599  		buckets, err := indexer.Buckets(height)
   600  		r.NoError(err)
   601  		r.Len(buckets, len(stakeData))
   602  	})
   603  
   604  	t.Run("BucketsByCandidate", func(t *testing.T) {
   605  		candidateMap := make(map[int]int)
   606  		for i := range stakeData {
   607  			candidateMap[stakeData[i].delegate]++
   608  		}
   609  		for cand := range candidateMap {
   610  			buckets, err := indexer.BucketsByCandidate(identityset.Address(cand), height)
   611  			r.NoError(err)
   612  			r.Len(buckets, candidateMap[cand])
   613  		}
   614  	})
   615  
   616  	t.Run("BucketsByIndices", func(t *testing.T) {
   617  		indices := []uint64{0, 1, 2, 3, 4, 5, 6}
   618  		buckets, err := indexer.BucketsByIndices(indices, height)
   619  		r.NoError(err)
   620  		expectedLen := 0
   621  		for _, idx := range indices {
   622  			if int(idx) < len(stakeData) {
   623  				expectedLen++
   624  			}
   625  		}
   626  		r.Len(buckets, expectedLen)
   627  	})
   628  
   629  	t.Run("TotalBucketCount", func(t *testing.T) {
   630  		tbc, err := indexer.TotalBucketCount(height)
   631  		r.NoError(err)
   632  		r.EqualValues(len(stakeData), tbc)
   633  	})
   634  
   635  	t.Run("CandidateVotes", func(t *testing.T) {
   636  		ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: 1}))
   637  		candidateMap := make(map[int]int64)
   638  		for i := range stakeData {
   639  			candidateMap[stakeData[i].delegate] += int64(stakeData[i].amount)
   640  		}
   641  		candidates := []int{1, 2, 3}
   642  		for _, cand := range candidates {
   643  			votes := candidateMap[cand]
   644  			cvotes, err := indexer.CandidateVotes(ctx, identityset.Address(cand), height)
   645  			r.NoError(err)
   646  			r.EqualValues(votes, cvotes.Uint64())
   647  		}
   648  	})
   649  }
   650  
   651  func TestContractStakingIndexerCacheClean(t *testing.T) {
   652  	r := require.New(t)
   653  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   654  	r.NoError(err)
   655  	defer testutil.CleanupPath(testDBPath)
   656  	cfg := db.DefaultConfig
   657  	cfg.DbPath = testDBPath
   658  	kvStore := db.NewBoltDB(cfg)
   659  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   660  		ContractAddress:      _testStakingContractAddress,
   661  		ContractDeployHeight: 0,
   662  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   663  		BlockInterval:        _blockInterval,
   664  	})
   665  	r.NoError(err)
   666  	r.NoError(indexer.Start(context.Background()))
   667  
   668  	// init bucket type
   669  	height := uint64(1)
   670  	handler := newContractStakingEventHandler(indexer.cache)
   671  	activateBucketType(r, handler, 10, 10, height)
   672  	activateBucketType(r, handler, 20, 20, height)
   673  	// create bucket
   674  	owner := identityset.Address(10)
   675  	delegate1 := identityset.Address(1)
   676  	delegate2 := identityset.Address(2)
   677  	stake(r, handler, owner, delegate1, 1, 10, 10, height)
   678  	stake(r, handler, owner, delegate1, 2, 20, 20, height)
   679  	stake(r, handler, owner, delegate2, 3, 20, 20, height)
   680  	stake(r, handler, owner, delegate2, 4, 20, 20, height)
   681  	abt, err := indexer.cache.ActiveBucketTypes(height - 1)
   682  	r.NoError(err)
   683  	r.Len(abt, 0)
   684  	bts, err := indexer.cache.Buckets(height - 1)
   685  	r.NoError(err)
   686  	r.Len(bts, 0)
   687  	r.NoError(indexer.commit(handler, height))
   688  	abt, err = indexer.cache.ActiveBucketTypes(height)
   689  	r.NoError(err)
   690  	r.Len(abt, 2)
   691  	bts, err = indexer.cache.Buckets(height)
   692  	r.NoError(err)
   693  	r.Len(bts, 4)
   694  
   695  	height++
   696  	handler = newContractStakingEventHandler(indexer.cache)
   697  	changeDelegate(r, handler, delegate1, 3)
   698  	transfer(r, handler, delegate1, 1)
   699  	bt, ok, err := indexer.Bucket(3, height-1)
   700  	r.NoError(err)
   701  	r.True(ok)
   702  	r.Equal(delegate2.String(), bt.Candidate.String())
   703  	bt, ok, err = indexer.Bucket(1, height-1)
   704  	r.NoError(err)
   705  	r.True(ok)
   706  	r.Equal(owner.String(), bt.Owner.String())
   707  	r.NoError(indexer.commit(handler, height))
   708  	bt, ok, err = indexer.Bucket(3, height)
   709  	r.NoError(err)
   710  	r.True(ok)
   711  	r.Equal(delegate1.String(), bt.Candidate.String())
   712  	bt, ok, err = indexer.Bucket(1, height)
   713  	r.NoError(err)
   714  	r.True(ok)
   715  	r.Equal(delegate1.String(), bt.Owner.String())
   716  }
   717  
   718  func TestContractStakingIndexerVotes(t *testing.T) {
   719  	r := require.New(t)
   720  	testDBPath, err := testutil.PathOfTempFile("staking.db")
   721  	r.NoError(err)
   722  	defer testutil.CleanupPath(testDBPath)
   723  	cfg := db.DefaultConfig
   724  	cfg.DbPath = testDBPath
   725  	kvStore := db.NewBoltDB(cfg)
   726  	indexer, err := NewContractStakingIndexer(kvStore, Config{
   727  		ContractAddress:      _testStakingContractAddress,
   728  		ContractDeployHeight: 0,
   729  		CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
   730  		BlockInterval:        _blockInterval,
   731  	})
   732  	r.NoError(err)
   733  	r.NoError(indexer.Start(context.Background()))
   734  	ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: 1}))
   735  
   736  	// init bucket type
   737  	height := uint64(1)
   738  	handler := newContractStakingEventHandler(indexer.cache)
   739  	activateBucketType(r, handler, 10, 10, height)
   740  	activateBucketType(r, handler, 20, 20, height)
   741  	activateBucketType(r, handler, 30, 20, height)
   742  	activateBucketType(r, handler, 60, 20, height)
   743  	// create bucket
   744  	owner := identityset.Address(10)
   745  	delegate1 := identityset.Address(1)
   746  	delegate2 := identityset.Address(2)
   747  	stake(r, handler, owner, delegate1, 1, 10, 10, height)
   748  	stake(r, handler, owner, delegate1, 2, 20, 20, height)
   749  	stake(r, handler, owner, delegate2, 3, 20, 20, height)
   750  	stake(r, handler, owner, delegate2, 4, 20, 20, height)
   751  	r.NoError(indexer.commit(handler, height))
   752  	votes, err := indexer.CandidateVotes(ctx, delegate1, height)
   753  	r.NoError(err)
   754  	r.EqualValues(30, votes.Uint64())
   755  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   756  	r.NoError(err)
   757  	r.EqualValues(40, votes.Uint64())
   758  	votes, err = indexer.CandidateVotes(ctx, owner, height)
   759  	r.EqualValues(0, votes.Uint64())
   760  
   761  	// change delegate bucket 3 to delegate1
   762  	height++
   763  	handler = newContractStakingEventHandler(indexer.cache)
   764  	changeDelegate(r, handler, delegate1, 3)
   765  	r.NoError(indexer.commit(handler, height))
   766  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   767  	r.NoError(err)
   768  	r.EqualValues(50, votes.Uint64())
   769  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   770  	r.NoError(err)
   771  	r.EqualValues(20, votes.Uint64())
   772  
   773  	// unlock bucket 1 & 4
   774  	height++
   775  	handler = newContractStakingEventHandler(indexer.cache)
   776  	unlock(r, handler, 1, height)
   777  	unlock(r, handler, 4, height)
   778  	r.NoError(indexer.commit(handler, height))
   779  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   780  	r.NoError(err)
   781  	r.EqualValues(50, votes.Uint64())
   782  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   783  	r.NoError(err)
   784  	r.EqualValues(20, votes.Uint64())
   785  
   786  	// unstake bucket 1 & lock 4
   787  	height++
   788  	handler = newContractStakingEventHandler(indexer.cache)
   789  	unstake(r, handler, 1, height)
   790  	lock(r, handler, 4, 20)
   791  	r.NoError(indexer.commit(handler, height))
   792  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   793  	r.NoError(err)
   794  	r.EqualValues(40, votes.Uint64())
   795  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   796  	r.NoError(err)
   797  	r.EqualValues(20, votes.Uint64())
   798  
   799  	// expand bucket 2
   800  	height++
   801  	handler = newContractStakingEventHandler(indexer.cache)
   802  	expandBucketType(r, handler, 2, 30, 20)
   803  	r.NoError(indexer.commit(handler, height))
   804  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   805  	r.NoError(err)
   806  	r.EqualValues(50, votes.Uint64())
   807  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   808  	r.NoError(err)
   809  	r.EqualValues(20, votes.Uint64())
   810  
   811  	// transfer bucket 4
   812  	height++
   813  	handler = newContractStakingEventHandler(indexer.cache)
   814  	transfer(r, handler, delegate2, 4)
   815  	r.NoError(indexer.commit(handler, height))
   816  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   817  	r.NoError(err)
   818  	r.EqualValues(50, votes.Uint64())
   819  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   820  	r.NoError(err)
   821  	r.EqualValues(20, votes.Uint64())
   822  
   823  	// create bucket 5, 6, 7
   824  	height++
   825  	handler = newContractStakingEventHandler(indexer.cache)
   826  	stake(r, handler, owner, delegate2, 5, 20, 20, height)
   827  	stake(r, handler, owner, delegate2, 6, 20, 20, height)
   828  	stake(r, handler, owner, delegate2, 7, 20, 20, height)
   829  	r.NoError(indexer.commit(handler, height))
   830  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   831  	r.NoError(err)
   832  	r.EqualValues(50, votes.Uint64())
   833  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   834  	r.NoError(err)
   835  	r.EqualValues(80, votes.Uint64())
   836  
   837  	// merge bucket 5, 6, 7
   838  	height++
   839  	handler = newContractStakingEventHandler(indexer.cache)
   840  	mergeBuckets(r, handler, []int64{5, 6, 7}, 60, 20)
   841  	r.NoError(indexer.commit(handler, height))
   842  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   843  	r.NoError(err)
   844  	r.EqualValues(50, votes.Uint64())
   845  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   846  	r.NoError(err)
   847  	r.EqualValues(80, votes.Uint64())
   848  
   849  	// unlock & unstake 5
   850  	height++
   851  	handler = newContractStakingEventHandler(indexer.cache)
   852  	unlock(r, handler, 5, height)
   853  	unstake(r, handler, 5, height)
   854  	r.NoError(indexer.commit(handler, height))
   855  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   856  	r.NoError(err)
   857  	r.EqualValues(50, votes.Uint64())
   858  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   859  	r.NoError(err)
   860  	r.EqualValues(20, votes.Uint64())
   861  
   862  	// create & merge bucket 8, 9, 10
   863  	height++
   864  	handler = newContractStakingEventHandler(indexer.cache)
   865  	stake(r, handler, owner, delegate1, 8, 20, 20, height)
   866  	stake(r, handler, owner, delegate2, 9, 20, 20, height)
   867  	stake(r, handler, owner, delegate2, 10, 20, 20, height)
   868  	mergeBuckets(r, handler, []int64{8, 9, 10}, 60, 20)
   869  	r.NoError(indexer.commit(handler, height))
   870  	votes, err = indexer.CandidateVotes(ctx, delegate1, height)
   871  	r.NoError(err)
   872  	r.EqualValues(110, votes.Uint64())
   873  	votes, err = indexer.CandidateVotes(ctx, delegate2, height)
   874  	r.NoError(err)
   875  	r.EqualValues(20, votes.Uint64())
   876  
   877  	t.Run("Height", func(t *testing.T) {
   878  		h, err := indexer.Height()
   879  		r.NoError(err)
   880  		r.EqualValues(height, h)
   881  	})
   882  
   883  	t.Run("BucketTypes", func(t *testing.T) {
   884  		bts, err := indexer.BucketTypes(height)
   885  		r.NoError(err)
   886  		r.Len(bts, 4)
   887  		slices.SortFunc(bts, func(i, j *staking.ContractStakingBucketType) bool {
   888  			return i.Amount.Int64() < j.Amount.Int64()
   889  		})
   890  		r.EqualValues(10, bts[0].Duration)
   891  		r.EqualValues(20, bts[1].Duration)
   892  		r.EqualValues(20, bts[2].Duration)
   893  		r.EqualValues(20, bts[3].Duration)
   894  		r.EqualValues(10, bts[0].Amount.Int64())
   895  		r.EqualValues(20, bts[1].Amount.Int64())
   896  		r.EqualValues(30, bts[2].Amount.Int64())
   897  		r.EqualValues(60, bts[3].Amount.Int64())
   898  	})
   899  
   900  	t.Run("Buckets", func(t *testing.T) {
   901  		bts, err := indexer.Buckets(height)
   902  		r.NoError(err)
   903  		r.Len(bts, 6)
   904  		slices.SortFunc(bts, func(i, j *staking.VoteBucket) bool {
   905  			return i.Index < j.Index
   906  		})
   907  		r.EqualValues(1, bts[0].Index)
   908  		r.EqualValues(2, bts[1].Index)
   909  		r.EqualValues(3, bts[2].Index)
   910  		r.EqualValues(4, bts[3].Index)
   911  		r.EqualValues(5, bts[4].Index)
   912  		r.EqualValues(8, bts[5].Index)
   913  		r.EqualValues(10*_blockInterval, bts[0].StakedDuration)
   914  		r.EqualValues(20*_blockInterval, bts[1].StakedDuration)
   915  		r.EqualValues(20*_blockInterval, bts[2].StakedDuration)
   916  		r.EqualValues(20*_blockInterval, bts[3].StakedDuration)
   917  		r.EqualValues(20*_blockInterval, bts[4].StakedDuration)
   918  		r.EqualValues(20*_blockInterval, bts[5].StakedDuration)
   919  		r.EqualValues(10, bts[0].StakedDurationBlockNumber)
   920  		r.EqualValues(20, bts[1].StakedDurationBlockNumber)
   921  		r.EqualValues(20, bts[2].StakedDurationBlockNumber)
   922  		r.EqualValues(20, bts[3].StakedDurationBlockNumber)
   923  		r.EqualValues(20, bts[4].StakedDurationBlockNumber)
   924  		r.EqualValues(20, bts[5].StakedDurationBlockNumber)
   925  		r.EqualValues(10, bts[0].StakedAmount.Int64())
   926  		r.EqualValues(30, bts[1].StakedAmount.Int64())
   927  		r.EqualValues(20, bts[2].StakedAmount.Int64())
   928  		r.EqualValues(20, bts[3].StakedAmount.Int64())
   929  		r.EqualValues(60, bts[4].StakedAmount.Int64())
   930  		r.EqualValues(60, bts[5].StakedAmount.Int64())
   931  		r.EqualValues(delegate1.String(), bts[0].Candidate.String())
   932  		r.EqualValues(delegate1.String(), bts[1].Candidate.String())
   933  		r.EqualValues(delegate1.String(), bts[2].Candidate.String())
   934  		r.EqualValues(delegate2.String(), bts[3].Candidate.String())
   935  		r.EqualValues(delegate2.String(), bts[4].Candidate.String())
   936  		r.EqualValues(delegate1.String(), bts[5].Candidate.String())
   937  		r.EqualValues(owner.String(), bts[0].Owner.String())
   938  		r.EqualValues(owner.String(), bts[1].Owner.String())
   939  		r.EqualValues(owner.String(), bts[2].Owner.String())
   940  		r.EqualValues(delegate2.String(), bts[3].Owner.String())
   941  		r.EqualValues(owner.String(), bts[4].Owner.String())
   942  		r.EqualValues(owner.String(), bts[5].Owner.String())
   943  		r.False(bts[0].AutoStake)
   944  		r.True(bts[1].AutoStake)
   945  		r.True(bts[2].AutoStake)
   946  		r.True(bts[3].AutoStake)
   947  		r.False(bts[4].AutoStake)
   948  		r.True(bts[5].AutoStake)
   949  		r.EqualValues(1, bts[0].CreateBlockHeight)
   950  		r.EqualValues(1, bts[1].CreateBlockHeight)
   951  		r.EqualValues(1, bts[2].CreateBlockHeight)
   952  		r.EqualValues(1, bts[3].CreateBlockHeight)
   953  		r.EqualValues(7, bts[4].CreateBlockHeight)
   954  		r.EqualValues(10, bts[5].CreateBlockHeight)
   955  		r.EqualValues(3, bts[0].StakeStartBlockHeight)
   956  		r.EqualValues(1, bts[1].StakeStartBlockHeight)
   957  		r.EqualValues(1, bts[2].StakeStartBlockHeight)
   958  		r.EqualValues(1, bts[3].StakeStartBlockHeight)
   959  		r.EqualValues(9, bts[4].StakeStartBlockHeight)
   960  		r.EqualValues(10, bts[5].StakeStartBlockHeight)
   961  		r.EqualValues(4, bts[0].UnstakeStartBlockHeight)
   962  		r.EqualValues(maxBlockNumber, bts[1].UnstakeStartBlockHeight)
   963  		r.EqualValues(maxBlockNumber, bts[2].UnstakeStartBlockHeight)
   964  		r.EqualValues(maxBlockNumber, bts[3].UnstakeStartBlockHeight)
   965  		r.EqualValues(9, bts[4].UnstakeStartBlockHeight)
   966  		r.EqualValues(maxBlockNumber, bts[5].UnstakeStartBlockHeight)
   967  		for _, b := range bts {
   968  			r.EqualValues(time.Time{}, b.CreateTime)
   969  			r.EqualValues(time.Time{}, b.StakeStartTime)
   970  			r.EqualValues(time.Time{}, b.UnstakeStartTime)
   971  			r.EqualValues(_testStakingContractAddress, b.ContractAddress)
   972  		}
   973  	})
   974  
   975  	t.Run("BucketsByCandidate", func(t *testing.T) {
   976  		d1Bts, err := indexer.BucketsByCandidate(delegate1, height)
   977  		r.NoError(err)
   978  		r.Len(d1Bts, 4)
   979  		slices.SortFunc(d1Bts, func(i, j *staking.VoteBucket) bool {
   980  			return i.Index < j.Index
   981  		})
   982  		r.EqualValues(1, d1Bts[0].Index)
   983  		r.EqualValues(2, d1Bts[1].Index)
   984  		r.EqualValues(3, d1Bts[2].Index)
   985  		r.EqualValues(8, d1Bts[3].Index)
   986  		d2Bts, err := indexer.BucketsByCandidate(delegate2, height)
   987  		r.NoError(err)
   988  		r.Len(d2Bts, 2)
   989  		slices.SortFunc(d2Bts, func(i, j *staking.VoteBucket) bool {
   990  			return i.Index < j.Index
   991  		})
   992  		r.EqualValues(4, d2Bts[0].Index)
   993  		r.EqualValues(5, d2Bts[1].Index)
   994  	})
   995  
   996  	t.Run("BucketsByIndices", func(t *testing.T) {
   997  		bts, err := indexer.BucketsByIndices([]uint64{1, 2, 3, 4, 5, 8}, height)
   998  		r.NoError(err)
   999  		r.Len(bts, 6)
  1000  	})
  1001  }
  1002  
  1003  func TestIndexer_ReadHeightRestriction(t *testing.T) {
  1004  	r := require.New(t)
  1005  
  1006  	cases := []struct {
  1007  		startHeight uint64
  1008  		height      uint64
  1009  		readHeight  uint64
  1010  		valid       bool
  1011  	}{
  1012  		{0, 0, 0, true},
  1013  		{0, 0, 1, false},
  1014  		{0, 2, 0, true},
  1015  		{0, 2, 1, true},
  1016  		{0, 2, 2, true},
  1017  		{0, 2, 3, false},
  1018  		{10, 0, 0, true},
  1019  		{10, 0, 1, true},
  1020  		{10, 0, 9, true},
  1021  		{10, 0, 10, false},
  1022  		{10, 0, 11, false},
  1023  		{10, 10, 0, true},
  1024  		{10, 10, 1, true},
  1025  		{10, 10, 9, true},
  1026  		{10, 10, 10, true},
  1027  		{10, 10, 11, false},
  1028  	}
  1029  
  1030  	for idx, c := range cases {
  1031  		name := strconv.FormatInt(int64(idx), 10)
  1032  		t.Run(name, func(t *testing.T) {
  1033  			// Create a new Indexer
  1034  			height := c.height
  1035  			startHeight := c.startHeight
  1036  			cfg := config.Default.DB
  1037  			dbPath, err := testutil.PathOfTempFile("db")
  1038  			r.NoError(err)
  1039  			cfg.DbPath = dbPath
  1040  			indexer, err := NewContractStakingIndexer(db.NewBoltDB(cfg), Config{
  1041  				ContractAddress:      identityset.Address(1).String(),
  1042  				ContractDeployHeight: startHeight,
  1043  				CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
  1044  				BlockInterval:        _blockInterval,
  1045  			})
  1046  			r.NoError(err)
  1047  			r.NoError(indexer.Start(context.Background()))
  1048  			defer func() {
  1049  				r.NoError(indexer.Stop(context.Background()))
  1050  				testutil.CleanupPath(dbPath)
  1051  			}()
  1052  			indexer.cache.putHeight(height)
  1053  			// check read api
  1054  			ctx := protocol.WithFeatureCtx(protocol.WithBlockCtx(genesis.WithGenesisContext(context.Background(), genesis.Default), protocol.BlockCtx{BlockHeight: 1}))
  1055  			h := c.readHeight
  1056  			delegate := identityset.Address(1)
  1057  			if c.valid {
  1058  				_, err = indexer.Buckets(h)
  1059  				r.NoError(err)
  1060  				_, err = indexer.BucketTypes(h)
  1061  				r.NoError(err)
  1062  				_, err = indexer.BucketsByCandidate(delegate, h)
  1063  				r.NoError(err)
  1064  				_, err = indexer.BucketsByIndices([]uint64{1, 2, 3, 4, 5, 8}, h)
  1065  				r.NoError(err)
  1066  				_, err = indexer.CandidateVotes(ctx, delegate, h)
  1067  				r.NoError(err)
  1068  				_, _, err = indexer.Bucket(1, h)
  1069  				r.NoError(err)
  1070  				_, err = indexer.TotalBucketCount(h)
  1071  				r.NoError(err)
  1072  			} else {
  1073  				_, err = indexer.Buckets(h)
  1074  				r.ErrorIs(err, ErrInvalidHeight)
  1075  				_, err = indexer.BucketTypes(h)
  1076  				r.ErrorIs(err, ErrInvalidHeight)
  1077  				_, err = indexer.BucketsByCandidate(delegate, h)
  1078  				r.ErrorIs(err, ErrInvalidHeight)
  1079  				_, err = indexer.BucketsByIndices([]uint64{1, 2, 3, 4, 5, 8}, h)
  1080  				r.ErrorIs(err, ErrInvalidHeight)
  1081  				_, err = indexer.CandidateVotes(ctx, delegate, h)
  1082  				r.ErrorIs(err, ErrInvalidHeight)
  1083  				_, _, err = indexer.Bucket(1, h)
  1084  				r.ErrorIs(err, ErrInvalidHeight)
  1085  				_, err = indexer.TotalBucketCount(h)
  1086  				r.ErrorIs(err, ErrInvalidHeight)
  1087  			}
  1088  		})
  1089  	}
  1090  }
  1091  
  1092  func TestIndexer_PutBlock(t *testing.T) {
  1093  	r := require.New(t)
  1094  
  1095  	cases := []struct {
  1096  		name           string
  1097  		height         uint64
  1098  		startHeight    uint64
  1099  		blockHeight    uint64
  1100  		expectedHeight uint64
  1101  		errMsg         string
  1102  	}{
  1103  		{"block < height < start", 10, 20, 9, 10, ""},
  1104  		{"block = height < start", 10, 20, 10, 10, ""},
  1105  		{"height < block < start", 10, 20, 11, 10, ""},
  1106  		{"height < block = start", 10, 20, 20, 20, ""},
  1107  		{"height < start < block", 10, 20, 21, 10, "invalid block height 21, expect 20"},
  1108  		{"block < start < height", 20, 10, 9, 20, ""},
  1109  		{"block = start < height", 20, 10, 10, 20, ""},
  1110  		{"start < block < height", 20, 10, 11, 20, ""},
  1111  		{"start < block = height", 20, 10, 20, 20, ""},
  1112  		{"start < height < block", 20, 10, 21, 21, ""},
  1113  		{"start < height < block+", 20, 10, 22, 20, "invalid block height 22, expect 21"},
  1114  	}
  1115  
  1116  	for _, c := range cases {
  1117  		t.Run(c.name, func(t *testing.T) {
  1118  			// Create a new Indexer
  1119  			height := c.height
  1120  			startHeight := c.startHeight
  1121  			cfg := config.Default.DB
  1122  			dbPath, err := testutil.PathOfTempFile("db")
  1123  			r.NoError(err)
  1124  			cfg.DbPath = dbPath
  1125  			indexer, err := NewContractStakingIndexer(db.NewBoltDB(cfg), Config{
  1126  				ContractAddress:      identityset.Address(1).String(),
  1127  				ContractDeployHeight: startHeight,
  1128  				CalculateVoteWeight:  calculateVoteWeightGen(genesis.Default.VoteWeightCalConsts),
  1129  				BlockInterval:        _blockInterval,
  1130  			})
  1131  			r.NoError(err)
  1132  			r.NoError(indexer.Start(context.Background()))
  1133  			defer func() {
  1134  				r.NoError(indexer.Stop(context.Background()))
  1135  				testutil.CleanupPath(dbPath)
  1136  			}()
  1137  			indexer.cache.putHeight(height)
  1138  			// Create a mock block
  1139  			builder := block.NewBuilder(block.NewRunnableActionsBuilder().Build())
  1140  			builder.SetHeight(c.blockHeight)
  1141  			blk, err := builder.SignAndBuild(identityset.PrivateKey(1))
  1142  			r.NoError(err)
  1143  			// Put the block
  1144  			err = indexer.PutBlock(context.Background(), &blk)
  1145  			if c.errMsg != "" {
  1146  				r.ErrorContains(err, c.errMsg)
  1147  			} else {
  1148  				r.NoError(err)
  1149  			}
  1150  			// Check the block height
  1151  			r.EqualValues(c.expectedHeight, indexer.cache.Height())
  1152  		})
  1153  	}
  1154  
  1155  }
  1156  
  1157  func BenchmarkIndexer_PutBlockBeforeContractHeight(b *testing.B) {
  1158  	// Create a new Indexer with a contract height of 100
  1159  	indexer := &Indexer{config: Config{ContractDeployHeight: 100}}
  1160  
  1161  	// Create a mock block with a height of 50
  1162  	blk := &block.Block{}
  1163  
  1164  	// Run the benchmark
  1165  	b.ResetTimer()
  1166  	for i := 0; i < b.N; i++ {
  1167  		err := indexer.PutBlock(context.Background(), blk)
  1168  		if err != nil {
  1169  			b.Fatal(err)
  1170  		}
  1171  	}
  1172  }
  1173  
  1174  func activateBucketType(r *require.Assertions, handler *contractStakingEventHandler, amount, duration int64, height uint64) {
  1175  	err := handler.handleBucketTypeActivatedEvent(eventParam{
  1176  		"amount":   big.NewInt(amount),
  1177  		"duration": big.NewInt(duration),
  1178  	}, height)
  1179  	r.NoError(err)
  1180  }
  1181  
  1182  func deactivateBucketType(r *require.Assertions, handler *contractStakingEventHandler, amount, duration int64, height uint64) {
  1183  	err := handler.handleBucketTypeDeactivatedEvent(eventParam{
  1184  		"amount":   big.NewInt(amount),
  1185  		"duration": big.NewInt(duration),
  1186  	}, height)
  1187  	r.NoError(err)
  1188  }
  1189  
  1190  func stake(r *require.Assertions, handler *contractStakingEventHandler, owner, candidate address.Address, token, amount, duration int64, height uint64) {
  1191  	err := handler.handleTransferEvent(eventParam{
  1192  		"to":      common.BytesToAddress(owner.Bytes()),
  1193  		"tokenId": big.NewInt(token),
  1194  	})
  1195  	r.NoError(err)
  1196  	err = handler.handleStakedEvent(eventParam{
  1197  		"tokenId":  big.NewInt(token),
  1198  		"delegate": common.BytesToAddress(candidate.Bytes()),
  1199  		"amount":   big.NewInt(amount),
  1200  		"duration": big.NewInt(duration),
  1201  	}, height)
  1202  	r.NoError(err)
  1203  }
  1204  
  1205  func unlock(r *require.Assertions, handler *contractStakingEventHandler, token int64, height uint64) {
  1206  	err := handler.handleUnlockedEvent(eventParam{
  1207  		"tokenId": big.NewInt(token),
  1208  	}, height)
  1209  	r.NoError(err)
  1210  }
  1211  
  1212  func lock(r *require.Assertions, handler *contractStakingEventHandler, token, duration int64) {
  1213  	err := handler.handleLockedEvent(eventParam{
  1214  		"tokenId":  big.NewInt(token),
  1215  		"duration": big.NewInt(duration),
  1216  	})
  1217  	r.NoError(err)
  1218  }
  1219  
  1220  func unstake(r *require.Assertions, handler *contractStakingEventHandler, token int64, height uint64) {
  1221  	err := handler.handleUnstakedEvent(eventParam{
  1222  		"tokenId": big.NewInt(token),
  1223  	}, height)
  1224  	r.NoError(err)
  1225  }
  1226  
  1227  func withdraw(r *require.Assertions, handler *contractStakingEventHandler, token int64) {
  1228  	err := handler.handleWithdrawalEvent(eventParam{
  1229  		"tokenId": big.NewInt(token),
  1230  	})
  1231  	r.NoError(err)
  1232  }
  1233  
  1234  func expandBucketType(r *require.Assertions, handler *contractStakingEventHandler, token, amount, duration int64) {
  1235  	err := handler.handleBucketExpandedEvent(eventParam{
  1236  		"tokenId":  big.NewInt(token),
  1237  		"amount":   big.NewInt(amount),
  1238  		"duration": big.NewInt(duration),
  1239  	})
  1240  	r.NoError(err)
  1241  }
  1242  
  1243  func transfer(r *require.Assertions, handler *contractStakingEventHandler, owner address.Address, token int64) {
  1244  	err := handler.handleTransferEvent(eventParam{
  1245  		"to":      common.BytesToAddress(owner.Bytes()),
  1246  		"tokenId": big.NewInt(token),
  1247  	})
  1248  	r.NoError(err)
  1249  }
  1250  
  1251  func changeDelegate(r *require.Assertions, handler *contractStakingEventHandler, delegate address.Address, token int64) {
  1252  	err := handler.handleDelegateChangedEvent(eventParam{
  1253  		"newDelegate": common.BytesToAddress(delegate.Bytes()),
  1254  		"tokenId":     big.NewInt(token),
  1255  	})
  1256  	r.NoError(err)
  1257  }
  1258  
  1259  func mergeBuckets(r *require.Assertions, handler *contractStakingEventHandler, tokenIds []int64, amount, duration int64) {
  1260  	tokens := make([]*big.Int, len(tokenIds))
  1261  	for i, token := range tokenIds {
  1262  		tokens[i] = big.NewInt(token)
  1263  	}
  1264  	err := handler.handleMergedEvent(eventParam{
  1265  		"amount":   big.NewInt(amount),
  1266  		"duration": big.NewInt(duration),
  1267  		"tokenIds": tokens,
  1268  	})
  1269  	r.NoError(err)
  1270  }