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