func TestLotteryNumSelection(t *testing.T) { // Test finding ticket indexes. seed := chainhash.HashFuncB([]byte{0x01}) prng := stake.NewHash256PRNG(seed) ticketsInPool := int64(56789) tooFewTickets := int64(4) justEnoughTickets := int64(5) ticketsPerBlock := 5 _, err := stake.FindTicketIdxs(tooFewTickets, ticketsPerBlock, prng) if err == nil { t.Errorf("got unexpected no error for FindTicketIdxs too few tickets " + "test") } tickets, err := stake.FindTicketIdxs(ticketsInPool, ticketsPerBlock, prng) if err != nil { t.Errorf("got unexpected error for FindTicketIdxs 1 test") } ticketsExp := []int{34850, 8346, 27636, 54482, 25482} if !reflect.DeepEqual(ticketsExp, tickets) { t.Errorf("Unexpected tickets selected; got %v, want %v", tickets, ticketsExp) } // Ensure that it can find all suitable ticket numbers in a small // bucket of tickets. tickets, err = stake.FindTicketIdxs(justEnoughTickets, ticketsPerBlock, prng) if err != nil { t.Errorf("got unexpected error for FindTicketIdxs 2 test") } ticketsExp = []int{3, 0, 4, 2, 1} if !reflect.DeepEqual(ticketsExp, tickets) { t.Errorf("Unexpected tickets selected; got %v, want %v", tickets, ticketsExp) } lastHashExp, _ := chainhash.NewHashFromStr("e97ce54aea63a883a82871e752c" + "6ec3c5731fffc63dafc3767c06861b0b2fa65") lastHash := prng.StateHash() if *lastHashExp != lastHash { t.Errorf("expected final state of %v, got %v", lastHashExp, lastHash) } }
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") }
// getWinningTicketsWithStore is a helper function that returns winning tickets // along with the ticket pool size and transaction store for the given node. // Note that this function evaluates the lottery data predominantly for mining // purposes; that is, it retrieves the lottery data which needs to go into // the next block when mining on top of this block. // This function is NOT safe for concurrent access. func (b *BlockChain) getWinningTicketsWithStore(node *blockNode) ([]chainhash.Hash, int, [6]byte, TicketStore, error) { if node.height < b.chainParams.StakeEnabledHeight { return []chainhash.Hash{}, 0, [6]byte{}, nil, nil } evalLotteryWinners := false if node.height >= b.chainParams.StakeValidationHeight-1 { evalLotteryWinners = true } block, err := b.getBlockFromHash(node.hash) if err != nil { return nil, 0, [6]byte{}, nil, err } headerB, err := node.header.Bytes() if err != nil { return nil, 0, [6]byte{}, nil, err } ticketStore, err := b.fetchTicketStore(node) if err != nil { return nil, 0, [6]byte{}, nil, fmt.Errorf("Failed to generate ticket store for node %v; "+ "error given: %v", node.hash, err) } if ticketStore != nil { // We need the viewpoint of spendable tickets given that the // current block was actually added. err = b.connectTickets(ticketStore, node, block) if err != nil { return nil, 0, [6]byte{}, nil, err } } // Sort the entire list of tickets lexicographically by sorting // each bucket and then appending it to the list. tpdBucketMap := make(map[uint8][]*TicketPatchData) for _, tpd := range ticketStore { // Bucket does not exist. if _, ok := tpdBucketMap[tpd.td.Prefix]; !ok { tpdBucketMap[tpd.td.Prefix] = make([]*TicketPatchData, 1) tpdBucketMap[tpd.td.Prefix][0] = tpd } else { // Bucket exists. data := tpdBucketMap[tpd.td.Prefix] tpdBucketMap[tpd.td.Prefix] = append(data, tpd) } } totalTickets := 0 sortedSlice := make([]*stake.TicketData, 0) for i := 0; i < stake.BucketsSize; i++ { ltb, err := b.GenerateLiveTicketBucket(ticketStore, tpdBucketMap, uint8(i)) if err != nil { h := node.hash str := fmt.Sprintf("Failed to generate a live ticket bucket "+ "to evaluate the lottery data for node %v, height %v! Error "+ "given: %v", h, node.height, err.Error()) return nil, 0, [6]byte{}, nil, fmt.Errorf(str) } mapLen := len(ltb) tempTdSlice := stake.NewTicketDataSlice(mapLen) itr := 0 // Iterator for _, td := range ltb { tempTdSlice[itr] = td itr++ totalTickets++ } sort.Sort(tempTdSlice) sortedSlice = append(sortedSlice, tempTdSlice...) } // Use the parent block's header to seed a PRNG that picks the // lottery winners. winningTickets := make([]chainhash.Hash, 0) var finalState [6]byte stateBuffer := make([]byte, 0, (b.chainParams.TicketsPerBlock+1)*chainhash.HashSize) if evalLotteryWinners { ticketsPerBlock := int(b.chainParams.TicketsPerBlock) prng := stake.NewHash256PRNG(headerB) ts, err := stake.FindTicketIdxs(int64(totalTickets), ticketsPerBlock, prng) if err != nil { return nil, 0, [6]byte{}, nil, err } for _, idx := range ts { winningTickets = append(winningTickets, sortedSlice[idx].SStxHash) stateBuffer = append(stateBuffer, sortedSlice[idx].SStxHash[:]...) } lastHash := prng.StateHash() stateBuffer = append(stateBuffer, lastHash[:]...) copy(finalState[:], chainhash.HashFuncB(stateBuffer)[0:6]) } return winningTickets, totalTickets, finalState, ticketStore, nil }
// connectTickets updates the passed map by removing removing any tickets // from the ticket pool that have been considered spent or missed in this block // according to the block header. Then, it connects all the newly mature tickets // to the passed map. func (b *BlockChain) connectTickets(tixStore TicketStore, node *blockNode, block *dcrutil.Block) error { if tixStore == nil { return fmt.Errorf("nil ticket store!") } // Nothing to do if tickets haven't yet possibly matured. height := node.height if height < b.chainParams.StakeEnabledHeight { return nil } parentBlock, err := b.GetBlockFromHash(node.parentHash) if err != nil { return err } revocations := node.header.Revocations tM := int64(b.chainParams.TicketMaturity) // Skip a number of validation steps before we requiring chain // voting. if node.height >= b.chainParams.StakeValidationHeight { regularTxTreeValid := dcrutil.IsFlagSet16(node.header.VoteBits, dcrutil.BlockValid) thisNodeStakeViewpoint := ViewpointPrevInvalidStake if regularTxTreeValid { thisNodeStakeViewpoint = ViewpointPrevValidStake } // We need the missed tickets bucket from the original perspective of // the node. missedTickets, err := b.GenerateMissedTickets(tixStore) if err != nil { return err } // TxStore at blockchain HEAD + TxTreeRegular of prevBlock (if // validated) for this node. txInputStoreStake, err := b.fetchInputTransactions(node, block, thisNodeStakeViewpoint) if err != nil { errStr := fmt.Sprintf("fetchInputTransactions failed for incoming "+ "node %v; error given: %v", node.hash, err) return errors.New(errStr) } // PART 1: Spend/miss winner tickets // Iterate through all the SSGen (vote) tx in the block and add them to // a map of tickets that were actually used. spentTicketsFromBlock := make(map[chainhash.Hash]bool) numberOfSSgen := 0 for _, staketx := range block.STransactions() { if is, _ := stake.IsSSGen(staketx); is { msgTx := staketx.MsgTx() sstxIn := msgTx.TxIn[1] // sstx input sstxHash := sstxIn.PreviousOutPoint.Hash originTx, exists := txInputStoreStake[sstxHash] if !exists { str := fmt.Sprintf("unable to find input transaction "+ "%v for transaction %v", sstxHash, staketx.Sha()) return ruleError(ErrMissingTx, str) } sstxHeight := originTx.BlockHeight // Check maturity of ticket; we can only spend the ticket after it // hits maturity at height + tM + 1. if (height - sstxHeight) < (tM + 1) { blockSha := block.Sha() errStr := fmt.Sprintf("Error: A ticket spend as an SSGen in "+ "block height %v was immature! Block sha %v", height, blockSha) return errors.New(errStr) } // Fill out the ticket data. spentTicketsFromBlock[sstxHash] = true numberOfSSgen++ } } // Obtain the TicketsPerBlock many tickets that were selected this round, // then check these against the tickets that were actually used to make // sure that any SSGen actually match the selected tickets. Commit the // spent or missed tickets to the ticket store after. spentAndMissedTickets := make(TicketStore) tixSpent := 0 tixMissed := 0 // Sort the entire list of tickets lexicographically by sorting // each bucket and then appending it to the list. Start by generating // a prefix matched map of tickets to speed up the lookup. tpdBucketMap := make(map[uint8][]*TicketPatchData) for _, tpd := range tixStore { // Bucket does not exist. if _, ok := tpdBucketMap[tpd.td.Prefix]; !ok { tpdBucketMap[tpd.td.Prefix] = make([]*TicketPatchData, 1) tpdBucketMap[tpd.td.Prefix][0] = tpd } else { // Bucket exists. data := tpdBucketMap[tpd.td.Prefix] tpdBucketMap[tpd.td.Prefix] = append(data, tpd) } } totalTickets := 0 sortedSlice := make([]*stake.TicketData, 0) for i := 0; i < stake.BucketsSize; i++ { ltb, err := b.GenerateLiveTicketBucket(tixStore, tpdBucketMap, uint8(i)) if err != nil { h := node.hash str := fmt.Sprintf("Failed to generate live ticket bucket "+ "%v for node %v, height %v! Error: %v", i, h, node.height, err.Error()) return fmt.Errorf(str) } mapLen := len(ltb) tempTdSlice := stake.NewTicketDataSlice(mapLen) itr := 0 // Iterator for _, td := range ltb { tempTdSlice[itr] = td itr++ totalTickets++ } sort.Sort(tempTdSlice) sortedSlice = append(sortedSlice, tempTdSlice...) } // Use the parent block's header to seed a PRNG that picks the // lottery winners. ticketsPerBlock := int(b.chainParams.TicketsPerBlock) pbhB, err := parentBlock.MsgBlock().Header.Bytes() if err != nil { return err } prng := stake.NewHash256PRNG(pbhB) ts, err := stake.FindTicketIdxs(int64(totalTickets), ticketsPerBlock, prng) if err != nil { return err } ticketsToSpendOrMiss := make([]*stake.TicketData, ticketsPerBlock, ticketsPerBlock) for i, idx := range ts { ticketsToSpendOrMiss[i] = sortedSlice[idx] } // Spend or miss these tickets by checking for their existence in the // passed spentTicketsFromBlock map. for _, ticket := range ticketsToSpendOrMiss { // Move the ticket from active tickets map into the used tickets // map if the ticket was spent. wasSpent, _ := spentTicketsFromBlock[ticket.SStxHash] if wasSpent { tpd := NewTicketPatchData(ticket, TiSpent, nil) spentAndMissedTickets[ticket.SStxHash] = tpd tixSpent++ } else { // Ticket missed being spent and --> false or nil tpd := NewTicketPatchData(ticket, TiMissed, nil) spentAndMissedTickets[ticket.SStxHash] = tpd tixMissed++ } } // This error is thrown if for some reason there exists an SSGen in // the block that doesn't spend a ticket from the eligible list of // tickets, thus making it invalid. if tixSpent != numberOfSSgen { errStr := fmt.Sprintf("an invalid number %v "+ "tickets was spent, but %v many tickets should "+ "have been spent!", tixSpent, numberOfSSgen) return errors.New(errStr) } if tixMissed != (ticketsPerBlock - numberOfSSgen) { errStr := fmt.Sprintf("an invalid number %v "+ "tickets was missed, but %v many tickets should "+ "have been missed!", tixMissed, ticketsPerBlock-numberOfSSgen) return errors.New(errStr) } if (tixSpent + tixMissed) != int(b.chainParams.TicketsPerBlock) { errStr := fmt.Sprintf("an invalid number %v "+ "tickets was spent and missed, but TicketsPerBlock %v many "+ "tickets should have been spent!", tixSpent, ticketsPerBlock) return errors.New(errStr) } // Calculate all the tickets expiring this block and mark them as missed. tpdBucketMap = make(map[uint8][]*TicketPatchData) for _, tpd := range tixStore { // Bucket does not exist. if _, ok := tpdBucketMap[tpd.td.Prefix]; !ok { tpdBucketMap[tpd.td.Prefix] = make([]*TicketPatchData, 1) tpdBucketMap[tpd.td.Prefix][0] = tpd } else { // Bucket exists. data := tpdBucketMap[tpd.td.Prefix] tpdBucketMap[tpd.td.Prefix] = append(data, tpd) } } toExpireHeight := node.height - int64(b.chainParams.TicketExpiry) if !(toExpireHeight < int64(b.chainParams.StakeEnabledHeight)) { for i := 0; i < stake.BucketsSize; i++ { // Generate the live ticket bucket. ltb, err := b.GenerateLiveTicketBucket(tixStore, tpdBucketMap, uint8(i)) if err != nil { return err } for _, ticket := range ltb { if ticket.BlockHeight == toExpireHeight { tpd := NewTicketPatchData(ticket, TiMissed, nil) spentAndMissedTickets[ticket.SStxHash] = tpd } } } } // Merge the ticket store patch containing the spent and missed tickets // with the ticket store. for hash, tpd := range spentAndMissedTickets { tixStore[hash] = tpd } // At this point our tixStore now contains all the spent and missed tx // as per this block. // PART 2: Remove tickets that were missed and are now revoked. // Iterate through all the SSGen (vote) tx in the block and add them to // a map of tickets that were actually used. revocationsFromBlock := make(map[chainhash.Hash]struct{}) numberOfSSRtx := 0 for _, staketx := range block.STransactions() { if is, _ := stake.IsSSRtx(staketx); is { msgTx := staketx.MsgTx() sstxIn := msgTx.TxIn[0] // sstx input sstxHash := sstxIn.PreviousOutPoint.Hash // Fill out the ticket data. revocationsFromBlock[sstxHash] = struct{}{} numberOfSSRtx++ } } if numberOfSSRtx != int(revocations) { errStr := fmt.Sprintf("an invalid revocations %v was calculated "+ "the block header indicates %v instead", numberOfSSRtx, revocations) return errors.New(errStr) } // Lookup the missed ticket. If we find it in the patch data, // modify the patch data so that it doesn't exist. // Otherwise, just modify load the missed ticket data from // the ticket db and create patch data based on that. for hash, _ := range revocationsFromBlock { ticketWasMissed := false if td, is := missedTickets[hash]; is { maturedHeight := td.BlockHeight // Check maturity of ticket; we can only spend the ticket after it // hits maturity at height + tM + 2. if height < maturedHeight+2 { blockSha := block.Sha() errStr := fmt.Sprintf("Error: A ticket spend as an "+ "SSRtx in block height %v was immature! Block sha %v", height, blockSha) return errors.New(errStr) } ticketWasMissed = true } if !ticketWasMissed { errStr := fmt.Sprintf("SSRtx spent missed sstx %v, "+ "but that missed sstx could not be found!", hash) return errors.New(errStr) } } } // PART 3: Add newly maturing tickets // This is the only chunk we need to do for blocks appearing before // stake validation height. // Calculate block number for where new tickets are maturing from and retrieve // this block from db. // Get the block that is maturing. matureNode, err := b.getNodeAtHeightFromTopNode(node, tM) if err != nil { return err } matureBlock, errBlock := b.getBlockFromHash(matureNode.hash) if errBlock != nil { return errBlock } // Maturing tickets are from the maturingBlock; fill out the ticket patch data // and then push them to the tixStore. for _, stx := range matureBlock.STransactions() { if is, _ := stake.IsSStx(stx); is { // Calculate the prefix for pre-sort. sstxHash := *stx.Sha() prefix := uint8(sstxHash[0]) // Fill out the ticket data. td := stake.NewTicketData(sstxHash, prefix, chainhash.Hash{}, height, false, // not missed false) // not expired tpd := NewTicketPatchData(td, TiAvailable, nil) tixStore[*stx.Sha()] = tpd } } return nil }