Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add edge case handling and tests for initConsistentUtreexoState #230

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We handle both of the cases literally just below at line 600.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention was to handle cases where either tipHash == nil or tipHeight < 0 happens on its own, which the logic at line 600 doesn’t cover.

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

// Handle negative tipHeight
if tipHeight < 0 {
return nil
}
Comment on lines +589 to +597
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tipHeight == -1 && tipHash.IsEqual(&empty) handles the initialization state when both tipHeight == -1 and tipHash == nil are true. However, I intended to handle tipHash == nil or tipHeight < 0 individually. While focusing solely on the "fresh node" scenario makes sense, explicitly handling tipHash == nil in other cases could improve stability and robustness.

// 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)
}
})
}
}
Loading