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