gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/wallet.go (about) 1 package wallet 2 3 // TODO: Theoretically, the transaction builder in this wallet supports 4 // multisig, but there are no automated tests to verify that. 5 6 import ( 7 "bytes" 8 "fmt" 9 "sort" 10 "sync" 11 12 bolt "github.com/coreos/bbolt" 13 "gitlab.com/NebulousLabs/errors" 14 "gitlab.com/NebulousLabs/threadgroup" 15 16 "gitlab.com/SiaPrime/SiaPrime/build" 17 "gitlab.com/SiaPrime/SiaPrime/crypto" 18 "gitlab.com/SiaPrime/SiaPrime/encoding" 19 "gitlab.com/SiaPrime/SiaPrime/modules" 20 "gitlab.com/SiaPrime/SiaPrime/persist" 21 siasync "gitlab.com/SiaPrime/SiaPrime/sync" 22 "gitlab.com/SiaPrime/SiaPrime/types" 23 ) 24 25 const ( 26 // RespendTimeout records the number of blocks that the wallet will wait 27 // before spending an output that has been spent in the past. If the 28 // transaction spending the output has not made it to the transaction pool 29 // after the limit, the assumption is that it never will. 30 RespendTimeout = 40 31 ) 32 33 var ( 34 errNilConsensusSet = errors.New("wallet cannot initialize with a nil consensus set") 35 errNilTpool = errors.New("wallet cannot initialize with a nil transaction pool") 36 ) 37 38 // spendableKey is a set of secret keys plus the corresponding unlock 39 // conditions. The public key can be derived from the secret key and then 40 // matched to the corresponding public keys in the unlock conditions. All 41 // addresses that are to be used in 'FundSiacoins' or 'FundSiafunds' in the 42 // transaction builder must conform to this form of spendable key. 43 type spendableKey struct { 44 UnlockConditions types.UnlockConditions 45 SecretKeys []crypto.SecretKey 46 } 47 48 // Wallet is an object that tracks balances, creates keys and addresses, 49 // manages building and sending transactions. 50 type Wallet struct { 51 // encrypted indicates whether the wallet has been encrypted (i.e. 52 // initialized). unlocked indicates whether the wallet is currently 53 // storing secret keys in memory. subscribed indicates whether the wallet 54 // has subscribed to the consensus set yet - the wallet is unable to 55 // subscribe to the consensus set until it has been unlocked for the first 56 // time. The primary seed is used to generate new addresses for the 57 // wallet. 58 encrypted bool 59 unlocked bool 60 subscribed bool 61 primarySeed modules.Seed 62 63 // The wallet's dependencies. 64 cs modules.ConsensusSet 65 tpool modules.TransactionPool 66 deps modules.Dependencies 67 68 // The following set of fields are responsible for tracking the confirmed 69 // outputs, and for being able to spend them. The seeds are used to derive 70 // the keys that are tracked on the blockchain. All keys are pregenerated 71 // from the seeds, when checking new outputs or spending outputs, the seeds 72 // are not referenced at all. The seeds are only stored so that the user 73 // may access them. 74 seeds []modules.Seed 75 keys map[types.UnlockHash]spendableKey 76 lookahead map[types.UnlockHash]uint64 77 watchedAddrs map[types.UnlockHash]struct{} 78 79 // unconfirmedProcessedTransactions tracks unconfirmed transactions. 80 // 81 // TODO: Replace this field with a linked list. Currently when a new 82 // transaction set diff is provided, the entire array needs to be 83 // reallocated. Since this can happen tens of times per second, and the 84 // array can have tens of thousands of elements, it's a performance issue. 85 unconfirmedSets map[modules.TransactionSetID][]types.TransactionID 86 unconfirmedProcessedTransactions []modules.ProcessedTransaction 87 88 // The wallet's database tracks its seeds, keys, outputs, and 89 // transactions. A global db transaction is maintained in memory to avoid 90 // excessive disk writes. Any operations involving dbTx must hold an 91 // exclusive lock. 92 // 93 // If dbRollback is set, then when the database syncs it will perform a 94 // rollback instead of a commit. For safety reasons, the db will close and 95 // the wallet will close if a rollback is performed. 96 db *persist.BoltDatabase 97 dbRollback bool 98 dbTx *bolt.Tx 99 100 persistDir string 101 log *persist.Logger 102 mu sync.RWMutex 103 104 // A separate TryMutex is used to protect against concurrent unlocking or 105 // initialization. 106 scanLock siasync.TryMutex 107 108 // The wallet's ThreadGroup tells tracked functions to shut down and 109 // blocks until they have all exited before returning from Close. 110 tg threadgroup.ThreadGroup 111 112 // defragDisabled determines if the wallet is set to defrag outputs once it 113 // reaches a certain threshold 114 defragDisabled bool 115 } 116 117 // Height return the internal processed consensus height of the wallet 118 func (w *Wallet) Height() (types.BlockHeight, error) { 119 if err := w.tg.Add(); err != nil { 120 return types.BlockHeight(0), modules.ErrWalletShutdown 121 } 122 defer w.tg.Done() 123 124 w.mu.Lock() 125 defer w.mu.Unlock() 126 w.syncDB() 127 128 var height uint64 129 err := w.db.View(func(tx *bolt.Tx) error { 130 return encoding.Unmarshal(tx.Bucket(bucketWallet).Get(keyConsensusHeight), &height) 131 }) 132 if err != nil { 133 return types.BlockHeight(0), err 134 } 135 return types.BlockHeight(height), nil 136 } 137 138 // LastAddresses returns the last n addresses starting at the last seedProgress 139 // for which an address was generated. If n is greater than the current 140 // progress, fewer than n keys will be returned. That means all addresses can 141 // be retrieved in reverse order by simply supplying math.MaxUint64 for n. 142 func (w *Wallet) LastAddresses(n uint64) ([]types.UnlockHash, error) { 143 if err := w.tg.Add(); err != nil { 144 return nil, modules.ErrWalletShutdown 145 } 146 defer w.tg.Done() 147 148 w.mu.Lock() 149 defer w.mu.Unlock() 150 151 // Get the current seed progress from disk. 152 var seedProgress uint64 153 err := w.db.View(func(tx *bolt.Tx) (err error) { 154 seedProgress, err = dbGetPrimarySeedProgress(tx) 155 return 156 }) 157 if err != nil { 158 return []types.UnlockHash{}, err 159 } 160 // At most seedProgess addresses can be requested. 161 if n > seedProgress { 162 n = seedProgress 163 } 164 start := seedProgress - n 165 // Generate the keys. 166 keys := generateKeys(w.primarySeed, start, n) 167 uhs := make([]types.UnlockHash, 0, len(keys)) 168 for i := len(keys) - 1; i >= 0; i-- { 169 uhs = append(uhs, keys[i].UnlockConditions.UnlockHash()) 170 } 171 return uhs, nil 172 } 173 174 // New creates a new wallet, loading any known addresses from the input file 175 // name and then using the file to save in the future. Keys and addresses are 176 // not loaded into the wallet during the call to 'new', but rather during the 177 // call to 'Unlock'. 178 func New(cs modules.ConsensusSet, tpool modules.TransactionPool, persistDir string) (*Wallet, error) { 179 return NewCustomWallet(cs, tpool, persistDir, modules.ProdDependencies) 180 } 181 182 // NewCustomWallet creates a new wallet using custom dependencies. 183 func NewCustomWallet(cs modules.ConsensusSet, tpool modules.TransactionPool, persistDir string, deps modules.Dependencies) (*Wallet, error) { 184 // Check for nil dependencies. 185 if cs == nil { 186 return nil, errNilConsensusSet 187 } 188 if tpool == nil { 189 return nil, errNilTpool 190 } 191 192 // Initialize the data structure. 193 w := &Wallet{ 194 cs: cs, 195 tpool: tpool, 196 197 keys: make(map[types.UnlockHash]spendableKey), 198 lookahead: make(map[types.UnlockHash]uint64), 199 watchedAddrs: make(map[types.UnlockHash]struct{}), 200 201 unconfirmedSets: make(map[modules.TransactionSetID][]types.TransactionID), 202 203 persistDir: persistDir, 204 205 deps: deps, 206 } 207 err := w.initPersist() 208 if err != nil { 209 return nil, err 210 } 211 return w, nil 212 } 213 214 // Close terminates all ongoing processes involving the wallet, enabling 215 // garbage collection. 216 func (w *Wallet) Close() error { 217 if err := w.tg.Stop(); err != nil { 218 return err 219 } 220 var errs []error 221 // Lock the wallet outside of mu.Lock because Lock uses its own mu.Lock. 222 // Once the wallet is locked it cannot be unlocked except using the 223 // unexported unlock method (w.Unlock returns an error if the wallet's 224 // ThreadGroup is stopped). 225 if w.managedUnlocked() { 226 if err := w.managedLock(); err != nil { 227 errs = append(errs, err) 228 } 229 } 230 231 w.cs.Unsubscribe(w) 232 w.tpool.Unsubscribe(w) 233 234 if err := w.log.Close(); err != nil { 235 errs = append(errs, fmt.Errorf("log.Close failed: %v", err)) 236 } 237 return build.JoinErrors(errs, "; ") 238 } 239 240 // AllAddresses returns all addresses that the wallet is able to spend from, 241 // including unseeded addresses. Addresses are returned sorted in byte-order. 242 func (w *Wallet) AllAddresses() ([]types.UnlockHash, error) { 243 if err := w.tg.Add(); err != nil { 244 return []types.UnlockHash{}, modules.ErrWalletShutdown 245 } 246 defer w.tg.Done() 247 248 w.mu.RLock() 249 defer w.mu.RUnlock() 250 251 addrs := make([]types.UnlockHash, 0, len(w.keys)) 252 for addr := range w.keys { 253 addrs = append(addrs, addr) 254 } 255 sort.Slice(addrs, func(i, j int) bool { 256 return bytes.Compare(addrs[i][:], addrs[j][:]) < 0 257 }) 258 return addrs, nil 259 } 260 261 // Rescanning reports whether the wallet is currently rescanning the 262 // blockchain. 263 func (w *Wallet) Rescanning() (bool, error) { 264 if err := w.tg.Add(); err != nil { 265 return false, modules.ErrWalletShutdown 266 } 267 defer w.tg.Done() 268 269 rescanning := !w.scanLock.TryLock() 270 if !rescanning { 271 w.scanLock.Unlock() 272 } 273 return rescanning, nil 274 } 275 276 // Settings returns the wallet's current settings 277 func (w *Wallet) Settings() (modules.WalletSettings, error) { 278 if err := w.tg.Add(); err != nil { 279 return modules.WalletSettings{}, modules.ErrWalletShutdown 280 } 281 defer w.tg.Done() 282 return modules.WalletSettings{ 283 NoDefrag: w.defragDisabled, 284 }, nil 285 } 286 287 // SetSettings will update the settings for the wallet. 288 func (w *Wallet) SetSettings(s modules.WalletSettings) error { 289 if err := w.tg.Add(); err != nil { 290 return modules.ErrWalletShutdown 291 } 292 defer w.tg.Done() 293 294 w.mu.Lock() 295 w.defragDisabled = s.NoDefrag 296 w.mu.Unlock() 297 return nil 298 }