diff --git a/blockindex/contractstaking/indexer.go b/blockindex/contractstaking/indexer.go index 67fcbe14a7..8f5004c630 100644 --- a/blockindex/contractstaking/indexer.go +++ b/blockindex/contractstaking/indexer.go @@ -81,36 +81,57 @@ func (s *Indexer) StartHeight() uint64 { // CandidateVotes returns the candidate votes func (s *Indexer) CandidateVotes(candidate address.Address, height uint64) (*big.Int, error) { + if s.isIgnored(height) { + return big.NewInt(0), nil + } return s.cache.CandidateVotes(candidate, height) } // Buckets returns the buckets func (s *Indexer) Buckets(height uint64) ([]*Bucket, error) { + if s.isIgnored(height) { + return []*Bucket{}, nil + } return s.cache.Buckets(height) } // Bucket returns the bucket func (s *Indexer) Bucket(id uint64, height uint64) (*Bucket, bool, error) { + if s.isIgnored(height) { + return nil, false, nil + } return s.cache.Bucket(id, height) } // BucketsByIndices returns the buckets by indices func (s *Indexer) BucketsByIndices(indices []uint64, height uint64) ([]*Bucket, error) { + if s.isIgnored(height) { + return []*Bucket{}, nil + } return s.cache.BucketsByIndices(indices, height) } // BucketsByCandidate returns the buckets by candidate func (s *Indexer) BucketsByCandidate(candidate address.Address, height uint64) ([]*Bucket, error) { + if s.isIgnored(height) { + return []*Bucket{}, nil + } return s.cache.BucketsByCandidate(candidate, height) } // TotalBucketCount returns the total bucket count including active and burnt buckets func (s *Indexer) TotalBucketCount(height uint64) (uint64, error) { + if s.isIgnored(height) { + return 0, nil + } return s.cache.TotalBucketCount(height) } // BucketTypes returns the active bucket types func (s *Indexer) BucketTypes(height uint64) ([]*BucketType, error) { + if s.isIgnored(height) { + return []*BucketType{}, nil + } btMap, err := s.cache.ActiveBucketTypes(height) if err != nil { return nil, err @@ -185,3 +206,10 @@ func (s *Indexer) reloadCache() error { func (s *Indexer) loadFromDB() error { return s.cache.LoadFromDB(s.kvstore) } + +// isIgnored returns true if before cotractDeployHeight. +// it aims to be compatible with blocks between feature hard-fork and contract deployed +// read interface should return empty result instead of invalid height error if it returns true +func (s *Indexer) isIgnored(height uint64) bool { + return height < s.contractDeployHeight +} diff --git a/blockindex/contractstaking/indexer_test.go b/blockindex/contractstaking/indexer_test.go index aaebac04cf..c263cbb640 100644 --- a/blockindex/contractstaking/indexer_test.go +++ b/blockindex/contractstaking/indexer_test.go @@ -8,6 +8,7 @@ package contractstaking import ( "context" "math/big" + "strconv" "sync" "testing" "time" @@ -916,29 +917,66 @@ func TestContractStakingIndexerVotes(t *testing.T) { r.NoError(err) r.Len(bts, 6) }) +} - t.Run("heightRestriction", func(t *testing.T) { - cases := []struct { - height uint64 - valid bool - }{ - {0, true}, - {height - 1, true}, - {height, true}, - {height + 1, false}, - } - for i := range cases { - h := cases[i].height - if cases[i].valid { +func TestIndexer_ReadHeightRestriction(t *testing.T) { + r := require.New(t) + + cases := []struct { + startHeight uint64 + height uint64 + readHeight uint64 + valid bool + }{ + {0, 0, 0, true}, + {0, 0, 1, false}, + {0, 2, 0, true}, + {0, 2, 1, true}, + {0, 2, 2, true}, + {0, 2, 3, false}, + {10, 0, 0, true}, + {10, 0, 1, true}, + {10, 0, 9, true}, + {10, 0, 10, false}, + {10, 0, 11, false}, + {10, 10, 0, true}, + {10, 10, 1, true}, + {10, 10, 9, true}, + {10, 10, 10, true}, + {10, 10, 11, false}, + } + + for idx, c := range cases { + name := strconv.FormatInt(int64(idx), 10) + t.Run(name, func(t *testing.T) { + // Create a new Indexer + height := c.height + startHeight := c.startHeight + cfg := config.Default.DB + dbPath, err := testutil.PathOfTempFile("db") + r.NoError(err) + cfg.DbPath = dbPath + indexer, err := NewContractStakingIndexer(db.NewBoltDB(cfg), identityset.Address(1).String(), startHeight) + r.NoError(err) + r.NoError(indexer.Start(context.Background())) + defer func() { + r.NoError(indexer.Stop(context.Background())) + testutil.CleanupPath(dbPath) + }() + indexer.cache.putHeight(height) + // check read api + h := c.readHeight + delegate := identityset.Address(1) + if c.valid { _, err = indexer.Buckets(h) r.NoError(err) _, err = indexer.BucketTypes(h) r.NoError(err) - _, err = indexer.BucketsByCandidate(delegate1, h) + _, err = indexer.BucketsByCandidate(delegate, h) r.NoError(err) _, err = indexer.BucketsByIndices([]uint64{1, 2, 3, 4, 5, 8}, h) r.NoError(err) - _, err = indexer.CandidateVotes(delegate1, h) + _, err = indexer.CandidateVotes(delegate, h) r.NoError(err) _, _, err = indexer.Bucket(1, h) r.NoError(err) @@ -949,19 +987,19 @@ func TestContractStakingIndexerVotes(t *testing.T) { r.ErrorIs(err, ErrInvalidHeight) _, err = indexer.BucketTypes(h) r.ErrorIs(err, ErrInvalidHeight) - _, err = indexer.BucketsByCandidate(delegate1, h) + _, err = indexer.BucketsByCandidate(delegate, h) r.ErrorIs(err, ErrInvalidHeight) _, err = indexer.BucketsByIndices([]uint64{1, 2, 3, 4, 5, 8}, h) r.ErrorIs(err, ErrInvalidHeight) - _, err = indexer.CandidateVotes(delegate1, h) + _, err = indexer.CandidateVotes(delegate, h) r.ErrorIs(err, ErrInvalidHeight) _, _, err = indexer.Bucket(1, h) r.ErrorIs(err, ErrInvalidHeight) _, err = indexer.TotalBucketCount(h) r.ErrorIs(err, ErrInvalidHeight) } - } - }) + }) + } } func TestIndexer_PutBlock(t *testing.T) { diff --git a/blockindex/sgd_indexer.go b/blockindex/sgd_indexer.go index 290975ceab..5fb1d31ecb 100644 --- a/blockindex/sgd_indexer.go +++ b/blockindex/sgd_indexer.go @@ -496,6 +496,10 @@ func (sgd *sgdRegistry) validateQueryHeight(height uint64) error { if height == 0 { return nil } + // Compatible with blocks between feature hard-fork and contract deployed + if height < sgd.startHeight { + return nil + } tipHeight, err := sgd.height() if err != nil { return err diff --git a/blockindex/sgd_indexer_test.go b/blockindex/sgd_indexer_test.go index f526d1b980..76e96f4401 100644 --- a/blockindex/sgd_indexer_test.go +++ b/blockindex/sgd_indexer_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "math/big" + "strconv" "sync/atomic" "testing" @@ -15,6 +16,8 @@ import ( "github.com/iotexproject/iotex-core/blockchain/block" "github.com/iotexproject/iotex-core/blockchain/genesis" "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/db/batch" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" "github.com/iotexproject/iotex-core/state" "github.com/iotexproject/iotex-core/test/identityset" "github.com/iotexproject/iotex-core/testutil" @@ -109,31 +112,6 @@ func TestNewSGDRegistry(t *testing.T) { r.Equal(receiverAddress, receiver) r.True(isApproved) r.Equal(_sgdPercentage, percentage) - - t.Run("heightRestriction", func(t *testing.T) { - cases := []struct { - height uint64 - isErr bool - }{ - {0, false}, - {1, true}, - {2, false}, - {3, true}, - } - for i := range cases { - if cases[i].isErr { - _, err = sgdRegistry.FetchContracts(ctx, cases[i].height) - r.ErrorContains(err, "invalid height") - _, _, _, err = sgdRegistry.CheckContract(ctx, registerAddress.String(), cases[i].height) - r.ErrorContains(err, "invalid height") - } else { - _, err = sgdRegistry.FetchContracts(ctx, cases[i].height) - r.Nil(err) - _, _, _, err = sgdRegistry.CheckContract(ctx, registerAddress.String(), cases[i].height) - r.Nil(err) - } - } - }) }) t.Run("disapproveContract", func(t *testing.T) { builder := block.NewTestingBuilder() @@ -182,6 +160,89 @@ func TestNewSGDRegistry(t *testing.T) { r.ErrorIs(err, state.ErrStateNotExist) }) }) + t.Run("heightRestriction", func(t *testing.T) { + cases := []struct { + startHeight uint64 + height uint64 + readHeight uint64 + valid bool + }{ + {0, 0, 0, true}, + {0, 0, 1, false}, + {0, 2, 0, true}, + {0, 2, 1, false}, + {0, 2, 2, true}, + {0, 2, 3, false}, + {10, 0, 0, true}, + {10, 0, 1, true}, + {10, 0, 9, true}, + {10, 0, 10, false}, + {10, 0, 11, false}, + {10, 10, 0, true}, + {10, 10, 1, true}, + {10, 10, 9, true}, + {10, 10, 10, true}, + {10, 10, 11, false}, + } + for i := range cases { + name := strconv.FormatInt(int64(i), 10) + t.Run(name, func(t *testing.T) { + testDBPath, err := testutil.PathOfTempFile("sgd") + r.NoError(err) + ctx := context.Background() + cfg := db.DefaultConfig + cfg.DbPath = testDBPath + kvStore := db.NewBoltDB(cfg) + sgdRegistry := &sgdRegistry{ + contract: _testSGDContractAddress, + startHeight: cases[i].startHeight, + kvStore: kvStore, + } + r.NoError(sgdRegistry.Start(ctx)) + defer func() { + r.NoError(sgdRegistry.Stop(ctx)) + testutil.CleanupPath(testDBPath) + }() + // register + nonce := uint64(0) + registerAddress, err := address.FromHex("5b38da6a701c568545dcfcb03fcb875f56beddc4") + r.NoError(err) + builder := block.NewTestingBuilder() + event := _sgdABI.Events["ContractRegistered"] + data, _ := hex.DecodeString("0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000078731d3ca6b7e34ac0f824c42a7cc18a495cabab") + exec, err := action.SignedExecution(_testSGDContractAddress, identityset.PrivateKey(27), atomic.AddUint64(&nonce, 1), big.NewInt(0), 10000000, big.NewInt(9000000000000), data) + r.NoError(err) + h, _ := exec.Hash() + logs := &action.Log{ + Address: _testSGDContractAddress, + Topics: []hash.Hash256{hash.Hash256(event.ID)}, + Data: data, + } + expectHeight, err := sgdRegistry.expectHeight() + r.NoError(err) + blk := createTestingBlock(builder, expectHeight, h, exec, logs) + r.NoError(sgdRegistry.PutBlock(ctx, blk)) + _, _, _, err = sgdRegistry.CheckContract(ctx, registerAddress.String(), 1) + r.NoError(err) + // update height + b := batch.NewBatch() + b.Put(_sgdToHeightNS, _sgdCurrentHeight, byteutil.Uint64ToBytesBigEndian(cases[i].height), "failed to put current height") + sgdRegistry.kvStore.WriteBatch(b) + // check + if !cases[i].valid { + _, err = sgdRegistry.FetchContracts(ctx, cases[i].readHeight) + r.ErrorContains(err, "invalid height") + _, _, _, err = sgdRegistry.CheckContract(ctx, registerAddress.String(), cases[i].readHeight) + r.ErrorContains(err, "invalid height") + } else { + _, err = sgdRegistry.FetchContracts(ctx, cases[i].readHeight) + r.Nil(err) + _, _, _, err = sgdRegistry.CheckContract(ctx, registerAddress.String(), cases[i].readHeight) + r.Nil(err) + } + }) + } + }) } func createTestingBlock(builder *block.TestingBuilder, height uint64, h hash.Hash256, act action.SealedEnvelope, logs *action.Log) *block.Block {