github.com/ethereum/go-ethereum@v1.16.1/accounts/keystore/account_cache.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package keystore 18 19 import ( 20 "bufio" 21 "encoding/json" 22 "fmt" 23 "os" 24 "path/filepath" 25 "slices" 26 "sort" 27 "strings" 28 "sync" 29 "time" 30 31 mapset "github.com/deckarep/golang-set/v2" 32 "github.com/ethereum/go-ethereum/accounts" 33 "github.com/ethereum/go-ethereum/common" 34 "github.com/ethereum/go-ethereum/log" 35 ) 36 37 // Minimum amount of time between cache reloads. This limit applies if the platform does 38 // not support change notifications. It also applies if the keystore directory does not 39 // exist yet, the code will attempt to create a watcher at most this often. 40 const minReloadInterval = 2 * time.Second 41 42 // byURL defines the sorting order for accounts. 43 func byURL(a, b accounts.Account) int { 44 return a.URL.Cmp(b.URL) 45 } 46 47 // AmbiguousAddrError is returned when an address matches multiple files. 48 type AmbiguousAddrError struct { 49 Addr common.Address 50 Matches []accounts.Account 51 } 52 53 func (err *AmbiguousAddrError) Error() string { 54 files := "" 55 for i, a := range err.Matches { 56 files += a.URL.Path 57 if i < len(err.Matches)-1 { 58 files += ", " 59 } 60 } 61 return fmt.Sprintf("multiple keys match address (%s)", files) 62 } 63 64 // accountCache is a live index of all accounts in the keystore. 65 type accountCache struct { 66 keydir string 67 watcher *watcher 68 mu sync.Mutex 69 all []accounts.Account 70 byAddr map[common.Address][]accounts.Account 71 throttle *time.Timer 72 notify chan struct{} 73 fileC fileCache 74 } 75 76 func newAccountCache(keydir string) (*accountCache, chan struct{}) { 77 ac := &accountCache{ 78 keydir: keydir, 79 byAddr: make(map[common.Address][]accounts.Account), 80 notify: make(chan struct{}, 1), 81 fileC: fileCache{all: mapset.NewThreadUnsafeSet[string]()}, 82 } 83 ac.watcher = newWatcher(ac) 84 return ac, ac.notify 85 } 86 87 func (ac *accountCache) accounts() []accounts.Account { 88 ac.maybeReload() 89 ac.mu.Lock() 90 defer ac.mu.Unlock() 91 cpy := make([]accounts.Account, len(ac.all)) 92 copy(cpy, ac.all) 93 return cpy 94 } 95 96 func (ac *accountCache) hasAddress(addr common.Address) bool { 97 ac.maybeReload() 98 ac.mu.Lock() 99 defer ac.mu.Unlock() 100 return len(ac.byAddr[addr]) > 0 101 } 102 103 func (ac *accountCache) add(newAccount accounts.Account) { 104 ac.mu.Lock() 105 defer ac.mu.Unlock() 106 107 i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 }) 108 if i < len(ac.all) && ac.all[i] == newAccount { 109 return 110 } 111 // newAccount is not in the cache. 112 ac.all = append(ac.all, accounts.Account{}) 113 copy(ac.all[i+1:], ac.all[i:]) 114 ac.all[i] = newAccount 115 ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount) 116 } 117 118 // note: removed needs to be unique here (i.e. both File and Address must be set). 119 func (ac *accountCache) delete(removed accounts.Account) { 120 ac.mu.Lock() 121 defer ac.mu.Unlock() 122 123 ac.all = removeAccount(ac.all, removed) 124 if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { 125 delete(ac.byAddr, removed.Address) 126 } else { 127 ac.byAddr[removed.Address] = ba 128 } 129 } 130 131 // deleteByFile removes an account referenced by the given path. 132 func (ac *accountCache) deleteByFile(path string) { 133 ac.mu.Lock() 134 defer ac.mu.Unlock() 135 i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Path >= path }) 136 137 if i < len(ac.all) && ac.all[i].URL.Path == path { 138 removed := ac.all[i] 139 ac.all = append(ac.all[:i], ac.all[i+1:]...) 140 if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { 141 delete(ac.byAddr, removed.Address) 142 } else { 143 ac.byAddr[removed.Address] = ba 144 } 145 } 146 } 147 148 // watcherStarted returns true if the watcher loop started running (even if it 149 // has since also ended). 150 func (ac *accountCache) watcherStarted() bool { 151 ac.mu.Lock() 152 defer ac.mu.Unlock() 153 return ac.watcher.running || ac.watcher.runEnded 154 } 155 156 func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account { 157 for i := range slice { 158 if slice[i] == elem { 159 return append(slice[:i], slice[i+1:]...) 160 } 161 } 162 return slice 163 } 164 165 // find returns the cached account for address if there is a unique match. 166 // The exact matching rules are explained by the documentation of accounts.Account. 167 // Callers must hold ac.mu. 168 func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { 169 // Limit search to address candidates if possible. 170 matches := ac.all 171 if (a.Address != common.Address{}) { 172 matches = ac.byAddr[a.Address] 173 } 174 if a.URL.Path != "" { 175 // If only the basename is specified, complete the path. 176 if !strings.ContainsRune(a.URL.Path, filepath.Separator) { 177 a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) 178 } 179 for i := range matches { 180 if matches[i].URL == a.URL { 181 return matches[i], nil 182 } 183 } 184 if (a.Address == common.Address{}) { 185 return accounts.Account{}, ErrNoMatch 186 } 187 } 188 switch len(matches) { 189 case 1: 190 return matches[0], nil 191 case 0: 192 return accounts.Account{}, ErrNoMatch 193 default: 194 err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} 195 copy(err.Matches, matches) 196 slices.SortFunc(err.Matches, byURL) 197 return accounts.Account{}, err 198 } 199 } 200 201 func (ac *accountCache) maybeReload() { 202 ac.mu.Lock() 203 204 if ac.watcher.running { 205 ac.mu.Unlock() 206 return // A watcher is running and will keep the cache up-to-date. 207 } 208 if ac.throttle == nil { 209 ac.throttle = time.NewTimer(0) 210 } else { 211 select { 212 case <-ac.throttle.C: 213 default: 214 ac.mu.Unlock() 215 return // The cache was reloaded recently. 216 } 217 } 218 // No watcher running, start it. 219 ac.watcher.start() 220 ac.throttle.Reset(minReloadInterval) 221 ac.mu.Unlock() 222 ac.scanAccounts() 223 } 224 225 func (ac *accountCache) close() { 226 ac.mu.Lock() 227 ac.watcher.close() 228 if ac.throttle != nil { 229 ac.throttle.Stop() 230 } 231 if ac.notify != nil { 232 close(ac.notify) 233 ac.notify = nil 234 } 235 ac.mu.Unlock() 236 } 237 238 // scanAccounts checks if any changes have occurred on the filesystem, and 239 // updates the account cache accordingly 240 func (ac *accountCache) scanAccounts() error { 241 // Scan the entire folder metadata for file changes 242 creates, deletes, updates, err := ac.fileC.scan(ac.keydir) 243 if err != nil { 244 log.Debug("Failed to reload keystore contents", "err", err) 245 return err 246 } 247 if creates.Cardinality() == 0 && deletes.Cardinality() == 0 && updates.Cardinality() == 0 { 248 return nil 249 } 250 // Create a helper method to scan the contents of the key files 251 var ( 252 buf = new(bufio.Reader) 253 key struct { 254 Address string `json:"address"` 255 } 256 ) 257 readAccount := func(path string) *accounts.Account { 258 fd, err := os.Open(path) 259 if err != nil { 260 log.Trace("Failed to open keystore file", "path", path, "err", err) 261 return nil 262 } 263 defer fd.Close() 264 buf.Reset(fd) 265 // Parse the address. 266 key.Address = "" 267 err = json.NewDecoder(buf).Decode(&key) 268 addr := common.HexToAddress(key.Address) 269 switch { 270 case err != nil: 271 log.Debug("Failed to decode keystore key", "path", path, "err", err) 272 case addr == common.Address{}: 273 log.Debug("Failed to decode keystore key", "path", path, "err", "missing or zero address") 274 default: 275 return &accounts.Account{ 276 Address: addr, 277 URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}, 278 } 279 } 280 return nil 281 } 282 // Process all the file diffs 283 start := time.Now() 284 285 for _, path := range creates.ToSlice() { 286 if a := readAccount(path); a != nil { 287 ac.add(*a) 288 } 289 } 290 for _, path := range deletes.ToSlice() { 291 ac.deleteByFile(path) 292 } 293 for _, path := range updates.ToSlice() { 294 ac.deleteByFile(path) 295 if a := readAccount(path); a != nil { 296 ac.add(*a) 297 } 298 } 299 end := time.Now() 300 301 select { 302 case ac.notify <- struct{}{}: 303 default: 304 } 305 log.Trace("Handled keystore changes", "time", end.Sub(start)) 306 return nil 307 }