github.com/ava-labs/subnet-evm@v0.6.4/accounts/keystore/account_cache_test.go (about)

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