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