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