8d066f1f42
This commit replaces ioutil.TempDir with t.TempDir in tests. The directory created by t.TempDir is automatically removed when the test and all its subtests complete. Prior to this commit, temporary directory created using ioutil.TempDir had to be removed manually by calling os.RemoveAll, which is omitted in some tests. The error handling boilerplate e.g. defer func() { if err := os.RemoveAll(dir); err != nil { t.Fatal(err) } } is also tedious, but t.TempDir handles this for us nicely. Reference: https://pkg.go.dev/testing#T.TempDir Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
458 lines
13 KiB
Go
458 lines
13 KiB
Go
// Copyright 2017 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package keystore
|
|
|
|
import (
|
|
"math/rand"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/ethereum/go-ethereum/event"
|
|
)
|
|
|
|
var testSigData = make([]byte, 32)
|
|
|
|
func TestKeyStore(t *testing.T) {
|
|
dir, ks := tmpKeyStore(t, true)
|
|
|
|
a, err := ks.NewAccount("foo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(a.URL.Path, dir) {
|
|
t.Errorf("account file %s doesn't have dir prefix", a.URL)
|
|
}
|
|
stat, err := os.Stat(a.URL.Path)
|
|
if err != nil {
|
|
t.Fatalf("account file %s doesn't exist (%v)", a.URL, err)
|
|
}
|
|
if runtime.GOOS != "windows" && stat.Mode() != 0600 {
|
|
t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600)
|
|
}
|
|
if !ks.HasAddress(a.Address) {
|
|
t.Errorf("HasAccount(%x) should've returned true", a.Address)
|
|
}
|
|
if err := ks.Update(a, "foo", "bar"); err != nil {
|
|
t.Errorf("Update error: %v", err)
|
|
}
|
|
if err := ks.Delete(a, "bar"); err != nil {
|
|
t.Errorf("Delete error: %v", err)
|
|
}
|
|
if common.FileExist(a.URL.Path) {
|
|
t.Errorf("account file %s should be gone after Delete", a.URL)
|
|
}
|
|
if ks.HasAddress(a.Address) {
|
|
t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address)
|
|
}
|
|
}
|
|
|
|
func TestSign(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, true)
|
|
|
|
pass := "" // not used but required by API
|
|
a1, err := ks.NewAccount(pass)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ks.Unlock(a1, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSignWithPassphrase(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, true)
|
|
|
|
pass := "passwd"
|
|
acc, err := ks.NewAccount(pass)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, unlocked := ks.unlocked[acc.Address]; unlocked {
|
|
t.Fatal("expected account to be locked")
|
|
}
|
|
|
|
_, err = ks.SignHashWithPassphrase(acc, pass, testSigData)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, unlocked := ks.unlocked[acc.Address]; unlocked {
|
|
t.Fatal("expected account to be locked")
|
|
}
|
|
|
|
if _, err = ks.SignHashWithPassphrase(acc, "invalid passwd", testSigData); err == nil {
|
|
t.Fatal("expected SignHashWithPassphrase to fail with invalid password")
|
|
}
|
|
}
|
|
|
|
func TestTimedUnlock(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, true)
|
|
|
|
pass := "foo"
|
|
a1, err := ks.NewAccount(pass)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Signing without passphrase fails because account is locked
|
|
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
|
if err != ErrLocked {
|
|
t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err)
|
|
}
|
|
|
|
// Signing with passphrase works
|
|
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Signing without passphrase works because account is temp unlocked
|
|
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
|
if err != nil {
|
|
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
|
}
|
|
|
|
// Signing fails again after automatic locking
|
|
time.Sleep(250 * time.Millisecond)
|
|
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
|
if err != ErrLocked {
|
|
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
|
|
}
|
|
}
|
|
|
|
func TestOverrideUnlock(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, false)
|
|
|
|
pass := "foo"
|
|
a1, err := ks.NewAccount(pass)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Unlock indefinitely.
|
|
if err = ks.TimedUnlock(a1, pass, 5*time.Minute); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Signing without passphrase works because account is temp unlocked
|
|
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
|
if err != nil {
|
|
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
|
}
|
|
|
|
// reset unlock to a shorter period, invalidates the previous unlock
|
|
if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Signing without passphrase still works because account is temp unlocked
|
|
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
|
if err != nil {
|
|
t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
|
|
}
|
|
|
|
// Signing fails again after automatic locking
|
|
time.Sleep(250 * time.Millisecond)
|
|
_, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
|
|
if err != ErrLocked {
|
|
t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
|
|
}
|
|
}
|
|
|
|
// This test should fail under -race if signing races the expiration goroutine.
|
|
func TestSignRace(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, false)
|
|
|
|
// Create a test account.
|
|
a1, err := ks.NewAccount("")
|
|
if err != nil {
|
|
t.Fatal("could not create the test account", err)
|
|
}
|
|
|
|
if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil {
|
|
t.Fatal("could not unlock the test account", err)
|
|
}
|
|
end := time.Now().Add(500 * time.Millisecond)
|
|
for time.Now().Before(end) {
|
|
if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrLocked {
|
|
return
|
|
} else if err != nil {
|
|
t.Errorf("Sign error: %v", err)
|
|
return
|
|
}
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
t.Errorf("Account did not lock within the timeout")
|
|
}
|
|
|
|
// Tests that the wallet notifier loop starts and stops correctly based on the
|
|
// addition and removal of wallet event subscriptions.
|
|
func TestWalletNotifierLifecycle(t *testing.T) {
|
|
// Create a temporary kesytore to test with
|
|
_, ks := tmpKeyStore(t, false)
|
|
|
|
// Ensure that the notification updater is not running yet
|
|
time.Sleep(250 * time.Millisecond)
|
|
ks.mu.RLock()
|
|
updating := ks.updating
|
|
ks.mu.RUnlock()
|
|
|
|
if updating {
|
|
t.Errorf("wallet notifier running without subscribers")
|
|
}
|
|
// Subscribe to the wallet feed and ensure the updater boots up
|
|
updates := make(chan accounts.WalletEvent)
|
|
|
|
subs := make([]event.Subscription, 2)
|
|
for i := 0; i < len(subs); i++ {
|
|
// Create a new subscription
|
|
subs[i] = ks.Subscribe(updates)
|
|
|
|
// Ensure the notifier comes online
|
|
time.Sleep(250 * time.Millisecond)
|
|
ks.mu.RLock()
|
|
updating = ks.updating
|
|
ks.mu.RUnlock()
|
|
|
|
if !updating {
|
|
t.Errorf("sub %d: wallet notifier not running after subscription", i)
|
|
}
|
|
}
|
|
// Unsubscribe and ensure the updater terminates eventually
|
|
for i := 0; i < len(subs); i++ {
|
|
// Close an existing subscription
|
|
subs[i].Unsubscribe()
|
|
|
|
// Ensure the notifier shuts down at and only at the last close
|
|
for k := 0; k < int(walletRefreshCycle/(250*time.Millisecond))+2; k++ {
|
|
ks.mu.RLock()
|
|
updating = ks.updating
|
|
ks.mu.RUnlock()
|
|
|
|
if i < len(subs)-1 && !updating {
|
|
t.Fatalf("sub %d: event notifier stopped prematurely", i)
|
|
}
|
|
if i == len(subs)-1 && !updating {
|
|
return
|
|
}
|
|
time.Sleep(250 * time.Millisecond)
|
|
}
|
|
}
|
|
t.Errorf("wallet notifier didn't terminate after unsubscribe")
|
|
}
|
|
|
|
type walletEvent struct {
|
|
accounts.WalletEvent
|
|
a accounts.Account
|
|
}
|
|
|
|
// Tests that wallet notifications and correctly fired when accounts are added
|
|
// or deleted from the keystore.
|
|
func TestWalletNotifications(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, false)
|
|
|
|
// Subscribe to the wallet feed and collect events.
|
|
var (
|
|
events []walletEvent
|
|
updates = make(chan accounts.WalletEvent)
|
|
sub = ks.Subscribe(updates)
|
|
)
|
|
defer sub.Unsubscribe()
|
|
go func() {
|
|
for {
|
|
select {
|
|
case ev := <-updates:
|
|
events = append(events, walletEvent{ev, ev.Wallet.Accounts()[0]})
|
|
case <-sub.Err():
|
|
close(updates)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Randomly add and remove accounts.
|
|
var (
|
|
live = make(map[common.Address]accounts.Account)
|
|
wantEvents []walletEvent
|
|
)
|
|
for i := 0; i < 1024; i++ {
|
|
if create := len(live) == 0 || rand.Int()%4 > 0; create {
|
|
// Add a new account and ensure wallet notifications arrives
|
|
account, err := ks.NewAccount("")
|
|
if err != nil {
|
|
t.Fatalf("failed to create test account: %v", err)
|
|
}
|
|
live[account.Address] = account
|
|
wantEvents = append(wantEvents, walletEvent{accounts.WalletEvent{Kind: accounts.WalletArrived}, account})
|
|
} else {
|
|
// Delete a random account.
|
|
var account accounts.Account
|
|
for _, a := range live {
|
|
account = a
|
|
break
|
|
}
|
|
if err := ks.Delete(account, ""); err != nil {
|
|
t.Fatalf("failed to delete test account: %v", err)
|
|
}
|
|
delete(live, account.Address)
|
|
wantEvents = append(wantEvents, walletEvent{accounts.WalletEvent{Kind: accounts.WalletDropped}, account})
|
|
}
|
|
}
|
|
|
|
// Shut down the event collector and check events.
|
|
sub.Unsubscribe()
|
|
for ev := range updates {
|
|
events = append(events, walletEvent{ev, ev.Wallet.Accounts()[0]})
|
|
}
|
|
checkAccounts(t, live, ks.Wallets())
|
|
checkEvents(t, wantEvents, events)
|
|
}
|
|
|
|
// TestImportExport tests the import functionality of a keystore.
|
|
func TestImportECDSA(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, true)
|
|
key, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate key: %v", key)
|
|
}
|
|
if _, err = ks.ImportECDSA(key, "old"); err != nil {
|
|
t.Errorf("importing failed: %v", err)
|
|
}
|
|
if _, err = ks.ImportECDSA(key, "old"); err == nil {
|
|
t.Errorf("importing same key twice succeeded")
|
|
}
|
|
if _, err = ks.ImportECDSA(key, "new"); err == nil {
|
|
t.Errorf("importing same key twice succeeded")
|
|
}
|
|
}
|
|
|
|
// TestImportECDSA tests the import and export functionality of a keystore.
|
|
func TestImportExport(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, true)
|
|
acc, err := ks.NewAccount("old")
|
|
if err != nil {
|
|
t.Fatalf("failed to create account: %v", acc)
|
|
}
|
|
json, err := ks.Export(acc, "old", "new")
|
|
if err != nil {
|
|
t.Fatalf("failed to export account: %v", acc)
|
|
}
|
|
_, ks2 := tmpKeyStore(t, true)
|
|
if _, err = ks2.Import(json, "old", "old"); err == nil {
|
|
t.Errorf("importing with invalid password succeeded")
|
|
}
|
|
acc2, err := ks2.Import(json, "new", "new")
|
|
if err != nil {
|
|
t.Errorf("importing failed: %v", err)
|
|
}
|
|
if acc.Address != acc2.Address {
|
|
t.Error("imported account does not match exported account")
|
|
}
|
|
if _, err = ks2.Import(json, "new", "new"); err == nil {
|
|
t.Errorf("importing a key twice succeeded")
|
|
}
|
|
|
|
}
|
|
|
|
// TestImportRace tests the keystore on races.
|
|
// This test should fail under -race if importing races.
|
|
func TestImportRace(t *testing.T) {
|
|
_, ks := tmpKeyStore(t, true)
|
|
acc, err := ks.NewAccount("old")
|
|
if err != nil {
|
|
t.Fatalf("failed to create account: %v", acc)
|
|
}
|
|
json, err := ks.Export(acc, "old", "new")
|
|
if err != nil {
|
|
t.Fatalf("failed to export account: %v", acc)
|
|
}
|
|
_, ks2 := tmpKeyStore(t, true)
|
|
var atom uint32
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
for i := 0; i < 2; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
if _, err := ks2.Import(json, "new", "new"); err != nil {
|
|
atomic.AddUint32(&atom, 1)
|
|
}
|
|
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if atom != 1 {
|
|
t.Errorf("Import is racy")
|
|
}
|
|
}
|
|
|
|
// checkAccounts checks that all known live accounts are present in the wallet list.
|
|
func checkAccounts(t *testing.T, live map[common.Address]accounts.Account, wallets []accounts.Wallet) {
|
|
if len(live) != len(wallets) {
|
|
t.Errorf("wallet list doesn't match required accounts: have %d, want %d", len(wallets), len(live))
|
|
return
|
|
}
|
|
liveList := make([]accounts.Account, 0, len(live))
|
|
for _, account := range live {
|
|
liveList = append(liveList, account)
|
|
}
|
|
sort.Sort(accountsByURL(liveList))
|
|
for j, wallet := range wallets {
|
|
if accs := wallet.Accounts(); len(accs) != 1 {
|
|
t.Errorf("wallet %d: contains invalid number of accounts: have %d, want 1", j, len(accs))
|
|
} else if accs[0] != liveList[j] {
|
|
t.Errorf("wallet %d: account mismatch: have %v, want %v", j, accs[0], liveList[j])
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkEvents checks that all events in 'want' are present in 'have'. Events may be present multiple times.
|
|
func checkEvents(t *testing.T, want []walletEvent, have []walletEvent) {
|
|
for _, wantEv := range want {
|
|
nmatch := 0
|
|
for ; len(have) > 0; nmatch++ {
|
|
if have[0].Kind != wantEv.Kind || have[0].a != wantEv.a {
|
|
break
|
|
}
|
|
have = have[1:]
|
|
}
|
|
if nmatch == 0 {
|
|
t.Fatalf("can't find event with Kind=%v for %x", wantEv.Kind, wantEv.a.Address)
|
|
}
|
|
}
|
|
}
|
|
|
|
func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) {
|
|
d := t.TempDir()
|
|
newKs := NewPlaintextKeyStore
|
|
if encrypted {
|
|
newKs = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) }
|
|
}
|
|
return d, newKs(d)
|
|
}
|