// DebugTicketDBSpentBucketString prints the contents of the spent tickets // database bucket indicated to a string that is returned. If the verbose // flag is indicated, the contents of each ticket are printed as well. func DebugTicketDBSpentBucketString(tmdb *stake.TicketDB, height int64, verbose bool) (string, error) { var buffer bytes.Buffer str := fmt.Sprintf("Contents of spent ticket bucket height %v:\n", height) buffer.WriteString(str) bucketTickets, err := tmdb.DumpSpentTickets(height) if err != nil { return "", err } for hash, td := range bucketTickets { missedStr := "" if td.Missed { missedStr = "Missed" } else { missedStr = "Spent" } str = fmt.Sprintf("%v (%v)\n", hash, missedStr) buffer.WriteString(str) if verbose { str = fmt.Sprintf("%v\n", DebugTicketDataString(td)) buffer.WriteString(str) } } return buffer.String(), nil }
// cloneTicketDB makes a deep copy of a ticket DB by // serializing it to a gob and then deserializing it // into an empty container. func cloneTicketDB(tmdb *stake.TicketDB) (stake.TicketMaps, error) { mapsPointer := tmdb.DumpMapsPointer() mapsBytes, err := mapsPointer.GobEncode() if err != nil { return stake.TicketMaps{}, fmt.Errorf("clone db error: could not serialize ticketMaps") } var mapsCopy stake.TicketMaps if err := mapsCopy.GobDecode(mapsBytes); err != nil { return stake.TicketMaps{}, fmt.Errorf("clone db error: could not deserialize " + "ticketMaps") } return mapsCopy, nil }
// DebugTicketDBLiveString prints out the number of tickets in each // bucket of the ticket database as a string. func DebugTicketDBLiveString(tmdb *stake.TicketDB, chainParams *chaincfg.Params) (string, error) { var buffer bytes.Buffer buffer.WriteString("\n") for i := 0; i < stake.BucketsSize; i++ { bucketTickets, err := tmdb.DumpLiveTickets(uint8(i)) if err != nil { return "", err } str := fmt.Sprintf("%v: %v\t", i, len(bucketTickets)) buffer.WriteString(str) // Add newlines. if (i+1)%4 == 0 { buffer.WriteString("\n") } } return buffer.String(), nil }
// DebugTicketDBMissedString prints out the contents of the missed ticket // database to a string. If verbose is indicated, the ticket data itself // is printed along with the ticket hashes. func DebugTicketDBMissedString(tmdb *stake.TicketDB, verbose bool) (string, error) { var buffer bytes.Buffer str := fmt.Sprintf("Contents of missed ticket database:\n") buffer.WriteString(str) bucketTickets, err := tmdb.DumpMissedTickets() if err != nil { return "", err } for hash, td := range bucketTickets { str = fmt.Sprintf("%v\n", hash) buffer.WriteString(str) if verbose { str = fmt.Sprintf("%v\n", DebugTicketDataString(td)) buffer.WriteString(str) } } return buffer.String(), nil }
// DebugTicketDBLiveBucketString returns a string containing the ticket hashes // found in a specific bucket of the live ticket database. If the verbose flag // is called, it dumps the contents of the ticket data as well. func DebugTicketDBLiveBucketString(tmdb *stake.TicketDB, bucket uint8, verbose bool) (string, error) { var buffer bytes.Buffer str := fmt.Sprintf("Contents of live ticket bucket %v:\n", bucket) buffer.WriteString(str) bucketTickets, err := tmdb.DumpLiveTickets(bucket) if err != nil { return "", err } for hash, td := range bucketTickets { str = fmt.Sprintf("%v\n", hash) buffer.WriteString(str) if verbose { str = fmt.Sprintf("%v\n", DebugTicketDataString(td)) buffer.WriteString(str) } } return buffer.String(), nil }
// TicketDbThumbprint takes all the tickets in the respective ticket db, // sorts them, hashes their contents into a list, and then hashes that list. // The resultant hash is the thumbprint of the ticket database, and should // be the same across all clients that are synced to the same block. Returns // an array of hashes len 3, containing (1) live tickets (2) spent tickets // and (3) missed tickets. // Do NOT use on mainnet or in production. For debug use only! Make sure // the blockchain is frozen when you call this function. func TicketDbThumbprint(tmdb *stake.TicketDB, chainParams *chaincfg.Params) ([]*chainhash.Hash, error) { // Container for the three master hashes to go into. dbThumbprints := make([]*chainhash.Hash, 3, 3) // (1) Live tickets. allLiveTickets := stake.NewTicketDataSliceEmpty() for i := 0; i < stake.BucketsSize; i++ { bucketTickets, err := tmdb.DumpLiveTickets(uint8(i)) if err != nil { return nil, err } for _, td := range bucketTickets { allLiveTickets = append(allLiveTickets, td) } } // Sort by the number data hash, since we already have this implemented // and it's also unique. sort.Sort(allLiveTickets) // Create a buffer, dump all the data into it, and hash. var buf bytes.Buffer for _, td := range allLiveTickets { writeTicketDataToBuf(&buf, td) } liveHash := chainhash.HashFunc(buf.Bytes()) liveThumbprint, err := chainhash.NewHash(liveHash[:]) if err != nil { return nil, err } dbThumbprints[0] = liveThumbprint // (2) Spent tickets. height := tmdb.GetTopBlock() allSpentTickets := stake.NewTicketDataSliceEmpty() for i := int64(chainParams.StakeEnabledHeight); i <= height; i++ { bucketTickets, err := tmdb.DumpSpentTickets(i) if err != nil { return nil, err } for _, td := range bucketTickets { allSpentTickets = append(allSpentTickets, td) } } sort.Sort(allSpentTickets) buf.Reset() // Flush buffer for _, td := range allSpentTickets { writeTicketDataToBuf(&buf, td) } spentHash := chainhash.HashFunc(buf.Bytes()) spentThumbprint, err := chainhash.NewHash(spentHash[:]) if err != nil { return nil, err } dbThumbprints[1] = spentThumbprint // (3) Missed tickets. allMissedTickets := stake.NewTicketDataSliceEmpty() missedTickets, err := tmdb.DumpMissedTickets() if err != nil { return nil, err } for _, td := range missedTickets { allMissedTickets = append(allMissedTickets, td) } sort.Sort(allMissedTickets) buf.Reset() // Flush buffer missedHash := chainhash.HashFunc(buf.Bytes()) missedThumbprint, err := chainhash.NewHash(missedHash[:]) if err != nil { return nil, err } dbThumbprints[2] = missedThumbprint return dbThumbprints, nil }
func TestTicketDB(t *testing.T) { // Declare some useful variables testBCHeight := int64(168) // Set up a DB database, err := database.CreateDB("leveldb", "ticketdb_test") if err != nil { t.Errorf("Db create error: %v", err.Error()) } // Make a new tmdb to fill with dummy live and used tickets var tmdb stake.TicketDB tmdb.Initialize(simNetParams, database) filename := filepath.Join("..", "/../blockchain/testdata", "blocks0to168.bz2") fi, err := os.Open(filename) bcStream := bzip2.NewReader(fi) defer fi.Close() // Create a buffer of the read file bcBuf := new(bytes.Buffer) bcBuf.ReadFrom(bcStream) // Create decoder from the buffer and a map to store the data bcDecoder := gob.NewDecoder(bcBuf) blockchain := make(map[int64][]byte) // Decode the blockchain into the map if err := bcDecoder.Decode(&blockchain); err != nil { t.Errorf("error decoding test blockchain") } var CopyOfMapsAtBlock50, CopyOfMapsAtBlock168 stake.TicketMaps var ticketsToSpendIn167 []chainhash.Hash var sortedTickets167 []*stake.TicketData for i := int64(0); i <= testBCHeight; i++ { block, err := dcrutil.NewBlockFromBytes(blockchain[i]) if err != nil { t.Errorf("block deserialization error on block %v", i) } block.SetHeight(i) database.InsertBlock(block) tmdb.InsertBlock(block) if i == 50 { // Create snapshot of tmdb at block 50 CopyOfMapsAtBlock50, err = cloneTicketDB(&tmdb) if err != nil { t.Errorf("db cloning at block 50 failure! %v", err) } } // Test to make sure that ticket selection is working correctly. if i == 167 { // Sort the entire list of tickets lexicographically by sorting // each bucket and then appending it to the list. Then store it // to use in the next block. totalTickets := 0 sortedSlice := make([]*stake.TicketData, 0) for i := 0; i < stake.BucketsSize; i++ { tix, err := tmdb.DumpLiveTickets(uint8(i)) if err != nil { t.Errorf("error dumping live tickets") } mapLen := len(tix) totalTickets += mapLen tempTdSlice := stake.NewTicketDataSlice(mapLen) itr := 0 // Iterator for _, td := range tix { tempTdSlice[itr] = td itr++ } sort.Sort(tempTdSlice) sortedSlice = append(sortedSlice, tempTdSlice...) } sortedTickets167 = sortedSlice } if i == 168 { parentBlock, err := dcrutil.NewBlockFromBytes(blockchain[i-1]) if err != nil { t.Errorf("block deserialization error on block %v", i-1) } pbhB, err := parentBlock.MsgBlock().Header.Bytes() if err != nil { t.Errorf("block header serialization error") } prng := stake.NewHash256PRNG(pbhB) ts, err := stake.FindTicketIdxs(int64(len(sortedTickets167)), int(simNetParams.TicketsPerBlock), prng) if err != nil { t.Errorf("failure on FindTicketIdxs") } for _, idx := range ts { ticketsToSpendIn167 = append(ticketsToSpendIn167, sortedTickets167[idx].SStxHash) } // Make sure that the tickets that were supposed to be spent or // missed were. spentTix, err := tmdb.DumpSpentTickets(i) if err != nil { t.Errorf("DumpSpentTickets failure") } for _, h := range ticketsToSpendIn167 { if _, ok := spentTix[h]; !ok { t.Errorf("missing ticket %v that should have been missed "+ "or spent in block %v", h, i) } } // Create snapshot of tmdb at block 168 CopyOfMapsAtBlock168, err = cloneTicketDB(&tmdb) if err != nil { t.Errorf("db cloning at block 168 failure! %v", err) } } } // Remove five blocks from HEAD~1 _, _, _, err = tmdb.RemoveBlockToHeight(50) if err != nil { t.Errorf("error: %v", err) } // Test if the roll back was symmetric to the earlier snapshot if !reflect.DeepEqual(tmdb.DumpMapsPointer(), CopyOfMapsAtBlock50) { t.Errorf("The td did not restore to a previous block height correctly!") } // Test rescanning a ticket db err = tmdb.RescanTicketDB() if err != nil { t.Errorf("rescanticketdb err: %v", err.Error()) } // Test if the db file storage was symmetric to the earlier snapshot if !reflect.DeepEqual(tmdb.DumpMapsPointer(), CopyOfMapsAtBlock168) { t.Errorf("The td did not rescan to HEAD correctly!") } err = os.Mkdir("testdata/", os.FileMode(0700)) if err != nil { t.Error(err) } // Store the ticket db to disk err = tmdb.Store("testdata/", "testtmdb") if err != nil { t.Errorf("error: %v", err) } var tmdb2 stake.TicketDB err = tmdb2.LoadTicketDBs("testdata/", "testtmdb", simNetParams, database) if err != nil { t.Errorf("error: %v", err) } // Test if the db file storage was symmetric to previously rescanned one if !reflect.DeepEqual(tmdb.DumpMapsPointer(), tmdb2.DumpMapsPointer()) { t.Errorf("The td did not rescan to a previous block height correctly!") } tmdb2.Close() // Test dumping missing tickets from block 152 missedIn152, _ := chainhash.NewHashFromStr( "84f7f866b0af1cc278cb8e0b2b76024a07542512c76487c83628c14c650de4fa") tmdb.RemoveBlockToHeight(152) missedTix, err := tmdb.DumpMissedTickets() if err != nil { t.Errorf("err dumping missed tix: %v", err.Error()) } if _, exists := missedTix[*missedIn152]; !exists { t.Errorf("couldn't finding missed tx 1 %v in tmdb @ block 152!", missedIn152) } tmdb.RescanTicketDB() // Make sure that the revoked map contains the revoked tx revokedSlice := []*chainhash.Hash{missedIn152} revokedTix, err := tmdb.DumpRevokedTickets() if err != nil { t.Errorf("err dumping missed tix: %v", err.Error()) } if len(revokedTix) != 1 { t.Errorf("revoked ticket map is wrong len, got %v, want %v", len(revokedTix), 1) } _, wasMissedIn152 := revokedTix[*revokedSlice[0]] ticketsRevoked := wasMissedIn152 if !ticketsRevoked { t.Errorf("revoked ticket map did not include tickets missed in " + "block 152 and later revoked") } database.Close() tmdb.Close() os.RemoveAll("ticketdb_test") os.Remove("./ticketdb_test.ver") os.Remove("testdata/testtmdb") os.Remove("testdata") }
// chainSetup is used to create a new db and chain instance with the genesis // block already inserted. In addition to the new chain instnce, it returns // a teardown function the caller should invoke when done testing to clean up. func chainSetup(dbName string, params *chaincfg.Params) (*blockchain.BlockChain, func(), error) { if !isSupportedDbType(testDbType) { return nil, nil, fmt.Errorf("unsupported db type %v", testDbType) } // Handle memory database specially since it doesn't need the disk // specific handling. var db database.Db tmdb := new(stake.TicketDB) var teardown func() if testDbType == "memdb" { ndb, err := database.CreateDB(testDbType) if err != nil { return nil, nil, fmt.Errorf("error creating db: %v", err) } db = ndb // Setup a teardown function for cleaning up. This function is // returned to the caller to be invoked when it is done testing. teardown = func() { tmdb.Close() db.Close() } } else { // Create the root directory for test databases. if !fileExists(testDbRoot) { if err := os.MkdirAll(testDbRoot, 0700); err != nil { err := fmt.Errorf("unable to create test db "+ "root: %v", err) return nil, nil, err } } // Create a new database to store the accepted blocks into. dbPath := filepath.Join(testDbRoot, dbName) _ = os.RemoveAll(dbPath) ndb, err := database.CreateDB(testDbType, dbPath) if err != nil { return nil, nil, fmt.Errorf("error creating db: %v", err) } db = ndb // Setup a teardown function for cleaning up. This function is // returned to the caller to be invoked when it is done testing. teardown = func() { dbVersionPath := filepath.Join(testDbRoot, dbName+".ver") tmdb.Close() db.Sync() db.Close() os.RemoveAll(dbPath) os.Remove(dbVersionPath) os.RemoveAll(testDbRoot) } } // Insert the main network genesis block. This is part of the initial // database setup. genesisBlock := dcrutil.NewBlock(params.GenesisBlock) genesisBlock.SetHeight(int64(0)) _, err := db.InsertBlock(genesisBlock) if err != nil { teardown() err := fmt.Errorf("failed to insert genesis block: %v", err) return nil, nil, err } // Start the ticket database. tmdb.Initialize(params, db) tmdb.RescanTicketDB() chain := blockchain.New(db, tmdb, params, nil) return chain, teardown, nil }