github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/accounts/keystore/account_cache_test.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 "errors" 21 "fmt" 22 "math/rand" 23 "os" 24 "path/filepath" 25 "reflect" 26 "slices" 27 "testing" 28 "time" 29 30 "github.com/cespare/cp" 31 "github.com/davecgh/go-spew/spew" 32 "github.com/ethereum/go-ethereum/accounts" 33 "github.com/ethereum/go-ethereum/common" 34 ) 35 36 var ( 37 cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore")) 38 cachetestAccounts = []accounts.Account{ 39 { 40 Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), 41 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")}, 42 }, 43 { 44 Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), 45 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, 46 }, 47 { 48 Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), 49 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, 50 }, 51 } 52 ) 53 54 // waitWatcherStart waits up to 1s for the keystore watcher to start. 55 func waitWatcherStart(ks *KeyStore) bool { 56 // On systems where file watch is not supported, just return "ok". 57 if !ks.cache.watcher.enabled() { 58 return true 59 } 60 // The watcher should start, and then exit. 61 for t0 := time.Now(); time.Since(t0) < 1*time.Second; time.Sleep(100 * time.Millisecond) { 62 if ks.cache.watcherStarted() { 63 return true 64 } 65 } 66 return false 67 } 68 69 func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { 70 var list []accounts.Account 71 for t0 := time.Now(); time.Since(t0) < 5*time.Second; time.Sleep(100 * time.Millisecond) { 72 list = ks.Accounts() 73 if reflect.DeepEqual(list, wantAccounts) { 74 // ks should have also received change notifications 75 select { 76 case <-ks.changes: 77 default: 78 return errors.New("wasn't notified of new accounts") 79 } 80 return nil 81 } 82 } 83 return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts) 84 } 85 86 func TestWatchNewFile(t *testing.T) { 87 t.Parallel() 88 89 dir, ks := tmpKeyStore(t) 90 91 // Ensure the watcher is started before adding any files. 92 ks.Accounts() 93 if !waitWatcherStart(ks) { 94 t.Fatal("keystore watcher didn't start in time") 95 } 96 // Move in the files. 97 wantAccounts := make([]accounts.Account, len(cachetestAccounts)) 98 for i := range cachetestAccounts { 99 wantAccounts[i] = accounts.Account{ 100 Address: cachetestAccounts[i].Address, 101 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, 102 } 103 if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { 104 t.Fatal(err) 105 } 106 } 107 108 // ks should see the accounts. 109 if err := waitForAccounts(wantAccounts, ks); err != nil { 110 t.Error(err) 111 } 112 } 113 114 func TestWatchNoDir(t *testing.T) { 115 t.Parallel() 116 // Create ks but not the directory that it watches. 117 dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watchnodir-test-%d-%d", os.Getpid(), rand.Int())) 118 ks := NewKeyStore(dir, LightScryptN, LightScryptP) 119 list := ks.Accounts() 120 if len(list) > 0 { 121 t.Error("initial account list not empty:", list) 122 } 123 // The watcher should start, and then exit. 124 if !waitWatcherStart(ks) { 125 t.Fatal("keystore watcher didn't start in time") 126 } 127 // Create the directory and copy a key file into it. 128 os.MkdirAll(dir, 0700) 129 defer os.RemoveAll(dir) 130 file := filepath.Join(dir, "aaa") 131 if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { 132 t.Fatal(err) 133 } 134 135 // ks should see the account. 136 wantAccounts := []accounts.Account{cachetestAccounts[0]} 137 wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} 138 for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { 139 list = ks.Accounts() 140 if reflect.DeepEqual(list, wantAccounts) { 141 // ks should have also received change notifications 142 select { 143 case <-ks.changes: 144 default: 145 t.Fatalf("wasn't notified of new accounts") 146 } 147 return 148 } 149 time.Sleep(d) 150 } 151 t.Errorf("\ngot %v\nwant %v", list, wantAccounts) 152 } 153 154 func TestCacheInitialReload(t *testing.T) { 155 t.Parallel() 156 cache, _ := newAccountCache(cachetestDir) 157 accounts := cache.accounts() 158 if !reflect.DeepEqual(accounts, cachetestAccounts) { 159 t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) 160 } 161 } 162 163 func TestCacheAddDeleteOrder(t *testing.T) { 164 t.Parallel() 165 cache, _ := newAccountCache("testdata/no-such-dir") 166 cache.watcher.running = true // prevent unexpected reloads 167 168 accs := []accounts.Account{ 169 { 170 Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), 171 URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, 172 }, 173 { 174 Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), 175 URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, 176 }, 177 { 178 Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), 179 URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, 180 }, 181 { 182 Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), 183 URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, 184 }, 185 { 186 Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), 187 URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"}, 188 }, 189 { 190 Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), 191 URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, 192 }, 193 { 194 Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), 195 URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, 196 }, 197 } 198 for _, a := range accs { 199 cache.add(a) 200 } 201 // Add some of them twice to check that they don't get reinserted. 202 cache.add(accs[0]) 203 cache.add(accs[2]) 204 205 // Check that the account list is sorted by filename. 206 wantAccounts := make([]accounts.Account, len(accs)) 207 copy(wantAccounts, accs) 208 slices.SortFunc(wantAccounts, byURL) 209 list := cache.accounts() 210 if !reflect.DeepEqual(list, wantAccounts) { 211 t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) 212 } 213 for _, a := range accs { 214 if !cache.hasAddress(a.Address) { 215 t.Errorf("expected hasAccount(%x) to return true", a.Address) 216 } 217 } 218 if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { 219 t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) 220 } 221 222 // Delete a few keys from the cache. 223 for i := 0; i < len(accs); i += 2 { 224 cache.delete(wantAccounts[i]) 225 } 226 cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) 227 228 // Check content again after deletion. 229 wantAccountsAfterDelete := []accounts.Account{ 230 wantAccounts[1], 231 wantAccounts[3], 232 wantAccounts[5], 233 } 234 list = cache.accounts() 235 if !reflect.DeepEqual(list, wantAccountsAfterDelete) { 236 t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) 237 } 238 for _, a := range wantAccountsAfterDelete { 239 if !cache.hasAddress(a.Address) { 240 t.Errorf("expected hasAccount(%x) to return true", a.Address) 241 } 242 } 243 if cache.hasAddress(wantAccounts[0].Address) { 244 t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) 245 } 246 } 247 248 func TestCacheFind(t *testing.T) { 249 t.Parallel() 250 dir := filepath.Join("testdata", "dir") 251 cache, _ := newAccountCache(dir) 252 cache.watcher.running = true // prevent unexpected reloads 253 254 accs := []accounts.Account{ 255 { 256 Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), 257 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, 258 }, 259 { 260 Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), 261 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, 262 }, 263 { 264 Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), 265 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, 266 }, 267 { 268 Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), 269 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, 270 }, 271 } 272 for _, a := range accs { 273 cache.add(a) 274 } 275 276 nomatchAccount := accounts.Account{ 277 Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), 278 URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, 279 } 280 tests := []struct { 281 Query accounts.Account 282 WantResult accounts.Account 283 WantError error 284 }{ 285 // by address 286 {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, 287 // by file 288 {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, 289 // by basename 290 {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]}, 291 // by file and address 292 {Query: accs[0], WantResult: accs[0]}, 293 // ambiguous address, tie resolved by file 294 {Query: accs[2], WantResult: accs[2]}, 295 // ambiguous address error 296 { 297 Query: accounts.Account{Address: accs[2].Address}, 298 WantError: &AmbiguousAddrError{ 299 Addr: accs[2].Address, 300 Matches: []accounts.Account{accs[2], accs[3]}, 301 }, 302 }, 303 // no match error 304 {Query: nomatchAccount, WantError: ErrNoMatch}, 305 {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, 306 {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch}, 307 {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, 308 } 309 for i, test := range tests { 310 a, err := cache.find(test.Query) 311 if !reflect.DeepEqual(err, test.WantError) { 312 t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) 313 continue 314 } 315 if a != test.WantResult { 316 t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) 317 continue 318 } 319 } 320 } 321 322 // TestUpdatedKeyfileContents tests that updating the contents of a keystore file 323 // is noticed by the watcher, and the account cache is updated accordingly 324 func TestUpdatedKeyfileContents(t *testing.T) { 325 t.Parallel() 326 327 // Create a temporary keystore to test with 328 dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int())) 329 ks := NewKeyStore(dir, LightScryptN, LightScryptP) 330 331 list := ks.Accounts() 332 if len(list) > 0 { 333 t.Error("initial account list not empty:", list) 334 } 335 if !waitWatcherStart(ks) { 336 t.Fatal("keystore watcher didn't start in time") 337 } 338 // Create the directory and copy a key file into it. 339 os.MkdirAll(dir, 0700) 340 defer os.RemoveAll(dir) 341 file := filepath.Join(dir, "aaa") 342 343 // Place one of our testfiles in there 344 if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { 345 t.Fatal(err) 346 } 347 348 // ks should see the account. 349 wantAccounts := []accounts.Account{cachetestAccounts[0]} 350 wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} 351 if err := waitForAccounts(wantAccounts, ks); err != nil { 352 t.Error(err) 353 return 354 } 355 // needed so that modTime of `file` is different to its current value after forceCopyFile 356 os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) 357 358 // Now replace file contents 359 if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil { 360 t.Fatal(err) 361 return 362 } 363 wantAccounts = []accounts.Account{cachetestAccounts[1]} 364 wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} 365 if err := waitForAccounts(wantAccounts, ks); err != nil { 366 t.Errorf("First replacement failed") 367 t.Error(err) 368 return 369 } 370 371 // needed so that modTime of `file` is different to its current value after forceCopyFile 372 os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) 373 374 // Now replace file contents again 375 if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil { 376 t.Fatal(err) 377 return 378 } 379 wantAccounts = []accounts.Account{cachetestAccounts[2]} 380 wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} 381 if err := waitForAccounts(wantAccounts, ks); err != nil { 382 t.Errorf("Second replacement failed") 383 t.Error(err) 384 return 385 } 386 387 // needed so that modTime of `file` is different to its current value after os.WriteFile 388 os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) 389 390 // Now replace file contents with crap 391 if err := os.WriteFile(file, []byte("foo"), 0600); err != nil { 392 t.Fatal(err) 393 return 394 } 395 if err := waitForAccounts([]accounts.Account{}, ks); err != nil { 396 t.Errorf("Emptying account file failed") 397 t.Error(err) 398 return 399 } 400 } 401 402 // forceCopyFile is like cp.CopyFile, but doesn't complain if the destination exists. 403 func forceCopyFile(dst, src string) error { 404 data, err := os.ReadFile(src) 405 if err != nil { 406 return err 407 } 408 return os.WriteFile(dst, data, 0644) 409 }