gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/filesystem/siafile/persist_test.go (about)

     1  package siafile
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"gitlab.com/NebulousLabs/errors"
    16  	"gitlab.com/NebulousLabs/fastrand"
    17  	"gitlab.com/NebulousLabs/writeaheadlog"
    18  
    19  	"gitlab.com/SkynetLabs/skyd/skymodules"
    20  	"go.sia.tech/siad/crypto"
    21  	"go.sia.tech/siad/modules"
    22  	"go.sia.tech/siad/types"
    23  )
    24  
    25  // closeFileInTest is a small helper for calling close on a file in a test
    26  func closeFileInTest(t *testing.T, f *os.File) {
    27  	err := f.Close()
    28  	if err != nil {
    29  		t.Fatal(err)
    30  	}
    31  }
    32  
    33  // equalFiles is a helper that compares two SiaFiles for equality.
    34  func equalFiles(sf, sf2 *SiaFile) error {
    35  	// Backup the metadata structs for both files.
    36  	md := sf.staticMetadata
    37  	md2 := sf2.staticMetadata
    38  	// Compare the timestamps first since they can't be compared with
    39  	// DeepEqual.
    40  	if sf.staticMetadata.AccessTime.Unix() != sf2.staticMetadata.AccessTime.Unix() {
    41  		return errors.New("AccessTime's don't match")
    42  	}
    43  	if sf.staticMetadata.ChangeTime.Unix() != sf2.staticMetadata.ChangeTime.Unix() {
    44  		return errors.New("ChangeTime's don't match")
    45  	}
    46  	if sf.staticMetadata.CreateTime.Unix() != sf2.staticMetadata.CreateTime.Unix() {
    47  		return errors.New("CreateTime's don't match")
    48  	}
    49  	if sf.staticMetadata.ModTime.Unix() != sf2.staticMetadata.ModTime.Unix() {
    50  		return errors.New("ModTime's don't match")
    51  	}
    52  	if sf.staticMetadata.LastHealthCheckTime.Unix() != sf2.staticMetadata.LastHealthCheckTime.Unix() {
    53  		return errors.New("LastHealthCheckTime's don't match")
    54  	}
    55  	ec := sf.ErasureCode()
    56  	ec2 := sf2.ErasureCode()
    57  	if ec.Identifier() != ec2.Identifier() {
    58  		return errors.New("erasure coder doesn't match")
    59  	}
    60  	// Set the timestamps to zero for DeepEqual.
    61  	sf.staticMetadata.AccessTime = time.Time{}
    62  	sf.staticMetadata.ChangeTime = time.Time{}
    63  	sf.staticMetadata.CreateTime = time.Time{}
    64  	sf.staticMetadata.ModTime = time.Time{}
    65  	sf.staticMetadata.LastHealthCheckTime = time.Time{}
    66  	sf.staticMetadata.staticErasureCode = nil
    67  	sf2.staticMetadata.AccessTime = time.Time{}
    68  	sf2.staticMetadata.ChangeTime = time.Time{}
    69  	sf2.staticMetadata.CreateTime = time.Time{}
    70  	sf2.staticMetadata.ModTime = time.Time{}
    71  	sf2.staticMetadata.LastHealthCheckTime = time.Time{}
    72  	sf2.staticMetadata.staticErasureCode = nil
    73  	// Compare the rest of sf and sf2.
    74  	if !reflect.DeepEqual(sf.staticMetadata, sf2.staticMetadata) {
    75  		fmt.Println(sf.staticMetadata)
    76  		fmt.Println(sf2.staticMetadata)
    77  		return errors.New("sf metadata doesn't equal sf2 metadata")
    78  	}
    79  	if !reflect.DeepEqual(sf.pubKeyTable, sf2.pubKeyTable) {
    80  		fmt.Println(sf.pubKeyTable)
    81  		fmt.Println(sf2.pubKeyTable)
    82  		return errors.New("sf pubKeyTable doesn't equal sf2 pubKeyTable")
    83  	}
    84  	if sf.numChunks != sf2.numChunks {
    85  		return errors.New(fmt.Sprint("sf numChunks doesn't equal sf2 numChunks", sf.numChunks, sf2.numChunks))
    86  	}
    87  	if sf.siaFilePath != sf2.siaFilePath {
    88  		return fmt.Errorf("sf2 filepath was %v but should be %v",
    89  			sf2.siaFilePath, sf.siaFilePath)
    90  	}
    91  	// Restore the original metadata.
    92  	sf.staticMetadata = md
    93  	sf2.staticMetadata = md2
    94  	return nil
    95  }
    96  
    97  // addRandomHostKeys adds n random host keys to the SiaFile's pubKeyTable. It
    98  // doesn't write them to disk.
    99  func (sf *SiaFile) addRandomHostKeys(n int) {
   100  	for i := 0; i < n; i++ {
   101  		// Create random specifier and key.
   102  		algorithm := types.Specifier{}
   103  		fastrand.Read(algorithm[:])
   104  
   105  		// Create random key.
   106  		key := fastrand.Bytes(32)
   107  
   108  		// Append new key to slice.
   109  		sf.pubKeyTable = append(sf.pubKeyTable, HostPublicKey{
   110  			PublicKey: types.SiaPublicKey{
   111  				Algorithm: algorithm,
   112  				Key:       key,
   113  			},
   114  			Used: true,
   115  		})
   116  	}
   117  }
   118  
   119  // customTestFileAndWAL creates an empty SiaFile for testing and also returns
   120  // the WAL used in the creation and the path of the WAL.
   121  func customTestFileAndWAL(siaFilePath, source string, rc skymodules.ErasureCoder, sk crypto.CipherKey, fileSize uint64, numChunks int, fileMode os.FileMode) (*SiaFile, *writeaheadlog.WAL, string) {
   122  	// Create the path to the file.
   123  	dir, _ := filepath.Split(siaFilePath)
   124  	err := os.MkdirAll(dir, 0700)
   125  	if err != nil {
   126  		panic(err)
   127  	}
   128  	// Create a test wal
   129  	wal, walPath := newTestWAL()
   130  	// Create the file.
   131  	sf, err := New(siaFilePath, source, wal, rc, sk, fileSize, fileMode)
   132  	if err != nil {
   133  		panic(err)
   134  	}
   135  	if sf.staticMetadata.StaticVersion != metadataVersion {
   136  		panic("incorrect metadata version for new file")
   137  	}
   138  	// Check that the number of chunks in the file is correct.
   139  	if numChunks >= 0 && sf.numChunks != numChunks {
   140  		panic(fmt.Sprintf("newTestFile didn't create the expected number of chunks: %v %v %v", sf.numChunks, numChunks, fileSize))
   141  	}
   142  	return sf, wal, walPath
   143  }
   144  
   145  // newBlankTestFileAndWAL is like customTestFileAndWAL but uses random params
   146  // and allows the caller to specify how many chunks the file should at least
   147  // contain.
   148  func newBlankTestFileAndWAL(minChunks int) (*SiaFile, *writeaheadlog.WAL, string) {
   149  	siaFilePath, _, source, rc, sk, fileSize, numChunks, fileMode := newTestFileParams(minChunks, true)
   150  	return customTestFileAndWAL(siaFilePath, source, rc, sk, fileSize, numChunks, fileMode)
   151  }
   152  
   153  // newBlankTestFile is a helper method to create a SiaFile for testing without
   154  // any hosts or uploaded pieces.
   155  func newBlankTestFile() *SiaFile {
   156  	sf, _, _ := newBlankTestFileAndWAL(1)
   157  	return sf
   158  }
   159  
   160  // newTestFile creates a SiaFile for testing where each chunk has a random
   161  // number of pieces.
   162  func newTestFile() *SiaFile {
   163  	sf := newBlankTestFile()
   164  	// Add pieces to each chunk.
   165  	for chunkIndex := 0; chunkIndex < sf.numChunks; chunkIndex++ {
   166  		for pieceIndex := 0; pieceIndex < sf.ErasureCode().NumPieces(); pieceIndex++ {
   167  			numPieces := fastrand.Intn(3) // up to 2 hosts for each piece
   168  			for i := 0; i < numPieces; i++ {
   169  				pk := types.SiaPublicKey{Key: fastrand.Bytes(crypto.EntropySize)}
   170  				mr := crypto.Hash{}
   171  				fastrand.Read(mr[:])
   172  				if err := sf.AddPiece(pk, uint64(chunkIndex), uint64(pieceIndex), mr); err != nil {
   173  					panic(err)
   174  				}
   175  			}
   176  		}
   177  	}
   178  	return sf
   179  }
   180  
   181  // newTestFileParams creates the required parameters for creating a siafile and
   182  // creates a directory for the file
   183  func newTestFileParams(minChunks int, partialChunk bool) (string, skymodules.SiaPath, string, skymodules.ErasureCoder, crypto.CipherKey, uint64, int, os.FileMode) {
   184  	rc, err := skymodules.NewRSCode(10, 20)
   185  	if err != nil {
   186  		panic(err)
   187  	}
   188  	return newTestFileParamsWithRC(minChunks, partialChunk, rc)
   189  }
   190  
   191  // newTestFileParamsWithRC creates the required parameters for creating a siafile and
   192  // creates a directory for the file.
   193  func newTestFileParamsWithRC(minChunks int, partialChunk bool, rc skymodules.ErasureCoder) (string, skymodules.SiaPath, string, skymodules.ErasureCoder, crypto.CipherKey, uint64, int, os.FileMode) {
   194  	// Create arguments for new file.
   195  	sk := crypto.GenerateSiaKey(crypto.TypeDefaultRenter)
   196  	pieceSize := modules.SectorSize - sk.Type().Overhead()
   197  	siaPath := skymodules.RandomSiaPath()
   198  	numChunks := fastrand.Intn(10) + minChunks
   199  	chunkSize := pieceSize * uint64(rc.MinPieces())
   200  	fileSize := chunkSize * uint64(numChunks)
   201  	if partialChunk {
   202  		fileSize-- // force partial chunk
   203  	}
   204  	fileMode := os.FileMode(777)
   205  	source := string(hex.EncodeToString(fastrand.Bytes(8)))
   206  
   207  	// Create the path to the file.
   208  	siaFilePath := siaPath.SiaFileSysPath(filepath.Join(os.TempDir(), "siafiles", hex.EncodeToString(fastrand.Bytes(16))))
   209  	dir, _ := filepath.Split(siaFilePath)
   210  	if err := os.MkdirAll(dir, 0700); err != nil {
   211  		panic(err)
   212  	}
   213  	return siaFilePath, siaPath, source, rc, sk, fileSize, numChunks, fileMode
   214  }
   215  
   216  // newTestWal is a helper method to create a WAL for testing.
   217  func newTestWAL() (*writeaheadlog.WAL, string) {
   218  	// Create the wal.
   219  	walsDir := filepath.Join(os.TempDir(), "wals")
   220  	if err := os.MkdirAll(walsDir, 0700); err != nil {
   221  		panic(err)
   222  	}
   223  	walFilePath := filepath.Join(walsDir, hex.EncodeToString(fastrand.Bytes(8)))
   224  	_, wal, err := writeaheadlog.New(walFilePath)
   225  	if err != nil {
   226  		panic(err)
   227  	}
   228  	return wal, walFilePath
   229  }
   230  
   231  // TestNewFile tests that a new file has the correct contents and size and that
   232  // loading it from disk also works.
   233  func TestNewFile(t *testing.T) {
   234  	if testing.Short() {
   235  		t.SkipNow()
   236  	}
   237  	t.Parallel()
   238  
   239  	// Create a siafile without a partial chunk.
   240  	siaFilePath, _, source, rc, sk, fileSize, numChunks, fileMode := newTestFileParams(1, false)
   241  	sf, _, _ := customTestFileAndWAL(siaFilePath, source, rc, sk, fileSize, numChunks, fileMode)
   242  
   243  	// Add pieces to each chunk.
   244  	for chunkIndex := 0; chunkIndex < sf.numChunks; chunkIndex++ {
   245  		for pieceIndex := 0; pieceIndex < sf.ErasureCode().NumPieces(); pieceIndex++ {
   246  			numPieces := fastrand.Intn(3) // up to 2 hosts for each piece
   247  			for i := 0; i < numPieces; i++ {
   248  				pk := types.SiaPublicKey{Key: fastrand.Bytes(crypto.EntropySize)}
   249  				mr := crypto.Hash{}
   250  				fastrand.Read(mr[:])
   251  				if err := sf.AddPiece(pk, uint64(chunkIndex), uint64(pieceIndex), mr); err != nil {
   252  					panic(err)
   253  				}
   254  			}
   255  		}
   256  	}
   257  
   258  	// Check that StaticPagesPerChunk was set correctly.
   259  	if sf.staticMetadata.StaticPagesPerChunk != numChunkPagesRequired(sf.staticMetadata.staticErasureCode.NumPieces()) {
   260  		t.Fatal("StaticPagesPerChunk wasn't set correctly")
   261  	}
   262  
   263  	// Marshal the metadata.
   264  	md, err := marshalMetadata(sf.staticMetadata)
   265  	if err != nil {
   266  		t.Fatal(err)
   267  	}
   268  	// Marshal the pubKeyTable.
   269  	pkt, err := marshalPubKeyTable(sf.pubKeyTable)
   270  	if err != nil {
   271  		t.Fatal(err)
   272  	}
   273  	// Marshal the chunks.
   274  	var chunks [][]byte
   275  	var chunksMarshaled []chunk
   276  	err = sf.iterateChunksReadonly(func(chunk chunk) error {
   277  		c := marshalChunk(chunk)
   278  		chunks = append(chunks, c)
   279  		return nil
   280  	})
   281  	if err != nil {
   282  		t.Fatal(err)
   283  	}
   284  
   285  	// Save the SiaFile to make sure cached fields are persisted too.
   286  	if err := sf.saveFile(chunksMarshaled); err != nil {
   287  		t.Fatal(err)
   288  	}
   289  
   290  	// Open the file.
   291  	f, err := os.OpenFile(sf.siaFilePath, os.O_RDWR, 777)
   292  	if err != nil {
   293  		t.Fatal("Failed to open file", err)
   294  	}
   295  	defer closeFileInTest(t, f)
   296  	// Check the filesize. It should be equal to the offset of the last chunk
   297  	// on disk + its marshaled length.
   298  	fi, err := f.Stat()
   299  	if err != nil {
   300  		t.Fatal(err)
   301  	}
   302  	// If the file only has 1 partial chunk and no full chunk don't do this check.
   303  	if fi.Size() != sf.chunkOffset(sf.numChunks-1)+int64(len(chunks[len(chunks)-1])) {
   304  		t.Fatal("file doesn't have right size")
   305  	}
   306  	// Compare the metadata to the on-disk metadata.
   307  	readMD := make([]byte, len(md))
   308  	_, err = f.ReadAt(readMD, 0)
   309  	if err != nil {
   310  		t.Fatal(err)
   311  	}
   312  	if !bytes.Equal(readMD, md) {
   313  		t.Log(string(readMD))
   314  		t.Log(string(md))
   315  		t.Fatal("metadata doesn't equal on-disk metadata")
   316  	}
   317  	// Compare the pubKeyTable to the on-disk pubKeyTable.
   318  	readPKT := make([]byte, len(pkt))
   319  	_, err = f.ReadAt(readPKT, sf.staticMetadata.PubKeyTableOffset)
   320  	if err != nil {
   321  		t.Fatal(err)
   322  	}
   323  	if !bytes.Equal(readPKT, pkt) {
   324  		t.Fatal("pubKeyTable doesn't equal on-disk pubKeyTable")
   325  	}
   326  	// Compare the chunks to the on-disk chunks one-by-one.
   327  	readChunk := make([]byte, int(sf.staticMetadata.StaticPagesPerChunk)*pageSize)
   328  	err = sf.iterateChunksReadonly(func(chunk chunk) error {
   329  		_, err := f.ReadAt(readChunk, sf.chunkOffset(chunk.Index))
   330  		if err != nil && !errors.Contains(err, io.EOF) {
   331  			t.Fatal(err)
   332  		}
   333  		if !bytes.Equal(readChunk[:len(chunks[chunk.Index])], chunks[chunk.Index]) {
   334  			t.Fatal("readChunks don't equal on-disk readChunks")
   335  		}
   336  		return nil
   337  	})
   338  	if err != nil {
   339  		t.Fatal(err)
   340  	}
   341  	// Load the file from disk and check that they are the same.
   342  	sf2, err := LoadSiaFile(sf.siaFilePath, sf.wal)
   343  	if err != nil {
   344  		t.Fatal("failed to load SiaFile from disk", err)
   345  	}
   346  	// Compare the files.
   347  	if err := equalFiles(sf, sf2); err != nil {
   348  		t.Fatal(err)
   349  	}
   350  }
   351  
   352  // TestCreateReadInsertUpdate tests if an update can be created using createInsertUpdate
   353  // and if the created update can be read using readInsertUpdate.
   354  func TestCreateReadInsertUpdate(t *testing.T) {
   355  	if testing.Short() {
   356  		t.SkipNow()
   357  	}
   358  	t.Parallel()
   359  
   360  	sf := newTestFile()
   361  	// Create update randomly
   362  	index := int64(fastrand.Intn(100))
   363  	data := fastrand.Bytes(10)
   364  	update := sf.createInsertUpdate(index, data)
   365  	// Read update
   366  	readPath, readIndex, readData, err := readInsertUpdate(update)
   367  	if err != nil {
   368  		t.Fatal("Failed to read update", err)
   369  	}
   370  	// Compare values
   371  	if readPath != sf.siaFilePath {
   372  		t.Error("paths doesn't match")
   373  	}
   374  	if readIndex != index {
   375  		t.Error("index doesn't match")
   376  	}
   377  	if !bytes.Equal(readData, data) {
   378  		t.Error("data doesn't match")
   379  	}
   380  }
   381  
   382  // TestCreateReadDeleteUpdate tests if an update can be created using
   383  // createDeleteUpdate and if the created update can be read using
   384  // readDeleteUpdate.
   385  func TestCreateReadDeleteUpdate(t *testing.T) {
   386  	if testing.Short() {
   387  		t.SkipNow()
   388  	}
   389  	t.Parallel()
   390  
   391  	sf := newTestFile()
   392  	update := sf.createDeleteUpdate()
   393  	// Read update
   394  	path := readDeleteUpdate(update)
   395  	// Compare values
   396  	if path != sf.siaFilePath {
   397  		t.Error("paths doesn't match")
   398  	}
   399  }
   400  
   401  // TestDelete tests if deleting a siafile removes the file from disk and sets
   402  // the deleted flag correctly.
   403  func TestDelete(t *testing.T) {
   404  	if testing.Short() {
   405  		t.SkipNow()
   406  	}
   407  	t.Parallel()
   408  
   409  	// Create SiaFileSet with SiaFile
   410  	entry := newTestFile()
   411  	// Delete file.
   412  	if err := entry.Delete(); err != nil {
   413  		t.Fatal("Failed to delete file", err)
   414  	}
   415  	// Check if file was deleted and if deleted flag was set.
   416  	if !entry.Deleted() {
   417  		t.Fatal("Deleted flag was not set correctly")
   418  	}
   419  	if _, err := os.Open(entry.siaFilePath); !os.IsNotExist(err) {
   420  		t.Fatal("Expected a file doesn't exist error but got", err)
   421  	}
   422  }
   423  
   424  // TestRename tests if renaming a siafile moves the file correctly and also
   425  // updates the metadata.
   426  func TestRename(t *testing.T) {
   427  	if testing.Short() {
   428  		t.SkipNow()
   429  	}
   430  	t.Parallel()
   431  
   432  	// Create SiaFileSet with SiaFile
   433  	entry := newTestFile()
   434  
   435  	// Create new paths for the file.
   436  	oldSiaFilePath := entry.SiaFilePath()
   437  	newSiaFilePath := strings.TrimSuffix(entry.SiaFilePath(), skymodules.SiaFileExtension) + "_renamed" + skymodules.SiaFileExtension
   438  
   439  	// Rename file
   440  	if err := entry.Rename(newSiaFilePath); err != nil {
   441  		t.Fatal("Failed to rename file", err)
   442  	}
   443  
   444  	// Check if the file was moved.
   445  	if _, err := os.Open(oldSiaFilePath); !os.IsNotExist(err) {
   446  		t.Fatal("Expected a file doesn't exist error but got", err)
   447  	}
   448  	f, err := os.Open(newSiaFilePath)
   449  	if err != nil {
   450  		t.Fatal("Failed to open file at new location", err)
   451  	}
   452  	if err := f.Close(); err != nil {
   453  		t.Fatal(err)
   454  	}
   455  	// Check the metadata.
   456  	if entry.siaFilePath != newSiaFilePath {
   457  		t.Fatal("SiaFilePath wasn't updated correctly")
   458  	}
   459  	if entry.SiaFilePath() != newSiaFilePath {
   460  		t.Fatal("SiaPath wasn't updated correctly", entry.SiaFilePath(), newSiaFilePath)
   461  	}
   462  }
   463  
   464  // TestApplyUpdates tests a variety of functions that are used to apply
   465  // updates.
   466  func TestApplyUpdates(t *testing.T) {
   467  	if testing.Short() {
   468  		t.SkipNow()
   469  	}
   470  	t.Parallel()
   471  
   472  	t.Run("TestApplyUpdates", func(t *testing.T) {
   473  		siaFile := newTestFile()
   474  		testApply(t, siaFile, ApplyUpdates)
   475  	})
   476  	t.Run("TestSiaFileApplyUpdates", func(t *testing.T) {
   477  		siaFile := newTestFile()
   478  		testApply(t, siaFile, siaFile.applyUpdates)
   479  	})
   480  	t.Run("TestCreateAndApplyTransaction", func(t *testing.T) {
   481  		siaFile := newTestFile()
   482  		testApply(t, siaFile, siaFile.createAndApplyTransaction)
   483  	})
   484  }
   485  
   486  // TestSaveSmallHeader tests the saveHeader method for a header that is not big
   487  // enough to need more than a single page on disk.
   488  func TestSaveSmallHeader(t *testing.T) {
   489  	if testing.Short() {
   490  		t.SkipNow()
   491  	}
   492  	t.Parallel()
   493  
   494  	sf := newBlankTestFile()
   495  
   496  	// Add some host keys.
   497  	sf.addRandomHostKeys(10)
   498  
   499  	// Save the header.
   500  	updates, err := sf.saveHeaderUpdates()
   501  	if err != nil {
   502  		t.Fatal("Failed to create updates to save header", err)
   503  	}
   504  	if err := sf.createAndApplyTransaction(updates...); err != nil {
   505  		t.Fatal("Failed to save header", err)
   506  	}
   507  
   508  	// Manually open the file to check its contents.
   509  	f, err := os.Open(sf.siaFilePath)
   510  	if err != nil {
   511  		t.Fatal("Failed to open file", err)
   512  	}
   513  	defer closeFileInTest(t, f)
   514  
   515  	// Make sure the metadata was written to disk correctly.
   516  	rawMetadata, err := marshalMetadata(sf.staticMetadata)
   517  	if err != nil {
   518  		t.Fatal("Failed to marshal metadata", err)
   519  	}
   520  	readMetadata := make([]byte, len(rawMetadata))
   521  	if _, err := f.ReadAt(readMetadata, 0); err != nil {
   522  		t.Fatal("Failed to read metadata", err)
   523  	}
   524  	if !bytes.Equal(rawMetadata, readMetadata) {
   525  		fmt.Println(string(rawMetadata))
   526  		fmt.Println(string(readMetadata))
   527  		t.Fatal("Metadata on disk doesn't match marshaled metadata")
   528  	}
   529  
   530  	// Make sure that the pubKeyTable was written to disk correctly.
   531  	rawPubKeyTAble, err := marshalPubKeyTable(sf.pubKeyTable)
   532  	if err != nil {
   533  		t.Fatal("Failed to marshal pubKeyTable", err)
   534  	}
   535  	readPubKeyTable := make([]byte, len(rawPubKeyTAble))
   536  	if _, err := f.ReadAt(readPubKeyTable, sf.staticMetadata.PubKeyTableOffset); err != nil {
   537  		t.Fatal("Failed to read pubKeyTable", err)
   538  	}
   539  	if !bytes.Equal(rawPubKeyTAble, readPubKeyTable) {
   540  		t.Fatal("pubKeyTable on disk doesn't match marshaled pubKeyTable")
   541  	}
   542  }
   543  
   544  // TestSaveLargeHeader tests the saveHeader method for a header that uses more than a single page on disk and forces a call to allocateHeaderPage
   545  func TestSaveLargeHeader(t *testing.T) {
   546  	if testing.Short() {
   547  		t.SkipNow()
   548  	}
   549  	t.Parallel()
   550  
   551  	sf := newBlankTestFile()
   552  
   553  	// Add some host keys. This should force the SiaFile to allocate a new page
   554  	// for the pubKeyTable.
   555  	sf.addRandomHostKeys(100)
   556  
   557  	// Open the file.
   558  	f, err := os.OpenFile(sf.siaFilePath, os.O_RDWR, 777)
   559  	if err != nil {
   560  		t.Fatal("Failed to open file", err)
   561  	}
   562  	defer closeFileInTest(t, f)
   563  
   564  	// Write some data right after the ChunkOffset as a checksum.
   565  	chunkData := fastrand.Bytes(100)
   566  	_, err = f.WriteAt(chunkData, sf.staticMetadata.ChunkOffset)
   567  	if err != nil {
   568  		t.Fatal("Failed to write random chunk data", err)
   569  	}
   570  
   571  	// Save the header.
   572  	updates, err := sf.saveHeaderUpdates()
   573  	if err != nil {
   574  		t.Fatal("Failed to create updates to save header", err)
   575  	}
   576  	if err := sf.createAndApplyTransaction(updates...); err != nil {
   577  		t.Fatal("Failed to save header", err)
   578  	}
   579  
   580  	// Make sure the chunkOffset was updated correctly.
   581  	if sf.staticMetadata.ChunkOffset != 2*pageSize {
   582  		t.Fatal("ChunkOffset wasn't updated correctly", sf.staticMetadata.ChunkOffset, 2*pageSize)
   583  	}
   584  
   585  	// Make sure that the checksum was moved correctly.
   586  	readChunkData := make([]byte, len(chunkData))
   587  	if _, err := f.ReadAt(readChunkData, sf.staticMetadata.ChunkOffset); err != nil {
   588  		t.Fatal("Checksum wasn't moved correctly")
   589  	}
   590  
   591  	// Make sure the metadata was written to disk correctly.
   592  	rawMetadata, err := marshalMetadata(sf.staticMetadata)
   593  	if err != nil {
   594  		t.Fatal("Failed to marshal metadata", err)
   595  	}
   596  	readMetadata := make([]byte, len(rawMetadata))
   597  	if _, err := f.ReadAt(readMetadata, 0); err != nil {
   598  		t.Fatal("Failed to read metadata", err)
   599  	}
   600  	if !bytes.Equal(rawMetadata, readMetadata) {
   601  		fmt.Println(string(rawMetadata))
   602  		fmt.Println(string(readMetadata))
   603  		t.Fatal("Metadata on disk doesn't match marshaled metadata")
   604  	}
   605  
   606  	// Make sure that the pubKeyTable was written to disk correctly.
   607  	rawPubKeyTAble, err := marshalPubKeyTable(sf.pubKeyTable)
   608  	if err != nil {
   609  		t.Fatal("Failed to marshal pubKeyTable", err)
   610  	}
   611  	readPubKeyTable := make([]byte, len(rawPubKeyTAble))
   612  	if _, err := f.ReadAt(readPubKeyTable, sf.staticMetadata.PubKeyTableOffset); err != nil {
   613  		t.Fatal("Failed to read pubKeyTable", err)
   614  	}
   615  	if !bytes.Equal(rawPubKeyTAble, readPubKeyTable) {
   616  		t.Fatal("pubKeyTable on disk doesn't match marshaled pubKeyTable")
   617  	}
   618  }
   619  
   620  // testApply tests if a given method applies a set of updates correctly.
   621  func testApply(t *testing.T, siaFile *SiaFile, apply func(...writeaheadlog.Update) error) {
   622  	// Create an update that writes random data to a random index i.
   623  	index := fastrand.Intn(100) + 1
   624  	data := fastrand.Bytes(100)
   625  	update := siaFile.createInsertUpdate(int64(index), data)
   626  
   627  	// Apply update.
   628  	if err := apply(update); err != nil {
   629  		t.Fatal("Failed to apply update", err)
   630  	}
   631  	// Open file.
   632  	file, err := os.Open(siaFile.siaFilePath)
   633  	if err != nil {
   634  		t.Fatal("Failed to open file", err)
   635  	}
   636  	// Check if correct data was written.
   637  	readData := make([]byte, len(data))
   638  	if _, err := file.ReadAt(readData, int64(index)); err != nil {
   639  		t.Fatal("Failed to read written data back from disk", err)
   640  	}
   641  	if !bytes.Equal(data, readData) {
   642  		t.Fatal("Read data doesn't equal written data")
   643  	}
   644  }
   645  
   646  // TestUpdateUsedHosts tests the updateUsedHosts method.
   647  func TestUpdateUsedHosts(t *testing.T) {
   648  	if testing.Short() {
   649  		t.SkipNow()
   650  	}
   651  	t.Parallel()
   652  
   653  	sf := newBlankTestFile()
   654  	sf.addRandomHostKeys(10)
   655  
   656  	// All the host keys should be used.
   657  	for _, entry := range sf.pubKeyTable {
   658  		if !entry.Used {
   659  			t.Fatal("all hosts are expected to be used at the beginning of the test")
   660  		}
   661  	}
   662  
   663  	// Report only half the hosts as still being used.
   664  	var used []types.SiaPublicKey
   665  	for i, entry := range sf.pubKeyTable {
   666  		if i%2 == 0 {
   667  			used = append(used, entry.PublicKey)
   668  		}
   669  	}
   670  	updates, err := sf.updateUsedHosts(used)
   671  	if err != nil {
   672  		t.Fatal("failed to update hosts", err)
   673  	}
   674  	err = sf.createAndApplyTransaction(updates...)
   675  	if err != nil {
   676  		t.Fatal(err)
   677  	}
   678  
   679  	// Create a map of the used keys for faster lookups.
   680  	usedMap := make(map[string]struct{})
   681  	for _, key := range used {
   682  		usedMap[key.String()] = struct{}{}
   683  	}
   684  
   685  	// Check that the flag was set correctly.
   686  	for _, entry := range sf.pubKeyTable {
   687  		_, exists := usedMap[entry.PublicKey.String()]
   688  		if entry.Used != exists {
   689  			t.Errorf("expected flag to be %v but was %v", exists, entry.Used)
   690  		}
   691  	}
   692  
   693  	// Reload the siafile to see if the flags were also persisted.
   694  	sf, err = LoadSiaFile(sf.siaFilePath, sf.wal)
   695  	if err != nil {
   696  		t.Fatal(err)
   697  	}
   698  
   699  	// Check that the flags are still set correctly.
   700  	for _, entry := range sf.pubKeyTable {
   701  		_, exists := usedMap[entry.PublicKey.String()]
   702  		if entry.Used != exists {
   703  			t.Errorf("expected flag to be %v but was %v", exists, entry.Used)
   704  		}
   705  	}
   706  
   707  	// Also check the flags in order. Making sure that persisting them didn't
   708  	// change the order.
   709  	for i, entry := range sf.pubKeyTable {
   710  		expectedUsed := i%2 == 0
   711  		if entry.Used != expectedUsed {
   712  			t.Errorf("expected flag to be %v but was %v", expectedUsed, entry.Used)
   713  		}
   714  	}
   715  }
   716  
   717  // TestChunkOffset tests the chunkOffset method.
   718  func TestChunkOffset(t *testing.T) {
   719  	if testing.Short() {
   720  		t.SkipNow()
   721  	}
   722  	t.Parallel()
   723  
   724  	sf := newTestFile()
   725  
   726  	// Set the static pages per chunk to a random value.
   727  	sf.staticMetadata.StaticPagesPerChunk = uint8(fastrand.Intn(5)) + 1
   728  
   729  	// Calculate the offset of the first chunk.
   730  	offset1 := sf.chunkOffset(0)
   731  	if expectedOffset := sf.staticMetadata.ChunkOffset; expectedOffset != offset1 {
   732  		t.Fatalf("expected offset %v but got %v", sf.staticMetadata.ChunkOffset, offset1)
   733  	}
   734  
   735  	// Calculate the offset of the second chunk.
   736  	offset2 := sf.chunkOffset(1)
   737  	if expectedOffset := offset1 + int64(sf.staticMetadata.StaticPagesPerChunk)*pageSize; expectedOffset != offset2 {
   738  		t.Fatalf("expected offset %v but got %v", expectedOffset, offset2)
   739  	}
   740  
   741  	// Make sure that the offsets we calculated are not the same due to not
   742  	// initializing the file correctly.
   743  	if offset2 == offset1 {
   744  		t.Fatal("the calculated offsets are the same")
   745  	}
   746  }
   747  
   748  // TestSaveChunk checks that saveChunk creates an updated which if applied
   749  // writes the correct data to disk.
   750  func TestSaveChunk(t *testing.T) {
   751  	if testing.Short() {
   752  		t.SkipNow()
   753  	}
   754  	t.Parallel()
   755  
   756  	sf := newTestFile()
   757  
   758  	// Choose a random chunk from the file and replace it.
   759  	chunkIndex := fastrand.Intn(sf.numChunks)
   760  	chunk := randomChunk()
   761  	chunk.Index = chunkIndex
   762  
   763  	// Write the chunk to disk using saveChunk.
   764  	update := sf.saveChunkUpdate(chunk)
   765  	if err := sf.createAndApplyTransaction(update); err != nil {
   766  		t.Fatal(err)
   767  	}
   768  
   769  	// Marshal the chunk.
   770  	marshaledChunk := marshalChunk(chunk)
   771  
   772  	// Read the chunk from disk.
   773  	f, err := os.Open(sf.siaFilePath)
   774  	if err != nil {
   775  		t.Fatal(err)
   776  	}
   777  	defer closeFileInTest(t, f)
   778  
   779  	readChunk := make([]byte, len(marshaledChunk))
   780  	if n, err := f.ReadAt(readChunk, sf.chunkOffset(chunkIndex)); err != nil {
   781  		t.Fatal(err, n, len(marshaledChunk))
   782  	}
   783  
   784  	// The marshaled chunk should equal the chunk we read from disk.
   785  	if !bytes.Equal(readChunk, marshaledChunk) {
   786  		t.Fatal("marshaled chunk doesn't equal chunk on disk", len(readChunk), len(marshaledChunk))
   787  	}
   788  }
   789  
   790  // TestCreateAndApplyTransactionPanic verifies that the
   791  // createAndApplyTransaction helpers panic when the updates can't be applied.
   792  func TestCreateAndApplyTransactionPanic(t *testing.T) {
   793  	if testing.Short() {
   794  		t.SkipNow()
   795  	}
   796  	t.Parallel()
   797  
   798  	// Create invalid update that triggers a panic.
   799  	update := writeaheadlog.Update{
   800  		Name: "invalid name",
   801  	}
   802  
   803  	// Declare a helper to check for a panic.
   804  	assertRecover := func() {
   805  		if r := recover(); r == nil {
   806  			t.Fatalf("Expected a panic")
   807  		}
   808  	}
   809  
   810  	// Run the test for both the method and function
   811  	sf := newBlankTestFile()
   812  	func() {
   813  		defer assertRecover()
   814  		_ = sf.createAndApplyTransaction(update)
   815  	}()
   816  	func() {
   817  		defer assertRecover()
   818  		_ = createAndApplyTransaction(sf.wal, update)
   819  	}()
   820  }
   821  
   822  // TestDeleteUpdateRegression is a regression test that ensure apply updates
   823  // won't panic when called with a set of updates with the last one being
   824  // a delete update.
   825  func TestDeleteUpdateRegression(t *testing.T) {
   826  	if testing.Short() {
   827  		t.SkipNow()
   828  	}
   829  	t.Parallel()
   830  
   831  	// Create siafile
   832  	sf := newBlankTestFile()
   833  
   834  	// Apply updates with the last update as a delete update. This use to trigger
   835  	// a panic. No need to check the return value as we are only concerned with the
   836  	// panic
   837  	update := sf.createDeleteUpdate()
   838  	sf.createAndApplyTransaction(update, update)
   839  }