decred.org/dcrwallet/v3@v3.1.0/wallet/udb/stake.go (about)

     1  // Copyright (c) 2015-2017 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package udb
     6  
     7  import (
     8  	"bytes"
     9  	"sync"
    10  	"time"
    11  
    12  	"decred.org/dcrwallet/v3/errors"
    13  	"decred.org/dcrwallet/v3/wallet/walletdb"
    14  	"github.com/decred/dcrd/chaincfg/chainhash"
    15  	"github.com/decred/dcrd/chaincfg/v3"
    16  	"github.com/decred/dcrd/dcrutil/v4"
    17  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    18  	"github.com/decred/dcrd/wire"
    19  )
    20  
    21  // sstxRecord is the structure for a stored SStx.
    22  type sstxRecord struct {
    23  	tx          *dcrutil.Tx
    24  	ts          time.Time
    25  	voteBitsSet bool   // Removed in version 3
    26  	voteBits    uint16 // Removed in version 3
    27  	voteBitsExt []byte // Removed in version 3
    28  }
    29  
    30  // TicketStatus is the current status of a stake pool ticket.
    31  type TicketStatus uint8
    32  
    33  const (
    34  	// TSImmatureOrLive indicates that the ticket is either
    35  	// live or immature.
    36  	TSImmatureOrLive = iota
    37  
    38  	// TSVoted indicates that the ticket was spent as a vote.
    39  	TSVoted
    40  
    41  	// TSMissed indicates that the ticket was spent as a
    42  	// revocation.
    43  	TSMissed
    44  )
    45  
    46  // PoolTicket is a 73-byte struct that is used to preserve user's
    47  // ticket information when they have an account at the stake pool.
    48  type PoolTicket struct {
    49  	Ticket       chainhash.Hash
    50  	HeightTicket uint32 // Not used or guaranteed to be correct
    51  	Status       TicketStatus
    52  	HeightSpent  uint32
    53  	SpentBy      chainhash.Hash
    54  }
    55  
    56  // StakePoolUser is a list of tickets for a given user (P2SH
    57  // address) in the stake pool.
    58  type StakePoolUser struct {
    59  	Tickets        []*PoolTicket
    60  	InvalidTickets []*chainhash.Hash
    61  }
    62  
    63  // StakeStore represents a safely accessible database of
    64  // stake transactions.
    65  type StakeStore struct {
    66  	Params  *chaincfg.Params
    67  	Manager *Manager
    68  
    69  	ownedSStxs map[chainhash.Hash]struct{}
    70  	mtx        sync.RWMutex // only protects ownedSStxs
    71  }
    72  
    73  // checkHashInStore checks if a hash exists in ownedSStxs.
    74  func (s *StakeStore) checkHashInStore(hash *chainhash.Hash) bool {
    75  	_, exists := s.ownedSStxs[*hash]
    76  	return exists
    77  }
    78  
    79  // OwnTicket returns whether the ticket is tracked by the stake manager.
    80  func (s *StakeStore) OwnTicket(hash *chainhash.Hash) bool {
    81  	s.mtx.RLock()
    82  	owned := s.checkHashInStore(hash)
    83  	s.mtx.RUnlock()
    84  	return owned
    85  }
    86  
    87  // addHashToStore adds a hash into ownedSStxs.
    88  func (s *StakeStore) addHashToStore(hash *chainhash.Hash) {
    89  	s.ownedSStxs[*hash] = struct{}{}
    90  }
    91  
    92  // insertSStx inserts an SStx into the store.
    93  func (s *StakeStore) insertSStx(ns walletdb.ReadWriteBucket, sstx *dcrutil.Tx) error {
    94  	// If we already have the SStx, no need to
    95  	// try to include twice.
    96  	exists := s.checkHashInStore(sstx.Hash())
    97  	if exists {
    98  		log.Tracef("Attempted to insert SStx %v into the stake store, "+
    99  			"but the SStx already exists.", sstx.Hash())
   100  		return nil
   101  	}
   102  	record := &sstxRecord{
   103  		tx: sstx,
   104  		ts: time.Now(),
   105  	}
   106  
   107  	// Add the SStx to the database.
   108  	err := putSStxRecord(ns, record, DBVersion)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// Add the SStx's hash to the internal list in the store.
   114  	s.addHashToStore(sstx.Hash())
   115  
   116  	return nil
   117  }
   118  
   119  // InsertSStx is the exported version of insertSStx that is safe for concurrent
   120  // access.
   121  func (s *StakeStore) InsertSStx(ns walletdb.ReadWriteBucket, sstx *dcrutil.Tx) error {
   122  	s.mtx.Lock()
   123  	err := s.insertSStx(ns, sstx)
   124  	s.mtx.Unlock()
   125  	return err
   126  }
   127  
   128  // dumpSStxHashes dumps the hashes of all owned SStxs. Note
   129  // that this doesn't use the DB.
   130  func (s *StakeStore) dumpSStxHashes() []chainhash.Hash {
   131  	// Copy the hash list of sstxs. You could pass the pointer
   132  	// directly but you risk that the size of the internal
   133  	// ownedSStxs is later modified while the end user is
   134  	// working with the returned list.
   135  	ownedSStxs := make([]chainhash.Hash, len(s.ownedSStxs))
   136  
   137  	itr := 0
   138  	for hash := range s.ownedSStxs {
   139  		ownedSStxs[itr] = hash
   140  		itr++
   141  	}
   142  
   143  	return ownedSStxs
   144  }
   145  
   146  // DumpSStxHashes returns the hashes of all wallet ticket purchase transactions.
   147  func (s *StakeStore) DumpSStxHashes() []chainhash.Hash {
   148  	defer s.mtx.RUnlock()
   149  	s.mtx.RLock()
   150  
   151  	return s.dumpSStxHashes()
   152  }
   153  
   154  // dumpSStxHashes dumps the hashes of all owned SStxs for some address.
   155  func (s *StakeStore) dumpSStxHashesForAddress(ns walletdb.ReadBucket, addr stdaddr.Address) ([]chainhash.Hash, error) {
   156  	// Extract the HASH160 script hash; if it's not 20 bytes
   157  	// long, return an error.
   158  	var hash160 []byte
   159  	switch addr := addr.(type) {
   160  	case stdaddr.Hash160er:
   161  		hash160 = addr.Hash160()[:]
   162  	default:
   163  		err := errors.Errorf("cannot get hash160 from address %v", addr)
   164  		return nil, errors.E(errors.Invalid, err)
   165  	}
   166  	_, addrIsP2SH := addr.(*stdaddr.AddressScriptHashV0)
   167  
   168  	allTickets := s.dumpSStxHashes()
   169  	var ticketsForAddr []chainhash.Hash
   170  
   171  	// Access the database and store the result locally.
   172  	for _, h := range allTickets {
   173  		thisHash160, p2sh, err := fetchSStxRecordSStxTicketHash160(ns, &h, DBVersion)
   174  		if err != nil {
   175  			return nil, errors.E(errors.IO, err)
   176  		}
   177  		if addrIsP2SH != p2sh {
   178  			continue
   179  		}
   180  
   181  		if bytes.Equal(hash160, thisHash160) {
   182  			ticketsForAddr = append(ticketsForAddr, h)
   183  		}
   184  	}
   185  
   186  	return ticketsForAddr, nil
   187  }
   188  
   189  // DumpSStxHashesForAddress returns the hashes of all wallet ticket purchase
   190  // transactions for an address.
   191  func (s *StakeStore) DumpSStxHashesForAddress(ns walletdb.ReadBucket, addr stdaddr.Address) ([]chainhash.Hash, error) {
   192  	defer s.mtx.RUnlock()
   193  	s.mtx.RLock()
   194  
   195  	return s.dumpSStxHashesForAddress(ns, addr)
   196  }
   197  
   198  // sstxAddress returns the address for a given ticket.
   199  func (s *StakeStore) sstxAddress(ns walletdb.ReadBucket, hash *chainhash.Hash) (stdaddr.Address, error) {
   200  	// Access the database and store the result locally.
   201  	thisHash160, p2sh, err := fetchSStxRecordSStxTicketHash160(ns, hash, DBVersion)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  	var addr stdaddr.Address
   206  	if p2sh {
   207  		addr, err = stdaddr.NewAddressScriptHashV0FromHash(thisHash160, s.Params)
   208  	} else {
   209  		addr, err = stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(thisHash160, s.Params)
   210  	}
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	return addr, nil
   216  }
   217  
   218  // SStxAddress is the exported, concurrency safe version of sstxAddress.
   219  func (s *StakeStore) SStxAddress(ns walletdb.ReadBucket, hash *chainhash.Hash) (stdaddr.Address, error) {
   220  	return s.sstxAddress(ns, hash)
   221  }
   222  
   223  // TicketPurchase returns the ticket purchase transaction recorded in the "stake
   224  // manager" portion of the DB.
   225  //
   226  // TODO: This is redundant and should be looked up in from the transaction
   227  // manager.  Left for now for compatibility.
   228  func (s *StakeStore) TicketPurchase(dbtx walletdb.ReadTx, hash *chainhash.Hash) (*wire.MsgTx, error) {
   229  	ns := dbtx.ReadBucket(wstakemgrBucketKey)
   230  
   231  	ticketRecord, err := fetchSStxRecord(ns, hash, DBVersion)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	return ticketRecord.tx.MsgTx(), nil
   236  }
   237  
   238  // updateStakePoolUserTickets updates a stake pool ticket for a given user.
   239  // If the ticket does not currently exist in the database, it adds it. If it
   240  // does exist (the ticket hash exists), it replaces the old record.
   241  func (s *StakeStore) updateStakePoolUserTickets(ns walletdb.ReadWriteBucket, user stdaddr.Address, ticket *PoolTicket) error {
   242  	// Address must be a StakeAddress with support for the Hash160 method.
   243  	var hash160 *[20]byte
   244  	switch user := user.(type) {
   245  	case interface {
   246  		stdaddr.StakeAddress
   247  		stdaddr.Hash160er
   248  	}:
   249  		hash160 = user.Hash160()
   250  	default:
   251  		return errors.E(errors.Invalid, errors.Errorf("voting address type %T", user))
   252  	}
   253  
   254  	return updateStakePoolUserTickets(ns, *hash160, ticket)
   255  }
   256  
   257  // UpdateStakePoolUserTickets is the exported and concurrency safe form of
   258  // updateStakePoolUserTickets.
   259  func (s *StakeStore) UpdateStakePoolUserTickets(ns walletdb.ReadWriteBucket, user stdaddr.Address, ticket *PoolTicket) error {
   260  	return s.updateStakePoolUserTickets(ns, user, ticket)
   261  }
   262  
   263  // removeStakePoolUserInvalTickets prepares the user.Address and asks stakedb
   264  // to remove the formerly invalid tickets.
   265  func (s *StakeStore) removeStakePoolUserInvalTickets(ns walletdb.ReadWriteBucket, user stdaddr.Address, ticket *chainhash.Hash) error {
   266  	// Address must be a StakeAddress with support for the Hash160 method.
   267  	var hash160 *[20]byte
   268  	switch user := user.(type) {
   269  	case interface {
   270  		stdaddr.StakeAddress
   271  		Hash160() *[20]byte
   272  	}:
   273  		hash160 = user.Hash160()
   274  	default:
   275  		return errors.E(errors.Invalid, errors.Errorf("voting address type %T", user))
   276  	}
   277  
   278  	return removeStakePoolInvalUserTickets(ns, *hash160, ticket)
   279  }
   280  
   281  // RemoveStakePoolUserInvalTickets is the exported and concurrency safe form of
   282  // removetStakePoolUserInvalTickets.
   283  func (s *StakeStore) RemoveStakePoolUserInvalTickets(ns walletdb.ReadWriteBucket, user stdaddr.Address, ticket *chainhash.Hash) error {
   284  	return s.removeStakePoolUserInvalTickets(ns, user, ticket)
   285  }
   286  
   287  // updateStakePoolUserInvalTickets updates the list of invalid stake pool
   288  // tickets for a given user. If the ticket does not currently exist in the
   289  // database, it adds it.
   290  func (s *StakeStore) updateStakePoolUserInvalTickets(ns walletdb.ReadWriteBucket, user stdaddr.Address, ticket *chainhash.Hash) error {
   291  	// Address must be a StakeAddress with support for the Hash160 method.
   292  	var hash160 *[20]byte
   293  	switch user := user.(type) {
   294  	case interface {
   295  		stdaddr.StakeAddress
   296  		stdaddr.Hash160er
   297  	}:
   298  		hash160 = user.Hash160()
   299  	default:
   300  		return errors.E(errors.Invalid, errors.Errorf("voting address type %T", user))
   301  	}
   302  
   303  	return updateStakePoolInvalUserTickets(ns, *hash160, ticket)
   304  }
   305  
   306  // UpdateStakePoolUserInvalTickets is the exported and concurrency safe form of
   307  // updateStakePoolUserInvalTickets.
   308  func (s *StakeStore) UpdateStakePoolUserInvalTickets(ns walletdb.ReadWriteBucket, user stdaddr.Address, ticket *chainhash.Hash) error {
   309  	return s.updateStakePoolUserInvalTickets(ns, user, ticket)
   310  }
   311  
   312  func stakePoolUserInfo(ns walletdb.ReadBucket, user stdaddr.Address) (*StakePoolUser, error) {
   313  	// Address must be a StakeAddress with support for the Hash160 method.
   314  	var hash160 *[20]byte
   315  	switch user := user.(type) {
   316  	case interface {
   317  		stdaddr.StakeAddress
   318  		stdaddr.Hash160er
   319  	}:
   320  		hash160 = user.Hash160()
   321  	default:
   322  		return nil, errors.E(errors.Invalid, errors.Errorf("voting address type %T", user))
   323  	}
   324  
   325  	stakePoolUser := new(StakePoolUser)
   326  
   327  	// Catch missing user errors below and blank out the stake
   328  	// pool user information for the section if the user has
   329  	// no entries.
   330  	missingValidTickets, missingInvalidTickets := false, false
   331  
   332  	userTickets, err := fetchStakePoolUserTickets(ns, *hash160)
   333  	if err != nil {
   334  		missingValidTickets = errors.Is(err, errors.NotExist)
   335  		if !missingValidTickets {
   336  			return nil, err
   337  		}
   338  	}
   339  	if missingValidTickets {
   340  		userTickets = make([]*PoolTicket, 0)
   341  	}
   342  
   343  	invalTickets, err := fetchStakePoolUserInvalTickets(ns, *hash160)
   344  	if err != nil {
   345  		if errors.Is(err, errors.NotExist) {
   346  			missingInvalidTickets = true
   347  		}
   348  		if !missingInvalidTickets {
   349  			return nil, err
   350  		}
   351  	}
   352  	if missingInvalidTickets {
   353  		invalTickets = make([]*chainhash.Hash, 0)
   354  	}
   355  
   356  	stakePoolUser.Tickets = userTickets
   357  	stakePoolUser.InvalidTickets = invalTickets
   358  
   359  	return stakePoolUser, nil
   360  }
   361  
   362  // StakePoolUserInfo returns the stake pool user information for a given stake
   363  // pool user, keyed to their P2SH voting address.
   364  func (s *StakeStore) StakePoolUserInfo(ns walletdb.ReadBucket, user stdaddr.Address) (*StakePoolUser, error) {
   365  	return stakePoolUserInfo(ns, user)
   366  }
   367  
   368  // loadManager returns a new stake manager that results from loading it from
   369  // the passed opened database.  The public passphrase is required to decrypt the
   370  // public keys.
   371  func (s *StakeStore) loadOwnedSStxs(ns walletdb.ReadBucket) error {
   372  	// Regenerate the list of tickets.
   373  	// Perform all database lookups in a read-only view.
   374  	ticketList := make(map[chainhash.Hash]struct{})
   375  
   376  	// Open the sstx records database.
   377  	bucket := ns.NestedReadBucket(sstxRecordsBucketName)
   378  
   379  	// Store each key sequentially.
   380  	err := bucket.ForEach(func(k []byte, v []byte) error {
   381  		var errNewHash error
   382  		var hash *chainhash.Hash
   383  
   384  		hash, errNewHash = chainhash.NewHash(k)
   385  		if errNewHash != nil {
   386  			return errNewHash
   387  		}
   388  		ticketList[*hash] = struct{}{}
   389  		return nil
   390  	})
   391  	if err != nil {
   392  		return err
   393  	}
   394  
   395  	s.ownedSStxs = ticketList
   396  	return nil
   397  }
   398  
   399  // newStakeStore initializes a new stake store with the given parameters.
   400  func newStakeStore(params *chaincfg.Params, manager *Manager) *StakeStore {
   401  	return &StakeStore{
   402  		Params:     params,
   403  		Manager:    manager,
   404  		ownedSStxs: make(map[chainhash.Hash]struct{}),
   405  	}
   406  }
   407  
   408  // openStakeStore loads an existing stake manager from the given namespace,
   409  // waddrmgr, and network parameters.
   410  //
   411  // A NotExist error is returned returned when the stake store is not written to
   412  // the db.
   413  func openStakeStore(ns walletdb.ReadBucket, manager *Manager, params *chaincfg.Params) (*StakeStore, error) {
   414  	// Return an error if the manager has NOT already been created in the
   415  	// given database namespace.
   416  	if !stakeStoreExists(ns) {
   417  		return nil, errors.E(errors.NotExist, "no stake store")
   418  	}
   419  
   420  	ss := newStakeStore(params, manager)
   421  
   422  	err := ss.loadOwnedSStxs(ns)
   423  	if err != nil {
   424  		return nil, err
   425  	}
   426  
   427  	return ss, nil
   428  }