github.com/klaytn/klaytn@v1.12.1/accounts/keystore/account_cache_test.go (about)

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