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  }