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