diff --git a/Makefile b/Makefile index 1b327143d..60e01cee8 100644 --- a/Makefile +++ b/Makefile @@ -49,4 +49,4 @@ test:| go_version_check test-coverage:| go_version_check @go test -race -coverprofile=coverage.out -covermode=atomic $(PACKAGES) - + @go tool cover -html=coverage.out diff --git a/txpool/blocklist_test.go b/txpool/blocklist_test.go new file mode 100644 index 000000000..0ae3ca705 --- /dev/null +++ b/txpool/blocklist_test.go @@ -0,0 +1,164 @@ +package txpool + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/thor" +) + +// SetupTempFile creates a temporary file with dummy data and returns the file path. +func SetupTempFile(t *testing.T, dummyData string) string { + tempFile, err := os.CreateTemp("", "test_blocklist_*.txt") + if err != nil { + t.Fatalf("Failed to create temp file: %s", err) + } + testFilePath := tempFile.Name() + + err = os.WriteFile(testFilePath, []byte(dummyData), 0644) + if err != nil { + t.Fatalf("Failed to write to temp file: %s", err) + } + + // Close the file and return its path. + tempFile.Close() + return testFilePath +} + +func TestLoad(t *testing.T) { + dummyData := "0x25Df024637d4e56c1aE9563987Bf3e92C9f534c0\n0x25Df024637d4e56c1aE9563987Bf3e92C9f534c1" + testFilePath := SetupTempFile(t, dummyData) + + var bl blocklist + err := bl.Load(testFilePath) + if err != nil { + t.Errorf("Load failed: %s", err) + } + + // Clean up: delete the temporary file. + os.Remove(testFilePath) +} + +func TestLoadWithError(t *testing.T) { + dummyData := "0x25Df024637d4\n0x25Df024637d4e56c1aE956" + testFilePath := SetupTempFile(t, dummyData) + + var bl blocklist + err := bl.Load(testFilePath) + assert.Equal(t, err.Error(), "invalid length") + assert.False(t, IsBadTx(err)) + assert.False(t, IsTxRejected(err)) + + // Clean up: delete the test file. + os.Remove(testFilePath) +} + +func TestSave(t *testing.T) { + dummyData := "0x25Df024637d4e56c1aE9563987Bf3e92C9f534c0\n0x25Df024637d4e56c1aE9563987Bf3e92C9f534c1" + testFilePath := SetupTempFile(t, dummyData) + + var bl blocklist + err := bl.Load(testFilePath) + if err != nil { + t.Errorf("Load failed: %s", err) + } + + // Clean up: delete the test file. + os.Remove(testFilePath) + + // Test the Load function. + err = bl.Save(testFilePath) + if err != nil { + t.Errorf("Load failed: %s", err) + } + + fileContents, err := os.ReadFile(testFilePath) + str := string(fileContents) + assert.True(t, strings.Contains(str, "0x25df024637d4e56c1ae9563987bf3e92c9f534c0")) + assert.True(t, strings.Contains(str, "0x25df024637d4e56c1ae9563987bf3e92c9f534c1")) + + // Clean up: delete the test file. + os.Remove(testFilePath) +} + +func TestLen(t *testing.T) { + dummyData := "0x25Df024637d4e56c1aE9563987Bf3e92C9f534c0\n0x25Df024637d4e56c1aE9563987Bf3e92C9f534c1" + testFilePath := SetupTempFile(t, dummyData) + + var bl blocklist + err := bl.Load(testFilePath) + if err != nil { + t.Errorf("Load failed: %s", err) + } + + // Clean up: delete the test file. + os.Remove(testFilePath) + + listLength := bl.Len() + assert.Equal(t, listLength, 2) +} + +func TestFetch(t *testing.T) { + // Example data to be served by the mock server + data := "0x25Df024637d4e56c1aE9563987Bf3e92C9f534c0\n0x25Df024637d4e56c1aE9563987Bf3e92C9f534c1" + + expectedAddresses := []thor.Address{ + thor.MustParseAddress("0x25Df024637d4e56c1aE9563987Bf3e92C9f534c0"), + thor.MustParseAddress("0x25Df024637d4e56c1aE9563987Bf3e92C9f534c1"), + } + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // You can check headers, methods, etc. here + if r.Header.Get("if-none-match") == "some-etag" { + w.WriteHeader(http.StatusNotModified) + return + } + fmt.Fprint(w, data) + })) + defer server.Close() + + // Test scenarios + tests := []struct { + name string + etag *string + wantErr bool + }{ + {"Successful Fetch", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var bl blocklist + bl.list = make(map[thor.Address]bool) + + // Set up ETAG if needed + if tt.etag != nil { + *tt.etag = "some-etag" + } + + // Call the Fetch function + err := bl.Fetch(context.Background(), server.URL, tt.etag) + + // Check for errors + if (err != nil) != tt.wantErr { + t.Errorf("Fetch() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Check if the blocklist contains the expected addresses + for _, addr := range expectedAddresses { + if _, exists := bl.list[addr]; !exists { + t.Errorf("Fetch() missing address %s", addr) + } + } + + }) + } +} diff --git a/txpool/tx_object_map_test.go b/txpool/tx_object_map_test.go index 0a1f58525..ae5c7dafa 100644 --- a/txpool/tx_object_map_test.go +++ b/txpool/tx_object_map_test.go @@ -12,9 +12,70 @@ import ( "github.com/stretchr/testify/assert" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/muxdb" + "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/tx" ) +func TestGetByID(t *testing.T) { + db := muxdb.NewMem() + repo := newChainRepo(db) + + // Creating transactions + tx1 := newTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[0]) + tx2 := newTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[1]) + + // Resolving transactions into txObjects + txObj1, _ := resolveTx(tx1, false) + txObj2, _ := resolveTx(tx2, false) + + // Creating a new txObjectMap and adding transactions + m := newTxObjectMap() + assert.Nil(t, m.Add(txObj1, 1)) + assert.Nil(t, m.Add(txObj2, 1)) + + // Testing GetByID + retrievedTxObj1 := m.GetByID(txObj1.ID()) + assert.Equal(t, txObj1, retrievedTxObj1, "The retrieved transaction object should match the original for tx1") + + retrievedTxObj2 := m.GetByID(txObj2.ID()) + assert.Equal(t, txObj2, retrievedTxObj2, "The retrieved transaction object should match the original for tx2") + + // Testing retrieval of a non-existing transaction + nonExistingTxID := thor.Bytes32{} // An arbitrary non-existing ID + retrievedTxObj3 := m.GetByID(nonExistingTxID) + assert.Nil(t, retrievedTxObj3, "Retrieving a non-existing transaction should return nil") +} + +func TestFill(t *testing.T) { + db := muxdb.NewMem() + repo := newChainRepo(db) + + // Creating transactions + tx1 := newTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[0]) + tx2 := newTx(repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[1]) + + // Resolving transactions into txObjects + txObj1, _ := resolveTx(tx1, false) + txObj2, _ := resolveTx(tx2, false) + + // Creating a new txObjectMap + m := newTxObjectMap() + + // Filling the map with transactions + m.Fill([]*txObject{txObj1, txObj2}) + + // Asserting the length of the map + assert.Equal(t, 2, m.Len(), "Map should contain only 2 unique transactions") + + // Asserting the transactions are correctly added + assert.True(t, m.ContainsHash(txObj1.Hash()), "Map should contain txObj1") + assert.True(t, m.ContainsHash(txObj2.Hash()), "Map should contain txObj2") + + // Asserting duplicate handling + assert.Equal(t, m.GetByID(txObj1.ID()), txObj1, "Duplicate tx1 should not be added again") + assert.Equal(t, m.GetByID(txObj2.ID()), txObj2, "txObj2 should be retrievable by ID") +} + func TestTxObjMap(t *testing.T) { db := muxdb.NewMem() repo := newChainRepo(db) diff --git a/txpool/tx_object_test.go b/txpool/tx_object_test.go index 7eeede1e4..468e1a365 100644 --- a/txpool/tx_object_test.go +++ b/txpool/tx_object_test.go @@ -73,6 +73,47 @@ func newDelegatedTx(chainTag byte, clauses []*tx.Clause, gas uint64, blockRef tx return tx.WithSignature(sig) } +func SetupTest() (genesis.DevAccount, *chain.Repository, *block.Block, *state.State) { + acc := genesis.DevAccounts()[0] + + db := muxdb.NewMem() + repo := newChainRepo(db) + b0 := repo.GenesisBlock() + b1 := new(block.Builder).ParentID(b0.Header().ID()).GasLimit(10000000).TotalScore(100).Build() + repo.AddBlock(b1, nil, 0) + st := state.New(db, repo.GenesisBlock().Header().StateRoot(), 0, 0, 0) + + return acc, repo, b1, st +} + +func TestExecutableWithError(t *testing.T) { + acc, repo, b1, st := SetupTest() + + tests := []struct { + tx *tx.Transaction + expected bool + expectedErr string + }{ + {newTx(0, nil, 21000, tx.BlockRef{0}, 100, nil, tx.Features(0), acc), false, ""}, + } + + for _, tt := range tests { + txObj, err := resolveTx(tt.tx, false) + assert.Nil(t, err) + + // pass custom headID + chain := repo.NewChain(thor.Bytes32{0}) + + exe, err := txObj.Executable(chain, st, b1.Header()) + if tt.expectedErr != "" { + assert.Equal(t, tt.expectedErr, err.Error()) + } else { + assert.Equal(t, err.Error(), "leveldb: not found") + assert.Equal(t, tt.expected, exe) + } + } +} + func TestSort(t *testing.T) { objs := []*txObject{ {overallGasPrice: big.NewInt(10)}, @@ -99,14 +140,7 @@ func TestResolve(t *testing.T) { } func TestExecutable(t *testing.T) { - acc := genesis.DevAccounts()[0] - - db := muxdb.NewMem() - repo := newChainRepo(db) - b0 := repo.GenesisBlock() - b1 := new(block.Builder).ParentID(b0.Header().ID()).GasLimit(10000000).TotalScore(100).Build() - repo.AddBlock(b1, nil, 0) - st := state.New(db, repo.GenesisBlock().Header().StateRoot(), 0, 0, 0) + acc, repo, b1, st := SetupTest() tests := []struct { tx *tx.Transaction diff --git a/txpool/tx_pool_test.go b/txpool/tx_pool_test.go index 031cdb18d..8913c06d8 100644 --- a/txpool/tx_pool_test.go +++ b/txpool/tx_pool_test.go @@ -9,6 +9,9 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "math/big" + "net/http" + "net/http/httptest" "testing" "time" @@ -16,6 +19,7 @@ import ( "github.com/inconshreveable/log15" "github.com/stretchr/testify/assert" "github.com/vechain/thor/v2/block" + "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/muxdb" "github.com/vechain/thor/v2/state" @@ -40,6 +44,177 @@ func newPool(limit int, limitPerAccount int) *TxPool { MaxLifetime: time.Hour, }) } + +func newPoolWithParams(limit int, limitPerAccount int, BlocklistCacheFilePath string, BlocklistFetchURL string, timestamp uint64) *TxPool { + db := muxdb.NewMem() + gene := new(genesis.Builder). + GasLimit(thor.InitialGasLimit). + Timestamp(timestamp). + State(func(state *state.State) error { + bal, _ := new(big.Int).SetString("1000000000000000000000000000", 10) + for _, acc := range genesis.DevAccounts() { + state.SetBalance(acc.Address, bal) + state.SetEnergy(acc.Address, bal, timestamp) + } + return nil + }) + b0, _, _, _ := gene.Build(state.NewStater(db)) + repo, _ := chain.NewRepository(db, b0) + return New(repo, state.NewStater(db), Options{ + Limit: limit, + LimitPerAccount: limitPerAccount, + MaxLifetime: time.Hour, + BlocklistCacheFilePath: BlocklistCacheFilePath, + BlocklistFetchURL: BlocklistFetchURL, + }) +} + +func newHttpServer() *httptest.Server { + // Example data to be served by the mock server + data := "0x25Df024637d4e56c1aE9563987Bf3e92C9f534c0\n0x25Df024637d4e56c1aE9563987Bf3e92C9f534c1" + + // Create a mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // You can check headers, methods, etc. here + if r.Header.Get("if-none-match") == "some-etag" { + w.WriteHeader(http.StatusNotModified) + return + } + fmt.Fprint(w, data) + })) + return server +} + +func TestNewCloseWithServer(t *testing.T) { + server := newHttpServer() + defer server.Close() + + pool := newPoolWithParams(LIMIT, LIMIT_PER_ACCOUNT, "./", server.URL, uint64(time.Now().Unix())) + defer pool.Close() + + // Create a slice of transactions to be added to the pool. + txs := make(Tx.Transactions, 0, 15) + for i := 0; i < 15; i++ { + tx := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[i%len(genesis.DevAccounts())]) + txs = append(txs, tx) + } + + // Call the Fill method + pool.Fill(txs) + + // Add a delay of 2 seconds + time.Sleep(2 * time.Second) +} + +func FillPoolWithTxs(pool *TxPool, t *testing.T) { + // Create a slice of transactions to be added to the pool. + txs := make(Tx.Transactions, 0, 15) + for i := 0; i < 12; i++ { + tx := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[0]) + txs = append(txs, tx) + } + + // Call the Fill method + pool.Fill(txs) + + err := pool.Add(newTx(pool.repo.ChainTag(), nil, 21000, tx.NewBlockRef(10), 100, nil, Tx.Features(0), genesis.DevAccounts()[0])) + assert.Equal(t, err.Error(), "tx rejected: pool is full") + + // Add a delay of 2 seconds + time.Sleep(2 * time.Second) +} + +func TestAddWithFullErrorUnsyncedChain(t *testing.T) { + pool := newPool(LIMIT, LIMIT_PER_ACCOUNT) + defer pool.Close() + + FillPoolWithTxs(pool, t) + +} + +func TestAddWithFullErrorSyncedChain(t *testing.T) { + pool := newPoolWithParams(LIMIT, LIMIT_PER_ACCOUNT, "./", "", uint64(time.Now().Unix())) + defer pool.Close() + + FillPoolWithTxs(pool, t) +} + +func TestNewCloseWithError(t *testing.T) { + + pool := newPoolWithParams(LIMIT, LIMIT_PER_ACCOUNT, " ", " ", uint64(time.Now().Unix())+10000) + defer pool.Close() + + // Add a delay of 2 seconds + time.Sleep(2 * time.Second) +} + +func TestDump(t *testing.T) { + // Create a new transaction pool with specified limits + pool := newPool(LIMIT, LIMIT_PER_ACCOUNT) + defer pool.Close() + + // Create and add transactions to the pool + txsToAdd := make(tx.Transactions, 0, 5) + for i := 0; i < 5; i++ { + tx := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[i%len(genesis.DevAccounts())]) + txsToAdd = append(txsToAdd, tx) + assert.Nil(t, pool.Add(tx)) + } + + // Use the Dump method to retrieve all transactions in the pool + dumpedTxs := pool.Dump() + + // Check if the dumped transactions match the ones added + assert.Equal(t, len(txsToAdd), len(dumpedTxs), "Number of dumped transactions should match the number added") + + // Further checks can be done to ensure that each transaction in `dumpedTxs` is also in `txsToAdd` + for _, dumpedTx := range dumpedTxs { + found := false + for _, addedTx := range txsToAdd { + if dumpedTx.ID() == addedTx.ID() { + found = true + break + } + } + assert.True(t, found, "Dumped transaction should match one of the added transactions") + } +} + +func TestRemove(t *testing.T) { + pool := newPool(LIMIT, LIMIT_PER_ACCOUNT) + defer pool.Close() + + // Create and add a transaction to the pool + tx := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[0]) + assert.Nil(t, pool.Add(tx), "Adding transaction should not produce error") + + // Ensure the transaction is in the pool + assert.NotNil(t, pool.Get(tx.ID()), "Transaction should exist in the pool before removal") + + // Remove the transaction from the pool + removed := pool.Remove(tx.Hash(), tx.ID()) + assert.True(t, removed, "Transaction should be successfully removed") + + // Check that the transaction is no longer in the pool + assert.Nil(t, pool.Get(tx.ID()), "Transaction should not exist in the pool after removal") +} + +func TestRemoveWithError(t *testing.T) { + pool := newPool(LIMIT, LIMIT_PER_ACCOUNT) + defer pool.Close() + + // Create and add a transaction to the pool + tx := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[0]) + // assert.Nil(t, pool.Add(tx), "Adding transaction should not produce error") + + // Ensure the transaction is in the pool + assert.Nil(t, pool.Get(tx.ID()), "Transaction should exist in the pool before removal") + + // Remove the transaction from the pool + removed := pool.Remove(tx.Hash(), tx.ID()) + assert.False(t, removed, "Transaction should not be successfully removed as it doesn't exist") +} + func TestNewClose(t *testing.T) { pool := newPool(LIMIT, LIMIT_PER_ACCOUNT) defer pool.Close() @@ -125,6 +300,31 @@ func TestWashTxs(t *testing.T) { assert.Equal(t, 1, removedCount) } +func TestFillPool(t *testing.T) { + pool := newPool(LIMIT, LIMIT_PER_ACCOUNT) + defer pool.Close() + + // Create a slice of transactions to be added to the pool. + txs := make(Tx.Transactions, 0, 5) + for i := 0; i < 5; i++ { + tx := newTx(pool.repo.ChainTag(), nil, 21000, tx.BlockRef{}, 100, nil, tx.Features(0), genesis.DevAccounts()[i%len(genesis.DevAccounts())]) + txs = append(txs, tx) + } + + // Call the Fill method + pool.Fill(txs) + + // Check if the transactions are correctly added. + // This might require accessing internal state of TxPool or using provided methods. + for _, tx := range txs { + assert.NotNil(t, pool.Get(tx.ID()), "Transaction should exist in the pool") + } + + // Further checks can be made based on the behavior of your TxPool implementation. + // For example, checking if the pool size has increased by the expected amount. + assert.Equal(t, len(txs), pool.all.Len(), "Number of transactions in the pool should match the number added") +} + func TestAdd(t *testing.T) { pool := newPool(LIMIT, LIMIT_PER_ACCOUNT) defer pool.Close()