Skip to content

Commit

Permalink
indexers : add test cases for initConsistentUtreexoState and handle e…
Browse files Browse the repository at this point in the history
…dge condition

This commit improves initConsistentUtreexoState by:
1. Adding test cases for edge condition (e.g., tipHeight < 0, tipHash == nil).
2. Implementing explicit checks for invalid states:
   - Return early if tipHeight < 0.
   - Return an error if tipHash is nil.
3. Adding wrapper functions in indexers_test.go for reusable test chain setups.

These changes improve robustness, edge case handling, and test environment reusability.
  • Loading branch information
1amhesus committed Jan 2, 2025
1 parent 7111fb7 commit ae07014
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 2 deletions.
16 changes: 14 additions & 2 deletions blockchain/indexers/indexers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ func createDB(dbName string) (database.DB, string, error) {
return db, dbPath, nil
}

func CreateDBWrapper(dbName string) (database.DB, string, error) {
return createDB(dbName)
}

func initIndexes(dbPath string, db database.DB, params *chaincfg.Params) (
*Manager, []Indexer, error) {

Expand All @@ -91,11 +95,15 @@ func initIndexes(dbPath string, db database.DB, params *chaincfg.Params) (
return indexManager, indexes, nil
}

func InitIndexesWrapper(dbPath string, db database.DB, params *chaincfg.Params) (*Manager, []Indexer, error) {
return initIndexes(dbPath, db, params)
}

func indexersTestChain(testName string) (*blockchain.BlockChain, []Indexer, *chaincfg.Params, *Manager, func()) {
params := chaincfg.RegressionNetParams
params.CoinbaseMaturity = 1

db, dbPath, err := createDB(testName)
db, dbPath, err := CreateDBWrapper(testName)
tearDown := func() {
db.Close()
os.RemoveAll(dbPath)
Expand All @@ -107,7 +115,7 @@ func indexersTestChain(testName string) (*blockchain.BlockChain, []Indexer, *cha
}

// Create the indexes to be used in the chain.
indexManager, indexes, err := initIndexes(dbPath, db, &params)
indexManager, indexes, err := InitIndexesWrapper(dbPath, db, &params)
if err != nil {
tearDown()
os.RemoveAll(testDbRoot)
Expand All @@ -133,6 +141,10 @@ func indexersTestChain(testName string) (*blockchain.BlockChain, []Indexer, *cha
return chain, indexes, &params, indexManager, tearDown
}

func IndexersTestChainWrapper(testName string) (*blockchain.BlockChain, []Indexer, *chaincfg.Params, *Manager, func()) {
return indexersTestChain(testName)
}

// csnTestChain creates a chain using the compact utreexo state.
func csnTestChain(testName string) (*blockchain.BlockChain, *chaincfg.Params, func(), error) {
params := chaincfg.RegressionNetParams
Expand Down
9 changes: 9 additions & 0 deletions blockchain/indexers/utreexobackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,15 @@ func upgradeUtreexoState(cfg *UtreexoConfig, p *utreexo.MapPollard,
func (us *UtreexoState) initConsistentUtreexoState(chain *blockchain.BlockChain,
savedHash, tipHash *chainhash.Hash, tipHeight int32) error {

// Handle nil tipHash
if tipHash == nil {
return fmt.Errorf("tipHash is nil, cannot initialize Utreexo state")
}

// Handle negative tipHeight
if tipHeight < 0 {
return nil
}
// This is a new accumulator state that we're working with.
var empty chainhash.Hash
if tipHeight == -1 && tipHash.IsEqual(&empty) {
Expand Down
193 changes: 193 additions & 0 deletions blockchain/indexers/utreexobackend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ import (
"math/rand"
"os"
"testing"
"time"

"github.com/syndtr/goleveldb/leveldb"

//"github.com/utreexo/utreexod/blockchain/indexers"

"github.com/utreexo/utreexo"
"github.com/utreexo/utreexod/blockchain"
"github.com/utreexo/utreexod/btcutil"
"github.com/utreexo/utreexod/chaincfg"
"github.com/utreexo/utreexod/chaincfg/chainhash"
)

func TestUtreexoStateConsistencyWrite(t *testing.T) {
Expand Down Expand Up @@ -53,3 +61,188 @@ func TestUtreexoStateConsistencyWrite(t *testing.T) {
t.Fatalf("expected %v, got %v", numLeaves, gotNumLeaves)
}
}

func TestInitConsistentUtreexoState(t *testing.T) {
// Always remove the root on return.
defer os.RemoveAll(testDbRoot)

// Initialize a random number generator with the current time as the seed for unique randomness.
timenow := time.Now().UnixNano()
source := rand.NewSource(timenow)
rand := rand.New(source)

// Call IndexersTestChainWrapper to initialize the test environment
chain, indexes, params, manager, tearDown := IndexersTestChainWrapper("TestInitConsistentUtreexoState")
defer tearDown()

// Verify that the test environment has been initialized as expected
if chain == nil {
t.Fatalf("Failed to initialize blockchain")
}
if len(indexes) == 0 {
t.Fatalf("Failed to initialize indexes")
}
if params == nil {
t.Fatalf("Failed to initialize chain parameters")
}
if manager == nil {
t.Fatalf("Failed to initialize index manager")
}

var allSpends []*blockchain.SpendableOut
var nextSpends []*blockchain.SpendableOut
var blocks []*btcutil.Block

// Create a chain with 101 blocks.
nextBlock := btcutil.NewBlock(params.GenesisBlock)
// Create a slice with 101 Blocks.
blocks = append(blocks, nextBlock)

for i := 0; i < 100; i++ {
// Add a new block to the chain using the previous block and available UTXOs
newBlock, newSpendableOuts, err := blockchain.AddBlock(chain, nextBlock, nextSpends)
if err != nil {
t.Fatalf("timenow:%v. %v", timenow, err)
}
// Update the current block reference to the newly created block
nextBlock = newBlock
// Append the newly created block to the list of blocks
blocks = append(blocks, newBlock)
// Add the new UTXOs from the block to the global spendable outputs list
allSpends = append(allSpends, newSpendableOuts...)

// Shuffle and select UTXOs to be spent in the next block
var nextSpendsTmp []*blockchain.SpendableOut
for j := 0; j < len(allSpends); j++ {
// Randomly pick an index from the spendable outputs
randIdx := rand.Intn(len(allSpends))
// Select the spendable output and remove it from the global list
spend := allSpends[randIdx] // Get the UTXO
allSpends = append(allSpends[:randIdx], allSpends[randIdx+1:]...) // Remove the UTXO
nextSpendsTmp = append(nextSpendsTmp, spend) // Add to the temporary list
}
// Update the spendable outputs for the next block
nextSpends = nextSpendsTmp

// Every 10 blocks, flush the UTXO cache to the database
if i%10 == 0 {
// Commit the two base blocks to DB
if err := chain.FlushUtxoCache(blockchain.FlushRequired); err != nil {
t.Fatalf("timenow %v. TestInitConsistentUtreexoState fail. Unexpected error while flushing cache: %v", timenow, err)
}
}
}

// Create a temporary directory for LevelDB storage
dbPath := t.TempDir()
db, err := leveldb.OpenFile(dbPath, nil)
if err != nil {
t.Fatalf("Failed to initialize LevelDB: %v", err)
}
// Ensure the database is closed and the directory is removed after the test
defer func() {
db.Close()
os.RemoveAll(dbPath)
}()

// Initialize UtreexoState
p := utreexo.NewMapPollard(true)
cfg := &UtreexoConfig{MaxMemoryUsage: 1024 * 1024}
utreexoState := &UtreexoState{
config: cfg,
state: &p,
utreexoStateDB: db,
isFlushNeeded: func() bool {
return true
},
flushLeavesAndNodes: func(tx *leveldb.Transaction) error {
return manager.Flush(&chain.BestSnapshot().Hash, blockchain.FlushRequired, true)
},
}

// Assign managed blocks to variables as appropriate indexes
tipHash := blocks[100].Hash()
tipHeight := int32(100)
savedHash := blocks[99].Hash()
invalidHash := chainhash.Hash{0xaa, 0xbb, 0xcc} // Arbitrary invalid hash

// Define a set of test cases for initConsistentUtreexoState.
// Each test case specifies a name, savedHash, tipHash, tipHeight, and expected error outcome.
testCases := []struct {
name string
savedHash *chainhash.Hash
tipHash *chainhash.Hash
tipHeight int32
expectError bool
description string // Detailed description of the test case
}{
{
name: "Saved hash equals tip hash",
savedHash: tipHash,
tipHash: tipHash,
tipHeight: tipHeight,
expectError: false,
description: "The saved hash is identical to the tip hash; no error should occur.",
},
{
name: "Saved hash is not equal to tip hash",
savedHash: savedHash,
tipHash: tipHash,
tipHeight: tipHeight,
expectError: true,
description: "The saved hash differs from the tip hash; an error is expected.",
},
{
name: "Saved hash is nil",
savedHash: nil,
tipHash: tipHash,
tipHeight: tipHeight,
expectError: false,
description: "The saved hash is nil; this should initialize the state without errors.",
},
{
name: "Saved hash is not in chain",
savedHash: &invalidHash,
tipHash: tipHash,
tipHeight: tipHeight,
expectError: true,
description: "The saved hash does not exist in the chain; an error is expected.",
},
{
name: "Tip height is negative",
savedHash: savedHash,
tipHash: tipHash,
tipHeight: -1,
expectError: false,
description: "A negative tip height implies no processing is needed; no error should occur.",
},
{
name: "Tip hash is nil",
savedHash: savedHash,
tipHash: nil,
tipHeight: tipHeight,
expectError: true,
description: "A nil tip hash is invalid; an error is expected.",
},
{
name: "Current height less than tip height",
savedHash: blocks[50].Hash(),
tipHash: blocks[100].Hash(),
tipHeight: tipHeight,
expectError: true,
description: "The state should recover from block 51 to 100 without errors.",
},
}
// Run a subtest for each case using the test case name.
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := utreexoState.initConsistentUtreexoState(chain, tc.savedHash, tc.tipHash, tc.tipHeight)
if tc.expectError && err == nil {
t.Fatalf("Expected error but got none")
}
if !tc.expectError && err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
}
}

0 comments on commit ae07014

Please sign in to comment.