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