From 105f0bad80e6447409f16d0b5f1e293d455fa5ef Mon Sep 17 00:00:00 2001 From: Ryan Staudt Date: Sat, 17 Apr 2021 15:34:22 -0500 Subject: [PATCH] blockchain: Add utxoDatabaseInfo. The adds utxoDatabaseInfo which tracks the version, compression version, utxo set version, and creation time of the UTXO database. The UTXO database will be created as a separate database in a subsequent commit. This is part of moving the UTXO set and state to a separate database. --- blockchain/chainio.go | 7 +- blockchain/utxoio.go | 138 ++++++++++++++++++++++++++++++++++++++ blockchain/utxoio_test.go | 70 +++++++++++++++++++ 3 files changed, 211 insertions(+), 4 deletions(-) diff --git a/blockchain/chainio.go b/blockchain/chainio.go index 904fb300..82fc3c68 100644 --- a/blockchain/chainio.go +++ b/blockchain/chainio.go @@ -24,12 +24,11 @@ import ( ) const ( - // currentDatabaseVersion indicates what the current database - // version is. + // currentDatabaseVersion indicates the current database version. currentDatabaseVersion = 9 - // currentBlockIndexVersion indicates what the current block index - // database version. + // currentBlockIndexVersion indicates the current block index database + // version. currentBlockIndexVersion = 3 // blockHdrSize is the size of a block header. This is simply the diff --git a/blockchain/utxoio.go b/blockchain/utxoio.go index 02b4e7a7..cf7cc592 100644 --- a/blockchain/utxoio.go +++ b/blockchain/utxoio.go @@ -8,6 +8,7 @@ package blockchain import ( "fmt" "sync" + "time" "github.com/decred/dcrd/blockchain/standalone/v2" "github.com/decred/dcrd/chaincfg/chainhash" @@ -16,6 +17,29 @@ import ( ) var ( + // utxoDbInfoBucketName is the name of the database bucket used to house + // global versioning and date information for the UTXO database. + utxoDbInfoBucketName = []byte("dbinfo") + + // utxoDbInfoVersionKeyName is the name of the database key used to house the + // database version. It is itself under the utxoDbInfoBucketName bucket. + utxoDbInfoVersionKeyName = []byte("version") + + // utxoDbInfoCompressionVerKeyName is the name of the database key used to + // house the database compression version. It is itself under the + // utxoDbInfoBucketName bucket. + utxoDbInfoCompressionVerKeyName = []byte("compver") + + // utxoDbInfoUtxoVerKeyName is the name of the database key used to house the + // database UTXO set version. It is itself under the utxoDbInfoBucketName + // bucket. + utxoDbInfoUtxoVerKeyName = []byte("utxover") + + // utxoDbInfoCreatedKeyName is the name of the database key used to house + // the date the database was created. It is itself under the + // utxoDbInfoBucketName bucket. + utxoDbInfoCreatedKeyName = []byte("created") + // utxoSetBucketName is the name of the db bucket used to house the unspent // transaction output set. utxoSetBucketName = []byte("utxosetv3") @@ -25,6 +49,120 @@ var ( utxoSetStateKeyName = []byte("utxosetstate") ) +// ----------------------------------------------------------------------------- +// The UTXO database information contains information about the version and date +// of the UTXO database. +// +// It consists of a separate key for each individual piece of information: +// +// Key Value Size Description +// version uint32 4 bytes The version of the database +// compver uint32 4 bytes The script compression version of the database +// utxover uint32 4 bytes The UTXO set version of the database +// created uint64 8 bytes The date of the creation of the database +// ----------------------------------------------------------------------------- + +// utxoDatabaseInfo is the structure that holds the versioning and date +// information for the UTXO database. +type utxoDatabaseInfo struct { + version uint32 + compVer uint32 + utxoVer uint32 + created time.Time +} + +// dbPutUtxoDatabaseInfo uses an existing database transaction to store the +// database information. +func dbPutUtxoDatabaseInfo(dbTx database.Tx, dbi *utxoDatabaseInfo) error { + // uint32Bytes is a helper function to convert a uint32 to a byte slice + // using the byte order specified by the database namespace. + uint32Bytes := func(ui32 uint32) []byte { + var b [4]byte + byteOrder.PutUint32(b[:], ui32) + return b[:] + } + + // uint64Bytes is a helper function to convert a uint64 to a byte slice + // using the byte order specified by the database namespace. + uint64Bytes := func(ui64 uint64) []byte { + var b [8]byte + byteOrder.PutUint64(b[:], ui64) + return b[:] + } + + // Store the database version. + meta := dbTx.Metadata() + bucket := meta.Bucket(utxoDbInfoBucketName) + err := bucket.Put(utxoDbInfoVersionKeyName, uint32Bytes(dbi.version)) + if err != nil { + return err + } + + // Store the compression version. + err = bucket.Put(utxoDbInfoCompressionVerKeyName, uint32Bytes(dbi.compVer)) + if err != nil { + return err + } + + // Store the UTXO set version. + err = bucket.Put(utxoDbInfoUtxoVerKeyName, uint32Bytes(dbi.utxoVer)) + if err != nil { + return err + } + + // Store the database creation date. + return bucket.Put(utxoDbInfoCreatedKeyName, + uint64Bytes(uint64(dbi.created.Unix()))) +} + +// dbFetchUtxoDatabaseInfo uses an existing database transaction to fetch the +// database versioning and creation information. +func dbFetchUtxoDatabaseInfo(dbTx database.Tx) *utxoDatabaseInfo { + meta := dbTx.Metadata() + bucket := meta.Bucket(utxoDbInfoBucketName) + + // Uninitialized state. + if bucket == nil { + return nil + } + + // Load the database version. + var version uint32 + versionBytes := bucket.Get(utxoDbInfoVersionKeyName) + if versionBytes != nil { + version = byteOrder.Uint32(versionBytes) + } + + // Load the database compression version. + var compVer uint32 + compVerBytes := bucket.Get(utxoDbInfoCompressionVerKeyName) + if compVerBytes != nil { + compVer = byteOrder.Uint32(compVerBytes) + } + + // Load the database UTXO set version. + var utxoVer uint32 + utxoVerBytes := bucket.Get(utxoDbInfoUtxoVerKeyName) + if utxoVerBytes != nil { + utxoVer = byteOrder.Uint32(utxoVerBytes) + } + + // Load the database creation date. + var created time.Time + createdBytes := bucket.Get(utxoDbInfoCreatedKeyName) + if createdBytes != nil { + ts := byteOrder.Uint64(createdBytes) + created = time.Unix(int64(ts), 0) + } + + return &utxoDatabaseInfo{ + version: version, + compVer: compVer, + utxoVer: utxoVer, + created: created, + } +} + // ----------------------------------------------------------------------------- // The unspent transaction output (utxo) set consists of an entry for each // unspent output using a format that is optimized to reduce space using domain diff --git a/blockchain/utxoio_test.go b/blockchain/utxoio_test.go index 7b7054a2..c3113eb1 100644 --- a/blockchain/utxoio_test.go +++ b/blockchain/utxoio_test.go @@ -12,12 +12,82 @@ import ( "path/filepath" "reflect" "testing" + "time" "github.com/decred/dcrd/blockchain/stake/v4" "github.com/decred/dcrd/database/v2" "github.com/decred/dcrd/wire" ) +// TestDbFetchUtxoDatabaseInfo ensures that putting and fetching the utxo +// database info works as expected. +func TestDbFetchUtxoDatabaseInfo(t *testing.T) { + t.Parallel() + + // Create a test database. + dbPath := filepath.Join(os.TempDir(), "test-dbfetchutxodatabaseinfo") + _ = os.RemoveAll(dbPath) + db, err := database.Create("ffldb", dbPath, wire.MainNet) + if err != nil { + t.Fatalf("error creating test database: %v", err) + } + defer os.RemoveAll(dbPath) + defer db.Close() + + tests := []struct { + name string + dbInfo *utxoDatabaseInfo + }{{ + name: "without UTXO database info (fresh database)", + dbInfo: nil, + }, { + name: "with UTXO database info", + dbInfo: &utxoDatabaseInfo{ + version: 1, + compVer: 2, + utxoVer: 3, + created: time.Unix(1584246683, 0), // 2020-03-15 04:31:23 UTC + }, + }} + + for _, test := range tests { + if test.dbInfo != nil { + // Create the UTXO database info bucket. + err = db.Update(func(dbTx database.Tx) error { + _, err := dbTx.Metadata().CreateBucketIfNotExists(utxoDbInfoBucketName) + return err + }) + if err != nil { + t.Fatalf("%q: error creating UTXO bucket: %v", test.name, err) + } + + // Update the UTXO database info in the database. + err = db.Update(func(dbTx database.Tx) error { + return dbPutUtxoDatabaseInfo(dbTx, test.dbInfo) + }) + if err != nil { + t.Fatalf("%q: error putting UTXO database info: %v", test.name, err) + } + } + + // Fetch the UTXO database info from the database. + err = db.View(func(dbTx database.Tx) error { + gotDbInfo := dbFetchUtxoDatabaseInfo(dbTx) + + // Ensure that the fetched UTXO database info matches the expected UTXO + // database info. + if !reflect.DeepEqual(gotDbInfo, test.dbInfo) { + t.Fatalf("%q: mismatched db info:\nwant: %+v\n got: %+v\n", test.name, + test.dbInfo, gotDbInfo) + } + return nil + }) + if err != nil { + t.Fatalf("%q: error fetching UTXO database info: %v", test.name, err) + } + } +} + // TestUtxoSerialization ensures serializing and deserializing unspent // transaction output entries works as expected. func TestUtxoSerialization(t *testing.T) {