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 }