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