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