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