From 9ade2ea98310fbc4cc3015ae0823482a59880a2e Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 01/69] account management keystore --- accounts/URL.go | 56 +++ accounts/accounts.go | 161 +++++++ accounts/event/feed.go | 206 ++++++++ accounts/event/subscription.go | 85 ++++ accounts/hd.go | 141 ++++++ accounts/helper.go | 59 +++ accounts/keystore/account_cache.go | 270 +++++++++++ accounts/keystore/account_cache_test.go | 393 +++++++++++++++ accounts/keystore/file_cache.go | 77 +++ accounts/keystore/key.go | 196 ++++++++ accounts/keystore/keystore.go | 427 +++++++++++++++++ accounts/keystore/keystore_test.go | 448 ++++++++++++++++++ accounts/keystore/passphrase.go | 342 +++++++++++++ accounts/keystore/passphrase_test.go | 45 ++ accounts/keystore/presale.go | 131 +++++ accounts/keystore/testdata/dupes/1 | 1 + accounts/keystore/testdata/dupes/2 | 1 + accounts/keystore/testdata/dupes/foo | 1 + .../keystore/testdata/keystore/.hiddenfile | 1 + accounts/keystore/testdata/keystore/README | 21 + ...--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 | 1 + accounts/keystore/testdata/keystore/aaa | 1 + accounts/keystore/testdata/keystore/empty | 0 .../fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e | 1 + accounts/keystore/testdata/keystore/garbage | Bin 0 -> 300 bytes .../keystore/testdata/keystore/no-address | 1 + accounts/keystore/testdata/keystore/zero | 1 + accounts/keystore/testdata/keystore/zzz | 1 + .../cb61d5a9c4896fb9658090b597ef0e7be6f7b67e | 1 + .../keystore/testdata/v1_test_vector.json | 28 ++ .../keystore/testdata/v3_test_vector.json | 97 ++++ .../keystore/testdata/very-light-scrypt.json | 1 + accounts/keystore/wallet.go | 97 ++++ accounts/keystore/watch.go | 99 ++++ accounts/scwallet/wallet.go | 1 + accounts/usbwallet/wallet.go | 1 + crypto/crypto.go | 41 ++ go.mod | 4 + go.sum | 6 + 39 files changed, 3444 insertions(+) create mode 100644 accounts/URL.go create mode 100644 accounts/accounts.go create mode 100644 accounts/event/feed.go create mode 100644 accounts/event/subscription.go create mode 100644 accounts/hd.go create mode 100644 accounts/helper.go create mode 100644 accounts/keystore/account_cache.go create mode 100644 accounts/keystore/account_cache_test.go create mode 100644 accounts/keystore/file_cache.go create mode 100644 accounts/keystore/key.go create mode 100644 accounts/keystore/keystore.go create mode 100644 accounts/keystore/keystore_test.go create mode 100644 accounts/keystore/passphrase.go create mode 100644 accounts/keystore/passphrase_test.go create mode 100644 accounts/keystore/presale.go create mode 100644 accounts/keystore/testdata/dupes/1 create mode 100644 accounts/keystore/testdata/dupes/2 create mode 100644 accounts/keystore/testdata/dupes/foo create mode 100644 accounts/keystore/testdata/keystore/.hiddenfile create mode 100644 accounts/keystore/testdata/keystore/README create mode 100644 accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 create mode 100644 accounts/keystore/testdata/keystore/aaa create mode 100644 accounts/keystore/testdata/keystore/empty create mode 100644 accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e create mode 100644 accounts/keystore/testdata/keystore/garbage create mode 100644 accounts/keystore/testdata/keystore/no-address create mode 100644 accounts/keystore/testdata/keystore/zero create mode 100644 accounts/keystore/testdata/keystore/zzz create mode 100644 accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e create mode 100644 accounts/keystore/testdata/v1_test_vector.json create mode 100644 accounts/keystore/testdata/v3_test_vector.json create mode 100644 accounts/keystore/testdata/very-light-scrypt.json create mode 100644 accounts/keystore/wallet.go create mode 100644 accounts/keystore/watch.go create mode 100644 accounts/scwallet/wallet.go create mode 100644 accounts/usbwallet/wallet.go diff --git a/accounts/URL.go b/accounts/URL.go new file mode 100644 index 0000000000..9a2867f88b --- /dev/null +++ b/accounts/URL.go @@ -0,0 +1,56 @@ +package accounts + +import ( + "encoding/json" + "errors" + "strings" +) + +type URL struct { + Scheme string + Path string +} + +func parseURL(url string) (URL, error) { + urlParts := strings.Split(url, "://") + if len(urlParts) != 2 || urlParts[0] == "" { + return URL{}, errors.New("protocol scheme missing") + } + return URL{ + Scheme: urlParts[0], + Path: urlParts[1], + }, nil +} + +func (u URL) String() string { + if u.Scheme != "" { + return u.Scheme + "://" + u.Path + } + return u.Path +} + +func (u URL) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} + +func (u *URL) UnmarshalJSON(input []byte) error { + var textURL string + err := json.Unmarshal(input, &textURL) + if err != nil { + return err + } + url, err := parseURL(textURL) + if err != nil { + return err + } + u.Scheme = url.Scheme + u.Path = url.Path + return nil +} + +func (u URL) Cmp(url URL) int { + if u.Scheme == url.Scheme { + return strings.Compare(u.Path, url.Path) + } + return strings.Compare(u.Scheme, url.Scheme) +} diff --git a/accounts/accounts.go b/accounts/accounts.go new file mode 100644 index 0000000000..8e09d5b8d3 --- /dev/null +++ b/accounts/accounts.go @@ -0,0 +1,161 @@ +package accounts + +import ( + "fmt" + "math/big" + + "github.com/0xPolygon/polygon-edge/state" + "github.com/0xPolygon/polygon-edge/types" + "golang.org/x/crypto/sha3" +) + +type Account struct { + Address types.Address `json:"address"` + URL URL `json:"url"` +} + +// Wallet represents a software or hardware wallet that might contain one or more +// accounts (derived from the same seed). +type Wallet interface { + // URL retrieves the canonical path under which this wallet is reachable. It is + // used by upper layers to define a sorting order over all wallets from multiple + // backends. + URL() URL + + // Status returns a textual status to aid the user in the current state of the + // wallet. It also returns an error indicating any failure the wallet might have + // encountered. + Status() (string, error) + + // Open initializes access to a wallet instance. It is not meant to unlock or + // decrypt account keys, rather simply to establish a connection to hardware + // wallets and/or to access derivation seeds. + // + // The passphrase parameter may or may not be used by the implementation of a + // particular wallet instance. The reason there is no passwordless open method + // is to strive towards a uniform wallet handling, oblivious to the different + // backend providers. + // + // Please note, if you open a wallet, you must close it to release any allocated + // resources (especially important when working with hardware wallets). + Open(passphrase string) error + + // Close releases any resources held by an open wallet instance. + Close() error + + // Accounts retrieves the list of signing accounts the wallet is currently aware + // of. For hierarchical deterministic wallets, the list will not be exhaustive, + // rather only contain the accounts explicitly pinned during account derivation. + Accounts() []Account + + // Contains returns whether an account is part of this particular wallet or not. + Contains(account Account) bool + + // Derive attempts to explicitly derive a hierarchical deterministic account at + // the specified derivation path. If requested, the derived account will be added + // to the wallet's tracked account list. + Derive(path DerivationPath, pin bool) (Account, error) + + // SelfDerive sets a base account derivation path from which the wallet attempts + // to discover non zero accounts and automatically add them to list of tracked + // accounts. + // + // Note, self derivation will increment the last component of the specified path + // opposed to descending into a child path to allow discovering accounts starting + // from non zero components. + // + // Some hardware wallets switched derivation paths through their evolution, so + // this method supports providing multiple bases to discover old user accounts + // too. Only the last base will be used to derive the next empty account. + // + // You can disable automatic account discovery by calling SelfDerive with a nil + // chain state reader. + SelfDerive(bases []DerivationPath, state *state.Transition) + + // SignData requests the wallet to sign the hash of the given data + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the wallet requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code to verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignDataWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignData(account Account, mimeType string, data []byte) ([]byte, error) + + // SignDataWithPassphrase is identical to SignData, but also takes a password + // NOTE: there's a chance that an erroneous call might mistake the two strings, and + // supply password in the mimetype field, or vice versa. Thus, an implementation + // should never echo the mimetype or return the mimetype in the error-response + SignDataWithPassphrase(account Account, passphrase, mimeType string, data []byte) ([]byte, error) + + // SignText requests the wallet to sign the hash of a given piece of data, prefixed + // by the Ethereum prefix scheme + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the wallet requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code to verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignTextWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + // + // This method should return the signature in 'canonical' format, with v 0 or 1. + SignText(account Account, text []byte) ([]byte, error) + + // SignTextWithPassphrase is identical to Signtext, but also takes a password + SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error) + + // SignTx requests the wallet to sign the given transaction. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the wallet requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code to verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignTxWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + + // SignTxWithPassphrase is identical to SignTx, but also takes a password + SignTxWithPassphrase(account Account, passphrase string, + tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) +} + +func TextHash(data []byte) []byte { + hash, _ := TextAndHash(data) + return hash +} + +func TextAndHash(data []byte) ([]byte, string) { + msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) + hasher := sha3.NewLegacyKeccak256() + hasher.Write([]byte(msg)) + return hasher.Sum(nil), msg +} + +type WalletEventType int + +const ( + // WalletArrived is fired when a new wallet is detected either via USB or via + // a filesystem event in the keystore. + WalletArrived WalletEventType = iota + + // WalletOpened is fired when a wallet is successfully opened with the purpose + // of starting any background processes such as automatic key derivation. + WalletOpened + + // WalletDropped + WalletDropped +) + +// WalletEvent is an event fired by an account backend when a wallet arrival or +// departure is detected. +type WalletEvent struct { + Wallet Wallet // Wallet instance arrived or departed + Kind WalletEventType // Event type that happened in the system +} diff --git a/accounts/event/feed.go b/accounts/event/feed.go new file mode 100644 index 0000000000..67de773ad5 --- /dev/null +++ b/accounts/event/feed.go @@ -0,0 +1,206 @@ +package event + +import ( + "errors" + "reflect" + "sync" +) + +var errBadChannel = errors.New("event: Subscribe argument does not have sendable channel type") + +// Feed implements one-to-many subscriptions where the carrier of events is a channel. +// Values sent to a Feed are delivered to all subscribed channels simultaneously. +// +// Feeds can only be used with a single type. The type is determined by the first Send or +// Subscribe operation. Subsequent calls to these methods panic if the type does not +// match. +// +// The zero value is ready to use. +type Feed struct { + once sync.Once // ensures that init only runs once + sendLock chan struct{} // sendLock has a one-element buffer and is empty when held.It protects sendCases. + removeSub chan interface{} // interrupts Send + sendCases caseList // the active set of select cases used by Send + + // The inbox holds newly subscribed channels until they are added to sendCases. + mu sync.Mutex + inbox caseList + etype reflect.Type +} + +// This is the index of the first actual subscription channel in sendCases. +// sendCases[0] is a SelectRecv case for the removeSub channel. +const firstSubSendCase = 1 + +type feedTypeError struct { + got, want reflect.Type + op string +} + +func (e feedTypeError) Error() string { + return "event: wrong type in " + e.op + " got " + e.got.String() + ", want " + e.want.String() +} + +func (f *Feed) init(etype reflect.Type) { + f.etype = etype + f.removeSub = make(chan interface{}) + f.sendLock = make(chan struct{}, 1) + f.sendLock <- struct{}{} + f.sendCases = caseList{{Chan: reflect.ValueOf(f.removeSub), Dir: reflect.SelectRecv}} +} + +// Subscribe adds a channel to the feed. Future sends will be delivered on the channel +// until the subscription is canceled. All channels added must have the same element type. +// +// The channel should have ample buffer space to avoid blocking other subscribers. +// Slow subscribers are not dropped. +func (f *Feed) Subscribe(channel interface{}) Subscription { + chanval := reflect.ValueOf(channel) + chantyp := chanval.Type() + if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 { + panic(errBadChannel) + } + sub := &feedSub{feed: f, channel: chanval, err: make(chan error, 1)} + + f.once.Do(func() { f.init(chantyp.Elem()) }) + if f.etype != chantyp.Elem() { + panic(feedTypeError{op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)}) + } + + f.mu.Lock() + defer f.mu.Unlock() + // Add the select case to the inbox. + // The next Send will add it to f.sendCases. + cas := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval} + f.inbox = append(f.inbox, cas) + return sub +} + +func (f *Feed) remove(sub *feedSub) { + // Delete from inbox first, which covers channels + // that have not been added to f.sendCases yet. + ch := sub.channel.Interface() + f.mu.Lock() + index := f.inbox.find(ch) + if index != -1 { + f.inbox = f.inbox.delete(index) + f.mu.Unlock() + return + } + f.mu.Unlock() + + select { + case f.removeSub <- ch: + // Send will remove the channel from f.sendCases. + case <-f.sendLock: + // No Send is in progress, delete the channel now that we have the send lock. + f.sendCases = f.sendCases.delete(f.sendCases.find(ch)) + f.sendLock <- struct{}{} + } +} + +// Send delivers to all subscribed channels simultaneously. +// It returns the number of subscribers that the value was sent to. +func (f *Feed) Send(value interface{}) (nsent int) { + rvalue := reflect.ValueOf(value) + + f.once.Do(func() { f.init(rvalue.Type()) }) + if f.etype != rvalue.Type() { + panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype}) + } + + <-f.sendLock + + // Add new cases from the inbox after taking the send lock. + f.mu.Lock() + f.sendCases = append(f.sendCases, f.inbox...) + f.inbox = nil + f.mu.Unlock() + + // Set the sent value on all channels. + for i := firstSubSendCase; i < len(f.sendCases); i++ { + f.sendCases[i].Send = rvalue + } + + // Send until all channels except removeSub have been chosen. 'cases' tracks a prefix + // of sendCases. When a send succeeds, the corresponding case moves to the end of + // 'cases' and it shrinks by one element. + cases := f.sendCases + for { + // Fast path: try sending without blocking before adding to the select set. + // This should usually succeed if subscribers are fast enough and have free + // buffer space. + for i := firstSubSendCase; i < len(cases); i++ { + if cases[i].Chan.TrySend(rvalue) { + nsent++ + cases = cases.deactivate(i) + i-- + } + } + if len(cases) == firstSubSendCase { + break + } + // Select on all the receivers, waiting for them to unblock. + chosen, recv, _ := reflect.Select(cases) + if chosen == 0 /* <-f.removeSub */ { + index := f.sendCases.find(recv.Interface()) + f.sendCases = f.sendCases.delete(index) + if index >= 0 && index < len(cases) { + // Shrink 'cases' too because the removed case was still active. + cases = f.sendCases[:len(cases)-1] + } + } else { + cases = cases.deactivate(chosen) + nsent++ + } + } + + // Forget about the sent value and hand off the send lock. + for i := firstSubSendCase; i < len(f.sendCases); i++ { + f.sendCases[i].Send = reflect.Value{} + } + f.sendLock <- struct{}{} + return nsent +} + +type feedSub struct { + feed *Feed + channel reflect.Value + errOnce sync.Once + err chan error +} + +func (sub *feedSub) Unsubscribe() { + sub.errOnce.Do(func() { + sub.feed.remove(sub) + close(sub.err) + }) +} + +func (sub *feedSub) Err() <-chan error { + return sub.err +} + +type caseList []reflect.SelectCase + +// find returns the index of a case containing the given channel. +func (cs caseList) find(channel interface{}) int { + for i, cas := range cs { + if cas.Chan.Interface() == channel { + return i + } + } + return -1 +} + +// delete removes the given case from cs. +func (cs caseList) delete(index int) caseList { + return append(cs[:index], cs[index+1:]...) +} + +// deactivate moves the case at index into the non-accessible portion of the cs slice. +func (cs caseList) deactivate(index int) caseList { + last := len(cs) - 1 + cs[index], cs[last] = cs[last], cs[index] + return cs[:last] +} diff --git a/accounts/event/subscription.go b/accounts/event/subscription.go new file mode 100644 index 0000000000..f76e7c050f --- /dev/null +++ b/accounts/event/subscription.go @@ -0,0 +1,85 @@ +package event + +import ( + "sync" +) + +// The error channel is closed when the subscription ends successfully (i.e. when the +// source of events is closed). It is also closed when Unsubscribe is called. +// +// The Unsubscribe method cancels the sending of events. You must call Unsubscribe in all +// cases to ensure that resources related to the subscription are released. It can be +// called any number of times. +type Subscription interface { + Err() <-chan error // returns the error channel + Unsubscribe() // cancels sending of events, closing the error channel +} + +// SubscriptionScope provides a facility to unsubscribe multiple subscriptions at once. +// +// For code that handle more than one subscription, a scope can be used to conveniently +// unsubscribe all of them with a single call. The example demonstrates a typical use in a +// larger program. +// +// The zero value is ready to use. +type SubscriptionScope struct { + mu sync.Mutex + subs map[*scopeSub]struct{} + closed bool +} + +type scopeSub struct { + sc *SubscriptionScope + s Subscription +} + +// Track starts tracking a subscription. If the scope is closed, Track returns nil. The +// returned subscription is a wrapper. Unsubscribing the wrapper removes it from the +// scope. +func (sc *SubscriptionScope) Track(s Subscription) Subscription { + sc.mu.Lock() + defer sc.mu.Unlock() + if sc.closed { + return nil + } + if sc.subs == nil { + sc.subs = make(map[*scopeSub]struct{}) + } + ss := &scopeSub{sc, s} + sc.subs[ss] = struct{}{} + return ss +} + +// Close calls Unsubscribe on all tracked subscriptions and prevents further additions to +// the tracked set. Calls to Track after Close return nil. +func (sc *SubscriptionScope) Close() { + sc.mu.Lock() + defer sc.mu.Unlock() + if sc.closed { + return + } + sc.closed = true + for s := range sc.subs { + s.s.Unsubscribe() + } + sc.subs = nil +} + +// Count returns the number of tracked subscriptions. +// It is meant to be used for debugging. +func (sc *SubscriptionScope) Count() int { + sc.mu.Lock() + defer sc.mu.Unlock() + return len(sc.subs) +} + +func (s *scopeSub) Unsubscribe() { + s.s.Unsubscribe() + s.sc.mu.Lock() + defer s.sc.mu.Unlock() + delete(s.sc.subs, s) +} + +func (s *scopeSub) Err() <-chan error { + return s.s.Err() +} diff --git a/accounts/hd.go b/accounts/hd.go new file mode 100644 index 0000000000..09026825bc --- /dev/null +++ b/accounts/hd.go @@ -0,0 +1,141 @@ +package accounts + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "math/big" + "strings" +) + +// DefaultRootDerivationPath is the root path to which custom derivation endpoints +// are appended. As such, the first account will be at m/44'/60'/0'/0, the second +// at m/44'/60'/0'/1, etc. +var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} + +// DefaultBaseDerivationPath is the base path from which custom derivation endpoints +// are incremented. As such, the first account will be at m/44'/60'/0'/0/0, the second +// at m/44'/60'/0'/0/1, etc. +var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0} + +// LegacyLedgerBaseDerivationPath is the legacy base path from which custom derivation +// endpoints are incremented. As such, the first account will be at m/44'/60'/0'/0, the +// second at m/44'/60'/0'/1, etc. +var LegacyLedgerBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} + +// DerivationPath represents the computer friendly version of a hierarchical +// deterministic wallet account derivation path. +// +// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +// defines derivation paths to be of the form: +// +// m / purpose' / coin_type' / account' / change / address_index +// +// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki +// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and +// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns +// the `coin_type` 60' (or 0x8000003C) to Ethereum. +// +// The root path for Ethereum is m/44'/60'/0'/0 according to the specification +// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone +// yet whether accounts should increment the last component or the children of +// that. We will go with the simpler approach of incrementing the last component. +type DerivationPath []uint32 + +func ParseDerivationPath(path string) (DerivationPath, error) { + var result DerivationPath + + components := strings.Split(path, "/") + switch { + case len(components) == 0: + return nil, errors.New("empty derivation path") + case strings.TrimSpace(components[0]) == "": + return nil, errors.New("use 'm/' prefix for abolute path or no leading '/' for relative paths") + case strings.TrimSpace(components[0]) == "m": + components = components[1:] + default: + result = append(result, DefaultBaseDerivationPath...) + } + + for _, component := range components { + component = strings.TrimSpace(component) + var value uint32 + + if strings.HasSuffix(component, "'") { + value = 0x80000000 + component = strings.TrimSpace(strings.TrimSuffix(component, "'")) + } + + bigValue, ok := new(big.Int).SetString(component, 0) + if !ok { + return nil, fmt.Errorf("invalid component: %s", component) + } + max := math.MaxUint32 - value + if bigValue.Sign() < 0 || bigValue.Cmp(big.NewInt(int64(max))) > 0 { + if value == 0 { + return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigValue, max) + } + return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigValue, max) + } + value += uint32(bigValue.Uint64()) + + result = append(result, value) + } + + return result, nil +} + +func (path DerivationPath) String() string { + result := "m" + for _, component := range path { + var hardened bool + if component >= 0x80000000 { + component -= 0x80000000 + hardened = true + } + result = fmt.Sprintf("%s/%d", result, component) + if hardened { + result += "'" + } + } + return result +} + +func (path DerivationPath) MarshalJSON() ([]byte, error) { + return json.Marshal(path.String()) +} + +func (path *DerivationPath) UnmarshalJSON(b []byte) error { + var dp string + var err error + if err = json.Unmarshal(b, &dp); err != nil { + return err + } + *path, err = ParseDerivationPath(dp) + return err +} + +func DefaultIterator(base DerivationPath) func() DerivationPath { + path := make(DerivationPath, len(base)) + + copy(path[:], base[:]) + path[len(path)-1]-- + + return func() DerivationPath { + path[len(path)-1]++ + return path + } +} + +func LedgerLiveIterator(base DerivationPath) func() DerivationPath { + path := make(DerivationPath, len(base)) + + copy(path[:], base[:]) + path[2]-- + + return func() DerivationPath { + path[2]++ + return path + } +} diff --git a/accounts/helper.go b/accounts/helper.go new file mode 100644 index 0000000000..703aed4a54 --- /dev/null +++ b/accounts/helper.go @@ -0,0 +1,59 @@ +package accounts + +import ( + "errors" + "fmt" + + "github.com/0xPolygon/polygon-edge/types" +) + +var ( + ErrUnknownAccount = errors.New("unknown account") + + ErrUnknownWallet = errors.New("unknown wallet") + + ErrNotSupported = errors.New("not supported") + + ErrInvalidPassphrase = errors.New("invalid password") + + ErrWalletAlreadyOpen = errors.New("wallet already open") + + ErrWalletClosed = errors.New("wallet closed") + + ErrNoMatch = errors.New("no key for given address or file") + ErrDecrypt = errors.New("could not decrypt key with given password") + + // ErrAccountAlreadyExists is returned if an account attempted to import is + // already present in the keystore. + ErrAccountAlreadyExists = errors.New("account already exists") +) + +type AuthNeededError struct { + Needed string +} + +func NewAuthNeededError(needed string) error { + return &AuthNeededError{ + Needed: needed, + } +} + +func (err *AuthNeededError) Error() string { + return fmt.Sprintf("authentication needed: %s", err.Needed) +} + +type AmbiguousAddrError struct { + Addr types.Address + Matches []Account +} + +func (err *AmbiguousAddrError) Error() string { + files := "" + for i, a := range err.Matches { + files += a.URL.Path + if i < len(err.Matches)-1 { + files += ", " + } + } + return fmt.Sprintf("multiple keys match address (%s)", files) +} diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go new file mode 100644 index 0000000000..eee29c3707 --- /dev/null +++ b/accounts/keystore/account_cache.go @@ -0,0 +1,270 @@ +package keystore + +import ( + "bufio" + "encoding/json" + "os" + "path/filepath" + "slices" + "sort" + "strings" + "sync" + "time" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/types" + mapset "github.com/deckarep/golang-set/v2" +) + +func byURL(a, b accounts.Account) int { + return a.URL.Cmp(b.URL) +} + +// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs. +const KeyStoreScheme = "keystore" +const minReloadInterval = 2 * time.Second + +// accountCache is a live index of all accounts in the keystore. +type accountCache struct { + keydir string + watcher *watcher + mu sync.Mutex + all []accounts.Account + byAddr map[types.Address][]accounts.Account + throttle *time.Timer + notify chan struct{} + fileC fileCache +} + +func newAccountCache(keyDir string) (*accountCache, chan struct{}) { + ac := &accountCache{ + keydir: keyDir, + byAddr: make(map[types.Address][]accounts.Account), + notify: make(chan struct{}, 1), + fileC: fileCache{all: mapset.NewThreadUnsafeSet[string]()}, + } + ac.watcher = newWatcher(ac) + + return ac, ac.notify +} + +func (ac *accountCache) accounts() []accounts.Account { + ac.maybeReload() + ac.mu.Lock() + defer ac.mu.Unlock() + cpy := make([]accounts.Account, len(ac.all)) + copy(cpy, ac.all) + return cpy +} + +func (ac *accountCache) hasAddress(addr types.Address) bool { + ac.maybeReload() + ac.mu.Lock() + defer ac.mu.Unlock() + return len(ac.byAddr[addr]) > 0 +} + +func (ac *accountCache) add(newAccount accounts.Account) { + ac.mu.Lock() + defer ac.mu.Unlock() + + i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 }) + if i < len(ac.all) && ac.all[i] == newAccount { + return + } + // newAccount is not in the cache. + ac.all = append(ac.all, accounts.Account{}) + copy(ac.all[i+1:], ac.all[i:]) + ac.all[i] = newAccount + ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount) +} + +// note: removed needs to be unique here (i.e. both File and Address must be set). +func (ac *accountCache) delete(removed accounts.Account) { + ac.mu.Lock() + defer ac.mu.Unlock() + + ac.all = removeAccount(ac.all, removed) + if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { + delete(ac.byAddr, removed.Address) + } else { + ac.byAddr[removed.Address] = ba + } +} + +// deleteByFile removes an account referenced by the given path. +func (ac *accountCache) deleteByFile(path string) { + ac.mu.Lock() + defer ac.mu.Unlock() + i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Path >= path }) + + if i < len(ac.all) && ac.all[i].URL.Path == path { + removed := ac.all[i] + ac.all = append(ac.all[:i], ac.all[i+1:]...) + if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { + delete(ac.byAddr, removed.Address) + } else { + ac.byAddr[removed.Address] = ba + } + } +} + +// watcherStarted returns true if the watcher loop started running (even if it +// has since also ended). +func (ac *accountCache) watcherStarted() bool { + ac.mu.Lock() + defer ac.mu.Unlock() + return ac.watcher.running || ac.watcher.runEnded +} + +func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account { + for i := range slice { + if slice[i] == elem { + return append(slice[:i], slice[i+1:]...) + } + } + return slice +} + +// find returns the cached account for address if there is a unique match. +// The exact matching rules are explained by the documentation of accounts.Account. +// Callers must hold ac.mu. +func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { + // Limit search to address candidates if possible. + matches := ac.all + if (a.Address != types.Address{}) { + matches = ac.byAddr[a.Address] + } + if a.URL.Path != "" { + // If only the basename is specified, complete the path. + if !strings.ContainsRune(a.URL.Path, filepath.Separator) { + a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) + } + for i := range matches { + if matches[i].URL == a.URL { + return matches[i], nil + } + } + if (a.Address == types.Address{}) { + return accounts.Account{}, accounts.ErrNoMatch + } + } + switch len(matches) { + case 1: + return matches[0], nil + case 0: + return accounts.Account{}, accounts.ErrNoMatch + default: + err := &accounts.AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} + copy(err.Matches, matches) + slices.SortFunc(err.Matches, byURL) + return accounts.Account{}, err + } +} + +func (ac *accountCache) maybeReload() { + ac.mu.Lock() + + if ac.watcher.running { + ac.mu.Unlock() + return // A watcher is running and will keep the cache up-to-date. + } + if ac.throttle == nil { + ac.throttle = time.NewTimer(0) + } else { + select { + case <-ac.throttle.C: + default: + ac.mu.Unlock() + return // The cache was reloaded recently. + } + } + // No watcher running, start it. + ac.watcher.start() + ac.throttle.Reset(minReloadInterval) + ac.mu.Unlock() + ac.scanAccounts() +} +func (ac *accountCache) close() { + ac.mu.Lock() + ac.watcher.close() + if ac.throttle != nil { + ac.throttle.Stop() + } + if ac.notify != nil { + close(ac.notify) + ac.notify = nil + } + ac.mu.Unlock() +} + +// scanAccounts checks if any changes have occurred on the filesystem, and +// updates the account cache accordingly +func (ac *accountCache) scanAccounts() error { + // Scan the entire folder metadata for file changes + creates, deletes, updates, err := ac.fileC.scan(ac.keydir) + if err != nil { + //TO DO log.Debug("Failed to reload keystore contents", "err", err) + return err + } + if creates.Cardinality() == 0 && deletes.Cardinality() == 0 && updates.Cardinality() == 0 { + return nil + } + // Create a helper method to scan the contents of the key files + var ( + buf = new(bufio.Reader) + key struct { + Address string `json:"address"` + } + ) + readAccount := func(path string) *accounts.Account { + fd, err := os.Open(path) + if err != nil { + //TO DO log.Trace("Failed to open keystore file", "path", path, "err", err) + return nil + } + defer fd.Close() + buf.Reset(fd) + // Parse the address. + key.Address = "" + err = json.NewDecoder(buf).Decode(&key) + addr := types.StringToAddress(key.Address) + switch { + case err != nil: + //TO DO log.Debug("Failed to decode keystore key", "path", path, "err", err) + case addr == types.Address{}: + //TO DO log.Debug("Failed to decode keystore key", "path", path, "err", "missing or zero address") + default: + return &accounts.Account{ + Address: addr, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}, + } + } + return nil + } + // Process all the file diffs + //start := time.Now() + + for _, path := range creates.ToSlice() { + if a := readAccount(path); a != nil { + ac.add(*a) + } + } + for _, path := range deletes.ToSlice() { + ac.deleteByFile(path) + } + for _, path := range updates.ToSlice() { + ac.deleteByFile(path) + if a := readAccount(path); a != nil { + ac.add(*a) + } + } + //end := time.Now() + + select { + case ac.notify <- struct{}{}: + default: + } + //TO DO log.Trace("Handled keystore changes", "time", end.Sub(start)) + return nil +} diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go new file mode 100644 index 0000000000..a9d47d3fcf --- /dev/null +++ b/accounts/keystore/account_cache_test.go @@ -0,0 +1,393 @@ +package keystore + +import ( + "errors" + "fmt" + "math/rand" + "os" + "path/filepath" + "reflect" + "slices" + "testing" + "time" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/types" + "github.com/cespare/cp" + "github.com/davecgh/go-spew/spew" +) + +var ( + cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore")) + cachetestAccounts = []accounts.Account{ + { + Address: types.StringToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")}, + }, + { + Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, + }, + { + Address: types.StringToAddress("289d485d9771714cce91d3393d764e1311907acc"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, + }, + } +) + +// waitWatcherStart waits up to 1s for the keystore watcher to start. +func waitWatcherStart(ks *KeyStore) bool { + // On systems where file watch is not supported, just return "ok". + if !ks.cache.watcher.enabled() { + return true + } + // The watcher should start, and then exit. + for t0 := time.Now(); time.Since(t0) < 1*time.Second; time.Sleep(100 * time.Millisecond) { + if ks.cache.watcherStarted() { + return true + } + } + return false +} + +func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { + var list []accounts.Account + for t0 := time.Now(); time.Since(t0) < 5*time.Second; time.Sleep(100 * time.Millisecond) { + list = ks.Accounts() + if reflect.DeepEqual(list, wantAccounts) { + // ks should have also received change notifications + select { + case <-ks.changes: + default: + return errors.New("wasn't notified of new accounts") + } + return nil + } + } + return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts) +} + +func TestWatchNewFile(t *testing.T) { + t.Parallel() + + dir, ks := tmpKeyStore(t) + + // Ensure the watcher is started before adding any files. + ks.Accounts() + if !waitWatcherStart(ks) { + t.Fatal("keystore watcher didn't start in time") + } + // Move in the files. + wantAccounts := make([]accounts.Account, len(cachetestAccounts)) + for i := range cachetestAccounts { + wantAccounts[i] = accounts.Account{ + Address: cachetestAccounts[i].Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, + } + if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { + t.Fatal(err) + } + } + + // ks should see the accounts. + if err := waitForAccounts(wantAccounts, ks); err != nil { + t.Error(err) + } +} + +func TestWatchNoDir(t *testing.T) { + t.Parallel() + // Create ks but not the directory that it watches. + dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watchnodir-test-%d-%d", os.Getpid(), rand.Int())) + ks := NewKeyStore(dir, LightScryptN, LightScryptP) + list := ks.Accounts() + if len(list) > 0 { + t.Error("initial account list not empty:", list) + } + // The watcher should start, and then exit. + if !waitWatcherStart(ks) { + t.Fatal("keystore watcher didn't start in time") + } + // Create the directory and copy a key file into it. + os.MkdirAll(dir, 0700) + defer os.RemoveAll(dir) + file := filepath.Join(dir, "aaa") + if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { + t.Fatal(err) + } + + // ks should see the account. + wantAccounts := []accounts.Account{cachetestAccounts[0]} + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { + list = ks.Accounts() + if reflect.DeepEqual(list, wantAccounts) { + // ks should have also received change notifications + select { + case <-ks.changes: + default: + t.Fatalf("wasn't notified of new accounts") + } + return + } + time.Sleep(d) + } + t.Errorf("\ngot %v\nwant %v", list, wantAccounts) +} + +func TestCacheInitialReload(t *testing.T) { + t.Parallel() + cache, _ := newAccountCache(cachetestDir) + accounts := cache.accounts() + if !reflect.DeepEqual(accounts, cachetestAccounts) { + t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) + } +} + +func TestCacheAddDeleteOrder(t *testing.T) { + t.Parallel() + cache, _ := newAccountCache("testdata/no-such-dir") + cache.watcher.running = true // prevent unexpected reloads + + accs := []accounts.Account{ + { + Address: types.StringToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, + }, + { + Address: types.StringToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, + }, + { + Address: types.StringToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, + }, + { + Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, + }, + { + Address: types.StringToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"}, + }, + { + Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, + }, + { + Address: types.StringToAddress("289d485d9771714cce91d3393d764e1311907acc"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, + }, + } + for _, a := range accs { + cache.add(a) + } + // Add some of them twice to check that they don't get reinserted. + cache.add(accs[0]) + cache.add(accs[2]) + + // Check that the account list is sorted by filename. + wantAccounts := make([]accounts.Account, len(accs)) + copy(wantAccounts, accs) + slices.SortFunc(wantAccounts, byURL) + list := cache.accounts() + if !reflect.DeepEqual(list, wantAccounts) { + t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) + } + for _, a := range accs { + if !cache.hasAddress(a.Address) { + t.Errorf("expected hasAccount(%x) to return true", a.Address) + } + } + if cache.hasAddress(types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { + t.Errorf("expected hasAccount(%x) to return false", types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) + } + + // Delete a few keys from the cache. + for i := 0; i < len(accs); i += 2 { + cache.delete(wantAccounts[i]) + } + cache.delete(accounts.Account{Address: types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) + + // Check content again after deletion. + wantAccountsAfterDelete := []accounts.Account{ + wantAccounts[1], + wantAccounts[3], + wantAccounts[5], + } + list = cache.accounts() + if !reflect.DeepEqual(list, wantAccountsAfterDelete) { + t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) + } + for _, a := range wantAccountsAfterDelete { + if !cache.hasAddress(a.Address) { + t.Errorf("expected hasAccount(%x) to return true", a.Address) + } + } + if cache.hasAddress(wantAccounts[0].Address) { + t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) + } +} + +func TestCacheFind(t *testing.T) { + t.Parallel() + dir := filepath.Join("testdata", "dir") + cache, _ := newAccountCache(dir) + cache.watcher.running = true // prevent unexpected reloads + + accs := []accounts.Account{ + { + Address: types.StringToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, + }, + { + Address: types.StringToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, + }, + { + Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, + }, + { + Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, + }, + } + for _, a := range accs { + cache.add(a) + } + + nomatchAccount := accounts.Account{ + Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, + } + tests := []struct { + Query accounts.Account + WantResult accounts.Account + WantError error + }{ + // by address + {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, + // by file + {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, + // by basename + {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]}, + // by file and address + {Query: accs[0], WantResult: accs[0]}, + // ambiguous address, tie resolved by file + {Query: accs[2], WantResult: accs[2]}, + // ambiguous address error + { + Query: accounts.Account{Address: accs[2].Address}, + WantError: &accounts.AmbiguousAddrError{ + Addr: accs[2].Address, + Matches: []accounts.Account{accs[2], accs[3]}, + }, + }, + // no match error + {Query: nomatchAccount, WantError: accounts.ErrNoMatch}, + {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: accounts.ErrNoMatch}, + {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: accounts.ErrNoMatch}, + {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: accounts.ErrNoMatch}, + } + for i, test := range tests { + a, err := cache.find(test.Query) + if !reflect.DeepEqual(err, test.WantError) { + t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) + continue + } + if a != test.WantResult { + t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) + continue + } + } +} + +// TestUpdatedKeyfileContents tests that updating the contents of a keystore file +// is noticed by the watcher, and the account cache is updated accordingly +func TestUpdatedKeyfileContents(t *testing.T) { + t.Parallel() + + // Create a temporary keystore to test with + dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int())) + ks := NewKeyStore(dir, LightScryptN, LightScryptP) + + list := ks.Accounts() + if len(list) > 0 { + t.Error("initial account list not empty:", list) + } + if !waitWatcherStart(ks) { + t.Fatal("keystore watcher didn't start in time") + } + // Create the directory and copy a key file into it. + os.MkdirAll(dir, 0700) + defer os.RemoveAll(dir) + file := filepath.Join(dir, "aaa") + + // Place one of our testfiles in there + if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { + t.Fatal(err) + } + + // ks should see the account. + wantAccounts := []accounts.Account{cachetestAccounts[0]} + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + if err := waitForAccounts(wantAccounts, ks); err != nil { + t.Error(err) + return + } + // needed so that modTime of `file` is different to its current value after forceCopyFile + os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) + + // Now replace file contents + if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil { + t.Fatal(err) + return + } + wantAccounts = []accounts.Account{cachetestAccounts[1]} + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + if err := waitForAccounts(wantAccounts, ks); err != nil { + t.Errorf("First replacement failed") + t.Error(err) + return + } + + // needed so that modTime of `file` is different to its current value after forceCopyFile + os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) + + // Now replace file contents again + if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil { + t.Fatal(err) + return + } + wantAccounts = []accounts.Account{cachetestAccounts[2]} + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + if err := waitForAccounts(wantAccounts, ks); err != nil { + t.Errorf("Second replacement failed") + t.Error(err) + return + } + + // needed so that modTime of `file` is different to its current value after os.WriteFile + os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) + + // Now replace file contents with crap + if err := os.WriteFile(file, []byte("foo"), 0600); err != nil { + t.Fatal(err) + return + } + if err := waitForAccounts([]accounts.Account{}, ks); err != nil { + t.Errorf("Emptying account file failed") + t.Error(err) + return + } +} + +// forceCopyFile is like cp.CopyFile, but doesn't complain if the destination exists. +func forceCopyFile(dst, src string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + return os.WriteFile(dst, data, 0644) +} diff --git a/accounts/keystore/file_cache.go b/accounts/keystore/file_cache.go new file mode 100644 index 0000000000..98f3f09b91 --- /dev/null +++ b/accounts/keystore/file_cache.go @@ -0,0 +1,77 @@ +package keystore + +import ( + "os" + "path/filepath" + "strings" + "sync" + "time" + + mapset "github.com/deckarep/golang-set/v2" +) + +// fileCache is a cache of files seen during scan of keystore. +type fileCache struct { + all mapset.Set[string] // Set of all files from the keystore folder + lastMod time.Time // Last time instance when a file was modified + mu sync.Mutex +} + +func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string], mapset.Set[string], error) { + // List all the files from the keystore folder + files, err := os.ReadDir(keyDir) + if err != nil { + return nil, nil, nil, err + } + + fc.mu.Lock() + defer fc.mu.Unlock() + + all := mapset.NewThreadUnsafeSet[string]() + mods := mapset.NewThreadUnsafeSet[string]() + + var newLastMod time.Time + for _, fi := range files { + if nonKeyFile(fi) { + continue + } + + path := filepath.Join(keyDir, fi.Name()) + + all.Add(path) + + info, err := fi.Info() + if err != nil { + return nil, nil, nil, err + } + + modified := info.ModTime() + if modified.After(fc.lastMod) { + mods.Add(path) + } + if modified.After(newLastMod) { + newLastMod = modified + } + } + + deletes := fc.all.Difference(all) + creates := all.Difference(fc.all) + updates := mods.Difference(creates) + + fc.all, fc.lastMod = all, newLastMod + + return creates, deletes, updates, nil +} + +// nonKeyFile ignores editor backups, hidden files and folders/symlinks. +func nonKeyFile(fi os.DirEntry) bool { + // Skip editor backups and UNIX-style hidden files. + if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") { + return true + } + // Skip misc special files, directories (yes, symlinks too). + if fi.IsDir() || !fi.Type().IsRegular() { + return true + } + return false +} diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go new file mode 100644 index 0000000000..bc81f2919d --- /dev/null +++ b/accounts/keystore/key.go @@ -0,0 +1,196 @@ +package keystore + +import ( + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/types" + "github.com/google/uuid" +) + +const ( + version = 3 +) + +type Key struct { + Id uuid.UUID + + Address types.Address + + PrivateKey *ecdsa.PrivateKey +} + +type keyStore interface { + // Loads and decrypts the key from disk. + GetKey(addr types.Address, filename string, auth string) (*Key, error) + // Writes and encrypts the key. + StoreKey(filename string, k *Key, auth string) error + // Joins filename with the key directory unless it is already absolute. + JoinPath(filename string) string +} + +type plainKeyJSON struct { + Address string `json:"address"` + PrivateKey string `json:"privatekey"` + Id string `json:"id"` + Version int `json:"version"` +} + +type encryptedKeyJSONV3 struct { + Address string `json:"address"` + Crypto CryptoJSON `json:"crypto"` + Id string `json:"id"` + Version int `json:"version"` +} + +type encryptedKeyJSONV1 struct { + Address string `json:"address"` + Crypto CryptoJSON `json:"crypto"` + Id string `json:"id"` + Version string `json:"version"` +} + +type CryptoJSON struct { + Cipher string `json:"cipher"` + CipherText string `json:"ciphertext"` + CipherParams cipherparamsJSON `json:"cipherparams"` + KDF string `json:"kdf"` + KDFParams map[string]interface{} `json:"kdfparams"` + MAC string `json:"mac"` +} + +type cipherparamsJSON struct { + IV string `json:"iv"` +} + +// TO DO marshall private key +func (k *Key) MarshalJSON() (j []byte, err error) { + privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) //get more time for this + if err != nil { + return nil, err + } + + jStruct := plainKeyJSON{ + hex.EncodeToString(k.Address[:]), + hex.EncodeToString(privKey), + k.Id.String(), + version, + } + j, err = json.Marshal(jStruct) + return j, err +} + +func (k *Key) UnmarshalJSON(j []byte) (err error) { + keyJSON := new(plainKeyJSON) + err = json.Unmarshal(j, &keyJSON) + if err != nil { + return err + } + + u := new(uuid.UUID) + *u, err = uuid.Parse(keyJSON.Id) + if err != nil { + return err + } + k.Id = *u + addr, err := hex.DecodeString(keyJSON.Address) + if err != nil { + return err + } + privkey, err := crypto.HexToECDSA(keyJSON.PrivateKey) + if err != nil { + return err + } + + k.Address = types.BytesToAddress(addr) + k.PrivateKey = privkey + + return nil +} + +// TO DO newKeyFromECDSA +func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { + id, err := uuid.NewRandom() + if err != nil { + panic(fmt.Sprintf("Could not create random uuid: %v", err)) + } + key := &Key{ + Id: id, + Address: crypto.PubKeyToAddress(&privateKeyECDSA.PublicKey), // get more time for this pointer + PrivateKey: privateKeyECDSA, + } + return key +} + +// keyFileName implements the naming convention for keyfiles: +// UTC---
+func keyFileName(keyAddr types.Address) string { + ts := time.Now().UTC() + return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) +} + +func toISO8601(t time.Time) string { + var tz string + name, offset := t.Zone() + if name == "UTC" { + tz = "Z" + } else { + tz = fmt.Sprintf("%03d00", offset/3600) + } + return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", + t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) +} + +func writeTemporaryKeyFile(file string, content []byte) (string, error) { + // Create the keystore directory with appropriate permissions + // in case it is not present yet. + const dirPerm = 0700 + if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil { + return "", err + } + // Atomic write: create a temporary hidden file first + // then move it into place. TempFile assigns mode 0600. + f, err := os.CreateTemp(filepath.Dir(file), "."+filepath.Base(file)+".tmp") + if err != nil { + return "", err + } + if _, err := f.Write(content); err != nil { + f.Close() + os.Remove(f.Name()) + return "", err + } + f.Close() + return f.Name(), nil +} + +func newKey(rand io.Reader) (*Key, error) { + privateKeyECDSA, err := crypto.GenerateECDSAPrivateKey() //TO DO maybe not valid + if err != nil { + return nil, err + } + return newKeyFromECDSA(privateKeyECDSA), nil +} + +func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) { + key, err := newKey(rand) + if err != nil { + return nil, accounts.Account{}, err + } + a := accounts.Account{ + Address: key.Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}, + } + if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { + zeroKey(key.PrivateKey) + return nil, a, err + } + return key, a, err +} diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go new file mode 100644 index 0000000000..4abfa8b02f --- /dev/null +++ b/accounts/keystore/keystore.go @@ -0,0 +1,427 @@ +package keystore + +import ( + "crypto/ecdsa" + crand "crypto/rand" + "errors" + "math/big" + "os" + "path/filepath" + "runtime" + "sync" + "time" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/types" +) + +var ( + ErrLocked = accounts.NewAuthNeededError("password or unlock") + ErrNoMatch = errors.New("no key for given address or file") + ErrDecrypt = errors.New("could not decrypt key with given password") + + // ErrAccountAlreadyExists is returned if an account attempted to import is + // already present in the keystore. + ErrAccountAlreadyExists = errors.New("account already exists") +) + +// Maximum time between wallet refreshes (if filesystem notifications don't work). +const walletRefreshCycle = 3 * time.Second + +// KeyStore manages a key storage directory on disk. +type KeyStore struct { + storage keyStore // Storage backend, might be cleartext or encrypted + cache *accountCache // In-memory account cache over the filesystem storage + changes chan struct{} // Channel receiving change notifications from the cache + unlocked map[types.Address]*unlocked // Currently unlocked account (decrypted private keys) + + wallets []accounts.Wallet // Wallet wrappers around the individual key files + updateFeed event.Feed // Event feed to notify wallet additions/removals + updateScope event.SubscriptionScope // Subscription scope tracking current live listeners + updating bool // Whether the event notification loop is running + + mu sync.RWMutex + importMu sync.Mutex // Import Mutex locks the import to prevent two insertions from racing +} + +type unlocked struct { + *Key + abort chan struct{} +} + +func NewKeyStore(keyDir string, scryptN, scryptP int) *KeyStore { + keyDir, _ = filepath.Abs(keyDir) + ks := &KeyStore{storage: &keyStorePassphrase{keyDir, scryptN, scryptP, false}} + ks.init(keyDir) + return ks +} + +func (ks *KeyStore) init(keyDir string) { + ks.mu.Lock() + defer ks.mu.Unlock() + + ks.unlocked = make(map[types.Address]*unlocked) + ks.cache, ks.changes = newAccountCache(keyDir) + + runtime.SetFinalizer(ks, func(m *KeyStore) { + m.cache.close() + }) + + accs := ks.cache.accounts() + ks.wallets = make([]accounts.Wallet, len(accs)) + for i := 0; i < len(accs); i++ { + ks.wallets[i] = &keyStoreWallet{account: accs[i], keyStore: ks} + } +} + +func (ks *KeyStore) Wallets() []accounts.Wallet { + // Make sure the list of wallets is in sync with the account cache + ks.refreshWallets() + + ks.mu.RLock() + defer ks.mu.RUnlock() + + cpy := make([]accounts.Wallet, len(ks.wallets)) + copy(cpy, ks.wallets) + return cpy +} + +// zeroKey zeroes a private key in memory. +func zeroKey(k *ecdsa.PrivateKey) { + b := k.D.Bits() + clear(b) +} + +func (ks *KeyStore) refreshWallets() { + ks.mu.Lock() + accs := ks.cache.accounts() + + var ( + wallets = make([]accounts.Wallet, 0, len(accs)) + events []accounts.WalletEvent + ) + + for _, account := range accs { + for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 { + events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Kind: accounts.WalletDropped}) + ks.wallets = ks.wallets[1:] + } + + if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 { + wallet := &keyStoreWallet{account: account, keyStore: ks} + + events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) + wallets = append(wallets, wallet) + continue + } + + if ks.wallets[0].Accounts()[0] == account { + wallets = append(wallets, ks.wallets[0]) + ks.wallets = ks.wallets[1:] + } + } + + for _, wallet := range ks.wallets { + events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped}) + } + + ks.wallets = wallets + ks.mu.Unlock() + + for _, event := range events { + ks.updateFeed.Send(event) + _ = event + } +} + +func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + ks.mu.Lock() + defer ks.mu.Unlock() + + sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink)) + + if !ks.updating { + ks.updating = true + go ks.updater() + } + + return sub +} + +func (ks *KeyStore) updater() { + for { + select { + case <-ks.changes: + case <-time.After(walletRefreshCycle): + } + + ks.refreshWallets() + + ks.mu.Lock() + if ks.updateScope.Count() == 0 { + ks.updating = false + ks.mu.Unlock() + return + } + ks.mu.Unlock() + } +} + +func (ks *KeyStore) HasAddress(addr types.Address) bool { + return ks.cache.hasAddress(addr) +} + +func (ks *KeyStore) Accounts() []accounts.Account { + return ks.cache.accounts() +} + +func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { + // Decrypting the key isn't really necessary, but we do + // it anyway to check the password and zero out the key + // immediately afterwards. + a, key, err := ks.getDecryptedKey(a, passphrase) + if key != nil { + zeroKey(key.PrivateKey) + } + if err != nil { + return err + } + // The order is crucial here. The key is dropped from the + // cache after the file is gone so that a reload happening in + // between won't insert it into the cache again. + err = os.Remove(a.URL.Path) + if err == nil { + ks.cache.delete(a) + ks.refreshWallets() + } + return err +} + +func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { + // Look up the key to sign with and abort if it cannot be found + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrLocked + } + // Sign the hash using plain ECDSA operations + return crypto.Sign(unlockedKey.PrivateKey, hash) +} + +func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, ok := ks.unlocked[a.Address] + if !ok { + return nil, ErrLocked + } + + signer := crypto.LatestSignerForChainID(chainID.Uint64()) + return signer.SignTx(tx, unlockedKey.PrivateKey) +} + +func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + + defer zeroKey(key.PrivateKey) + + return crypto.Sign(key.PrivateKey, hash) +} + +func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + + defer zeroKey(key.PrivateKey) + + signer := crypto.LatestSignerForChainID(chainID.Uint64()) + + return signer.SignTx(tx, key.PrivateKey) +} + +// Unlock unlocks the given account indefinitely. +func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error { + return ks.TimedUnlock(a, passphrase, 0) +} + +// Lock removes the private key with the given address from memory. +func (ks *KeyStore) Lock(addr types.Address) error { + ks.mu.Lock() + if unl, found := ks.unlocked[addr]; found { + ks.mu.Unlock() + ks.expire(addr, unl, time.Duration(0)*time.Nanosecond) + } else { + ks.mu.Unlock() + } + return nil +} + +func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + + ks.mu.Lock() + defer ks.mu.Unlock() + u, ok := ks.unlocked[a.Address] + if ok { + if u.abort == nil { + zeroKey(key.PrivateKey) + return nil + } + + close(u.abort) + } + + if timeout > 0 { + u = &unlocked{Key: key, abort: make(chan struct{})} + go ks.expire(a.Address, u, timeout) + } else { + u = &unlocked{Key: key} + } + + ks.unlocked[a.Address] = u + return nil +} + +func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) { + ks.cache.maybeReload() + ks.cache.mu.Lock() + a, err := ks.cache.find(a) + ks.cache.mu.Unlock() + return a, err +} + +func (ks *KeyStore) expire(addr types.Address, u *unlocked, timeout time.Duration) { + t := time.NewTimer(timeout) + defer t.Stop() + select { + case <-u.abort: + // just quit + case <-t.C: + ks.mu.Lock() + // only drop if it's still the same key instance that dropLater + // was launched with. we can check that using pointer equality + // because the map stores a new pointer every time the key is + // unlocked. + if ks.unlocked[addr] == u { + zeroKey(u.PrivateKey) + delete(ks.unlocked, addr) + } + ks.mu.Unlock() + } +} + +func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { + a, err := ks.Find(a) + if err != nil { + return a, nil, err + } + key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth) + return a, key, err +} + +func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { + _, account, err := storeNewKey(ks.storage, crand.Reader, passphrase) + + if err != nil { + return accounts.Account{}, err + } + + ks.cache.add(account) + ks.refreshWallets() + return account, nil +} + +func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + var N, P int + if store, ok := ks.storage.(*keyStorePassphrase); ok { + N, P = store.scryptN, store.scryptP + } else { + N, P = StandardScryptN, StandardScryptP + } + return EncryptKey(key, newPassphrase, N, P) +} + +func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) { + key, err := DecryptKey(keyJSON, passphrase) + if key != nil && key.PrivateKey != nil { + defer zeroKey(key.PrivateKey) + } + if err != nil { + return accounts.Account{}, err + } + ks.importMu.Lock() + defer ks.importMu.Unlock() + + if ks.cache.hasAddress(key.Address) { + return accounts.Account{ + Address: key.Address, + }, ErrAccountAlreadyExists + } + + return ks.importKey(key, newPassphrase) +} + +func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) { + ks.importMu.Lock() + defer ks.importMu.Unlock() + + key := newKeyFromECDSA(priv) + if ks.cache.hasAddress(key.Address) { + return accounts.Account{ + Address: key.Address, + }, ErrAccountAlreadyExists + } + return ks.importKey(key, passphrase) +} + +func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} + if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { + return accounts.Account{}, err + } + ks.cache.add(a) + ks.refreshWallets() + return a, nil +} + +func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + + return ks.storage.StoreKey(a.URL.Path, key, newPassphrase) +} + +func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { + a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase) + if err != nil { + return a, err + } + ks.cache.add(a) + ks.refreshWallets() + return a, nil +} + +func (ks *KeyStore) isUpdating() bool { + ks.mu.RLock() + defer ks.mu.RUnlock() + return ks.updating +} diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go new file mode 100644 index 0000000000..30c8433c1e --- /dev/null +++ b/accounts/keystore/keystore_test.go @@ -0,0 +1,448 @@ +package keystore + +import ( + "math/rand" + "os" + "runtime" + "slices" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/helper/common" + "github.com/0xPolygon/polygon-edge/types" +) + +var testSigData = make([]byte, 32) + +func TestKeyStore(t *testing.T) { + t.Parallel() + dir, ks := tmpKeyStore(t) + + 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.FileExists(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) { + t.Parallel() + _, ks := tmpKeyStore(t) + + 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) { + t.Parallel() + _, ks := tmpKeyStore(t) + + 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) { + t.Parallel() + _, ks := tmpKeyStore(t) + + 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) { + t.Parallel() + _, ks := tmpKeyStore(t) + + 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) { + t.Parallel() + _, ks := tmpKeyStore(t) + + // 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") +} + +// waitForKsUpdating waits until the updating-status of the ks reaches the +// desired wantStatus. +// It waits for a maximum time of maxTime, and returns false if it does not +// finish in time +func waitForKsUpdating(t *testing.T, ks *KeyStore, wantStatus bool, maxTime time.Duration) bool { + t.Helper() + // Wait max 250 ms, then return false + for t0 := time.Now(); time.Since(t0) < maxTime; { + if ks.isUpdating() == wantStatus { + return true + } + time.Sleep(25 * time.Millisecond) + } + return false +} + +// 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) { + t.Parallel() + // Create a temporary keystore to test with + _, ks := tmpKeyStore(t) + + // Ensure that the notification updater is not running yet + time.Sleep(250 * time.Millisecond) + + if ks.isUpdating() { + 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) + if !waitForKsUpdating(t, ks, true, 250*time.Millisecond) { + t.Errorf("sub %d: wallet notifier not running after subscription", i) + } + } + // Close all but one sub + for i := 0; i < len(subs)-1; i++ { + // Close an existing subscription + subs[i].Unsubscribe() + } + // Check that it is still running + time.Sleep(250 * time.Millisecond) + + if !ks.isUpdating() { + t.Fatal("event notifier stopped prematurely") + } + // Unsubscribe the last one and ensure the updater terminates eventually. + subs[len(subs)-1].Unsubscribe() + if !waitForKsUpdating(t, ks, false, 4*time.Second) { + 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) { + t.Parallel() + _, ks := tmpKeyStore(t) + + // 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[types.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) +} + +// TestImportECDSA tests the import functionality of a keystore. +func TestImportECDSA(t *testing.T) { + t.Parallel() + _, ks := tmpKeyStore(t) + key, err := crypto.GenerateECDSAPrivateKey() + 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") + } +} + +// TestImportExport tests the import and export functionality of a keystore. +func TestImportExport(t *testing.T) { + t.Parallel() + _, ks := tmpKeyStore(t) + 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) + 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) { + t.Parallel() + _, ks := tmpKeyStore(t) + 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) + var atom atomic.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 { + atom.Add(1) + } + }() + } + wg.Wait() + if atom.Load() != 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[types.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) + } + slices.SortFunc(liveList, byURL) + 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) (string, *KeyStore) { + d := t.TempDir() + return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP) +} diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go new file mode 100644 index 0000000000..e8543de6db --- /dev/null +++ b/accounts/keystore/passphrase.go @@ -0,0 +1,342 @@ +package keystore + +import ( + "bytes" + "crypto/aes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/types" + "github.com/google/uuid" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" +) + +const ( + keyHeaderKDF = "scrypt" + + // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptN = 1 << 18 + + // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptP = 1 + + // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptN = 1 << 12 + + // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptP = 6 + + scryptR = 8 + scryptDKLen = 32 +) + +type keyStorePassphrase struct { + keysDirPath string + scryptN int + scryptP int + // skipKeyFileVerification disables the security-feature which does + // reads and decrypts any newly created keyfiles. This should be 'false' in all + // cases except tests -- setting this to 'true' is not recommended. + skipKeyFileVerification bool +} + +func (ks keyStorePassphrase) GetKey(addr types.Address, filename, auth string) (*Key, error) { + keyJSON, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + key, err := DecryptKey(keyJSON, auth) + if err != nil { + return nil, err + } + + if key.Address != addr { + return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, addr) + } + + return key, nil +} + +// StoreKey generates a key, encrypts with 'auth' and stores in the given directory +func StoreKey(dir, auth string, scryptN, scryptP int) (accounts.Account, error) { + _, a, err := storeNewKey(&keyStorePassphrase{dir, scryptN, scryptP, false}, rand.Reader, auth) + return a, err +} + +func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error { + keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) + if err != nil { + return err + } + // Write into temporary file + tmpName, err := writeTemporaryKeyFile(filename, keyjson) + if err != nil { + return err + } + if !ks.skipKeyFileVerification { + // Verify that we can decrypt the file with the given password. + _, err = ks.GetKey(key.Address, tmpName, auth) + if err != nil { + msg := "An error was encountered when saving and verifying the keystore file. \n" + + "This indicates that the keystore is corrupted. \n" + + "The corrupted file is stored at \n%v\n" + + "Please file a ticket at:\n\n" + + "https://github.com/ethereum/go-ethereum/issues." + + "The error was : %s" + //lint:ignore ST1005 This is a message for the user + return fmt.Errorf(msg, tmpName, err) + } + } + return os.Rename(tmpName, filename) +} + +func (ks keyStorePassphrase) JoinPath(filename string) string { + if filepath.IsAbs(filename) { + return filename + } + return filepath.Join(ks.keysDirPath, filename) +} + +// EncryptDataV3 encrypts the data given as 'data' with the password 'auth'. +func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { + salt := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + panic("reading from crypto/rand failed: " + err.Error()) + } + derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return CryptoJSON{}, err + } + encryptKey := derivedKey[:16] + + iv := make([]byte, aes.BlockSize) // 16 + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + panic("reading from crypto/rand failed: " + err.Error()) + } + cipherText, err := aesCTRXOR(encryptKey, data, iv) + if err != nil { + return CryptoJSON{}, err + } + mac := crypto.Keccak256(derivedKey[16:32], cipherText) + + scryptParamsJSON := make(map[string]interface{}, 5) + scryptParamsJSON["n"] = scryptN + scryptParamsJSON["r"] = scryptR + scryptParamsJSON["p"] = scryptP + scryptParamsJSON["dklen"] = scryptDKLen + scryptParamsJSON["salt"] = hex.EncodeToString(salt) + cipherParamsJSON := cipherparamsJSON{ + IV: hex.EncodeToString(iv), + } + + cryptoStruct := CryptoJSON{ + Cipher: "aes-128-ctr", + CipherText: hex.EncodeToString(cipherText), + CipherParams: cipherParamsJSON, + KDF: keyHeaderKDF, + KDFParams: scryptParamsJSON, + MAC: hex.EncodeToString(mac), + } + return cryptoStruct, nil +} + +// EncryptKey encrypts a key using the specified scrypt parameters into a json +// blob that can be decrypted later on. +func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { + keyBytes, err := crypto.MarshalECDSAPrivateKey(key.PrivateKey) //TO DO maybe wrong + cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) + if err != nil { + return nil, err + } + encryptedKeyJSONV3 := encryptedKeyJSONV3{ + hex.EncodeToString(key.Address[:]), + cryptoStruct, + key.Id.String(), + version, + } + return json.Marshal(encryptedKeyJSONV3) +} + +// DecryptKey decrypts a key from a json blob, returning the private key itself. +func DecryptKey(keyjson []byte, auth string) (*Key, error) { + // Parse the json into a simple map to fetch the key version + m := make(map[string]interface{}) + + if err := json.Unmarshal(keyjson, &m); err != nil { + return nil, err + } + + // Depending on the version try to parse one way or another + var ( + keyBytes, keyId []byte + err error + ) + if version, ok := m["version"].(string); ok && version == "1" { + k := new(encryptedKeyJSONV1) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV1(k, auth) + } else { + k := new(encryptedKeyJSONV3) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV3(k, auth) + } + // Handle any decryption errors and return the key + if err != nil { + return nil, err + } + key, err := crypto.DToECDSA(keyBytes, true) //TO DO maybe wrong + if err != nil { + return nil, fmt.Errorf("invalid key: %w", err) + } + id, err := uuid.FromBytes(keyId) + if err != nil { + return nil, fmt.Errorf("invalid UUID: %w", err) + } + return &Key{ + Id: id, + Address: crypto.PubKeyToAddress(&key.PublicKey), + PrivateKey: key, + }, nil +} + +func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { + if cryptoJson.Cipher != "aes-128-ctr" { + return nil, fmt.Errorf("cipher not supported: %v", cryptoJson.Cipher) + } + mac, err := hex.DecodeString(cryptoJson.MAC) + if err != nil { + return nil, err + } + + iv, err := hex.DecodeString(cryptoJson.CipherParams.IV) + if err != nil { + return nil, err + } + + cipherText, err := hex.DecodeString(cryptoJson.CipherText) + if err != nil { + return nil, err + } + + derivedKey, err := getKDFKey(cryptoJson, auth) + if err != nil { + return nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, accounts.ErrDecrypt + } + + plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, err + } + return plainText, err +} + +func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) { + if keyProtected.Version != version { + return nil, nil, fmt.Errorf("version not supported: %v", keyProtected.Version) + } + keyUUID, err := uuid.Parse(keyProtected.Id) + if err != nil { + return nil, nil, err + } + keyId = keyUUID[:] + plainText, err := DecryptDataV3(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { + keyUUID, err := uuid.Parse(keyProtected.Id) + if err != nil { + return nil, nil, err + } + keyId = keyUUID[:] + mac, err := hex.DecodeString(keyProtected.Crypto.MAC) + if err != nil { + return nil, nil, err + } + + iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) + if err != nil { + return nil, nil, err + } + + cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) + if err != nil { + return nil, nil, err + } + + derivedKey, err := getKDFKey(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, nil, accounts.ErrDecrypt + } + + plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { + authArray := []byte(auth) + salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) + if err != nil { + return nil, err + } + dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) + + if cryptoJSON.KDF == keyHeaderKDF { + n := ensureInt(cryptoJSON.KDFParams["n"]) + r := ensureInt(cryptoJSON.KDFParams["r"]) + p := ensureInt(cryptoJSON.KDFParams["p"]) + return scrypt.Key(authArray, salt, n, r, p, dkLen) + } else if cryptoJSON.KDF == "pbkdf2" { + c := ensureInt(cryptoJSON.KDFParams["c"]) + prf := cryptoJSON.KDFParams["prf"].(string) + if prf != "hmac-sha256" { + return nil, fmt.Errorf("unsupported PBKDF2 PRF: %s", prf) + } + key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) + return key, nil + } + + return nil, fmt.Errorf("unsupported KDF: %s", cryptoJSON.KDF) +} + +func ensureInt(x interface{}) int { + res, ok := x.(int) + if !ok { + res = int(x.(float64)) + } + return res +} diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go new file mode 100644 index 0000000000..9dadfcf67e --- /dev/null +++ b/accounts/keystore/passphrase_test.go @@ -0,0 +1,45 @@ +package keystore + +import ( + "os" + "testing" + + "github.com/0xPolygon/polygon-edge/types" +) + +const ( + veryLightScryptN = 2 + veryLightScryptP = 1 +) + +// Tests that a json key file can be decrypted and encrypted in multiple rounds. +func TestKeyEncryptDecrypt(t *testing.T) { + t.Parallel() + keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") + if err != nil { + t.Fatal(err) + } + password := "" + address := types.StringToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") + + // Do a few rounds of decryption and encryption + for i := 0; i < 3; i++ { + // Try a bad password first + if _, err := DecryptKey(keyjson, password+"bad"); err == nil { + t.Errorf("test %d: json key decrypted with bad password", i) + } + // Decrypt with the correct password + key, err := DecryptKey(keyjson, password) + if err != nil { + t.Fatalf("test %d: json key failed to decrypt: %v", i, err) + } + if key.Address != address { + t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address) + } + // Recrypt with a new password and start over + password += "new data appended" // nolint: gosec + if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { + t.Errorf("test %d: failed to re-encrypt key %v", i, err) + } + } +} diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go new file mode 100644 index 0000000000..ed932d980c --- /dev/null +++ b/accounts/keystore/presale.go @@ -0,0 +1,131 @@ +package keystore + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/google/uuid" + "golang.org/x/crypto/pbkdf2" +) + +func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, *Key, error) { + key, err := decryptPreSaleKey(keyJSON, password) + if err != nil { + return accounts.Account{}, nil, err + } + key.Id, err = uuid.NewRandom() + if err != nil { + return accounts.Account{}, nil, err + } + a := accounts.Account{ + Address: key.Address, + URL: accounts.URL{ + Scheme: KeyStoreScheme, + Path: keyStore.JoinPath(keyFileName(key.Address)), + }, + } + err = keyStore.StoreKey(a.URL.Path, key, password) + return a, key, err +} + +func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error) { + preSaleKeyStruct := struct { + EncSeed string + EthAddr string + Email string + BtcAddr string + }{} + err = json.Unmarshal(fileContent, &preSaleKeyStruct) + if err != nil { + return nil, err + } + encSeedBytes, err := hex.DecodeString(preSaleKeyStruct.EncSeed) + if err != nil { + return nil, errors.New("invalid hex in encSeed") + } + + if len(encSeedBytes) < 16 { + return nil, errors.New("invalid encSeed, too short") + } + iv := encSeedBytes[:16] + cipherText := encSeedBytes[16:] + passBytes := []byte(password) + derivedKey := pbkdf2.Key(passBytes, passBytes, 2000, 16, sha256.New) + plainText, err := aesCBCDecrypt(derivedKey, cipherText, iv) + + if err != nil { + return nil, err + } + ethPriv := crypto.Keccak256(plainText) + ecKey, err := crypto.BytesToECDSAPrivateKey(ethPriv) + if err != nil { + return nil, err + } + + key = &Key{ + Id: uuid.UUID{}, + Address: crypto.PubKeyToAddress(&ecKey.PublicKey), + PrivateKey: ecKey, + } + derivedAddr := hex.EncodeToString(key.Address.Bytes()) // needed because .Hex() gives leading "0x" + expectedAddr := preSaleKeyStruct.EthAddr + if derivedAddr != expectedAddr { + err = fmt.Errorf("decrypted addr '%s' not equal to expected addr '%s'", derivedAddr, expectedAddr) + } + return key, err +} + +func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { + // AES-128 is selected due to size of encryptKey. + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + stream := cipher.NewCTR(aesBlock, iv) + outText := make([]byte, len(inText)) + stream.XORKeyStream(outText, inText) + return outText, err +} + +func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + decrypter := cipher.NewCBCDecrypter(aesBlock, iv) + paddedPlaintext := make([]byte, len(cipherText)) + decrypter.CryptBlocks(paddedPlaintext, cipherText) + plaintext := pkcs7Unpad(paddedPlaintext) + if plaintext == nil { + return nil, accounts.ErrDecrypt + } + return plaintext, err +} + +// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes +func pkcs7Unpad(in []byte) []byte { + if len(in) == 0 { + return nil + } + + padding := in[len(in)-1] + if int(padding) > len(in) || padding > aes.BlockSize { + return nil + } else if padding == 0 { + return nil + } + + for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { + if in[i] != padding { + return nil + } + } + return in[:len(in)-int(padding)] +} diff --git a/accounts/keystore/testdata/dupes/1 b/accounts/keystore/testdata/dupes/1 new file mode 100644 index 0000000000..a3868ec6d5 --- /dev/null +++ b/accounts/keystore/testdata/dupes/1 @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/dupes/2 b/accounts/keystore/testdata/dupes/2 new file mode 100644 index 0000000000..a3868ec6d5 --- /dev/null +++ b/accounts/keystore/testdata/dupes/2 @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/dupes/foo b/accounts/keystore/testdata/dupes/foo new file mode 100644 index 0000000000..c57060aea0 --- /dev/null +++ b/accounts/keystore/testdata/dupes/foo @@ -0,0 +1 @@ +{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/.hiddenfile b/accounts/keystore/testdata/keystore/.hiddenfile new file mode 100644 index 0000000000..d91faccdeb --- /dev/null +++ b/accounts/keystore/testdata/keystore/.hiddenfile @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} diff --git a/accounts/keystore/testdata/keystore/README b/accounts/keystore/testdata/keystore/README new file mode 100644 index 0000000000..6af9ac3f1b --- /dev/null +++ b/accounts/keystore/testdata/keystore/README @@ -0,0 +1,21 @@ +This directory contains accounts for testing. +The password that unlocks them is "foobar". + +The "good" key files which are supposed to be loadable are: + +- File: UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 + Address: 0x7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +- File: aaa + Address: 0xf466859ead1932d743d622cb74fc058882e8648a +- File: zzz + Address: 0x289d485d9771714cce91d3393d764e1311907acc + +The other files (including this README) are broken in various ways +and should not be picked up by package accounts: + +- File: no-address (missing address field, otherwise same as "aaa") +- File: garbage (file with random data) +- File: empty (file with no content) +- File: swapfile~ (should be skipped) +- File: .hiddenfile (should be skipped) +- File: foo/... (should be skipped because it is a directory) diff --git a/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 new file mode 100644 index 0000000000..c57060aea0 --- /dev/null +++ b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 @@ -0,0 +1 @@ +{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/aaa b/accounts/keystore/testdata/keystore/aaa new file mode 100644 index 0000000000..a3868ec6d5 --- /dev/null +++ b/accounts/keystore/testdata/keystore/aaa @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/empty b/accounts/keystore/testdata/keystore/empty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e new file mode 100644 index 0000000000..309841e524 --- /dev/null +++ b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e @@ -0,0 +1 @@ +{"address":"fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e","crypto":{"cipher":"aes-128-ctr","ciphertext":"8124d5134aa4a927c79fd852989e4b5419397566f04b0936a1eb1d168c7c68a5","cipherparams":{"iv":"e2febe17176414dd2cda28287947eb2f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"44b415ede89f3bdd6830390a21b78965f571b347a589d1d943029f016c5e8bd5"},"mac":"5e149ff25bfd9dd45746a84bb2bcd2f015f2cbca2b6d25c5de8c29617f71fe5b"},"id":"d6ac5452-2b2c-4d3c-ad80-4bf0327d971c","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/garbage b/accounts/keystore/testdata/keystore/garbage new file mode 100644 index 0000000000000000000000000000000000000000..ff45091e714078dd7d3b4ea95964452e33a895f7 GIT binary patch literal 300 zcmV+{0n`3r1xkOa0KiH=0-y31ays31&4D+~b{#6-MH z)8?iosg+26q81!5ujp29iM}4_d}^;*-$8$htAbEpk(KDl*$;NvD$v8GZL@TRuT#)+ zq*|PXNljY5_xwCfoMayTjJ(vY;=t!uVJT5-Fn0O7W{#e;Ho?+NsQQi=!GV>j#9U#& zAbp7L1M-8N-V+7}EDxG9CNuhKbj?($B?=E1a1Xi%v;bYvR+C$EjApbg!W^>zB$Cd( z+NKd!El}@p)NJLnQ}B=D#e5uCh87_~lKd2z=idP7$= 0 { + return nil, errors.New("invalid private key, >= N") + } + + if priv.D.Sign() <= 0 { + return nil, errors.New("invalid private key, zero or negative") + } + + priv.PublicKey.X, priv.PublicKey.Y = btcec.S256().ScalarBaseMult(D) + if priv.PublicKey.X == nil { + return nil, errors.New("invalid private key") + } + + return priv, nil + +} diff --git a/go.mod b/go.mod index e2fd41e89a..cbbd9c5124 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,8 @@ require ( pgregory.net/rapid v1.1.0 ) +require github.com/fsnotify/fsnotify v1.7.0 // indirect + require ( cloud.google.com/go/auth v0.4.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect @@ -80,6 +82,7 @@ require ( github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect + github.com/cespare/cp v1.1.1 github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/continuity v0.3.0 // indirect @@ -87,6 +90,7 @@ require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/deckarep/golang-set/v2 v2.6.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect diff --git a/go.sum b/go.sum index 224f624a71..b440d4309f 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4r github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= +github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -130,6 +132,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= @@ -177,6 +181,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= From 020ea482671cd53b4a47febd758b96ca906983ba Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 02/69] added manager --- accounts/accounts.go | 19 ++ accounts/accounts_test.go | 17 ++ accounts/hd.go | 6 +- accounts/hd_test.go | 102 ++++++++++ accounts/helper.go | 30 +++ accounts/keystore/account_cache.go | 23 ++- accounts/keystore/account_cache_test.go | 16 +- accounts/keystore/key.go | 31 ++- accounts/keystore/keystore.go | 9 +- accounts/keystore/keystore_fuzz_test.go | 20 ++ accounts/keystore/keystore_test.go | 3 +- accounts/keystore/plain.go | 52 +++++ accounts/keystore/plain_test.go | 251 ++++++++++++++++++++++++ accounts/keystore/presale.go | 2 +- accounts/manager.go | 224 +++++++++++++++++++++ accounts/scwallet/wallet.go | 1 - accounts/{URL.go => url.go} | 0 accounts/url_test.go | 84 ++++++++ accounts/usbwallet/wallet.go | 1 - 19 files changed, 865 insertions(+), 26 deletions(-) create mode 100644 accounts/accounts_test.go create mode 100644 accounts/hd_test.go create mode 100644 accounts/keystore/keystore_fuzz_test.go create mode 100644 accounts/keystore/plain.go create mode 100644 accounts/keystore/plain_test.go create mode 100644 accounts/manager.go delete mode 100644 accounts/scwallet/wallet.go rename accounts/{URL.go => url.go} (100%) create mode 100644 accounts/url_test.go delete mode 100644 accounts/usbwallet/wallet.go diff --git a/accounts/accounts.go b/accounts/accounts.go index 8e09d5b8d3..2726c5cdaa 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" + "github.com/0xPolygon/polygon-edge/accounts/event" "github.com/0xPolygon/polygon-edge/state" "github.com/0xPolygon/polygon-edge/types" "golang.org/x/crypto/sha3" @@ -159,3 +160,21 @@ type WalletEvent struct { Wallet Wallet // Wallet instance arrived or departed Kind WalletEventType // Event type that happened in the system } + +type Backend interface { + // Wallets retrieves the list of wallets the backend is currently aware of. + // + // The returned wallets are not opened by default. For software HD wallets this + // means that no base seeds are decrypted, and for hardware wallets that no actual + // connection is established. + // + // The resulting wallet list will be sorted alphabetically based on its internal + // URL assigned by the backend. Since wallets (especially hardware) may come and + // go, the same wallet might appear at a different positions in the list during + // subsequent retrievals. + Wallets() []Wallet + + // Subscribe creates an async subscription to receive notifications when the + // backend detects the arrival or departure of a wallet. + Subscribe(sink chan<- WalletEvent) event.Subscription +} diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go new file mode 100644 index 0000000000..df617e287f --- /dev/null +++ b/accounts/accounts_test.go @@ -0,0 +1,17 @@ +package accounts + +import ( + "bytes" + "testing" + + "github.com/0xPolygon/polygon-edge/types" +) + +func TestTextHash(t *testing.T) { + t.Parallel() + hash := TextHash([]byte("Hello Joe")) + want := types.StringToBytes("0xa080337ae51c4e064c189e113edd0ba391df9206e2f49db658bb32cf2911730b") + if !bytes.Equal(hash, want) { + t.Fatalf("wrong hash: %x", hash) + } +} diff --git a/accounts/hd.go b/accounts/hd.go index 09026825bc..fe167a9fc9 100644 --- a/accounts/hd.go +++ b/accounts/hd.go @@ -55,7 +55,11 @@ func ParseDerivationPath(path string) (DerivationPath, error) { case strings.TrimSpace(components[0]) == "m": components = components[1:] default: - result = append(result, DefaultBaseDerivationPath...) + result = append(result, DefaultRootDerivationPath...) + } + + if len(components) == 0 { + return nil, errors.New("empty derivation path") } for _, component := range components { diff --git a/accounts/hd_test.go b/accounts/hd_test.go new file mode 100644 index 0000000000..a5ff0f719e --- /dev/null +++ b/accounts/hd_test.go @@ -0,0 +1,102 @@ +package accounts + +import ( + "fmt" + "reflect" + "testing" +) + +func TestHDPathParsing(t *testing.T) { + t.Parallel() + tests := []struct { + input string + output DerivationPath + }{ + // Plain absolute derivation paths + {"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, + {"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + {"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, + {"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + + // Plain relative derivation paths + {"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, + {"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, + {"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, + {"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, + {"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, + + // Hexadecimal absolute derivation paths + {"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, + {"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + {"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, + {"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + + // Hexadecimal relative derivation paths + {"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, + {"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, + {"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, + {"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, + {"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, + + // Weird inputs just to ensure they work + {" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + + // Invalid derivation paths + {"", nil}, // Empty relative derivation path + {"m", nil}, // Empty absolute derivation path + {"m/", nil}, // Missing last derivation component + {"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error + {"m/2147483648'", nil}, // Overflows 32 bit integer + {"m/-1'", nil}, // Cannot contain negative number + } + for i, tt := range tests { + if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) { + t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output) + } else if path == nil && err == nil { + t.Errorf("test %d: nil path and error: %v", i, err) + } + } +} + +func testDerive(t *testing.T, next func() DerivationPath, expected []string) { + t.Helper() + for i, want := range expected { + if have := next(); fmt.Sprintf("%v", have) != want { + t.Errorf("step %d, have %v, want %v", i, have, want) + } + } +} + +func TestHdPathIteration(t *testing.T) { + t.Parallel() + testDerive(t, DefaultIterator(DefaultBaseDerivationPath), + []string{ + "m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1", + "m/44'/60'/0'/0/2", "m/44'/60'/0'/0/3", + "m/44'/60'/0'/0/4", "m/44'/60'/0'/0/5", + "m/44'/60'/0'/0/6", "m/44'/60'/0'/0/7", + "m/44'/60'/0'/0/8", "m/44'/60'/0'/0/9", + }) + + testDerive(t, DefaultIterator(LegacyLedgerBaseDerivationPath), + []string{ + "m/44'/60'/0'/0", "m/44'/60'/0'/1", + "m/44'/60'/0'/2", "m/44'/60'/0'/3", + "m/44'/60'/0'/4", "m/44'/60'/0'/5", + "m/44'/60'/0'/6", "m/44'/60'/0'/7", + "m/44'/60'/0'/8", "m/44'/60'/0'/9", + }) + + testDerive(t, LedgerLiveIterator(DefaultBaseDerivationPath), + []string{ + "m/44'/60'/0'/0/0", "m/44'/60'/1'/0/0", + "m/44'/60'/2'/0/0", "m/44'/60'/3'/0/0", + "m/44'/60'/4'/0/0", "m/44'/60'/5'/0/0", + "m/44'/60'/6'/0/0", "m/44'/60'/7'/0/0", + "m/44'/60'/8'/0/0", "m/44'/60'/9'/0/0", + }) +} diff --git a/accounts/helper.go b/accounts/helper.go index 703aed4a54..050d868c8a 100644 --- a/accounts/helper.go +++ b/accounts/helper.go @@ -1,8 +1,10 @@ package accounts import ( + "encoding/json" "errors" "fmt" + "os" "github.com/0xPolygon/polygon-edge/types" ) @@ -57,3 +59,31 @@ func (err *AmbiguousAddrError) Error() string { } return fmt.Sprintf("multiple keys match address (%s)", files) } + +func LoadJSON(file string, val interface{}) error { + content, err := os.ReadFile(file) + if err != nil { + return err + } + if err := json.Unmarshal(content, val); err != nil { + if syntaxerr, ok := err.(*json.SyntaxError); ok { + line := findLine(content, syntaxerr.Offset) + return fmt.Errorf("JSON syntax error at %v:%v: %v", file, line, err) + } + return fmt.Errorf("JSON unmarshal error in %v: %v", file, err) + } + return nil +} + +func findLine(data []byte, offset int64) (line int) { + line = 1 + for i, r := range string(data) { + if int64(i) >= offset { + return + } + if r == '\n' { + line++ + } + } + return +} diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index eee29c3707..0f3122eefd 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -14,6 +14,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/types" mapset "github.com/deckarep/golang-set/v2" + "github.com/hashicorp/go-hclog" ) func byURL(a, b accounts.Account) int { @@ -26,6 +27,7 @@ const minReloadInterval = 2 * time.Second // accountCache is a live index of all accounts in the keystore. type accountCache struct { + logger hclog.Logger keydir string watcher *watcher mu sync.Mutex @@ -36,8 +38,9 @@ type accountCache struct { fileC fileCache } -func newAccountCache(keyDir string) (*accountCache, chan struct{}) { +func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan struct{}) { ac := &accountCache{ + logger: logger, keydir: keyDir, byAddr: make(map[types.Address][]accounts.Account), notify: make(chan struct{}, 1), @@ -183,7 +186,9 @@ func (ac *accountCache) maybeReload() { ac.watcher.start() ac.throttle.Reset(minReloadInterval) ac.mu.Unlock() - ac.scanAccounts() + if err := ac.scanAccounts(); err != nil { + ac.logger.Info("reload", "failed to scan accounts", err) + } } func (ac *accountCache) close() { ac.mu.Lock() @@ -204,7 +209,7 @@ func (ac *accountCache) scanAccounts() error { // Scan the entire folder metadata for file changes creates, deletes, updates, err := ac.fileC.scan(ac.keydir) if err != nil { - //TO DO log.Debug("Failed to reload keystore contents", "err", err) + ac.logger.Debug("Failed to reload keystore contents", "err", err) return err } if creates.Cardinality() == 0 && deletes.Cardinality() == 0 && updates.Cardinality() == 0 { @@ -220,7 +225,7 @@ func (ac *accountCache) scanAccounts() error { readAccount := func(path string) *accounts.Account { fd, err := os.Open(path) if err != nil { - //TO DO log.Trace("Failed to open keystore file", "path", path, "err", err) + ac.logger.Trace("Failed to open keystore file", "path", path, "err", err) return nil } defer fd.Close() @@ -231,9 +236,9 @@ func (ac *accountCache) scanAccounts() error { addr := types.StringToAddress(key.Address) switch { case err != nil: - //TO DO log.Debug("Failed to decode keystore key", "path", path, "err", err) + ac.logger.Debug("Failed to decode keystore key", "path", path, "err", err) case addr == types.Address{}: - //TO DO log.Debug("Failed to decode keystore key", "path", path, "err", "missing or zero address") + ac.logger.Debug("Failed to decode keystore key", "path", path, "err", "missing or zero address") default: return &accounts.Account{ Address: addr, @@ -243,7 +248,7 @@ func (ac *accountCache) scanAccounts() error { return nil } // Process all the file diffs - //start := time.Now() + start := time.Now() for _, path := range creates.ToSlice() { if a := readAccount(path); a != nil { @@ -259,12 +264,12 @@ func (ac *accountCache) scanAccounts() error { ac.add(*a) } } - //end := time.Now() + end := time.Now() select { case ac.notify <- struct{}{}: default: } - //TO DO log.Trace("Handled keystore changes", "time", end.Sub(start)) + ac.logger.Trace("Handled keystore changes", "time", end.Sub(start)) return nil } diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index a9d47d3fcf..737c1ec483 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -15,6 +15,8 @@ import ( "github.com/0xPolygon/polygon-edge/types" "github.com/cespare/cp" "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" ) var ( @@ -99,7 +101,7 @@ func TestWatchNoDir(t *testing.T) { t.Parallel() // Create ks but not the directory that it watches. dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watchnodir-test-%d-%d", os.Getpid(), rand.Int())) - ks := NewKeyStore(dir, LightScryptN, LightScryptP) + ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) list := ks.Accounts() if len(list) > 0 { t.Error("initial account list not empty:", list) @@ -109,7 +111,7 @@ func TestWatchNoDir(t *testing.T) { t.Fatal("keystore watcher didn't start in time") } // Create the directory and copy a key file into it. - os.MkdirAll(dir, 0700) + require.NoError(t, os.MkdirAll(dir, 0700)) defer os.RemoveAll(dir) file := filepath.Join(dir, "aaa") if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { @@ -137,7 +139,7 @@ func TestWatchNoDir(t *testing.T) { func TestCacheInitialReload(t *testing.T) { t.Parallel() - cache, _ := newAccountCache(cachetestDir) + cache, _ := newAccountCache(cachetestDir, hclog.NewNullLogger()) accounts := cache.accounts() if !reflect.DeepEqual(accounts, cachetestAccounts) { t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) @@ -146,7 +148,7 @@ func TestCacheInitialReload(t *testing.T) { func TestCacheAddDeleteOrder(t *testing.T) { t.Parallel() - cache, _ := newAccountCache("testdata/no-such-dir") + cache, _ := newAccountCache("testdata/no-such-dir", hclog.NewNullLogger()) cache.watcher.running = true // prevent unexpected reloads accs := []accounts.Account{ @@ -232,7 +234,7 @@ func TestCacheAddDeleteOrder(t *testing.T) { func TestCacheFind(t *testing.T) { t.Parallel() dir := filepath.Join("testdata", "dir") - cache, _ := newAccountCache(dir) + cache, _ := newAccountCache(dir, hclog.NewNullLogger()) cache.watcher.running = true // prevent unexpected reloads accs := []accounts.Account{ @@ -310,7 +312,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { // Create a temporary keystore to test with dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int())) - ks := NewKeyStore(dir, LightScryptN, LightScryptP) + ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) list := ks.Accounts() if len(list) > 0 { @@ -320,7 +322,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { t.Fatal("keystore watcher didn't start in time") } // Create the directory and copy a key file into it. - os.MkdirAll(dir, 0700) + require.NoError(t, os.MkdirAll(dir, 0700)) defer os.RemoveAll(dir) file := filepath.Join(dir, "aaa") diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index bc81f2919d..833735d0ef 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -1,6 +1,7 @@ package keystore import ( + "bytes" "crypto/ecdsa" "encoding/hex" "encoding/json" @@ -8,11 +9,13 @@ import ( "io" "os" "path/filepath" + "strings" "time" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" + "github.com/btcsuite/btcd/btcec/v2" "github.com/google/uuid" ) @@ -124,7 +127,7 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { } key := &Key{ Id: id, - Address: crypto.PubKeyToAddress(&privateKeyECDSA.PublicKey), // get more time for this pointer + Address: crypto.PubKeyToAddress(&privateKeyECDSA.PublicKey), //TO DO get more time for this pointer PrivateKey: privateKeyECDSA, } return key @@ -179,6 +182,24 @@ func newKey(rand io.Reader) (*Key, error) { return newKeyFromECDSA(privateKeyECDSA), nil } +func NewKeyForDirectICAP(rand io.Reader) *Key { + randBytes := make([]byte, 64) + _, err := rand.Read(randBytes) + if err != nil { + panic("key generation: could not read from random source: " + err.Error()) + } + reader := bytes.NewReader(randBytes) + privateKeyECDSA, err := ecdsa.GenerateKey(btcec.S256(), reader) + if err != nil { + panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) + } + key := newKeyFromECDSA(privateKeyECDSA) + if !strings.HasPrefix(key.Address.String(), "0x00") { + return NewKeyForDirectICAP(rand) + } + return key +} + func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) { key, err := newKey(rand) if err != nil { @@ -194,3 +215,11 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou } return key, a, err } + +func writeKeyFile(file string, content []byte) error { + name, err := writeTemporaryKeyFile(file, content) + if err != nil { + return err + } + return os.Rename(name, file) +} diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 4abfa8b02f..93b1534613 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -15,6 +15,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts/event" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" + "github.com/hashicorp/go-hclog" ) var ( @@ -51,19 +52,19 @@ type unlocked struct { abort chan struct{} } -func NewKeyStore(keyDir string, scryptN, scryptP int) *KeyStore { +func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { keyDir, _ = filepath.Abs(keyDir) ks := &KeyStore{storage: &keyStorePassphrase{keyDir, scryptN, scryptP, false}} - ks.init(keyDir) + ks.init(keyDir, logger) return ks } -func (ks *KeyStore) init(keyDir string) { +func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { ks.mu.Lock() defer ks.mu.Unlock() ks.unlocked = make(map[types.Address]*unlocked) - ks.cache, ks.changes = newAccountCache(keyDir) + ks.cache, ks.changes = newAccountCache(keyDir, logger) runtime.SetFinalizer(ks, func(m *KeyStore) { m.cache.close() diff --git a/accounts/keystore/keystore_fuzz_test.go b/accounts/keystore/keystore_fuzz_test.go new file mode 100644 index 0000000000..a00b8bd0e9 --- /dev/null +++ b/accounts/keystore/keystore_fuzz_test.go @@ -0,0 +1,20 @@ +package keystore + +import ( + "testing" + + "github.com/hashicorp/go-hclog" +) + +func FuzzPassword(f *testing.F) { + f.Fuzz(func(t *testing.T, password string) { + ks := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger()) + a, err := ks.NewAccount(password) + if err != nil { + t.Fatal(err) + } + if err := ks.Unlock(a, password); err != nil { + t.Fatal(err) + } + }) +} diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 30c8433c1e..155e53fd53 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -16,6 +16,7 @@ import ( "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/types" + "github.com/hashicorp/go-hclog" ) var testSigData = make([]byte, 32) @@ -444,5 +445,5 @@ func checkEvents(t *testing.T, want []walletEvent, have []walletEvent) { func tmpKeyStore(t *testing.T) (string, *KeyStore) { d := t.TempDir() - return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP) + return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) } diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go new file mode 100644 index 0000000000..d82388ba97 --- /dev/null +++ b/accounts/keystore/plain.go @@ -0,0 +1,52 @@ +package keystore + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/0xPolygon/polygon-edge/types" +) + +type keyStorePlain struct { + keysDirPath string +} + +func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, error) { + fd, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fd.Close() + key := new(Key) + + fileStat, err := fd.Stat() + if err != nil { + return nil, err + } + keyBytes := make([]byte, fileStat.Size()) + _, err = fd.Read(keyBytes) + if err := json.Unmarshal(keyBytes, key); err != nil { + return nil, err + } + if key.Address != addr { + return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr) + } + return key, nil +} + +func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error { + content, err := json.Marshal(key) + if err != nil { + return err + } + return writeKeyFile(filename, content) +} + +func (ks keyStorePlain) JoinPath(filename string) string { + if filepath.IsAbs(filename) { + return filename + } + return filepath.Join(ks.keysDirPath, filename) +} diff --git a/accounts/keystore/plain_test.go b/accounts/keystore/plain_test.go new file mode 100644 index 0000000000..81eafd4352 --- /dev/null +++ b/accounts/keystore/plain_test.go @@ -0,0 +1,251 @@ +package keystore + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/helper/common" + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/require" +) + +func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) { + d := t.TempDir() + if encrypted { + ks = &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} + } else { + ks = &keyStorePlain{d} + } + return d, ks +} + +func TestKeyStorePlain(t *testing.T) { + t.Parallel() + _, ks := tmpKeyStoreIface(t, false) + + pass := "" // not used but required by API + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.Address, k2.Address) { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { + t.Fatal(err) + } +} + +func TestKeyStorePassphrase(t *testing.T) { + t.Parallel() + _, ks := tmpKeyStoreIface(t, true) + + pass := "foo" + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.Address, k2.Address) { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { + t.Fatal(err) + } +} + +func TestKeyStorePassphraseDecryptionFail(t *testing.T) { + t.Parallel() + _, ks := tmpKeyStoreIface(t, true) + + pass := "foo" + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err.Error() != ErrDecrypt.Error() { + t.Fatalf("wrong error for invalid password\ngot %q\nwant %q", err, ErrDecrypt) + } +} + +func TestImportPreSaleKey(t *testing.T) { + t.Parallel() + dir, ks := tmpKeyStoreIface(t, true) + + // file content of a presale key file generated with: + // python pyethsaletool.py genwallet + // with password "foo" + fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" + pass := "foo" + account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) + if err != nil { + t.Fatal(err) + } + if account.Address != types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { + t.Errorf("imported account has wrong address %x", account.Address) + } + if !strings.HasPrefix(account.URL.Path, dir) { + t.Errorf("imported account file not in keystore directory: %q", account.URL) + } +} + +// Test and utils for the key store tests in the Ethereum JSON tests; +// testdataKeyStoreTests/basic_tests.json +type KeyStoreTestV3 struct { + Json encryptedKeyJSONV3 + Password string + Priv string +} + +type KeyStoreTestV1 struct { + Json encryptedKeyJSONV1 + Password string + Priv string +} + +func TestV3_PBKDF2_1(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["wikipage_test_vector_pbkdf2"], t) +} + +var testsSubmodule = filepath.Join("..", "..", "tests", "testdata", "KeyStoreTests") + +func skipIfSubmoduleMissing(t *testing.T) { + if !common.FileExists(testsSubmodule) { + t.Skipf("can't find JSON tests from submodule at %s", testsSubmodule) + } +} + +func TestV3_PBKDF2_2(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) + testDecryptV3(tests["test1"], t) +} + +func TestV3_PBKDF2_3(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) + testDecryptV3(tests["python_generated_test_with_odd_iv"], t) +} + +func TestV3_PBKDF2_4(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) + testDecryptV3(tests["evilnonce"], t) +} + +func TestV3_Scrypt_1(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["wikipage_test_vector_scrypt"], t) +} + +func TestV3_Scrypt_2(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) + testDecryptV3(tests["test2"], t) +} + +func TestV1_1(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV1("testdata/v1_test_vector.json", t) + testDecryptV1(tests["test1"], t) +} + +func TestV1_2(t *testing.T) { + t.Parallel() + ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP, true} + addr := types.StringToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") + file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" + k, err := ks.GetKey(addr, file, "g") + if err != nil { + t.Fatal(err) + } + + privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) + require.NoError(t, err) + privHex := hex.EncodeToString(privKey) + expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" + if privHex != expectedHex { + t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) + } +} + +func testDecryptV3(test KeyStoreTestV3, t *testing.T) { + privBytes, _, err := decryptKeyV3(&test.Json, test.Password) + if err != nil { + t.Fatal(err) + } + privHex := hex.EncodeToString(privBytes) + if test.Priv != privHex { + t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) + } +} + +func testDecryptV1(test KeyStoreTestV1, t *testing.T) { + privBytes, _, err := decryptKeyV1(&test.Json, test.Password) + if err != nil { + t.Fatal(err) + } + privHex := hex.EncodeToString(privBytes) + if test.Priv != privHex { + t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) + } +} + +func loadKeyStoreTestV3(file string, t *testing.T) map[string]KeyStoreTestV3 { + tests := make(map[string]KeyStoreTestV3) + err := accounts.LoadJSON(file, &tests) + if err != nil { + t.Fatal(err) + } + return tests +} + +func loadKeyStoreTestV1(file string, t *testing.T) map[string]KeyStoreTestV1 { + tests := make(map[string]KeyStoreTestV1) + err := accounts.LoadJSON(file, &tests) + if err != nil { + t.Fatal(err) + } + return tests +} + +func TestKeyForDirectICAP(t *testing.T) { + t.Parallel() + key := NewKeyForDirectICAP(rand.Reader) + if !strings.HasPrefix(types.AddressToString(key.Address), "0x00") { + t.Errorf("Expected first address byte to be zero, have: %s", types.AddressToString(key.Address)) + } +} + +func TestV3_31_Byte_Key(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["31_byte_key"], t) +} + +func TestV3_30_Byte_Key(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["30_byte_key"], t) +} diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index ed932d980c..d90cb2acb7 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -64,7 +64,7 @@ func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error return nil, err } ethPriv := crypto.Keccak256(plainText) - ecKey, err := crypto.BytesToECDSAPrivateKey(ethPriv) + ecKey, err := crypto.DToECDSA(ethPriv, false) if err != nil { return nil, err } diff --git a/accounts/manager.go b/accounts/manager.go new file mode 100644 index 0000000000..ac604bd148 --- /dev/null +++ b/accounts/manager.go @@ -0,0 +1,224 @@ +package accounts + +import ( + "reflect" + "sort" + "sync" + + "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/types" +) + +const managerSubBufferSize = 50 + +type Config struct { + InsecureUnlockAllowed bool +} + +type newBackendEvent struct { + backend Backend + + processed chan struct{} +} + +type Manager struct { + config *Config + backends map[reflect.Type][]Backend + updaters []event.Subscription + updates chan WalletEvent + newBackends chan newBackendEvent + wallets []Wallet + + feed event.Feed + + quit chan chan error + + term chan struct{} + lock sync.RWMutex +} + +func NewManager(config *Config, backends ...Backend) *Manager { + var wallets []Wallet + + for _, backend := range backends { + wallets = merge(wallets, backend.Wallets()...) + } + + updates := make(chan WalletEvent, managerSubBufferSize) + + subs := make([]event.Subscription, len(backends)) + for i, backend := range backends { + subs[i] = backend.Subscribe(updates) + } + + am := &Manager{ + config: config, + backends: make(map[reflect.Type][]Backend), + updaters: subs, + updates: updates, + newBackends: make(chan newBackendEvent), + wallets: wallets, + quit: make(chan chan error), + term: make(chan struct{}), + } + for _, backend := range backends { + kind := reflect.TypeOf(backend) + am.backends[kind] = append(am.backends[kind], backend) + } + go am.update() + + return am +} + +func (am *Manager) Close() error { + for _, w := range am.wallets { + w.Close() + } + + errc := make(chan error) + am.quit <- errc + + return <-errc +} + +func (am *Manager) Config() *Config { + return am.config +} + +func (am *Manager) AddBackend(backend Backend) { + done := make(chan struct{}) + am.newBackends <- newBackendEvent{backend, done} + <-done +} + +func (am *Manager) update() { + defer func() { + am.lock.Lock() + for _, sub := range am.updaters { + sub.Unsubscribe() + } + am.updaters = nil + am.lock.Unlock() + }() + + for { + select { + case event := <-am.updates: + am.lock.Lock() + switch event.Kind { + case WalletArrived: + am.wallets = merge(am.wallets, event.Wallet) + case WalletDropped: + am.wallets = drop(am.wallets, event.Wallet) + } + am.lock.Unlock() + + am.feed.Send(event) + case event := <-am.newBackends: + am.lock.Lock() + + backend := event.backend + am.wallets = merge(am.wallets, backend.Wallets()...) + am.updaters = append(am.updaters, backend.Subscribe(am.updates)) + kind := reflect.TypeOf(backend) + am.backends[kind] = append(am.backends[kind], backend) + am.lock.Unlock() + close(event.processed) + case errc := <-am.quit: + errc <- nil + + close(am.term) + return + } + } +} + +func (am *Manager) Backends(kind reflect.Type) []Backend { + am.lock.RLock() + defer am.lock.RUnlock() + + return am.backends[kind] +} + +func (am *Manager) Wallets() []Wallet { + am.lock.RLock() + defer am.lock.RUnlock() + + return am.walletsNoLock() +} + +func (am *Manager) walletsNoLock() []Wallet { + cpy := make([]Wallet, len(am.wallets)) + copy(cpy, am.wallets) + return cpy +} + +func (am *Manager) Wallet(url string) (Wallet, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + parsed, err := parseURL(url) + if err != nil { + return nil, err + } + for _, wallet := range am.walletsNoLock() { + if wallet.URL() == parsed { + return wallet, nil + } + } + + return nil, ErrUnknownWallet +} + +func (am *Manager) Accounts() []types.Address { + am.lock.RLock() + defer am.lock.RUnlock() + + addresses := make([]types.Address, 0) + for _, wallet := range am.wallets { + for _, account := range wallet.Accounts() { + addresses = append(addresses, account.Address) + } + } + + return addresses +} + +func (am *Manager) Find(account Account) (Wallet, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + for _, wallet := range am.wallets { + if wallet.Contains(account) { + return wallet, nil + } + } + return nil, ErrUnknownAccount +} + +func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { + return am.feed.Subscribe(sink) +} + +func merge(slice []Wallet, wallets ...Wallet) []Wallet { + for _, wallet := range wallets { + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) + if n == len(slice) { + continue + } + slice = append(slice[:n], slice[n+1:]...) + } + return slice +} + +func drop(slice []Wallet, wallets ...Wallet) []Wallet { + for _, wallet := range wallets { + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) + + if n == len(slice) { + continue + } + slice = append(slice[:n], slice[n+1:]...) + } + return slice +} diff --git a/accounts/scwallet/wallet.go b/accounts/scwallet/wallet.go deleted file mode 100644 index afef900d27..0000000000 --- a/accounts/scwallet/wallet.go +++ /dev/null @@ -1 +0,0 @@ -package scwallet diff --git a/accounts/URL.go b/accounts/url.go similarity index 100% rename from accounts/URL.go rename to accounts/url.go diff --git a/accounts/url_test.go b/accounts/url_test.go new file mode 100644 index 0000000000..de4368a348 --- /dev/null +++ b/accounts/url_test.go @@ -0,0 +1,84 @@ +package accounts + +import "testing" + +func TestURLParsing(t *testing.T) { + t.Parallel() + url, err := parseURL("https://ethereum.org") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if url.Scheme != "https" { + t.Errorf("expected: %v, got: %v", "https", url.Scheme) + } + if url.Path != "ethereum.org" { + t.Errorf("expected: %v, got: %v", "ethereum.org", url.Path) + } + + for _, u := range []string{"ethereum.org", ""} { + if _, err = parseURL(u); err == nil { + t.Errorf("input %v, expected err, got: nil", u) + } + } +} + +func TestURLString(t *testing.T) { + t.Parallel() + url := URL{Scheme: "https", Path: "ethereum.org"} + if url.String() != "https://ethereum.org" { + t.Errorf("expected: %v, got: %v", "https://ethereum.org", url.String()) + } + + url = URL{Scheme: "", Path: "ethereum.org"} + if url.String() != "ethereum.org" { + t.Errorf("expected: %v, got: %v", "ethereum.org", url.String()) + } +} + +func TestURLMarshalJSON(t *testing.T) { + t.Parallel() + url := URL{Scheme: "https", Path: "ethereum.org"} + json, err := url.MarshalJSON() + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if string(json) != "\"https://ethereum.org\"" { + t.Errorf("expected: %v, got: %v", "\"https://ethereum.org\"", string(json)) + } +} + +func TestURLUnmarshalJSON(t *testing.T) { + t.Parallel() + url := &URL{} + err := url.UnmarshalJSON([]byte("\"https://ethereum.org\"")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if url.Scheme != "https" { + t.Errorf("expected: %v, got: %v", "https", url.Scheme) + } + if url.Path != "ethereum.org" { + t.Errorf("expected: %v, got: %v", "https", url.Path) + } +} + +func TestURLComparison(t *testing.T) { + t.Parallel() + tests := []struct { + urlA URL + urlB URL + expect int + }{ + {URL{"https", "ethereum.org"}, URL{"https", "ethereum.org"}, 0}, + {URL{"http", "ethereum.org"}, URL{"https", "ethereum.org"}, -1}, + {URL{"https", "ethereum.org/a"}, URL{"https", "ethereum.org"}, 1}, + {URL{"https", "abc.org"}, URL{"https", "ethereum.org"}, -1}, + } + + for i, tt := range tests { + result := tt.urlA.Cmp(tt.urlB) + if result != tt.expect { + t.Errorf("test %d: cmp mismatch: expected: %d, got: %d", i, tt.expect, result) + } + } +} diff --git a/accounts/usbwallet/wallet.go b/accounts/usbwallet/wallet.go deleted file mode 100644 index dc6cb767e8..0000000000 --- a/accounts/usbwallet/wallet.go +++ /dev/null @@ -1 +0,0 @@ -package usbwallet From 3a597ffc3ded50adf705ab6549b416049818186d Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 03/69] some lint fix --- accounts/accounts.go | 1 + accounts/event/feed.go | 21 +++++++-- accounts/event/subscription.go | 7 +++ accounts/hd.go | 15 +++++- accounts/hd_test.go | 1 + accounts/helper.go | 9 ++-- accounts/keystore/account_cache.go | 14 ++++-- accounts/keystore/account_cache_test.go | 15 ++++-- accounts/keystore/key.go | 22 ++++----- accounts/keystore/keystore.go | 12 +++-- accounts/keystore/keystore_test.go | 32 ++++++++----- accounts/keystore/passphrase.go | 62 ++++++++++++------------- accounts/keystore/passphrase_test.go | 2 +- accounts/keystore/plain.go | 9 +--- accounts/keystore/plain_test.go | 59 +++++++++++------------ accounts/keystore/presale.go | 4 +- accounts/keystore/wallet.go | 12 +++-- accounts/keystore/watch.go | 26 +++++++---- accounts/manager.go | 10 ++++ accounts/url_test.go | 44 ++++++++++-------- consensus/polybft/wallet/account.go | 2 +- 21 files changed, 233 insertions(+), 146 deletions(-) diff --git a/accounts/accounts.go b/accounts/accounts.go index 2726c5cdaa..fb3b44b080 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -136,6 +136,7 @@ func TextAndHash(data []byte) ([]byte, string) { msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) hasher := sha3.NewLegacyKeccak256() hasher.Write([]byte(msg)) + return hasher.Sum(nil), msg } diff --git a/accounts/event/feed.go b/accounts/event/feed.go index 67de773ad5..c08fb8cc66 100644 --- a/accounts/event/feed.go +++ b/accounts/event/feed.go @@ -57,14 +57,17 @@ func (f *Feed) init(etype reflect.Type) { func (f *Feed) Subscribe(channel interface{}) Subscription { chanval := reflect.ValueOf(channel) chantyp := chanval.Type() + if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 { - panic(errBadChannel) + panic(errBadChannel) //nolint:gocritic } + sub := &feedSub{feed: f, channel: chanval, err: make(chan error, 1)} f.once.Do(func() { f.init(chantyp.Elem()) }) + if f.etype != chantyp.Elem() { - panic(feedTypeError{op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)}) + panic(feedTypeError{op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)}) //nolint:gocritic } f.mu.Lock() @@ -73,6 +76,7 @@ func (f *Feed) Subscribe(channel interface{}) Subscription { // The next Send will add it to f.sendCases. cas := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval} f.inbox = append(f.inbox, cas) + return sub } @@ -82,11 +86,14 @@ func (f *Feed) remove(sub *feedSub) { ch := sub.channel.Interface() f.mu.Lock() index := f.inbox.find(ch) + if index != -1 { f.inbox = f.inbox.delete(index) f.mu.Unlock() + return } + f.mu.Unlock() select { @@ -105,8 +112,9 @@ func (f *Feed) Send(value interface{}) (nsent int) { rvalue := reflect.ValueOf(value) f.once.Do(func() { f.init(rvalue.Type()) }) + if f.etype != rvalue.Type() { - panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype}) + panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype}) //nolint:gocritic } <-f.sendLock @@ -126,6 +134,7 @@ func (f *Feed) Send(value interface{}) (nsent int) { // of sendCases. When a send succeeds, the corresponding case moves to the end of // 'cases' and it shrinks by one element. cases := f.sendCases + for { // Fast path: try sending without blocking before adding to the select set. // This should usually succeed if subscribers are fast enough and have free @@ -137,14 +146,17 @@ func (f *Feed) Send(value interface{}) (nsent int) { i-- } } + if len(cases) == firstSubSendCase { break } // Select on all the receivers, waiting for them to unblock. chosen, recv, _ := reflect.Select(cases) + if chosen == 0 /* <-f.removeSub */ { index := f.sendCases.find(recv.Interface()) f.sendCases = f.sendCases.delete(index) + if index >= 0 && index < len(cases) { // Shrink 'cases' too because the removed case was still active. cases = f.sendCases[:len(cases)-1] @@ -160,6 +172,7 @@ func (f *Feed) Send(value interface{}) (nsent int) { f.sendCases[i].Send = reflect.Value{} } f.sendLock <- struct{}{} + return nsent } @@ -190,6 +203,7 @@ func (cs caseList) find(channel interface{}) int { return i } } + return -1 } @@ -202,5 +216,6 @@ func (cs caseList) delete(index int) caseList { func (cs caseList) deactivate(index int) caseList { last := len(cs) - 1 cs[index], cs[last] = cs[last], cs[index] + return cs[:last] } diff --git a/accounts/event/subscription.go b/accounts/event/subscription.go index f76e7c050f..853925ce28 100644 --- a/accounts/event/subscription.go +++ b/accounts/event/subscription.go @@ -39,14 +39,18 @@ type scopeSub struct { func (sc *SubscriptionScope) Track(s Subscription) Subscription { sc.mu.Lock() defer sc.mu.Unlock() + if sc.closed { return nil } + if sc.subs == nil { sc.subs = make(map[*scopeSub]struct{}) } + ss := &scopeSub{sc, s} sc.subs[ss] = struct{}{} + return ss } @@ -58,10 +62,12 @@ func (sc *SubscriptionScope) Close() { if sc.closed { return } + sc.closed = true for s := range sc.subs { s.s.Unsubscribe() } + sc.subs = nil } @@ -70,6 +76,7 @@ func (sc *SubscriptionScope) Close() { func (sc *SubscriptionScope) Count() int { sc.mu.Lock() defer sc.mu.Unlock() + return len(sc.subs) } diff --git a/accounts/hd.go b/accounts/hd.go index fe167a9fc9..e13aa68d38 100644 --- a/accounts/hd.go +++ b/accounts/hd.go @@ -44,9 +44,10 @@ var LegacyLedgerBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 type DerivationPath []uint32 func ParseDerivationPath(path string) (DerivationPath, error) { - var result DerivationPath + var result DerivationPath //nolint:prealloc components := strings.Split(path, "/") + switch { case len(components) == 0: return nil, errors.New("empty derivation path") @@ -64,6 +65,7 @@ func ParseDerivationPath(path string) (DerivationPath, error) { for _, component := range components { component = strings.TrimSpace(component) + var value uint32 if strings.HasSuffix(component, "'") { @@ -75,13 +77,17 @@ func ParseDerivationPath(path string) (DerivationPath, error) { if !ok { return nil, fmt.Errorf("invalid component: %s", component) } + max := math.MaxUint32 - value + if bigValue.Sign() < 0 || bigValue.Cmp(big.NewInt(int64(max))) > 0 { if value == 0 { return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigValue, max) } + return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigValue, max) } + value += uint32(bigValue.Uint64()) result = append(result, value) @@ -92,17 +98,22 @@ func ParseDerivationPath(path string) (DerivationPath, error) { func (path DerivationPath) String() string { result := "m" + for _, component := range path { var hardened bool + if component >= 0x80000000 { component -= 0x80000000 hardened = true } + result = fmt.Sprintf("%s/%d", result, component) + if hardened { result += "'" } } + return result } @@ -113,9 +124,11 @@ func (path DerivationPath) MarshalJSON() ([]byte, error) { func (path *DerivationPath) UnmarshalJSON(b []byte) error { var dp string var err error + if err = json.Unmarshal(b, &dp); err != nil { return err } + *path, err = ParseDerivationPath(dp) return err } diff --git a/accounts/hd_test.go b/accounts/hd_test.go index a5ff0f719e..acd73eeb5a 100644 --- a/accounts/hd_test.go +++ b/accounts/hd_test.go @@ -53,6 +53,7 @@ func TestHDPathParsing(t *testing.T) { {"m/2147483648'", nil}, // Overflows 32 bit integer {"m/-1'", nil}, // Cannot contain negative number } + for i, tt := range tests { if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) { t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output) diff --git a/accounts/helper.go b/accounts/helper.go index 050d868c8a..c870e676af 100644 --- a/accounts/helper.go +++ b/accounts/helper.go @@ -66,24 +66,27 @@ func LoadJSON(file string, val interface{}) error { return err } if err := json.Unmarshal(content, val); err != nil { - if syntaxerr, ok := err.(*json.SyntaxError); ok { + if syntaxerr, ok := err.(*json.SyntaxError); ok { //nolint:errorlint line := findLine(content, syntaxerr.Offset) - return fmt.Errorf("JSON syntax error at %v:%v: %v", file, line, err) + return fmt.Errorf("JSON syntax error at %v:%v: %w", file, line, err) } - return fmt.Errorf("JSON unmarshal error in %v: %v", file, err) + return fmt.Errorf("JSON unmarshal error in %v: %w", file, err) } return nil } func findLine(data []byte, offset int64) (line int) { line = 1 + for i, r := range string(data) { if int64(i) >= offset { return } + if r == '\n' { line++ } } + return } diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 0f3122eefd..3523a24374 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -46,7 +46,7 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st notify: make(chan struct{}, 1), fileC: fileCache{all: mapset.NewThreadUnsafeSet[string]()}, } - ac.watcher = newWatcher(ac) + ac.watcher = newWatcher(ac, logger) return ac, ac.notify } @@ -228,12 +228,14 @@ func (ac *accountCache) scanAccounts() error { ac.logger.Trace("Failed to open keystore file", "path", path, "err", err) return nil } + defer fd.Close() buf.Reset(fd) // Parse the address. key.Address = "" err = json.NewDecoder(buf).Decode(&key) addr := types.StringToAddress(key.Address) + switch { case err != nil: ac.logger.Debug("Failed to decode keystore key", "path", path, "err", err) @@ -245,31 +247,37 @@ func (ac *accountCache) scanAccounts() error { URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}, } } + return nil } // Process all the file diffs - start := time.Now() + start := time.Now().UTC() for _, path := range creates.ToSlice() { if a := readAccount(path); a != nil { ac.add(*a) } } + for _, path := range deletes.ToSlice() { ac.deleteByFile(path) } + for _, path := range updates.ToSlice() { ac.deleteByFile(path) + if a := readAccount(path); a != nil { ac.add(*a) } } - end := time.Now() + + end := time.Now().UTC() select { case ac.notify <- struct{}{}: default: } ac.logger.Trace("Handled keystore changes", "time", end.Sub(start)) + return nil } diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 737c1ec483..04c1776647 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -43,18 +43,20 @@ func waitWatcherStart(ks *KeyStore) bool { if !ks.cache.watcher.enabled() { return true } + // The watcher should start, and then exit. - for t0 := time.Now(); time.Since(t0) < 1*time.Second; time.Sleep(100 * time.Millisecond) { + for t0 := time.Now().UTC(); time.Since(t0) < 1*time.Second; time.Sleep(100 * time.Millisecond) { if ks.cache.watcherStarted() { return true } } + return false } func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { var list []accounts.Account - for t0 := time.Now(); time.Since(t0) < 5*time.Second; time.Sleep(100 * time.Millisecond) { + for t0 := time.Now().UTC(); time.Since(t0) < 5*time.Second; time.Sleep(100 * time.Millisecond) { list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { // ks should have also received change notifications @@ -63,6 +65,7 @@ func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { default: return errors.New("wasn't notified of new accounts") } + return nil } } @@ -296,10 +299,12 @@ func TestCacheFind(t *testing.T) { a, err := cache.find(test.Query) if !reflect.DeepEqual(err, test.WantError) { t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) + continue } if a != test.WantResult { t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) + continue } } @@ -339,7 +344,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { return } // needed so that modTime of `file` is different to its current value after forceCopyFile - os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) + require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) // Now replace file contents if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil { @@ -355,7 +360,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { } // needed so that modTime of `file` is different to its current value after forceCopyFile - os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) + require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) // Now replace file contents again if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil { @@ -371,7 +376,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { } // needed so that modTime of `file` is different to its current value after os.WriteFile - os.Chtimes(file, time.Now().Add(-time.Second), time.Now().Add(-time.Second)) + require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) // Now replace file contents with crap if err := os.WriteFile(file, []byte("foo"), 0600); err != nil { diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 833735d0ef..b054c60c96 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -24,7 +24,7 @@ const ( ) type Key struct { - Id uuid.UUID + ID uuid.UUID Address types.Address @@ -43,21 +43,21 @@ type keyStore interface { type plainKeyJSON struct { Address string `json:"address"` PrivateKey string `json:"privatekey"` - Id string `json:"id"` + ID string `json:"id"` Version int `json:"version"` } type encryptedKeyJSONV3 struct { Address string `json:"address"` Crypto CryptoJSON `json:"crypto"` - Id string `json:"id"` + ID string `json:"id"` Version int `json:"version"` } type encryptedKeyJSONV1 struct { Address string `json:"address"` Crypto CryptoJSON `json:"crypto"` - Id string `json:"id"` + ID string `json:"id"` Version string `json:"version"` } @@ -84,7 +84,7 @@ func (k *Key) MarshalJSON() (j []byte, err error) { jStruct := plainKeyJSON{ hex.EncodeToString(k.Address[:]), hex.EncodeToString(privKey), - k.Id.String(), + k.ID.String(), version, } j, err = json.Marshal(jStruct) @@ -99,11 +99,11 @@ func (k *Key) UnmarshalJSON(j []byte) (err error) { } u := new(uuid.UUID) - *u, err = uuid.Parse(keyJSON.Id) + *u, err = uuid.Parse(keyJSON.ID) if err != nil { return err } - k.Id = *u + k.ID = *u addr, err := hex.DecodeString(keyJSON.Address) if err != nil { return err @@ -123,10 +123,10 @@ func (k *Key) UnmarshalJSON(j []byte) (err error) { func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { id, err := uuid.NewRandom() if err != nil { - panic(fmt.Sprintf("Could not create random uuid: %v", err)) + panic(fmt.Sprintf("Could not create random uuid: %v", err)) //nolint:gocritic } key := &Key{ - Id: id, + ID: id, Address: crypto.PubKeyToAddress(&privateKeyECDSA.PublicKey), //TO DO get more time for this pointer PrivateKey: privateKeyECDSA, } @@ -186,12 +186,12 @@ func NewKeyForDirectICAP(rand io.Reader) *Key { randBytes := make([]byte, 64) _, err := rand.Read(randBytes) if err != nil { - panic("key generation: could not read from random source: " + err.Error()) + panic("key generation: could not read from random source: " + err.Error()) //nolint:gocritic } reader := bytes.NewReader(randBytes) privateKeyECDSA, err := ecdsa.GenerateKey(btcec.S256(), reader) if err != nil { - panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) + panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) //nolint:gocritic } key := newKeyFromECDSA(privateKeyECDSA) if !strings.HasPrefix(key.Address.String(), "0x00") { diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 93b1534613..f82511ecfe 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -99,7 +99,7 @@ func (ks *KeyStore) refreshWallets() { ks.mu.Lock() accs := ks.cache.accounts() - var ( + var ( //nolint:prealloc wallets = make([]accounts.Wallet, 0, len(accs)) events []accounts.WalletEvent ) @@ -115,6 +115,7 @@ func (ks *KeyStore) refreshWallets() { events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) wallets = append(wallets, wallet) + continue } @@ -226,7 +227,8 @@ func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *b return signer.SignTx(tx, unlockedKey.PrivateKey) } -func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) { +func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, + passphrase string, hash []byte) (signature []byte, err error) { _, key, err := ks.getDecryptedKey(a, passphrase) if err != nil { return nil, err @@ -237,7 +239,8 @@ func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string return crypto.Sign(key.PrivateKey, hash) } -func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { +func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, + tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { _, key, err := ks.getDecryptedKey(a, passphrase) if err != nil { return nil, err @@ -393,7 +396,8 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { - a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} + a := accounts.Account{Address: key.Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { return accounts.Account{}, err } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 155e53fd53..7d8b1f39b6 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -1,6 +1,7 @@ package keystore import ( + "errors" "math/rand" "os" "runtime" @@ -21,11 +22,13 @@ import ( var testSigData = make([]byte, 32) +const pass = "foo" + func TestKeyStore(t *testing.T) { t.Parallel() dir, ks := tmpKeyStore(t) - a, err := ks.NewAccount("foo") + a, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) } @@ -42,7 +45,7 @@ func TestKeyStore(t *testing.T) { if !ks.HasAddress(a.Address) { t.Errorf("HasAccount(%x) should've returned true", a.Address) } - if err := ks.Update(a, "foo", "bar"); err != nil { + if err := ks.Update(a, pass, "bar"); err != nil { t.Errorf("Update error: %v", err) } if err := ks.Delete(a, "bar"); err != nil { @@ -105,7 +108,6 @@ func TestTimedUnlock(t *testing.T) { t.Parallel() _, ks := tmpKeyStore(t) - pass := "foo" a1, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) @@ -113,7 +115,7 @@ func TestTimedUnlock(t *testing.T) { // Signing without passphrase fails because account is locked _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrLocked { + if !errors.Is(err, ErrLocked) { t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err) } @@ -131,7 +133,7 @@ func TestTimedUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrLocked { + if !errors.Is(err, ErrLocked) { t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } } @@ -140,7 +142,6 @@ func TestOverrideUnlock(t *testing.T) { t.Parallel() _, ks := tmpKeyStore(t) - pass := "foo" a1, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) @@ -171,7 +172,7 @@ func TestOverrideUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrLocked { + if !errors.Is(err, ErrLocked) { t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } } @@ -190,9 +191,9 @@ func TestSignRace(t *testing.T) { 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 { + end := time.Now().UTC().Add(500 * time.Millisecond) + for time.Now().UTC().Before(end) { + if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); errors.Is(err, ErrLocked) { return } else if err != nil { t.Errorf("Sign error: %v", err) @@ -210,7 +211,7 @@ func TestSignRace(t *testing.T) { func waitForKsUpdating(t *testing.T, ks *KeyStore, wantStatus bool, maxTime time.Duration) bool { t.Helper() // Wait max 250 ms, then return false - for t0 := time.Now(); time.Since(t0) < maxTime; { + for t0 := time.Now().UTC(); time.Since(t0) < maxTime; { if ks.isUpdating() == wantStatus { return true } @@ -273,7 +274,7 @@ func TestWalletNotifications(t *testing.T) { _, ks := tmpKeyStore(t) // Subscribe to the wallet feed and collect events. - var ( + var ( //nolint:prealloc events []walletEvent updates = make(chan accounts.WalletEvent) sub = ks.Subscribe(updates) @@ -310,6 +311,7 @@ func TestWalletNotifications(t *testing.T) { var account accounts.Account for _, a := range live { account = a + break } if err := ks.Delete(account, ""); err != nil { @@ -409,6 +411,8 @@ func TestImportRace(t *testing.T) { // checkAccounts checks that all known live accounts are present in the wallet list. func checkAccounts(t *testing.T, live map[types.Address]accounts.Account, wallets []accounts.Wallet) { + t.Helper() + if len(live) != len(wallets) { t.Errorf("wallet list doesn't match required accounts: have %d, want %d", len(wallets), len(live)) return @@ -429,6 +433,8 @@ func checkAccounts(t *testing.T, live map[types.Address]accounts.Account, wallet // 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) { + t.Helper() + for _, wantEv := range want { nmatch := 0 for ; len(have) > 0; nmatch++ { @@ -444,6 +450,8 @@ func checkEvents(t *testing.T, want []walletEvent, have []walletEvent) { } func tmpKeyStore(t *testing.T) (string, *KeyStore) { + t.Helper() + d := t.TempDir() return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) } diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index e8543de6db..2024ed3af4 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -91,13 +91,7 @@ func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) er // Verify that we can decrypt the file with the given password. _, err = ks.GetKey(key.Address, tmpName, auth) if err != nil { - msg := "An error was encountered when saving and verifying the keystore file. \n" + - "This indicates that the keystore is corrupted. \n" + - "The corrupted file is stored at \n%v\n" + - "Please file a ticket at:\n\n" + - "https://github.com/ethereum/go-ethereum/issues." + - "The error was : %s" - //lint:ignore ST1005 This is a message for the user + msg := "an error was encountered when saving and verifying the keystore file. \n" return fmt.Errorf(msg, tmpName, err) } } @@ -115,7 +109,7 @@ func (ks keyStorePassphrase) JoinPath(filename string) string { func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { salt := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, salt); err != nil { - panic("reading from crypto/rand failed: " + err.Error()) + panic("reading from crypto/rand failed: " + err.Error()) //nolint:gocritic } derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) if err != nil { @@ -125,7 +119,7 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) iv := make([]byte, aes.BlockSize) // 16 if _, err := io.ReadFull(rand.Reader, iv); err != nil { - panic("reading from crypto/rand failed: " + err.Error()) + panic("reading from crypto/rand failed: " + err.Error()) //nolint:gocritic } cipherText, err := aesCTRXOR(encryptKey, data, iv) if err != nil { @@ -158,6 +152,10 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) // blob that can be decrypted later on. func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { keyBytes, err := crypto.MarshalECDSAPrivateKey(key.PrivateKey) //TO DO maybe wrong + if err != nil { + return nil, err + } + cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) if err != nil { return nil, err @@ -165,7 +163,7 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { encryptedKeyJSONV3 := encryptedKeyJSONV3{ hex.EncodeToString(key.Address[:]), cryptoStruct, - key.Id.String(), + key.ID.String(), version, } return json.Marshal(encryptedKeyJSONV3) @@ -182,7 +180,7 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { // Depending on the version try to parse one way or another var ( - keyBytes, keyId []byte + keyBytes, keyID []byte err error ) if version, ok := m["version"].(string); ok && version == "1" { @@ -190,13 +188,13 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { if err := json.Unmarshal(keyjson, k); err != nil { return nil, err } - keyBytes, keyId, err = decryptKeyV1(k, auth) + keyBytes, keyID, err = decryptKeyV1(k, auth) } else { k := new(encryptedKeyJSONV3) if err := json.Unmarshal(keyjson, k); err != nil { return nil, err } - keyBytes, keyId, err = decryptKeyV3(k, auth) + keyBytes, keyID, err = decryptKeyV3(k, auth) } // Handle any decryption errors and return the key if err != nil { @@ -206,37 +204,37 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { if err != nil { return nil, fmt.Errorf("invalid key: %w", err) } - id, err := uuid.FromBytes(keyId) + id, err := uuid.FromBytes(keyID) if err != nil { return nil, fmt.Errorf("invalid UUID: %w", err) } return &Key{ - Id: id, + ID: id, Address: crypto.PubKeyToAddress(&key.PublicKey), PrivateKey: key, }, nil } -func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { - if cryptoJson.Cipher != "aes-128-ctr" { - return nil, fmt.Errorf("cipher not supported: %v", cryptoJson.Cipher) +func DecryptDataV3(cryptoJSON CryptoJSON, auth string) ([]byte, error) { + if cryptoJSON.Cipher != "aes-128-ctr" { + return nil, fmt.Errorf("cipher not supported: %v", cryptoJSON.Cipher) } - mac, err := hex.DecodeString(cryptoJson.MAC) + mac, err := hex.DecodeString(cryptoJSON.MAC) if err != nil { return nil, err } - iv, err := hex.DecodeString(cryptoJson.CipherParams.IV) + iv, err := hex.DecodeString(cryptoJSON.CipherParams.IV) if err != nil { return nil, err } - cipherText, err := hex.DecodeString(cryptoJson.CipherText) + cipherText, err := hex.DecodeString(cryptoJSON.CipherText) if err != nil { return nil, err } - derivedKey, err := getKDFKey(cryptoJson, auth) + derivedKey, err := getKDFKey(cryptoJSON, auth) if err != nil { return nil, err } @@ -253,28 +251,28 @@ func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { return plainText, err } -func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) { +func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyID []byte, err error) { if keyProtected.Version != version { return nil, nil, fmt.Errorf("version not supported: %v", keyProtected.Version) } - keyUUID, err := uuid.Parse(keyProtected.Id) + keyUUID, err := uuid.Parse(keyProtected.ID) if err != nil { return nil, nil, err } - keyId = keyUUID[:] + keyID = keyUUID[:] plainText, err := DecryptDataV3(keyProtected.Crypto, auth) if err != nil { return nil, nil, err } - return plainText, keyId, err + return plainText, keyID, err } -func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { - keyUUID, err := uuid.Parse(keyProtected.Id) +func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyID []byte, err error) { + keyUUID, err := uuid.Parse(keyProtected.ID) if err != nil { return nil, nil, err } - keyId = keyUUID[:] + keyID = keyUUID[:] mac, err := hex.DecodeString(keyProtected.Crypto.MAC) if err != nil { return nil, nil, err @@ -304,7 +302,7 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt if err != nil { return nil, nil, err } - return plainText, keyId, err + return plainText, keyID, err } func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { @@ -322,7 +320,7 @@ func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { return scrypt.Key(authArray, salt, n, r, p, dkLen) } else if cryptoJSON.KDF == "pbkdf2" { c := ensureInt(cryptoJSON.KDFParams["c"]) - prf := cryptoJSON.KDFParams["prf"].(string) + prf := cryptoJSON.KDFParams["prf"].(string) //nolint:forcetypeassert if prf != "hmac-sha256" { return nil, fmt.Errorf("unsupported PBKDF2 PRF: %s", prf) } @@ -336,7 +334,7 @@ func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { func ensureInt(x interface{}) int { res, ok := x.(int) if !ok { - res = int(x.(float64)) + res = int(x.(float64)) //nolint:forcetypeassert } return res } diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 9dadfcf67e..30ec8a0f86 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -37,7 +37,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address) } // Recrypt with a new password and start over - password += "new data appended" // nolint: gosec + password += "new data appended" // nolint:gosec if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { t.Errorf("test %d: failed to re-encrypt key %v", i, err) } diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go index d82388ba97..eb143b31f9 100644 --- a/accounts/keystore/plain.go +++ b/accounts/keystore/plain.go @@ -20,14 +20,7 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, } defer fd.Close() key := new(Key) - - fileStat, err := fd.Stat() - if err != nil { - return nil, err - } - keyBytes := make([]byte, fileStat.Size()) - _, err = fd.Read(keyBytes) - if err := json.Unmarshal(keyBytes, key); err != nil { + if err := json.NewDecoder(fd).Decode(key); err != nil { return nil, err } if key.Address != addr { diff --git a/accounts/keystore/plain_test.go b/accounts/keystore/plain_test.go index 81eafd4352..966ea8d10f 100644 --- a/accounts/keystore/plain_test.go +++ b/accounts/keystore/plain_test.go @@ -51,7 +51,6 @@ func TestKeyStorePassphrase(t *testing.T) { t.Parallel() _, ks := tmpKeyStoreIface(t, true) - pass := "foo" k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) @@ -72,7 +71,6 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { t.Parallel() _, ks := tmpKeyStoreIface(t, true) - pass := "foo" k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) @@ -90,7 +88,6 @@ func TestImportPreSaleKey(t *testing.T) { // python pyethsaletool.py genwallet // with password "foo" fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" - pass := "foo" account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) if err != nil { t.Fatal(err) @@ -106,21 +103,21 @@ func TestImportPreSaleKey(t *testing.T) { // Test and utils for the key store tests in the Ethereum JSON tests; // testdataKeyStoreTests/basic_tests.json type KeyStoreTestV3 struct { - Json encryptedKeyJSONV3 + JSON encryptedKeyJSONV3 Password string Priv string } type KeyStoreTestV1 struct { - Json encryptedKeyJSONV1 + JSON encryptedKeyJSONV1 Password string Priv string } func TestV3_PBKDF2_1(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["wikipage_test_vector_pbkdf2"], t) + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["wikipage_test_vector_pbkdf2"]) } var testsSubmodule = filepath.Join("..", "..", "tests", "testdata", "KeyStoreTests") @@ -134,41 +131,41 @@ func skipIfSubmoduleMissing(t *testing.T) { func TestV3_PBKDF2_2(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) - testDecryptV3(tests["test1"], t) + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["test1"]) } func TestV3_PBKDF2_3(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) - testDecryptV3(tests["python_generated_test_with_odd_iv"], t) + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["python_generated_test_with_odd_iv"]) } func TestV3_PBKDF2_4(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) - testDecryptV3(tests["evilnonce"], t) + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["evilnonce"]) } func TestV3_Scrypt_1(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["wikipage_test_vector_scrypt"], t) + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["wikipage_test_vector_scrypt"]) } func TestV3_Scrypt_2(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(filepath.Join(testsSubmodule, "basic_tests.json"), t) - testDecryptV3(tests["test2"], t) + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["test2"]) } func TestV1_1(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV1("testdata/v1_test_vector.json", t) - testDecryptV1(tests["test1"], t) + tests := loadKeyStoreTestV1(t, "testdata/v1_test_vector.json") + testDecryptV1(t, tests["test1"]) } func TestV1_2(t *testing.T) { @@ -190,8 +187,8 @@ func TestV1_2(t *testing.T) { } } -func testDecryptV3(test KeyStoreTestV3, t *testing.T) { - privBytes, _, err := decryptKeyV3(&test.Json, test.Password) +func testDecryptV3(t *testing.T, test KeyStoreTestV3) { + privBytes, _, err := decryptKeyV3(&test.JSON, test.Password) if err != nil { t.Fatal(err) } @@ -201,8 +198,8 @@ func testDecryptV3(test KeyStoreTestV3, t *testing.T) { } } -func testDecryptV1(test KeyStoreTestV1, t *testing.T) { - privBytes, _, err := decryptKeyV1(&test.Json, test.Password) +func testDecryptV1(t *testing.T, test KeyStoreTestV1) { + privBytes, _, err := decryptKeyV1(&test.JSON, test.Password) if err != nil { t.Fatal(err) } @@ -212,7 +209,9 @@ func testDecryptV1(test KeyStoreTestV1, t *testing.T) { } } -func loadKeyStoreTestV3(file string, t *testing.T) map[string]KeyStoreTestV3 { +func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { + t.Helper() + tests := make(map[string]KeyStoreTestV3) err := accounts.LoadJSON(file, &tests) if err != nil { @@ -221,7 +220,9 @@ func loadKeyStoreTestV3(file string, t *testing.T) map[string]KeyStoreTestV3 { return tests } -func loadKeyStoreTestV1(file string, t *testing.T) map[string]KeyStoreTestV1 { +func loadKeyStoreTestV1(t *testing.T, file string) map[string]KeyStoreTestV1 { + t.Helper() + tests := make(map[string]KeyStoreTestV1) err := accounts.LoadJSON(file, &tests) if err != nil { @@ -240,12 +241,12 @@ func TestKeyForDirectICAP(t *testing.T) { func TestV3_31_Byte_Key(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["31_byte_key"], t) + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["31_byte_key"]) } func TestV3_30_Byte_Key(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["30_byte_key"], t) + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["30_byte_key"]) } diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index d90cb2acb7..83ca43e060 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -20,7 +20,7 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou if err != nil { return accounts.Account{}, nil, err } - key.Id, err = uuid.NewRandom() + key.ID, err = uuid.NewRandom() if err != nil { return accounts.Account{}, nil, err } @@ -70,7 +70,7 @@ func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error } key = &Key{ - Id: uuid.UUID{}, + ID: uuid.UUID{}, Address: crypto.PubKeyToAddress(&ecKey.PublicKey), PrivateKey: ecKey, } diff --git a/accounts/keystore/wallet.go b/accounts/keystore/wallet.go index 025a36462c..cb1f728f9d 100644 --- a/accounts/keystore/wallet.go +++ b/accounts/keystore/wallet.go @@ -60,7 +60,8 @@ func (ksw *keyStoreWallet) SignData(account accounts.Account, mimeType string, d return ksw.signHash(account, crypto.Keccak256(data)) } -func (ksw *keyStoreWallet) SignDataWithPassphrase(account accounts.Account, passhphrase, mimeType string, data []byte) ([]byte, error) { +func (ksw *keyStoreWallet) SignDataWithPassphrase(account accounts.Account, + passhphrase, mimeType string, data []byte) ([]byte, error) { if !ksw.Contains(account) { return nil, accounts.ErrUnknownAccount } @@ -72,7 +73,8 @@ func (ksw *keyStoreWallet) SignText(account accounts.Account, text []byte) ([]by return ksw.signHash(account, accounts.TextHash(text)) } -func (ksw *keyStoreWallet) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { +func (ksw *keyStoreWallet) SignTextWithPassphrase(account accounts.Account, + passphrase string, text []byte) ([]byte, error) { if !ksw.Contains(account) { return nil, accounts.ErrUnknownAccount } @@ -80,7 +82,8 @@ func (ksw *keyStoreWallet) SignTextWithPassphrase(account accounts.Account, pass return ksw.keyStore.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text)) } -func (ksw *keyStoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { +func (ksw *keyStoreWallet) SignTx(account accounts.Account, + tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { if !ksw.Contains(account) { return nil, errors.New("unknown account") } @@ -88,7 +91,8 @@ func (ksw *keyStoreWallet) SignTx(account accounts.Account, tx *types.Transactio return ksw.keyStore.SignTx(account, tx, chainID) } -func (ksw *keyStoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { +func (ksw *keyStoreWallet) SignTxWithPassphrase(account accounts.Account, + passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { if !ksw.Contains(account) { return nil, errors.New("unknown account") } diff --git a/accounts/keystore/watch.go b/accounts/keystore/watch.go index 32489f9503..d143751db8 100644 --- a/accounts/keystore/watch.go +++ b/accounts/keystore/watch.go @@ -1,12 +1,15 @@ package keystore import ( + "os" "time" "github.com/fsnotify/fsnotify" + "github.com/hashicorp/go-hclog" ) type watcher struct { + logger hclog.Logger ac *accountCache running bool // set to true when runloop begins runEnded bool // set to true when runloop ends @@ -14,10 +17,11 @@ type watcher struct { quit chan struct{} } -func newWatcher(ac *accountCache) *watcher { +func newWatcher(ac *accountCache, logger hclog.Logger) *watcher { return &watcher{ - ac: ac, - quit: make(chan struct{}), + logger: logger, + ac: ac, + quit: make(chan struct{}), } } @@ -46,17 +50,22 @@ func (w *watcher) loop() { watcher, err := fsnotify.NewWatcher() if err != nil { - //TO DO add log + w.logger.Error("Failed to start filesystem watcher", "err", err) return } defer watcher.Close() if err := watcher.Add(w.ac.keydir); err != nil { - //TO DO logger + if !os.IsNotExist(err) { + w.logger.Info("Failed to watch keystore folder", "err", err) + } return } + w.logger.Trace("Started watching keystore folder", "folder", w.ac.keydir) + defer w.logger.Trace("Stopped watching keystore folder") + w.ac.mu.Lock() w.running = true w.ac.mu.Unlock() @@ -89,11 +98,12 @@ func (w *watcher) loop() { if !ok { return } - // TO DO log.Info("Filesystem watcher error", "err", err) + w.logger.Info("Filesystem watcher error", "err", err) case <-debounce.C: - w.ac.scanAccounts() + if err := w.ac.scanAccounts(); err != nil { + w.logger.Info("loop", "scanAccounts", err) + } rescanTriggered = false } } - } diff --git a/accounts/manager.go b/accounts/manager.go index ac604bd148..dfbf0608f8 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -47,6 +47,7 @@ func NewManager(config *Config, backends ...Backend) *Manager { updates := make(chan WalletEvent, managerSubBufferSize) subs := make([]event.Subscription, len(backends)) + for i, backend := range backends { subs[i] = backend.Subscribe(updates) } @@ -61,10 +62,12 @@ func NewManager(config *Config, backends ...Backend) *Manager { quit: make(chan chan error), term: make(chan struct{}), } + for _, backend := range backends { kind := reflect.TypeOf(backend) am.backends[kind] = append(am.backends[kind], backend) } + go am.update() return am @@ -94,9 +97,11 @@ func (am *Manager) AddBackend(backend Backend) { func (am *Manager) update() { defer func() { am.lock.Lock() + for _, sub := range am.updaters { sub.Unsubscribe() } + am.updaters = nil am.lock.Unlock() }() @@ -161,6 +166,7 @@ func (am *Manager) Wallet(url string) (Wallet, error) { if err != nil { return nil, err } + for _, wallet := range am.walletsNoLock() { if wallet.URL() == parsed { return wallet, nil @@ -175,6 +181,7 @@ func (am *Manager) Accounts() []types.Address { defer am.lock.RUnlock() addresses := make([]types.Address, 0) + for _, wallet := range am.wallets { for _, account := range wallet.Accounts() { addresses = append(addresses, account.Address) @@ -206,6 +213,7 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet { if n == len(slice) { continue } + slice = append(slice[:n], slice[n+1:]...) } return slice @@ -218,7 +226,9 @@ func drop(slice []Wallet, wallets ...Wallet) []Wallet { if n == len(slice) { continue } + slice = append(slice[:n], slice[n+1:]...) } + return slice } diff --git a/accounts/url_test.go b/accounts/url_test.go index de4368a348..2464484f9a 100644 --- a/accounts/url_test.go +++ b/accounts/url_test.go @@ -2,20 +2,26 @@ package accounts import "testing" +const ( + bladeURL = "blade.org" + bladeURLWithPrefix = "https://blade.org" + bladeURLJSON = "\"https://blade.org\"" +) + func TestURLParsing(t *testing.T) { t.Parallel() - url, err := parseURL("https://ethereum.org") + url, err := parseURL(bladeURLWithPrefix) if err != nil { t.Errorf("unexpected error: %v", err) } if url.Scheme != "https" { t.Errorf("expected: %v, got: %v", "https", url.Scheme) } - if url.Path != "ethereum.org" { - t.Errorf("expected: %v, got: %v", "ethereum.org", url.Path) + if url.Path != bladeURL { + t.Errorf("expected: %v, got: %v", bladeURL, url.Path) } - for _, u := range []string{"ethereum.org", ""} { + for _, u := range []string{bladeURL, ""} { if _, err = parseURL(u); err == nil { t.Errorf("input %v, expected err, got: nil", u) } @@ -24,40 +30,40 @@ func TestURLParsing(t *testing.T) { func TestURLString(t *testing.T) { t.Parallel() - url := URL{Scheme: "https", Path: "ethereum.org"} - if url.String() != "https://ethereum.org" { - t.Errorf("expected: %v, got: %v", "https://ethereum.org", url.String()) + url := URL{Scheme: "https", Path: bladeURL} + if url.String() != bladeURLWithPrefix { + t.Errorf("expected: %v, got: %v", bladeURLWithPrefix, url.String()) } - url = URL{Scheme: "", Path: "ethereum.org"} - if url.String() != "ethereum.org" { - t.Errorf("expected: %v, got: %v", "ethereum.org", url.String()) + url = URL{Scheme: "", Path: bladeURL} + if url.String() != bladeURL { + t.Errorf("expected: %v, got: %v", bladeURL, url.String()) } } func TestURLMarshalJSON(t *testing.T) { t.Parallel() - url := URL{Scheme: "https", Path: "ethereum.org"} + url := URL{Scheme: "https", Path: bladeURL} json, err := url.MarshalJSON() if err != nil { t.Errorf("unexpected error: %v", err) } - if string(json) != "\"https://ethereum.org\"" { - t.Errorf("expected: %v, got: %v", "\"https://ethereum.org\"", string(json)) + if string(json) != bladeURLJSON { + t.Errorf("expected: %v, got: %v", bladeURLJSON, string(json)) } } func TestURLUnmarshalJSON(t *testing.T) { t.Parallel() url := &URL{} - err := url.UnmarshalJSON([]byte("\"https://ethereum.org\"")) + err := url.UnmarshalJSON([]byte(bladeURLJSON)) if err != nil { t.Errorf("unexpected error: %v", err) } if url.Scheme != "https" { t.Errorf("expected: %v, got: %v", "https", url.Scheme) } - if url.Path != "ethereum.org" { + if url.Path != bladeURL { t.Errorf("expected: %v, got: %v", "https", url.Path) } } @@ -69,10 +75,10 @@ func TestURLComparison(t *testing.T) { urlB URL expect int }{ - {URL{"https", "ethereum.org"}, URL{"https", "ethereum.org"}, 0}, - {URL{"http", "ethereum.org"}, URL{"https", "ethereum.org"}, -1}, - {URL{"https", "ethereum.org/a"}, URL{"https", "ethereum.org"}, 1}, - {URL{"https", "abc.org"}, URL{"https", "ethereum.org"}, -1}, + {URL{"https", bladeURL}, URL{"https", bladeURL}, 0}, + {URL{"http", bladeURL}, URL{"https", bladeURL}, -1}, + {URL{"https", bladeURL + "/a"}, URL{"https", bladeURL}, 1}, + {URL{"https", "abc.org"}, URL{"https", bladeURL}, -1}, } for i, tt := range tests { diff --git a/consensus/polybft/wallet/account.go b/consensus/polybft/wallet/account.go index a467dc4d64..0a3903726f 100644 --- a/consensus/polybft/wallet/account.go +++ b/consensus/polybft/wallet/account.go @@ -119,5 +119,5 @@ func (a *Account) GetEcdsaPrivateKey() (*ecdsa.PrivateKey, error) { } func (a Account) Address() types.Address { - return types.Address(a.Ecdsa.Address()) + return a.Ecdsa.Address() } From c8195834c8a9e98969025606172e01385dad7612 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 04/69] feed test --- accounts/event/feed_test.go | 319 ++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 accounts/event/feed_test.go diff --git a/accounts/event/feed_test.go b/accounts/event/feed_test.go new file mode 100644 index 0000000000..b3aa1f673e --- /dev/null +++ b/accounts/event/feed_test.go @@ -0,0 +1,319 @@ +package event + +import ( + "errors" + "fmt" + "reflect" + "sync" + "testing" + "time" +) + +func TestFeedPanics(t *testing.T) { + { + var f Feed + f.Send(2) + want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(0)} + if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil { + t.Error(err) + } + } + { + var f Feed + ch := make(chan int) + f.Subscribe(ch) + want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(0)} + if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil { + t.Error(err) + } + } + { + var f Feed + f.Send(2) + want := feedTypeError{op: "Subscribe", got: reflect.TypeOf(make(chan uint64)), want: reflect.TypeOf(make(chan<- int))} + if err := checkPanic(want, func() { f.Subscribe(make(chan uint64)) }); err != nil { + t.Error(err) + } + } + { + var f Feed + if err := checkPanic(errBadChannel, func() { f.Subscribe(make(<-chan int)) }); err != nil { + t.Error(err) + } + } + { + var f Feed + if err := checkPanic(errBadChannel, func() { f.Subscribe(0) }); err != nil { + t.Error(err) + } + } +} + +func checkPanic(want error, fn func()) (err error) { + defer func() { + panic := recover() + if panic == nil { + err = errors.New("didn't panic") + } else if !reflect.DeepEqual(panic, want) { + err = fmt.Errorf("panicked with wrong error: got %q, want %q", panic, want) + } + }() + fn() + return nil +} + +func TestFeed(t *testing.T) { + var feed Feed + var done, subscribed sync.WaitGroup + subscriber := func(i int) { + defer done.Done() + + subchan := make(chan int) + sub := feed.Subscribe(subchan) + timeout := time.NewTimer(2 * time.Second) + defer timeout.Stop() + subscribed.Done() + + select { + case v := <-subchan: + if v != 1 { + t.Errorf("%d: received value %d, want 1", i, v) + } + case <-timeout.C: + t.Errorf("%d: receive timeout", i) + } + + sub.Unsubscribe() + select { + case _, ok := <-sub.Err(): + if ok { + t.Errorf("%d: error channel not closed after unsubscribe", i) + } + case <-timeout.C: + t.Errorf("%d: unsubscribe timeout", i) + } + } + + const n = 1000 + done.Add(n) + subscribed.Add(n) + for i := 0; i < n; i++ { + go subscriber(i) + } + subscribed.Wait() + if nsent := feed.Send(1); nsent != n { + t.Errorf("first send delivered %d times, want %d", nsent, n) + } + if nsent := feed.Send(2); nsent != 0 { + t.Errorf("second send delivered %d times, want 0", nsent) + } + done.Wait() +} + +func TestFeedSubscribeSameChannel(t *testing.T) { + var ( + feed Feed + done sync.WaitGroup + ch = make(chan int) + sub1 = feed.Subscribe(ch) + sub2 = feed.Subscribe(ch) + _ = feed.Subscribe(ch) + ) + expectSends := func(value, n int) { + if nsent := feed.Send(value); nsent != n { + t.Errorf("send delivered %d times, want %d", nsent, n) + } + done.Done() + } + expectRecv := func(wantValue, n int) { + for i := 0; i < n; i++ { + if v := <-ch; v != wantValue { + t.Errorf("received %d, want %d", v, wantValue) + } + } + } + + done.Add(1) + go expectSends(1, 3) + expectRecv(1, 3) + done.Wait() + + sub1.Unsubscribe() + + done.Add(1) + go expectSends(2, 2) + expectRecv(2, 2) + done.Wait() + + sub2.Unsubscribe() + + done.Add(1) + go expectSends(3, 1) + expectRecv(3, 1) + done.Wait() +} + +func TestFeedSubscribeBlockedPost(t *testing.T) { + var ( + feed Feed + nsends = 2000 + ch1 = make(chan int) + ch2 = make(chan int) + wg sync.WaitGroup + ) + defer wg.Wait() + + feed.Subscribe(ch1) + wg.Add(nsends) + for i := 0; i < nsends; i++ { + go func() { + feed.Send(99) + wg.Done() + }() + } + + sub2 := feed.Subscribe(ch2) + defer sub2.Unsubscribe() + + // We're done when ch1 has received N times. + // The number of receives on ch2 depends on scheduling. + for i := 0; i < nsends; { + select { + case <-ch1: + i++ + case <-ch2: + } + } +} + +func TestFeedUnsubscribeBlockedPost(t *testing.T) { + var ( + feed Feed + nsends = 200 + chans = make([]chan int, 2000) + subs = make([]Subscription, len(chans)) + bchan = make(chan int) + bsub = feed.Subscribe(bchan) + wg sync.WaitGroup + ) + for i := range chans { + chans[i] = make(chan int, nsends) + } + + // Queue up some Sends. None of these can make progress while bchan isn't read. + wg.Add(nsends) + for i := 0; i < nsends; i++ { + go func() { + feed.Send(99) + wg.Done() + }() + } + // Subscribe the other channels. + for i, ch := range chans { + subs[i] = feed.Subscribe(ch) + } + // Unsubscribe them again. + for _, sub := range subs { + sub.Unsubscribe() + } + // Unblock the Sends. + bsub.Unsubscribe() + wg.Wait() +} + +// Checks that unsubscribing a channel during Send works even if that +// channel has already been sent on. +func TestFeedUnsubscribeSentChan(t *testing.T) { + var ( + feed Feed + ch1 = make(chan int) + ch2 = make(chan int) + sub1 = feed.Subscribe(ch1) + sub2 = feed.Subscribe(ch2) + wg sync.WaitGroup + ) + defer sub2.Unsubscribe() + + wg.Add(1) + go func() { + feed.Send(0) + wg.Done() + }() + + // Wait for the value on ch1. + <-ch1 + // Unsubscribe ch1, removing it from the send cases. + sub1.Unsubscribe() + + // Receive ch2, finishing Send. + <-ch2 + wg.Wait() + + // Send again. This should send to ch2 only, so the wait group will unblock + // as soon as a value is received on ch2. + wg.Add(1) + go func() { + feed.Send(0) + wg.Done() + }() + <-ch2 + wg.Wait() +} + +func TestFeedUnsubscribeFromInbox(t *testing.T) { + var ( + feed Feed + ch1 = make(chan int) + ch2 = make(chan int) + sub1 = feed.Subscribe(ch1) + sub2 = feed.Subscribe(ch1) + sub3 = feed.Subscribe(ch2) + ) + if len(feed.inbox) != 3 { + t.Errorf("inbox length != 3 after subscribe") + } + if len(feed.sendCases) != 1 { + t.Errorf("sendCases is non-empty after unsubscribe") + } + + sub1.Unsubscribe() + sub2.Unsubscribe() + sub3.Unsubscribe() + if len(feed.inbox) != 0 { + t.Errorf("inbox is non-empty after unsubscribe") + } + if len(feed.sendCases) != 1 { + t.Errorf("sendCases is non-empty after unsubscribe") + } +} + +func BenchmarkFeedSend1000(b *testing.B) { + var ( + done sync.WaitGroup + feed Feed + nsubs = 1000 + ) + subscriber := func(ch <-chan int) { + for i := 0; i < b.N; i++ { + <-ch + } + done.Done() + } + done.Add(nsubs) + for i := 0; i < nsubs; i++ { + ch := make(chan int, 200) + feed.Subscribe(ch) + go subscriber(ch) + } + + // The actual benchmark. + b.ResetTimer() + for i := 0; i < b.N; i++ { + if feed.Send(i) != nsubs { + panic("wrong number of sends") + } + } + + b.StopTimer() + done.Wait() +} From 143282863617f66e715f50bf02044baadbe68d32 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 05/69] linters and test fix --- accounts/keystore/key.go | 8 ++++++++ accounts/keystore/plain_test.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index b054c60c96..2665478515 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -179,6 +179,7 @@ func newKey(rand io.Reader) (*Key, error) { if err != nil { return nil, err } + return newKeyFromECDSA(privateKeyECDSA), nil } @@ -188,15 +189,19 @@ func NewKeyForDirectICAP(rand io.Reader) *Key { if err != nil { panic("key generation: could not read from random source: " + err.Error()) //nolint:gocritic } + reader := bytes.NewReader(randBytes) privateKeyECDSA, err := ecdsa.GenerateKey(btcec.S256(), reader) if err != nil { panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) //nolint:gocritic } + key := newKeyFromECDSA(privateKeyECDSA) + if !strings.HasPrefix(key.Address.String(), "0x00") { return NewKeyForDirectICAP(rand) } + return key } @@ -205,14 +210,17 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou if err != nil { return nil, accounts.Account{}, err } + a := accounts.Account{ Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}, } + if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { zeroKey(key.PrivateKey) return nil, a, err } + return key, a, err } diff --git a/accounts/keystore/plain_test.go b/accounts/keystore/plain_test.go index 966ea8d10f..74ee6aba2c 100644 --- a/accounts/keystore/plain_test.go +++ b/accounts/keystore/plain_test.go @@ -123,6 +123,8 @@ func TestV3_PBKDF2_1(t *testing.T) { var testsSubmodule = filepath.Join("..", "..", "tests", "testdata", "KeyStoreTests") func skipIfSubmoduleMissing(t *testing.T) { + t.Helper() + if !common.FileExists(testsSubmodule) { t.Skipf("can't find JSON tests from submodule at %s", testsSubmodule) } @@ -188,6 +190,8 @@ func TestV1_2(t *testing.T) { } func testDecryptV3(t *testing.T, test KeyStoreTestV3) { + t.Helper() + privBytes, _, err := decryptKeyV3(&test.JSON, test.Password) if err != nil { t.Fatal(err) @@ -199,6 +203,8 @@ func testDecryptV3(t *testing.T, test KeyStoreTestV3) { } func testDecryptV1(t *testing.T, test KeyStoreTestV1) { + t.Helper() + privBytes, _, err := decryptKeyV1(&test.JSON, test.Password) if err != nil { t.Fatal(err) @@ -234,8 +240,8 @@ func loadKeyStoreTestV1(t *testing.T, file string) map[string]KeyStoreTestV1 { func TestKeyForDirectICAP(t *testing.T) { t.Parallel() key := NewKeyForDirectICAP(rand.Reader) - if !strings.HasPrefix(types.AddressToString(key.Address), "0x00") { - t.Errorf("Expected first address byte to be zero, have: %s", types.AddressToString(key.Address)) + if !strings.HasPrefix(key.Address.String(), "0x00") { + t.Errorf("Expected first address byte to be zero, have: %s", key.Address.String()) } } From 05b1bb03e44b6e477e9a3965a2df32e722c21358 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 06/69] lint fix --- accounts/accounts.go | 1 + accounts/accounts_test.go | 2 + accounts/event/feed.go | 1 + accounts/event/feed_test.go | 61 ++++++++++++++++++++-- accounts/event/subscription.go | 2 + accounts/hd.go | 6 +++ accounts/hd_test.go | 3 ++ accounts/helper.go | 7 +++ accounts/keystore/account_cache.go | 28 +++++++++++ accounts/keystore/account_cache_test.go | 50 ++++++++++++++++++ accounts/keystore/file_cache.go | 3 ++ accounts/keystore/key.go | 21 ++++++++ accounts/keystore/keystore.go | 35 +++++++++++++ accounts/keystore/keystore_fuzz_test.go | 2 + accounts/keystore/keystore_test.go | 67 +++++++++++++++++++++++++ accounts/keystore/passphrase.go | 40 ++++++++++++++- accounts/keystore/passphrase_test.go | 5 +- accounts/keystore/plain.go | 7 +++ accounts/keystore/plain_test.go | 38 ++++++++++++++ accounts/keystore/presale.go | 18 +++++++ accounts/keystore/watch.go | 11 ++++ accounts/manager.go | 6 +++ accounts/url.go | 9 ++++ accounts/url_test.go | 16 ++++++ 24 files changed, 432 insertions(+), 7 deletions(-) diff --git a/accounts/accounts.go b/accounts/accounts.go index fb3b44b080..27a6b101f6 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -129,6 +129,7 @@ type Wallet interface { func TextHash(data []byte) []byte { hash, _ := TextAndHash(data) + return hash } diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index df617e287f..6542534544 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -9,8 +9,10 @@ import ( func TestTextHash(t *testing.T) { t.Parallel() + hash := TextHash([]byte("Hello Joe")) want := types.StringToBytes("0xa080337ae51c4e064c189e113edd0ba391df9206e2f49db658bb32cf2911730b") + if !bytes.Equal(hash, want) { t.Fatalf("wrong hash: %x", hash) } diff --git a/accounts/event/feed.go b/accounts/event/feed.go index c08fb8cc66..4598f8b6da 100644 --- a/accounts/event/feed.go +++ b/accounts/event/feed.go @@ -84,6 +84,7 @@ func (f *Feed) remove(sub *feedSub) { // Delete from inbox first, which covers channels // that have not been added to f.sendCases yet. ch := sub.channel.Interface() + f.mu.Lock() index := f.inbox.find(ch) diff --git a/accounts/event/feed_test.go b/accounts/event/feed_test.go index b3aa1f673e..1c4e2bc078 100644 --- a/accounts/event/feed_test.go +++ b/accounts/event/feed_test.go @@ -12,37 +12,48 @@ import ( func TestFeedPanics(t *testing.T) { { var f Feed + f.Send(2) + want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(0)} + if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil { t.Error(err) } } { var f Feed + ch := make(chan int) f.Subscribe(ch) + want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(0)} + if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil { t.Error(err) } } { var f Feed + f.Send(2) + want := feedTypeError{op: "Subscribe", got: reflect.TypeOf(make(chan uint64)), want: reflect.TypeOf(make(chan<- int))} + if err := checkPanic(want, func() { f.Subscribe(make(chan uint64)) }); err != nil { t.Error(err) } } { var f Feed + if err := checkPanic(errBadChannel, func() { f.Subscribe(make(<-chan int)) }); err != nil { t.Error(err) } } { var f Feed + if err := checkPanic(errBadChannel, func() { f.Subscribe(0) }); err != nil { t.Error(err) } @@ -51,27 +62,33 @@ func TestFeedPanics(t *testing.T) { func checkPanic(want error, fn func()) (err error) { defer func() { - panic := recover() - if panic == nil { + panicErr := recover() + if panicErr == nil { err = errors.New("didn't panic") - } else if !reflect.DeepEqual(panic, want) { - err = fmt.Errorf("panicked with wrong error: got %q, want %q", panic, want) + } else if !reflect.DeepEqual(panicErr, want) { + err = fmt.Errorf("panicked with wrong error: got %w, want %w", panicErr, want) } }() + fn() + return nil } func TestFeed(t *testing.T) { var feed Feed + var done, subscribed sync.WaitGroup + subscriber := func(i int) { defer done.Done() subchan := make(chan int) sub := feed.Subscribe(subchan) timeout := time.NewTimer(2 * time.Second) + defer timeout.Stop() + subscribed.Done() select { @@ -95,18 +112,24 @@ func TestFeed(t *testing.T) { } const n = 1000 + done.Add(n) subscribed.Add(n) + for i := 0; i < n; i++ { go subscriber(i) } + subscribed.Wait() + if nsent := feed.Send(1); nsent != n { t.Errorf("first send delivered %d times, want %d", nsent, n) } + if nsent := feed.Send(2); nsent != 0 { t.Errorf("second send delivered %d times, want 0", nsent) } + done.Wait() } @@ -119,12 +142,15 @@ func TestFeedSubscribeSameChannel(t *testing.T) { sub2 = feed.Subscribe(ch) _ = feed.Subscribe(ch) ) + expectSends := func(value, n int) { if nsent := feed.Send(value); nsent != n { t.Errorf("send delivered %d times, want %d", nsent, n) } + done.Done() } + expectRecv := func(wantValue, n int) { for i := 0; i < n; i++ { if v := <-ch; v != wantValue { @@ -134,21 +160,27 @@ func TestFeedSubscribeSameChannel(t *testing.T) { } done.Add(1) + go expectSends(1, 3) + expectRecv(1, 3) done.Wait() sub1.Unsubscribe() done.Add(1) + go expectSends(2, 2) + expectRecv(2, 2) done.Wait() sub2.Unsubscribe() done.Add(1) + go expectSends(3, 1) + expectRecv(3, 1) done.Wait() } @@ -161,10 +193,12 @@ func TestFeedSubscribeBlockedPost(t *testing.T) { ch2 = make(chan int) wg sync.WaitGroup ) + defer wg.Wait() feed.Subscribe(ch1) wg.Add(nsends) + for i := 0; i < nsends; i++ { go func() { feed.Send(99) @@ -173,6 +207,7 @@ func TestFeedSubscribeBlockedPost(t *testing.T) { } sub2 := feed.Subscribe(ch2) + defer sub2.Unsubscribe() // We're done when ch1 has received N times. @@ -196,12 +231,14 @@ func TestFeedUnsubscribeBlockedPost(t *testing.T) { bsub = feed.Subscribe(bchan) wg sync.WaitGroup ) + for i := range chans { chans[i] = make(chan int, nsends) } // Queue up some Sends. None of these can make progress while bchan isn't read. wg.Add(nsends) + for i := 0; i < nsends; i++ { go func() { feed.Send(99) @@ -232,9 +269,11 @@ func TestFeedUnsubscribeSentChan(t *testing.T) { sub2 = feed.Subscribe(ch2) wg sync.WaitGroup ) + defer sub2.Unsubscribe() wg.Add(1) + go func() { feed.Send(0) wg.Done() @@ -252,10 +291,12 @@ func TestFeedUnsubscribeSentChan(t *testing.T) { // Send again. This should send to ch2 only, so the wait group will unblock // as soon as a value is received on ch2. wg.Add(1) + go func() { feed.Send(0) wg.Done() }() + <-ch2 wg.Wait() } @@ -269,9 +310,11 @@ func TestFeedUnsubscribeFromInbox(t *testing.T) { sub2 = feed.Subscribe(ch1) sub3 = feed.Subscribe(ch2) ) + if len(feed.inbox) != 3 { t.Errorf("inbox length != 3 after subscribe") } + if len(feed.sendCases) != 1 { t.Errorf("sendCases is non-empty after unsubscribe") } @@ -279,9 +322,11 @@ func TestFeedUnsubscribeFromInbox(t *testing.T) { sub1.Unsubscribe() sub2.Unsubscribe() sub3.Unsubscribe() + if len(feed.inbox) != 0 { t.Errorf("inbox is non-empty after unsubscribe") } + if len(feed.sendCases) != 1 { t.Errorf("sendCases is non-empty after unsubscribe") } @@ -293,24 +338,30 @@ func BenchmarkFeedSend1000(b *testing.B) { feed Feed nsubs = 1000 ) + subscriber := func(ch <-chan int) { for i := 0; i < b.N; i++ { <-ch } + done.Done() } + done.Add(nsubs) + for i := 0; i < nsubs; i++ { ch := make(chan int, 200) feed.Subscribe(ch) + go subscriber(ch) } // The actual benchmark. b.ResetTimer() + for i := 0; i < b.N; i++ { if feed.Send(i) != nsubs { - panic("wrong number of sends") + panic("wrong number of sends") //nolint:gocritic } } diff --git a/accounts/event/subscription.go b/accounts/event/subscription.go index 853925ce28..c9a143075f 100644 --- a/accounts/event/subscription.go +++ b/accounts/event/subscription.go @@ -59,11 +59,13 @@ func (sc *SubscriptionScope) Track(s Subscription) Subscription { func (sc *SubscriptionScope) Close() { sc.mu.Lock() defer sc.mu.Unlock() + if sc.closed { return } sc.closed = true + for s := range sc.subs { s.s.Unsubscribe() } diff --git a/accounts/hd.go b/accounts/hd.go index e13aa68d38..559abbc046 100644 --- a/accounts/hd.go +++ b/accounts/hd.go @@ -123,6 +123,7 @@ func (path DerivationPath) MarshalJSON() ([]byte, error) { func (path *DerivationPath) UnmarshalJSON(b []byte) error { var dp string + var err error if err = json.Unmarshal(b, &dp); err != nil { @@ -130,6 +131,7 @@ func (path *DerivationPath) UnmarshalJSON(b []byte) error { } *path, err = ParseDerivationPath(dp) + return err } @@ -137,10 +139,12 @@ func DefaultIterator(base DerivationPath) func() DerivationPath { path := make(DerivationPath, len(base)) copy(path[:], base[:]) + path[len(path)-1]-- return func() DerivationPath { path[len(path)-1]++ + return path } } @@ -149,10 +153,12 @@ func LedgerLiveIterator(base DerivationPath) func() DerivationPath { path := make(DerivationPath, len(base)) copy(path[:], base[:]) + path[2]-- return func() DerivationPath { path[2]++ + return path } } diff --git a/accounts/hd_test.go b/accounts/hd_test.go index acd73eeb5a..9ec71c8472 100644 --- a/accounts/hd_test.go +++ b/accounts/hd_test.go @@ -8,6 +8,7 @@ import ( func TestHDPathParsing(t *testing.T) { t.Parallel() + tests := []struct { input string output DerivationPath @@ -65,6 +66,7 @@ func TestHDPathParsing(t *testing.T) { func testDerive(t *testing.T, next func() DerivationPath, expected []string) { t.Helper() + for i, want := range expected { if have := next(); fmt.Sprintf("%v", have) != want { t.Errorf("step %d, have %v, want %v", i, have, want) @@ -74,6 +76,7 @@ func testDerive(t *testing.T, next func() DerivationPath, expected []string) { func TestHdPathIteration(t *testing.T) { t.Parallel() + testDerive(t, DefaultIterator(DefaultBaseDerivationPath), []string{ "m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1", diff --git a/accounts/helper.go b/accounts/helper.go index c870e676af..1a61ef8f7f 100644 --- a/accounts/helper.go +++ b/accounts/helper.go @@ -51,12 +51,15 @@ type AmbiguousAddrError struct { func (err *AmbiguousAddrError) Error() string { files := "" + for i, a := range err.Matches { files += a.URL.Path + if i < len(err.Matches)-1 { files += ", " } } + return fmt.Sprintf("multiple keys match address (%s)", files) } @@ -65,13 +68,17 @@ func LoadJSON(file string, val interface{}) error { if err != nil { return err } + if err := json.Unmarshal(content, val); err != nil { if syntaxerr, ok := err.(*json.SyntaxError); ok { //nolint:errorlint line := findLine(content, syntaxerr.Offset) + return fmt.Errorf("JSON syntax error at %v:%v: %w", file, line, err) } + return fmt.Errorf("JSON unmarshal error in %v: %w", file, err) } + return nil } diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 3523a24374..9ab9123b9a 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -55,15 +55,19 @@ func (ac *accountCache) accounts() []accounts.Account { ac.maybeReload() ac.mu.Lock() defer ac.mu.Unlock() + cpy := make([]accounts.Account, len(ac.all)) copy(cpy, ac.all) + return cpy } func (ac *accountCache) hasAddress(addr types.Address) bool { ac.maybeReload() + ac.mu.Lock() defer ac.mu.Unlock() + return len(ac.byAddr[addr]) > 0 } @@ -75,6 +79,7 @@ func (ac *accountCache) add(newAccount accounts.Account) { if i < len(ac.all) && ac.all[i] == newAccount { return } + // newAccount is not in the cache. ac.all = append(ac.all, accounts.Account{}) copy(ac.all[i+1:], ac.all[i:]) @@ -99,11 +104,13 @@ func (ac *accountCache) delete(removed accounts.Account) { func (ac *accountCache) deleteByFile(path string) { ac.mu.Lock() defer ac.mu.Unlock() + i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Path >= path }) if i < len(ac.all) && ac.all[i].URL.Path == path { removed := ac.all[i] ac.all = append(ac.all[:i], ac.all[i+1:]...) + if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { delete(ac.byAddr, removed.Address) } else { @@ -116,7 +123,9 @@ func (ac *accountCache) deleteByFile(path string) { // has since also ended). func (ac *accountCache) watcherStarted() bool { ac.mu.Lock() + defer ac.mu.Unlock() + return ac.watcher.running || ac.watcher.runEnded } @@ -126,6 +135,7 @@ func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.A return append(slice[:i], slice[i+1:]...) } } + return slice } @@ -138,20 +148,24 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { if (a.Address != types.Address{}) { matches = ac.byAddr[a.Address] } + if a.URL.Path != "" { // If only the basename is specified, complete the path. if !strings.ContainsRune(a.URL.Path, filepath.Separator) { a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) } + for i := range matches { if matches[i].URL == a.URL { return matches[i], nil } } + if (a.Address == types.Address{}) { return accounts.Account{}, accounts.ErrNoMatch } } + switch len(matches) { case 1: return matches[0], nil @@ -161,6 +175,7 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { err := &accounts.AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} copy(err.Matches, matches) slices.SortFunc(err.Matches, byURL) + return accounts.Account{}, err } } @@ -170,8 +185,10 @@ func (ac *accountCache) maybeReload() { if ac.watcher.running { ac.mu.Unlock() + return // A watcher is running and will keep the cache up-to-date. } + if ac.throttle == nil { ac.throttle = time.NewTimer(0) } else { @@ -179,6 +196,7 @@ func (ac *accountCache) maybeReload() { case <-ac.throttle.C: default: ac.mu.Unlock() + return // The cache was reloaded recently. } } @@ -186,20 +204,25 @@ func (ac *accountCache) maybeReload() { ac.watcher.start() ac.throttle.Reset(minReloadInterval) ac.mu.Unlock() + if err := ac.scanAccounts(); err != nil { ac.logger.Info("reload", "failed to scan accounts", err) } } + func (ac *accountCache) close() { ac.mu.Lock() ac.watcher.close() + if ac.throttle != nil { ac.throttle.Stop() } + if ac.notify != nil { close(ac.notify) ac.notify = nil } + ac.mu.Unlock() } @@ -210,8 +233,10 @@ func (ac *accountCache) scanAccounts() error { creates, deletes, updates, err := ac.fileC.scan(ac.keydir) if err != nil { ac.logger.Debug("Failed to reload keystore contents", "err", err) + return err } + if creates.Cardinality() == 0 && deletes.Cardinality() == 0 && updates.Cardinality() == 0 { return nil } @@ -222,14 +247,17 @@ func (ac *accountCache) scanAccounts() error { Address string `json:"address"` } ) + readAccount := func(path string) *accounts.Account { fd, err := os.Open(path) if err != nil { ac.logger.Trace("Failed to open keystore file", "path", path, "err", err) + return nil } defer fd.Close() + buf.Reset(fd) // Parse the address. key.Address = "" diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 04c1776647..3307d14d63 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -69,6 +69,7 @@ func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { return nil } } + return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts) } @@ -79,6 +80,7 @@ func TestWatchNewFile(t *testing.T) { // Ensure the watcher is started before adding any files. ks.Accounts() + if !waitWatcherStart(ks) { t.Fatal("keystore watcher didn't start in time") } @@ -89,6 +91,7 @@ func TestWatchNewFile(t *testing.T) { Address: cachetestAccounts[i].Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, } + if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { t.Fatal(err) } @@ -102,10 +105,12 @@ func TestWatchNewFile(t *testing.T) { func TestWatchNoDir(t *testing.T) { t.Parallel() + // Create ks but not the directory that it watches. dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watchnodir-test-%d-%d", os.Getpid(), rand.Int())) ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) list := ks.Accounts() + if len(list) > 0 { t.Error("initial account list not empty:", list) } @@ -115,8 +120,11 @@ func TestWatchNoDir(t *testing.T) { } // Create the directory and copy a key file into it. require.NoError(t, os.MkdirAll(dir, 0700)) + defer os.RemoveAll(dir) + file := filepath.Join(dir, "aaa") + if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { t.Fatal(err) } @@ -124,8 +132,10 @@ func TestWatchNoDir(t *testing.T) { // ks should see the account. wantAccounts := []accounts.Account{cachetestAccounts[0]} wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { list = ks.Accounts() + if reflect.DeepEqual(list, wantAccounts) { // ks should have also received change notifications select { @@ -133,17 +143,22 @@ func TestWatchNoDir(t *testing.T) { default: t.Fatalf("wasn't notified of new accounts") } + return } + time.Sleep(d) } + t.Errorf("\ngot %v\nwant %v", list, wantAccounts) } func TestCacheInitialReload(t *testing.T) { t.Parallel() + cache, _ := newAccountCache(cachetestDir, hclog.NewNullLogger()) accounts := cache.accounts() + if !reflect.DeepEqual(accounts, cachetestAccounts) { t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) } @@ -151,6 +166,7 @@ func TestCacheInitialReload(t *testing.T) { func TestCacheAddDeleteOrder(t *testing.T) { t.Parallel() + cache, _ := newAccountCache("testdata/no-such-dir", hclog.NewNullLogger()) cache.watcher.running = true // prevent unexpected reloads @@ -184,6 +200,7 @@ func TestCacheAddDeleteOrder(t *testing.T) { URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, }, } + for _, a := range accs { cache.add(a) } @@ -193,17 +210,23 @@ func TestCacheAddDeleteOrder(t *testing.T) { // Check that the account list is sorted by filename. wantAccounts := make([]accounts.Account, len(accs)) + copy(wantAccounts, accs) + slices.SortFunc(wantAccounts, byURL) + list := cache.accounts() + if !reflect.DeepEqual(list, wantAccounts) { t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) } + for _, a := range accs { if !cache.hasAddress(a.Address) { t.Errorf("expected hasAccount(%x) to return true", a.Address) } } + if cache.hasAddress(types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { t.Errorf("expected hasAccount(%x) to return false", types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) } @@ -212,6 +235,7 @@ func TestCacheAddDeleteOrder(t *testing.T) { for i := 0; i < len(accs); i += 2 { cache.delete(wantAccounts[i]) } + cache.delete(accounts.Account{Address: types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) // Check content again after deletion. @@ -220,15 +244,18 @@ func TestCacheAddDeleteOrder(t *testing.T) { wantAccounts[3], wantAccounts[5], } + list = cache.accounts() if !reflect.DeepEqual(list, wantAccountsAfterDelete) { t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) } + for _, a := range wantAccountsAfterDelete { if !cache.hasAddress(a.Address) { t.Errorf("expected hasAccount(%x) to return true", a.Address) } } + if cache.hasAddress(wantAccounts[0].Address) { t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) } @@ -236,8 +263,11 @@ func TestCacheAddDeleteOrder(t *testing.T) { func TestCacheFind(t *testing.T) { t.Parallel() + dir := filepath.Join("testdata", "dir") + cache, _ := newAccountCache(dir, hclog.NewNullLogger()) + cache.watcher.running = true // prevent unexpected reloads accs := []accounts.Account{ @@ -258,6 +288,7 @@ func TestCacheFind(t *testing.T) { URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, }, } + for _, a := range accs { cache.add(a) } @@ -266,6 +297,7 @@ func TestCacheFind(t *testing.T) { Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, } + tests := []struct { Query accounts.Account WantResult accounts.Account @@ -295,6 +327,7 @@ func TestCacheFind(t *testing.T) { {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: accounts.ErrNoMatch}, {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: accounts.ErrNoMatch}, } + for i, test := range tests { a, err := cache.find(test.Query) if !reflect.DeepEqual(err, test.WantError) { @@ -302,6 +335,7 @@ func TestCacheFind(t *testing.T) { continue } + if a != test.WantResult { t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) @@ -323,12 +357,14 @@ func TestUpdatedKeyfileContents(t *testing.T) { if len(list) > 0 { t.Error("initial account list not empty:", list) } + if !waitWatcherStart(ks) { t.Fatal("keystore watcher didn't start in time") } // Create the directory and copy a key file into it. require.NoError(t, os.MkdirAll(dir, 0700)) defer os.RemoveAll(dir) + file := filepath.Join(dir, "aaa") // Place one of our testfiles in there @@ -339,8 +375,10 @@ func TestUpdatedKeyfileContents(t *testing.T) { // ks should see the account. wantAccounts := []accounts.Account{cachetestAccounts[0]} wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + if err := waitForAccounts(wantAccounts, ks); err != nil { t.Error(err) + return } // needed so that modTime of `file` is different to its current value after forceCopyFile @@ -349,13 +387,17 @@ func TestUpdatedKeyfileContents(t *testing.T) { // Now replace file contents if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil { t.Fatal(err) + return } + wantAccounts = []accounts.Account{cachetestAccounts[1]} wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + if err := waitForAccounts(wantAccounts, ks); err != nil { t.Errorf("First replacement failed") t.Error(err) + return } @@ -365,13 +407,17 @@ func TestUpdatedKeyfileContents(t *testing.T) { // Now replace file contents again if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil { t.Fatal(err) + return } + wantAccounts = []accounts.Account{cachetestAccounts[2]} wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + if err := waitForAccounts(wantAccounts, ks); err != nil { t.Errorf("Second replacement failed") t.Error(err) + return } @@ -381,11 +427,14 @@ func TestUpdatedKeyfileContents(t *testing.T) { // Now replace file contents with crap if err := os.WriteFile(file, []byte("foo"), 0600); err != nil { t.Fatal(err) + return } + if err := waitForAccounts([]accounts.Account{}, ks); err != nil { t.Errorf("Emptying account file failed") t.Error(err) + return } } @@ -396,5 +445,6 @@ func forceCopyFile(dst, src string) error { if err != nil { return err } + return os.WriteFile(dst, data, 0644) } diff --git a/accounts/keystore/file_cache.go b/accounts/keystore/file_cache.go index 98f3f09b91..d41edc9115 100644 --- a/accounts/keystore/file_cache.go +++ b/accounts/keystore/file_cache.go @@ -31,6 +31,7 @@ func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string] mods := mapset.NewThreadUnsafeSet[string]() var newLastMod time.Time + for _, fi := range files { if nonKeyFile(fi) { continue @@ -49,6 +50,7 @@ func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string] if modified.After(fc.lastMod) { mods.Add(path) } + if modified.After(newLastMod) { newLastMod = modified } @@ -73,5 +75,6 @@ func nonKeyFile(fi os.DirEntry) bool { if fi.IsDir() || !fi.Type().IsRegular() { return true } + return false } diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 2665478515..33813de308 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -87,27 +87,34 @@ func (k *Key) MarshalJSON() (j []byte, err error) { k.ID.String(), version, } + j, err = json.Marshal(jStruct) + return j, err } func (k *Key) UnmarshalJSON(j []byte) (err error) { keyJSON := new(plainKeyJSON) + err = json.Unmarshal(j, &keyJSON) if err != nil { return err } u := new(uuid.UUID) + *u, err = uuid.Parse(keyJSON.ID) if err != nil { return err } + k.ID = *u + addr, err := hex.DecodeString(keyJSON.Address) if err != nil { return err } + privkey, err := crypto.HexToECDSA(keyJSON.PrivateKey) if err != nil { return err @@ -125,11 +132,13 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { if err != nil { panic(fmt.Sprintf("Could not create random uuid: %v", err)) //nolint:gocritic } + key := &Key{ ID: id, Address: crypto.PubKeyToAddress(&privateKeyECDSA.PublicKey), //TO DO get more time for this pointer PrivateKey: privateKeyECDSA, } + return key } @@ -137,17 +146,21 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { // UTC---
func keyFileName(keyAddr types.Address) string { ts := time.Now().UTC() + return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) } func toISO8601(t time.Time) string { var tz string + name, offset := t.Zone() + if name == "UTC" { tz = "Z" } else { tz = fmt.Sprintf("%03d00", offset/3600) } + return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) } @@ -165,12 +178,16 @@ func writeTemporaryKeyFile(file string, content []byte) (string, error) { if err != nil { return "", err } + if _, err := f.Write(content); err != nil { f.Close() os.Remove(f.Name()) + return "", err } + f.Close() + return f.Name(), nil } @@ -186,11 +203,13 @@ func newKey(rand io.Reader) (*Key, error) { func NewKeyForDirectICAP(rand io.Reader) *Key { randBytes := make([]byte, 64) _, err := rand.Read(randBytes) + if err != nil { panic("key generation: could not read from random source: " + err.Error()) //nolint:gocritic } reader := bytes.NewReader(randBytes) + privateKeyECDSA, err := ecdsa.GenerateKey(btcec.S256(), reader) if err != nil { panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) //nolint:gocritic @@ -218,6 +237,7 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { zeroKey(key.PrivateKey) + return nil, a, err } @@ -229,5 +249,6 @@ func writeKeyFile(file string, content []byte) error { if err != nil { return err } + return os.Rename(name, file) } diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index f82511ecfe..adf7c7b586 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -56,6 +56,7 @@ func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyS keyDir, _ = filepath.Abs(keyDir) ks := &KeyStore{storage: &keyStorePassphrase{keyDir, scryptN, scryptP, false}} ks.init(keyDir, logger) + return ks } @@ -72,6 +73,7 @@ func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { accs := ks.cache.accounts() ks.wallets = make([]accounts.Wallet, len(accs)) + for i := 0; i < len(accs); i++ { ks.wallets[i] = &keyStoreWallet{account: accs[i], keyStore: ks} } @@ -85,7 +87,9 @@ func (ks *KeyStore) Wallets() []accounts.Wallet { defer ks.mu.RUnlock() cpy := make([]accounts.Wallet, len(ks.wallets)) + copy(cpy, ks.wallets) + return cpy } @@ -164,7 +168,9 @@ func (ks *KeyStore) updater() { ks.mu.Lock() if ks.updateScope.Count() == 0 { ks.updating = false + ks.mu.Unlock() + return } ks.mu.Unlock() @@ -187,6 +193,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { if key != nil { zeroKey(key.PrivateKey) } + if err != nil { return err } @@ -198,6 +205,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { ks.cache.delete(a) ks.refreshWallets() } + return err } @@ -224,6 +232,7 @@ func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *b } signer := crypto.LatestSignerForChainID(chainID.Uint64()) + return signer.SignTx(tx, unlockedKey.PrivateKey) } @@ -261,12 +270,14 @@ func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error { // Lock removes the private key with the given address from memory. func (ks *KeyStore) Lock(addr types.Address) error { ks.mu.Lock() + if unl, found := ks.unlocked[addr]; found { ks.mu.Unlock() ks.expire(addr, unl, time.Duration(0)*time.Nanosecond) } else { ks.mu.Unlock() } + return nil } @@ -278,10 +289,12 @@ func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout t ks.mu.Lock() defer ks.mu.Unlock() + u, ok := ks.unlocked[a.Address] if ok { if u.abort == nil { zeroKey(key.PrivateKey) + return nil } @@ -290,12 +303,14 @@ func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout t if timeout > 0 { u = &unlocked{Key: key, abort: make(chan struct{})} + go ks.expire(a.Address, u, timeout) } else { u = &unlocked{Key: key} } ks.unlocked[a.Address] = u + return nil } @@ -304,12 +319,14 @@ func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) { ks.cache.mu.Lock() a, err := ks.cache.find(a) ks.cache.mu.Unlock() + return a, err } func (ks *KeyStore) expire(addr types.Address, u *unlocked, timeout time.Duration) { t := time.NewTimer(timeout) defer t.Stop() + select { case <-u.abort: // just quit @@ -323,6 +340,7 @@ func (ks *KeyStore) expire(addr types.Address, u *unlocked, timeout time.Duratio zeroKey(u.PrivateKey) delete(ks.unlocked, addr) } + ks.mu.Unlock() } } @@ -332,7 +350,9 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A if err != nil { return a, nil, err } + key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth) + return a, key, err } @@ -345,6 +365,7 @@ func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { ks.cache.add(account) ks.refreshWallets() + return account, nil } @@ -353,23 +374,29 @@ func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) if err != nil { return nil, err } + var N, P int + if store, ok := ks.storage.(*keyStorePassphrase); ok { N, P = store.scryptN, store.scryptP } else { N, P = StandardScryptN, StandardScryptP } + return EncryptKey(key, newPassphrase, N, P) } func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) { key, err := DecryptKey(keyJSON, passphrase) + if key != nil && key.PrivateKey != nil { defer zeroKey(key.PrivateKey) } + if err != nil { return accounts.Account{}, err } + ks.importMu.Lock() defer ks.importMu.Unlock() @@ -392,17 +419,21 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco Address: key.Address, }, ErrAccountAlreadyExists } + return ks.importKey(key, passphrase) } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} + if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { return accounts.Account{}, err } + ks.cache.add(a) ks.refreshWallets() + return a, nil } @@ -417,16 +448,20 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase) + if err != nil { return a, err } + ks.cache.add(a) ks.refreshWallets() + return a, nil } func (ks *KeyStore) isUpdating() bool { ks.mu.RLock() defer ks.mu.RUnlock() + return ks.updating } diff --git a/accounts/keystore/keystore_fuzz_test.go b/accounts/keystore/keystore_fuzz_test.go index a00b8bd0e9..a793ead816 100644 --- a/accounts/keystore/keystore_fuzz_test.go +++ b/accounts/keystore/keystore_fuzz_test.go @@ -9,10 +9,12 @@ import ( func FuzzPassword(f *testing.F) { f.Fuzz(func(t *testing.T, password string) { ks := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger()) + a, err := ks.NewAccount(password) if err != nil { t.Fatal(err) } + if err := ks.Unlock(a, password); err != nil { t.Fatal(err) } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 7d8b1f39b6..9d01e2efcf 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -26,34 +26,43 @@ const pass = "foo" func TestKeyStore(t *testing.T) { t.Parallel() + dir, ks := tmpKeyStore(t) a, err := ks.NewAccount(pass) 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, pass, "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.FileExists(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) } @@ -61,16 +70,20 @@ func TestKeyStore(t *testing.T) { func TestSign(t *testing.T) { t.Parallel() + _, ks := tmpKeyStore(t) 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) } @@ -81,6 +94,7 @@ func TestSignWithPassphrase(t *testing.T) { _, ks := tmpKeyStore(t) pass := "passwd" + acc, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) @@ -132,6 +146,7 @@ func TestTimedUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) if !errors.Is(err, ErrLocked) { t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) @@ -171,6 +186,7 @@ func TestOverrideUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) if !errors.Is(err, ErrLocked) { t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) @@ -191,16 +207,21 @@ func TestSignRace(t *testing.T) { if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil { t.Fatal("could not unlock the test account", err) } + end := time.Now().UTC().Add(500 * time.Millisecond) + for time.Now().UTC().Before(end) { if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); errors.Is(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") } @@ -215,8 +236,10 @@ func waitForKsUpdating(t *testing.T, ks *KeyStore, wantStatus bool, maxTime time if ks.isUpdating() == wantStatus { return true } + time.Sleep(25 * time.Millisecond) } + return false } @@ -240,6 +263,7 @@ func TestWalletNotifierLifecycle(t *testing.T) { for i := 0; i < len(subs); i++ { // Create a new subscription subs[i] = ks.Subscribe(updates) + if !waitForKsUpdating(t, ks, true, 250*time.Millisecond) { t.Errorf("sub %d: wallet notifier not running after subscription", i) } @@ -257,6 +281,7 @@ func TestWalletNotifierLifecycle(t *testing.T) { } // Unsubscribe the last one and ensure the updater terminates eventually. subs[len(subs)-1].Unsubscribe() + if !waitForKsUpdating(t, ks, false, 4*time.Second) { t.Errorf("wallet notifier didn't terminate after unsubscribe") } @@ -279,7 +304,9 @@ func TestWalletNotifications(t *testing.T) { updates = make(chan accounts.WalletEvent) sub = ks.Subscribe(updates) ) + defer sub.Unsubscribe() + go func() { for { select { @@ -287,6 +314,7 @@ func TestWalletNotifications(t *testing.T) { events = append(events, walletEvent{ev, ev.Wallet.Accounts()[0]}) case <-sub.Err(): close(updates) + return } } @@ -297,6 +325,7 @@ func TestWalletNotifications(t *testing.T) { live = make(map[types.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 @@ -304,19 +333,23 @@ func TestWalletNotifications(t *testing.T) { 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}) } @@ -324,9 +357,11 @@ func TestWalletNotifications(t *testing.T) { // 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) } @@ -334,17 +369,22 @@ func TestWalletNotifications(t *testing.T) { // TestImportECDSA tests the import functionality of a keystore. func TestImportECDSA(t *testing.T) { t.Parallel() + _, ks := tmpKeyStore(t) + key, err := crypto.GenerateECDSAPrivateKey() 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") } @@ -353,26 +393,34 @@ func TestImportECDSA(t *testing.T) { // TestImportExport tests the import and export functionality of a keystore. func TestImportExport(t *testing.T) { t.Parallel() + _, ks := tmpKeyStore(t) + 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) + 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") } @@ -382,28 +430,38 @@ func TestImportExport(t *testing.T) { // This test should fail under -race if importing races. func TestImportRace(t *testing.T) { t.Parallel() + _, ks := tmpKeyStore(t) + 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) + var atom atomic.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 { atom.Add(1) } }() } wg.Wait() + if atom.Load() != 1 { t.Errorf("Import is racy") } @@ -415,13 +473,18 @@ func checkAccounts(t *testing.T, live map[types.Address]accounts.Account, 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) } + slices.SortFunc(liveList, byURL) + 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)) @@ -437,12 +500,15 @@ 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) } @@ -453,5 +519,6 @@ func tmpKeyStore(t *testing.T) (string, *KeyStore) { t.Helper() d := t.TempDir() + return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) } diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 2024ed3af4..31b694f035 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -74,6 +74,7 @@ func (ks keyStorePassphrase) GetKey(addr types.Address, filename, auth string) ( // StoreKey generates a key, encrypts with 'auth' and stores in the given directory func StoreKey(dir, auth string, scryptN, scryptP int) (accounts.Account, error) { _, a, err := storeNewKey(&keyStorePassphrase{dir, scryptN, scryptP, false}, rand.Reader, auth) + return a, err } @@ -82,19 +83,23 @@ func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) er if err != nil { return err } + // Write into temporary file tmpName, err := writeTemporaryKeyFile(filename, keyjson) if err != nil { return err } + if !ks.skipKeyFileVerification { // Verify that we can decrypt the file with the given password. _, err = ks.GetKey(key.Address, tmpName, auth) if err != nil { - msg := "an error was encountered when saving and verifying the keystore file. \n" + msg := "an error was encountered when saving and verifying the keystore file" + return fmt.Errorf(msg, tmpName, err) } } + return os.Rename(tmpName, filename) } @@ -102,29 +107,35 @@ func (ks keyStorePassphrase) JoinPath(filename string) string { if filepath.IsAbs(filename) { return filename } + return filepath.Join(ks.keysDirPath, filename) } // EncryptDataV3 encrypts the data given as 'data' with the password 'auth'. func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { salt := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { panic("reading from crypto/rand failed: " + err.Error()) //nolint:gocritic } + derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) if err != nil { return CryptoJSON{}, err } + encryptKey := derivedKey[:16] iv := make([]byte, aes.BlockSize) // 16 if _, err := io.ReadFull(rand.Reader, iv); err != nil { panic("reading from crypto/rand failed: " + err.Error()) //nolint:gocritic } + cipherText, err := aesCTRXOR(encryptKey, data, iv) if err != nil { return CryptoJSON{}, err } + mac := crypto.Keccak256(derivedKey[16:32], cipherText) scryptParamsJSON := make(map[string]interface{}, 5) @@ -145,6 +156,7 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) KDFParams: scryptParamsJSON, MAC: hex.EncodeToString(mac), } + return cryptoStruct, nil } @@ -160,12 +172,14 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { if err != nil { return nil, err } + encryptedKeyJSONV3 := encryptedKeyJSONV3{ hex.EncodeToString(key.Address[:]), cryptoStruct, key.ID.String(), version, } + return json.Marshal(encryptedKeyJSONV3) } @@ -183,31 +197,39 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { keyBytes, keyID []byte err error ) + if version, ok := m["version"].(string); ok && version == "1" { k := new(encryptedKeyJSONV1) + if err := json.Unmarshal(keyjson, k); err != nil { return nil, err } + keyBytes, keyID, err = decryptKeyV1(k, auth) } else { k := new(encryptedKeyJSONV3) + if err := json.Unmarshal(keyjson, k); err != nil { return nil, err } + keyBytes, keyID, err = decryptKeyV3(k, auth) } // Handle any decryption errors and return the key if err != nil { return nil, err } + key, err := crypto.DToECDSA(keyBytes, true) //TO DO maybe wrong if err != nil { return nil, fmt.Errorf("invalid key: %w", err) } + id, err := uuid.FromBytes(keyID) if err != nil { return nil, fmt.Errorf("invalid UUID: %w", err) } + return &Key{ ID: id, Address: crypto.PubKeyToAddress(&key.PublicKey), @@ -219,6 +241,7 @@ func DecryptDataV3(cryptoJSON CryptoJSON, auth string) ([]byte, error) { if cryptoJSON.Cipher != "aes-128-ctr" { return nil, fmt.Errorf("cipher not supported: %v", cryptoJSON.Cipher) } + mac, err := hex.DecodeString(cryptoJSON.MAC) if err != nil { return nil, err @@ -248,6 +271,7 @@ func DecryptDataV3(cryptoJSON CryptoJSON, auth string) ([]byte, error) { if err != nil { return nil, err } + return plainText, err } @@ -255,15 +279,19 @@ func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byt if keyProtected.Version != version { return nil, nil, fmt.Errorf("version not supported: %v", keyProtected.Version) } + keyUUID, err := uuid.Parse(keyProtected.ID) if err != nil { return nil, nil, err } + keyID = keyUUID[:] + plainText, err := DecryptDataV3(keyProtected.Crypto, auth) if err != nil { return nil, nil, err } + return plainText, keyID, err } @@ -272,7 +300,9 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt if err != nil { return nil, nil, err } + keyID = keyUUID[:] + mac, err := hex.DecodeString(keyProtected.Crypto.MAC) if err != nil { return nil, nil, err @@ -302,29 +332,36 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt if err != nil { return nil, nil, err } + return plainText, keyID, err } func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { authArray := []byte(auth) + salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) if err != nil { return nil, err } + dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) if cryptoJSON.KDF == keyHeaderKDF { n := ensureInt(cryptoJSON.KDFParams["n"]) r := ensureInt(cryptoJSON.KDFParams["r"]) p := ensureInt(cryptoJSON.KDFParams["p"]) + return scrypt.Key(authArray, salt, n, r, p, dkLen) } else if cryptoJSON.KDF == "pbkdf2" { c := ensureInt(cryptoJSON.KDFParams["c"]) prf := cryptoJSON.KDFParams["prf"].(string) //nolint:forcetypeassert + if prf != "hmac-sha256" { return nil, fmt.Errorf("unsupported PBKDF2 PRF: %s", prf) } + key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) + return key, nil } @@ -336,5 +373,6 @@ func ensureInt(x interface{}) int { if !ok { res = int(x.(float64)) //nolint:forcetypeassert } + return res } diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 30ec8a0f86..9ea4b17787 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -15,10 +15,12 @@ const ( // Tests that a json key file can be decrypted and encrypted in multiple rounds. func TestKeyEncryptDecrypt(t *testing.T) { t.Parallel() + keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") if err != nil { t.Fatal(err) } + password := "" address := types.StringToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") @@ -33,11 +35,12 @@ func TestKeyEncryptDecrypt(t *testing.T) { if err != nil { t.Fatalf("test %d: json key failed to decrypt: %v", i, err) } + if key.Address != address { t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address) } // Recrypt with a new password and start over - password += "new data appended" // nolint:gosec + password += "new data appended" if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { t.Errorf("test %d: failed to re-encrypt key %v", i, err) } diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go index eb143b31f9..25b36a41b4 100644 --- a/accounts/keystore/plain.go +++ b/accounts/keystore/plain.go @@ -18,14 +18,19 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, if err != nil { return nil, err } + defer fd.Close() + key := new(Key) + if err := json.NewDecoder(fd).Decode(key); err != nil { return nil, err } + if key.Address != addr { return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr) } + return key, nil } @@ -34,6 +39,7 @@ func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error { if err != nil { return err } + return writeKeyFile(filename, content) } @@ -41,5 +47,6 @@ func (ks keyStorePlain) JoinPath(filename string) string { if filepath.IsAbs(filename) { return filename } + return filepath.Join(ks.keysDirPath, filename) } diff --git a/accounts/keystore/plain_test.go b/accounts/keystore/plain_test.go index 74ee6aba2c..eabf1ec88a 100644 --- a/accounts/keystore/plain_test.go +++ b/accounts/keystore/plain_test.go @@ -17,31 +17,40 @@ import ( ) func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) { + t.Helper() + d := t.TempDir() + if encrypted { ks = &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} } else { ks = &keyStorePlain{d} } + return d, ks } func TestKeyStorePlain(t *testing.T) { t.Parallel() + _, ks := tmpKeyStoreIface(t, false) pass := "" // not used but required by API + k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) } + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } + if !reflect.DeepEqual(k1.Address, k2.Address) { t.Fatal(err) } + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { t.Fatal(err) } @@ -49,19 +58,23 @@ func TestKeyStorePlain(t *testing.T) { func TestKeyStorePassphrase(t *testing.T) { t.Parallel() + _, ks := tmpKeyStoreIface(t, true) k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) } + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } + if !reflect.DeepEqual(k1.Address, k2.Address) { t.Fatal(err) } + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { t.Fatal(err) } @@ -69,12 +82,14 @@ func TestKeyStorePassphrase(t *testing.T) { func TestKeyStorePassphraseDecryptionFail(t *testing.T) { t.Parallel() + _, ks := tmpKeyStoreIface(t, true) k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) } + if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err.Error() != ErrDecrypt.Error() { t.Fatalf("wrong error for invalid password\ngot %q\nwant %q", err, ErrDecrypt) } @@ -82,19 +97,23 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { func TestImportPreSaleKey(t *testing.T) { t.Parallel() + dir, ks := tmpKeyStoreIface(t, true) // file content of a presale key file generated with: // python pyethsaletool.py genwallet // with password "foo" fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" + account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) if err != nil { t.Fatal(err) } + if account.Address != types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { t.Errorf("imported account has wrong address %x", account.Address) } + if !strings.HasPrefix(account.URL.Path, dir) { t.Errorf("imported account file not in keystore directory: %q", account.URL) } @@ -133,6 +152,7 @@ func skipIfSubmoduleMissing(t *testing.T) { func TestV3_PBKDF2_2(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) testDecryptV3(t, tests["test1"]) } @@ -140,6 +160,7 @@ func TestV3_PBKDF2_2(t *testing.T) { func TestV3_PBKDF2_3(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) testDecryptV3(t, tests["python_generated_test_with_odd_iv"]) } @@ -147,12 +168,14 @@ func TestV3_PBKDF2_3(t *testing.T) { func TestV3_PBKDF2_4(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) testDecryptV3(t, tests["evilnonce"]) } func TestV3_Scrypt_1(t *testing.T) { t.Parallel() + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") testDecryptV3(t, tests["wikipage_test_vector_scrypt"]) } @@ -160,21 +183,25 @@ func TestV3_Scrypt_1(t *testing.T) { func TestV3_Scrypt_2(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) testDecryptV3(t, tests["test2"]) } func TestV1_1(t *testing.T) { t.Parallel() + tests := loadKeyStoreTestV1(t, "testdata/v1_test_vector.json") testDecryptV1(t, tests["test1"]) } func TestV1_2(t *testing.T) { t.Parallel() + ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP, true} addr := types.StringToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" + k, err := ks.GetKey(addr, file, "g") if err != nil { t.Fatal(err) @@ -182,7 +209,9 @@ func TestV1_2(t *testing.T) { privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) require.NoError(t, err) + privHex := hex.EncodeToString(privKey) + expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" if privHex != expectedHex { t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) @@ -196,6 +225,7 @@ func testDecryptV3(t *testing.T, test KeyStoreTestV3) { if err != nil { t.Fatal(err) } + privHex := hex.EncodeToString(privBytes) if test.Priv != privHex { t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) @@ -209,6 +239,7 @@ func testDecryptV1(t *testing.T, test KeyStoreTestV1) { if err != nil { t.Fatal(err) } + privHex := hex.EncodeToString(privBytes) if test.Priv != privHex { t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) @@ -219,10 +250,12 @@ func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { t.Helper() tests := make(map[string]KeyStoreTestV3) + err := accounts.LoadJSON(file, &tests) if err != nil { t.Fatal(err) } + return tests } @@ -230,15 +263,18 @@ func loadKeyStoreTestV1(t *testing.T, file string) map[string]KeyStoreTestV1 { t.Helper() tests := make(map[string]KeyStoreTestV1) + err := accounts.LoadJSON(file, &tests) if err != nil { t.Fatal(err) } + return tests } func TestKeyForDirectICAP(t *testing.T) { t.Parallel() + key := NewKeyForDirectICAP(rand.Reader) if !strings.HasPrefix(key.Address.String(), "0x00") { t.Errorf("Expected first address byte to be zero, have: %s", key.Address.String()) @@ -247,12 +283,14 @@ func TestKeyForDirectICAP(t *testing.T) { func TestV3_31_Byte_Key(t *testing.T) { t.Parallel() + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") testDecryptV3(t, tests["31_byte_key"]) } func TestV3_30_Byte_Key(t *testing.T) { t.Parallel() + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") testDecryptV3(t, tests["30_byte_key"]) } diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index 83ca43e060..6e9848d438 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -20,10 +20,12 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou if err != nil { return accounts.Account{}, nil, err } + key.ID, err = uuid.NewRandom() if err != nil { return accounts.Account{}, nil, err } + a := accounts.Account{ Address: key.Address, URL: accounts.URL{ @@ -31,7 +33,9 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou Path: keyStore.JoinPath(keyFileName(key.Address)), }, } + err = keyStore.StoreKey(a.URL.Path, key, password) + return a, key, err } @@ -42,10 +46,12 @@ func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error Email string BtcAddr string }{} + err = json.Unmarshal(fileContent, &preSaleKeyStruct) if err != nil { return nil, err } + encSeedBytes, err := hex.DecodeString(preSaleKeyStruct.EncSeed) if err != nil { return nil, errors.New("invalid hex in encSeed") @@ -54,6 +60,7 @@ func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error if len(encSeedBytes) < 16 { return nil, errors.New("invalid encSeed, too short") } + iv := encSeedBytes[:16] cipherText := encSeedBytes[16:] passBytes := []byte(password) @@ -63,7 +70,9 @@ func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error if err != nil { return nil, err } + ethPriv := crypto.Keccak256(plainText) + ecKey, err := crypto.DToECDSA(ethPriv, false) if err != nil { return nil, err @@ -74,11 +83,14 @@ func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error Address: crypto.PubKeyToAddress(&ecKey.PublicKey), PrivateKey: ecKey, } + derivedAddr := hex.EncodeToString(key.Address.Bytes()) // needed because .Hex() gives leading "0x" expectedAddr := preSaleKeyStruct.EthAddr + if derivedAddr != expectedAddr { err = fmt.Errorf("decrypted addr '%s' not equal to expected addr '%s'", derivedAddr, expectedAddr) } + return key, err } @@ -88,9 +100,11 @@ func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { if err != nil { return nil, err } + stream := cipher.NewCTR(aesBlock, iv) outText := make([]byte, len(inText)) stream.XORKeyStream(outText, inText) + return outText, err } @@ -99,13 +113,16 @@ func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { if err != nil { return nil, err } + decrypter := cipher.NewCBCDecrypter(aesBlock, iv) paddedPlaintext := make([]byte, len(cipherText)) decrypter.CryptBlocks(paddedPlaintext, cipherText) + plaintext := pkcs7Unpad(paddedPlaintext) if plaintext == nil { return nil, accounts.ErrDecrypt } + return plaintext, err } @@ -127,5 +144,6 @@ func pkcs7Unpad(in []byte) []byte { return nil } } + return in[:len(in)-int(padding)] } diff --git a/accounts/keystore/watch.go b/accounts/keystore/watch.go index d143751db8..7b9ba0dac4 100644 --- a/accounts/keystore/watch.go +++ b/accounts/keystore/watch.go @@ -31,7 +31,9 @@ func (w *watcher) start() { if w.starting || w.running { return } + w.starting = true + go w.loop() } @@ -51,6 +53,7 @@ func (w *watcher) loop() { watcher, err := fsnotify.NewWatcher() if err != nil { w.logger.Error("Failed to start filesystem watcher", "err", err) + return } @@ -60,6 +63,7 @@ func (w *watcher) loop() { if !os.IsNotExist(err) { w.logger.Info("Failed to watch keystore folder", "err", err) } + return } @@ -67,7 +71,9 @@ func (w *watcher) loop() { defer w.logger.Trace("Stopped watching keystore folder") w.ac.mu.Lock() + w.running = true + w.ac.mu.Unlock() var ( @@ -79,7 +85,9 @@ func (w *watcher) loop() { if !debounce.Stop() { <-debounce.C } + defer debounce.Stop() + for { select { case <-w.quit: @@ -91,6 +99,7 @@ func (w *watcher) loop() { if !rescanTriggered { debounce.Reset(debounceDuration) + rescanTriggered = true } @@ -98,11 +107,13 @@ func (w *watcher) loop() { if !ok { return } + w.logger.Info("Filesystem watcher error", "err", err) case <-debounce.C: if err := w.ac.scanAccounts(); err != nil { w.logger.Info("loop", "scanAccounts", err) } + rescanTriggered = false } } diff --git a/accounts/manager.go b/accounts/manager.go index dfbf0608f8..87f137878a 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -90,7 +90,9 @@ func (am *Manager) Config() *Config { func (am *Manager) AddBackend(backend Backend) { done := make(chan struct{}) + am.newBackends <- newBackendEvent{backend, done} + <-done } @@ -133,6 +135,7 @@ func (am *Manager) update() { errc <- nil close(am.term) + return } } @@ -155,6 +158,7 @@ func (am *Manager) Wallets() []Wallet { func (am *Manager) walletsNoLock() []Wallet { cpy := make([]Wallet, len(am.wallets)) copy(cpy, am.wallets) + return cpy } @@ -200,6 +204,7 @@ func (am *Manager) Find(account Account) (Wallet, error) { return wallet, nil } } + return nil, ErrUnknownAccount } @@ -216,6 +221,7 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet { slice = append(slice[:n], slice[n+1:]...) } + return slice } diff --git a/accounts/url.go b/accounts/url.go index 9a2867f88b..1d63b2eb83 100644 --- a/accounts/url.go +++ b/accounts/url.go @@ -16,6 +16,7 @@ func parseURL(url string) (URL, error) { if len(urlParts) != 2 || urlParts[0] == "" { return URL{}, errors.New("protocol scheme missing") } + return URL{ Scheme: urlParts[0], Path: urlParts[1], @@ -26,6 +27,7 @@ func (u URL) String() string { if u.Scheme != "" { return u.Scheme + "://" + u.Path } + return u.Path } @@ -35,16 +37,22 @@ func (u URL) MarshalJSON() ([]byte, error) { func (u *URL) UnmarshalJSON(input []byte) error { var textURL string + err := json.Unmarshal(input, &textURL) + if err != nil { return err } + url, err := parseURL(textURL) + if err != nil { return err } + u.Scheme = url.Scheme u.Path = url.Path + return nil } @@ -52,5 +60,6 @@ func (u URL) Cmp(url URL) int { if u.Scheme == url.Scheme { return strings.Compare(u.Path, url.Path) } + return strings.Compare(u.Scheme, url.Scheme) } diff --git a/accounts/url_test.go b/accounts/url_test.go index 2464484f9a..56b7fc79a4 100644 --- a/accounts/url_test.go +++ b/accounts/url_test.go @@ -10,13 +10,17 @@ const ( func TestURLParsing(t *testing.T) { t.Parallel() + url, err := parseURL(bladeURLWithPrefix) + if err != nil { t.Errorf("unexpected error: %v", err) } + if url.Scheme != "https" { t.Errorf("expected: %v, got: %v", "https", url.Scheme) } + if url.Path != bladeURL { t.Errorf("expected: %v, got: %v", bladeURL, url.Path) } @@ -30,12 +34,15 @@ func TestURLParsing(t *testing.T) { func TestURLString(t *testing.T) { t.Parallel() + url := URL{Scheme: "https", Path: bladeURL} + if url.String() != bladeURLWithPrefix { t.Errorf("expected: %v, got: %v", bladeURLWithPrefix, url.String()) } url = URL{Scheme: "", Path: bladeURL} + if url.String() != bladeURL { t.Errorf("expected: %v, got: %v", bladeURL, url.String()) } @@ -43,11 +50,14 @@ func TestURLString(t *testing.T) { func TestURLMarshalJSON(t *testing.T) { t.Parallel() + url := URL{Scheme: "https", Path: bladeURL} + json, err := url.MarshalJSON() if err != nil { t.Errorf("unexpected error: %v", err) } + if string(json) != bladeURLJSON { t.Errorf("expected: %v, got: %v", bladeURLJSON, string(json)) } @@ -55,14 +65,18 @@ func TestURLMarshalJSON(t *testing.T) { func TestURLUnmarshalJSON(t *testing.T) { t.Parallel() + url := &URL{} + err := url.UnmarshalJSON([]byte(bladeURLJSON)) if err != nil { t.Errorf("unexpected error: %v", err) } + if url.Scheme != "https" { t.Errorf("expected: %v, got: %v", "https", url.Scheme) } + if url.Path != bladeURL { t.Errorf("expected: %v, got: %v", "https", url.Path) } @@ -70,6 +84,7 @@ func TestURLUnmarshalJSON(t *testing.T) { func TestURLComparison(t *testing.T) { t.Parallel() + tests := []struct { urlA URL urlB URL @@ -83,6 +98,7 @@ func TestURLComparison(t *testing.T) { for i, tt := range tests { result := tt.urlA.Cmp(tt.urlB) + if result != tt.expect { t.Errorf("test %d: cmp mismatch: expected: %d, got: %d", i, tt.expect, result) } From c092e4a3ab820fc9a0f87ff73c74a09ba4c2303c Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 07/69] test fix --- accounts/keystore/account_cache_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 3307d14d63..575ce45e6f 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -56,7 +56,7 @@ func waitWatcherStart(ks *KeyStore) bool { func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { var list []accounts.Account - for t0 := time.Now().UTC(); time.Since(t0) < 5*time.Second; time.Sleep(100 * time.Millisecond) { + for t0 := time.Now().UTC(); time.Since(t0) < 20*time.Second; time.Sleep(100 * time.Millisecond) { list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { // ks should have also received change notifications From 02407869d21829ca8cc66ddd32389c5085de2a5e Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 08/69] test fix --- accounts/keystore/account_cache_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 575ce45e6f..9ef956665d 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -385,7 +385,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) // Now replace file contents - if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil { + if err := forceCopyFile(cachetestAccounts[1].URL.Path, file); err != nil { t.Fatal(err) return @@ -405,7 +405,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) // Now replace file contents again - if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil { + if err := forceCopyFile(cachetestAccounts[2].URL.Path, file); err != nil { t.Fatal(err) return From 4c4ec19e8b83d75a961b40d422f317cba14c76a5 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 09/69] test fix --- accounts/keystore/account_cache_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 9ef956665d..797c58f3da 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -385,7 +385,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) // Now replace file contents - if err := forceCopyFile(cachetestAccounts[1].URL.Path, file); err != nil { + if err := cp.CopyFileOverwrite(file, cachetestAccounts[1].URL.Path); err != nil { t.Fatal(err) return @@ -405,7 +405,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) // Now replace file contents again - if err := forceCopyFile(cachetestAccounts[2].URL.Path, file); err != nil { + if err := cp.CopyFileOverwrite(file, cachetestAccounts[2].URL.Path); err != nil { t.Fatal(err) return From 99077a2a031195d0c8638129ba9c1457b04b3bf6 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 10/69] to abs path --- accounts/keystore/account_cache_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 797c58f3da..334bf5f0b6 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -350,7 +350,9 @@ func TestUpdatedKeyfileContents(t *testing.T) { t.Parallel() // Create a temporary keystore to test with - dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int())) + dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) + require.NoError(t, err) + ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) list := ks.Accounts() From aa371b6528dbeafcfa3cc312cff610c37718519b Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 11/69] custom json unmarshall --- accounts/keystore/plain.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go index 25b36a41b4..babe8dd987 100644 --- a/accounts/keystore/plain.go +++ b/accounts/keystore/plain.go @@ -6,7 +6,9 @@ import ( "os" "path/filepath" + "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" + "github.com/google/uuid" ) type keyStorePlain struct { @@ -19,11 +21,34 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, return nil, err } + key := new(Key) + defer fd.Close() - key := new(Key) + stat, err := fd.Stat() + if err != nil { + return nil, err + } + + var ( + dat map[string]interface{} + jsonData = make([]byte, stat.Size()) + ) + _, err = fd.Read(jsonData) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(jsonData, &dat); err != nil { + return nil, err + } + + key.Address = types.StringToAddress(dat["address"].(string)) - if err := json.NewDecoder(fd).Decode(key); err != nil { + key.ID = uuid.MustParse(dat["id"].(string)) + + key.PrivateKey, err = crypto.BytesToECDSAPrivateKey([]byte(dat["privatekey"].(string))) + if err != nil { return nil, err } From d7dc3d514325448f4e813927f71e5d8b41cc5235 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 12/69] commands added --- accounts/keystore/keystore.go | 16 ++++- command/accounts/common/params.go | 13 ++++ command/accounts/create/create.go | 67 ++++++++++++++++++ command/accounts/create/params.go | 38 +++++++++++ command/accounts/import/import.go | 110 ++++++++++++++++++++++++++++++ command/accounts/import/params.go | 40 +++++++++++ command/accounts/update/params.go | 11 +++ command/accounts/update/update.go | 48 +++++++++++++ 8 files changed, 340 insertions(+), 3 deletions(-) create mode 100644 command/accounts/common/params.go create mode 100644 command/accounts/create/create.go create mode 100644 command/accounts/create/params.go create mode 100644 command/accounts/import/import.go create mode 100644 command/accounts/import/params.go create mode 100644 command/accounts/update/params.go create mode 100644 command/accounts/update/update.go diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index adf7c7b586..aef76138d6 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -7,6 +7,7 @@ import ( "math/big" "os" "path/filepath" + "reflect" "runtime" "sync" "time" @@ -26,8 +27,12 @@ var ( // ErrAccountAlreadyExists is returned if an account attempted to import is // already present in the keystore. ErrAccountAlreadyExists = errors.New("account already exists") + + DefaultStorage, _ = filepath.Abs(filepath.Join("data-storage")) ) +var KeyStoreType = reflect.TypeOf(&KeyStore{}) + // Maximum time between wallet refreshes (if filesystem notifications don't work). const walletRefreshCycle = 3 * time.Second @@ -53,9 +58,14 @@ type unlocked struct { } func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { - keyDir, _ = filepath.Abs(keyDir) - ks := &KeyStore{storage: &keyStorePassphrase{keyDir, scryptN, scryptP, false}} - ks.init(keyDir, logger) + var ks *KeyStore + if keyDir == "" { + ks = &KeyStore{storage: &keyStorePassphrase{DefaultStorage, scryptN, scryptP, false}} + } else { + ks = &KeyStore{storage: &keyStorePassphrase{keyDir, scryptN, scryptP, false}} + } + + ks.init(keyDir, hclog.NewNullLogger()) //TO DO LOGGER return ks } diff --git a/command/accounts/common/params.go b/command/accounts/common/params.go new file mode 100644 index 0000000000..7779e57757 --- /dev/null +++ b/command/accounts/common/params.go @@ -0,0 +1,13 @@ +package common + +const ( + PrivateKeyFlag = "private-key" + KeyDirFlag = "key-dir" + PassphraseFlag = "passphrase" +) + +type CommonParams struct { + PrivateKey string + KeyDir string + Passphrase string +} diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go new file mode 100644 index 0000000000..f6e6cf6a63 --- /dev/null +++ b/command/accounts/create/create.go @@ -0,0 +1,67 @@ +package accounts + +import ( + "fmt" + + "github.com/0xPolygon/polygon-edge/accounts/keystore" + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/spf13/cobra" +) + +var ( + params createParams +) + +func GetCommand() *cobra.Command { + createCmd := &cobra.Command{ + Use: "create", + Short: "Create new account", + PreRun: runPreRun, + Run: runCommand, + } + + helper.RegisterJSONRPCFlag(createCmd) + + return createCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.Passphrase, + PassphraseFlag, + "", + "passphrase for access to private key", + ) + + cmd.Flags().StringVar( + ¶ms.ConfigDir, + ConfigDirFlag, + "", + "dir of config", + ) + + _ = cmd.MarkFlagRequired(PassphraseFlag) +} + +func runPreRun(cmd *cobra.Command, _ []string) { + +} + +func runCommand(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + + scryptN := keystore.StandardScryptN + scryptP := keystore.StandardScryptP + if false { + scryptN = keystore.LightScryptN + scryptP = keystore.LightScryptP + } + + account, err := keystore.StoreKey("", params.Passphrase, scryptN, scryptP) + if err != nil { + outputter.SetError(fmt.Errorf("Cant create account")) + } + + outputter.SetCommandResult(command.Results{&createResult{Address: account.Address, PrivateKeyPath: account.URL.Path}}) +} diff --git a/command/accounts/create/params.go b/command/accounts/create/params.go new file mode 100644 index 0000000000..0d7f55f270 --- /dev/null +++ b/command/accounts/create/params.go @@ -0,0 +1,38 @@ +package accounts + +import ( + "bytes" + "fmt" + + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/types" +) + +const ( + PassphraseFlag = "passphrase" + ConfigDirFlag = "config-dir" +) + +type createParams struct { + Passphrase string + ConfigDir string +} + +type createResult struct { + Address types.Address `json:"address"` + PrivateKeyPath string `json:"privkeypath"` +} + +func (i *createResult) GetOutput() string { + var buffer bytes.Buffer + + vals := make([]string, 0, 1) + vals = append(vals, fmt.Sprintf("Address|%s", i.Address.String())) + vals = append(vals, fmt.Sprintf("PrivateKeyPath:%s", i.PrivateKeyPath)) + + buffer.WriteString("\n[Import accounts]\n") + buffer.WriteString(helper.FormatKV(vals)) + buffer.WriteString("\n") + + return buffer.String() +} diff --git a/command/accounts/import/import.go b/command/accounts/import/import.go new file mode 100644 index 0000000000..5df68ca4be --- /dev/null +++ b/command/accounts/import/import.go @@ -0,0 +1,110 @@ +package accounts + +import ( + "encoding/hex" + "fmt" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/accounts/keystore" + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/spf13/cobra" +) + +var ( + params importParams +) + +func GetCommand() *cobra.Command { + importCmd := &cobra.Command{ + Use: "import", + Short: "Import existing account with private key and auth passphrase", + PreRun: runPreRun, + Run: runCommand, + } + + helper.RegisterJSONRPCFlag(importCmd) + setFlags(importCmd) + + return importCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.PrivateKey, + PrivateKeyFlag, + "", + "privateKey for import account", + ) + + cmd.Flags().StringVar( + ¶ms.KeyDir, + KeyDirFlag, + "", + "dir for document that contains private key", + ) + + cmd.Flags().StringVar( + ¶ms.Passphrase, + PassphraseFlag, + "", + "passphrase for access to private key", + ) + + cmd.Flags().StringVar( + ¶ms.ConfigDir, + ConfigDirFlag, + "", + "dir of config file", + ) + + _ = cmd.MarkFlagRequired(PassphraseFlag) +} + +func runPreRun(cmd *cobra.Command, _ []string) { + return +} + +func runCommand(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + + scryptN := keystore.StandardScryptN + scryptP := keystore.StandardScryptP + if false { + scryptN = keystore.LightScryptN + scryptP = keystore.LightScryptP + } + + am := accounts.NewManager(&accounts.Config{}) + + am.AddBackend(keystore.NewKeyStore(params.KeyDir, scryptN, scryptP, nil)) + + if params.PrivateKey == "" { + outputter.SetError(fmt.Errorf("private key empty")) + } + + dec, err := hex.DecodeString(params.PrivateKey) + if err != nil { + outputter.SetError(fmt.Errorf("failed to decode private key")) + } + + privKey, err := crypto.BytesToECDSAPrivateKey(dec) + if err != nil { + outputter.SetError(fmt.Errorf("Failed to initialize private key")) + } + + backends := am.Backends(keystore.KeyStoreType) + if len(backends) == 0 { + outputter.SetError(fmt.Errorf("keystore is not available")) + } + + ks := backends[0].(*keystore.KeyStore) + + acct, err := ks.ImportECDSA(privKey, params.Passphrase) + if err != nil { + outputter.SetError(fmt.Errorf("cannot import private key")) + } + + outputter.SetCommandResult(command.Results{&importResult{Address: acct.Address}}) +} diff --git a/command/accounts/import/params.go b/command/accounts/import/params.go new file mode 100644 index 0000000000..3fcca34aad --- /dev/null +++ b/command/accounts/import/params.go @@ -0,0 +1,40 @@ +package accounts + +import ( + "bytes" + "fmt" + + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/types" +) + +const ( + PrivateKeyFlag = "private-key" + KeyDirFlag = "key-dir" + PassphraseFlag = "passphrase" + ConfigDirFlag = "config-dir" +) + +type importParams struct { + PrivateKey string + KeyDir string + Passphrase string + ConfigDir string +} + +type importResult struct { + Address types.Address `json:"address"` +} + +func (i *importResult) GetOutput() string { + var buffer bytes.Buffer + + vals := make([]string, 0, 1) + vals = append(vals, fmt.Sprintf("Address|%s", i.Address.String())) + + buffer.WriteString("\n[Import accounts]\n") + buffer.WriteString(helper.FormatKV(vals)) + buffer.WriteString("\n") + + return buffer.String() +} diff --git a/command/accounts/update/params.go b/command/accounts/update/params.go new file mode 100644 index 0000000000..8eafd540ab --- /dev/null +++ b/command/accounts/update/params.go @@ -0,0 +1,11 @@ +package accounts + +const ( + AddressFlag = "address" + PassphraseFlag = "passphrase" +) + +type updateParams struct { + Address string + Passphrase string +} diff --git a/command/accounts/update/update.go b/command/accounts/update/update.go new file mode 100644 index 0000000000..e8ba62f065 --- /dev/null +++ b/command/accounts/update/update.go @@ -0,0 +1,48 @@ +package accounts + +import ( + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/spf13/cobra" +) + +var ( + params updateParams +) + +func GetCommand() *cobra.Command { + updateCmd := &cobra.Command{ + Use: "update", + Short: "Update existing account", + PreRunE: runPreRun, + RunE: runCommand, + } + + helper.RegisterJSONRPCFlag(updateCmd) + setFlags(updateCmd) + + return updateCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.Address, + AddressFlag, + "", + "address of account", + ) + + cmd.Flags().StringVar( + ¶ms.Passphrase, + PassphraseFlag, + "", + "passphrase for access to private key", + ) +} + +func runPreRun(cmd *cobra.Command, _ []string) error { + return nil +} + +func runCommand(cmd *cobra.Command, _ []string) error { + return nil +} From ad3f26fa02bf548af7da8468c4af2483feb999db Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 13/69] unnecessary files deletion --- accounts/hd.go | 164 -------------------------------------------- accounts/hd_test.go | 106 ---------------------------- 2 files changed, 270 deletions(-) delete mode 100644 accounts/hd.go delete mode 100644 accounts/hd_test.go diff --git a/accounts/hd.go b/accounts/hd.go deleted file mode 100644 index 559abbc046..0000000000 --- a/accounts/hd.go +++ /dev/null @@ -1,164 +0,0 @@ -package accounts - -import ( - "encoding/json" - "errors" - "fmt" - "math" - "math/big" - "strings" -) - -// DefaultRootDerivationPath is the root path to which custom derivation endpoints -// are appended. As such, the first account will be at m/44'/60'/0'/0, the second -// at m/44'/60'/0'/1, etc. -var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} - -// DefaultBaseDerivationPath is the base path from which custom derivation endpoints -// are incremented. As such, the first account will be at m/44'/60'/0'/0/0, the second -// at m/44'/60'/0'/0/1, etc. -var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0} - -// LegacyLedgerBaseDerivationPath is the legacy base path from which custom derivation -// endpoints are incremented. As such, the first account will be at m/44'/60'/0'/0, the -// second at m/44'/60'/0'/1, etc. -var LegacyLedgerBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} - -// DerivationPath represents the computer friendly version of a hierarchical -// deterministic wallet account derivation path. -// -// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki -// defines derivation paths to be of the form: -// -// m / purpose' / coin_type' / account' / change / address_index -// -// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki -// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and -// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns -// the `coin_type` 60' (or 0x8000003C) to Ethereum. -// -// The root path for Ethereum is m/44'/60'/0'/0 according to the specification -// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone -// yet whether accounts should increment the last component or the children of -// that. We will go with the simpler approach of incrementing the last component. -type DerivationPath []uint32 - -func ParseDerivationPath(path string) (DerivationPath, error) { - var result DerivationPath //nolint:prealloc - - components := strings.Split(path, "/") - - switch { - case len(components) == 0: - return nil, errors.New("empty derivation path") - case strings.TrimSpace(components[0]) == "": - return nil, errors.New("use 'm/' prefix for abolute path or no leading '/' for relative paths") - case strings.TrimSpace(components[0]) == "m": - components = components[1:] - default: - result = append(result, DefaultRootDerivationPath...) - } - - if len(components) == 0 { - return nil, errors.New("empty derivation path") - } - - for _, component := range components { - component = strings.TrimSpace(component) - - var value uint32 - - if strings.HasSuffix(component, "'") { - value = 0x80000000 - component = strings.TrimSpace(strings.TrimSuffix(component, "'")) - } - - bigValue, ok := new(big.Int).SetString(component, 0) - if !ok { - return nil, fmt.Errorf("invalid component: %s", component) - } - - max := math.MaxUint32 - value - - if bigValue.Sign() < 0 || bigValue.Cmp(big.NewInt(int64(max))) > 0 { - if value == 0 { - return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigValue, max) - } - - return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigValue, max) - } - - value += uint32(bigValue.Uint64()) - - result = append(result, value) - } - - return result, nil -} - -func (path DerivationPath) String() string { - result := "m" - - for _, component := range path { - var hardened bool - - if component >= 0x80000000 { - component -= 0x80000000 - hardened = true - } - - result = fmt.Sprintf("%s/%d", result, component) - - if hardened { - result += "'" - } - } - - return result -} - -func (path DerivationPath) MarshalJSON() ([]byte, error) { - return json.Marshal(path.String()) -} - -func (path *DerivationPath) UnmarshalJSON(b []byte) error { - var dp string - - var err error - - if err = json.Unmarshal(b, &dp); err != nil { - return err - } - - *path, err = ParseDerivationPath(dp) - - return err -} - -func DefaultIterator(base DerivationPath) func() DerivationPath { - path := make(DerivationPath, len(base)) - - copy(path[:], base[:]) - - path[len(path)-1]-- - - return func() DerivationPath { - path[len(path)-1]++ - - return path - } -} - -func LedgerLiveIterator(base DerivationPath) func() DerivationPath { - path := make(DerivationPath, len(base)) - - copy(path[:], base[:]) - - path[2]-- - - return func() DerivationPath { - path[2]++ - - return path - } -} diff --git a/accounts/hd_test.go b/accounts/hd_test.go deleted file mode 100644 index 9ec71c8472..0000000000 --- a/accounts/hd_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package accounts - -import ( - "fmt" - "reflect" - "testing" -) - -func TestHDPathParsing(t *testing.T) { - t.Parallel() - - tests := []struct { - input string - output DerivationPath - }{ - // Plain absolute derivation paths - {"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, - {"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, - {"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, - {"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, - {"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, - {"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, - - // Plain relative derivation paths - {"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, - {"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, - {"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, - {"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, - {"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, - - // Hexadecimal absolute derivation paths - {"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, - {"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, - {"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, - {"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, - {"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, - {"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, - - // Hexadecimal relative derivation paths - {"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0}}, - {"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 128}}, - {"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, - {"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 128}}, - {"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0, 0x80000000 + 0}}, - - // Weird inputs just to ensure they work - {" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, - - // Invalid derivation paths - {"", nil}, // Empty relative derivation path - {"m", nil}, // Empty absolute derivation path - {"m/", nil}, // Missing last derivation component - {"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error - {"m/2147483648'", nil}, // Overflows 32 bit integer - {"m/-1'", nil}, // Cannot contain negative number - } - - for i, tt := range tests { - if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) { - t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output) - } else if path == nil && err == nil { - t.Errorf("test %d: nil path and error: %v", i, err) - } - } -} - -func testDerive(t *testing.T, next func() DerivationPath, expected []string) { - t.Helper() - - for i, want := range expected { - if have := next(); fmt.Sprintf("%v", have) != want { - t.Errorf("step %d, have %v, want %v", i, have, want) - } - } -} - -func TestHdPathIteration(t *testing.T) { - t.Parallel() - - testDerive(t, DefaultIterator(DefaultBaseDerivationPath), - []string{ - "m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1", - "m/44'/60'/0'/0/2", "m/44'/60'/0'/0/3", - "m/44'/60'/0'/0/4", "m/44'/60'/0'/0/5", - "m/44'/60'/0'/0/6", "m/44'/60'/0'/0/7", - "m/44'/60'/0'/0/8", "m/44'/60'/0'/0/9", - }) - - testDerive(t, DefaultIterator(LegacyLedgerBaseDerivationPath), - []string{ - "m/44'/60'/0'/0", "m/44'/60'/0'/1", - "m/44'/60'/0'/2", "m/44'/60'/0'/3", - "m/44'/60'/0'/4", "m/44'/60'/0'/5", - "m/44'/60'/0'/6", "m/44'/60'/0'/7", - "m/44'/60'/0'/8", "m/44'/60'/0'/9", - }) - - testDerive(t, LedgerLiveIterator(DefaultBaseDerivationPath), - []string{ - "m/44'/60'/0'/0/0", "m/44'/60'/1'/0/0", - "m/44'/60'/2'/0/0", "m/44'/60'/3'/0/0", - "m/44'/60'/4'/0/0", "m/44'/60'/5'/0/0", - "m/44'/60'/6'/0/0", "m/44'/60'/7'/0/0", - "m/44'/60'/8'/0/0", "m/44'/60'/9'/0/0", - }) -} From 14a49f2c5287f2940cf7eb164456f05538b19ad7 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 14/69] interface unnecessary functions --- accounts/accounts.go | 22 ---------------------- accounts/keystore/wallet.go | 7 ------- 2 files changed, 29 deletions(-) diff --git a/accounts/accounts.go b/accounts/accounts.go index 27a6b101f6..31bab41358 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -5,7 +5,6 @@ import ( "math/big" "github.com/0xPolygon/polygon-edge/accounts/event" - "github.com/0xPolygon/polygon-edge/state" "github.com/0xPolygon/polygon-edge/types" "golang.org/x/crypto/sha3" ) @@ -52,27 +51,6 @@ type Wallet interface { // Contains returns whether an account is part of this particular wallet or not. Contains(account Account) bool - // Derive attempts to explicitly derive a hierarchical deterministic account at - // the specified derivation path. If requested, the derived account will be added - // to the wallet's tracked account list. - Derive(path DerivationPath, pin bool) (Account, error) - - // SelfDerive sets a base account derivation path from which the wallet attempts - // to discover non zero accounts and automatically add them to list of tracked - // accounts. - // - // Note, self derivation will increment the last component of the specified path - // opposed to descending into a child path to allow discovering accounts starting - // from non zero components. - // - // Some hardware wallets switched derivation paths through their evolution, so - // this method supports providing multiple bases to discover old user accounts - // too. Only the last base will be used to derive the next empty account. - // - // You can disable automatic account discovery by calling SelfDerive with a nil - // chain state reader. - SelfDerive(bases []DerivationPath, state *state.Transition) - // SignData requests the wallet to sign the hash of the given data // It looks up the account specified either solely via its address contained within, // or optionally with the aid of any location metadata from the embedded URL field. diff --git a/accounts/keystore/wallet.go b/accounts/keystore/wallet.go index cb1f728f9d..2db4b6df12 100644 --- a/accounts/keystore/wallet.go +++ b/accounts/keystore/wallet.go @@ -6,7 +6,6 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/crypto" - "github.com/0xPolygon/polygon-edge/state" "github.com/0xPolygon/polygon-edge/types" ) @@ -42,12 +41,6 @@ func (ksw *keyStoreWallet) Contains(account accounts.Account) bool { return account.Address == ksw.account.Address && (account.URL == accounts.URL{} || account.URL == ksw.account.URL) } -func (ksw *keyStoreWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { - return accounts.Account{}, accounts.ErrNotSupported -} - -func (ksw *keyStoreWallet) SelfDerive(bases []accounts.DerivationPath, state *state.Transition) {} - func (ksw *keyStoreWallet) signHash(account accounts.Account, hash []byte) ([]byte, error) { if !ksw.Contains(account) { return nil, accounts.ErrUnknownAccount From cad8261d813a75d9f7d0bf529f9eba47ffd3881f Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 15/69] manager integration --- accounts/event/feed_test.go | 2 +- accounts/keystore/account_cache_test.go | 2 +- accounts/keystore/key.go | 6 +++--- accounts/keystore/keystore.go | 2 +- accounts/keystore/plain.go | 6 +++--- accounts/manager.go | 5 ++++- command/accounts/import/import.go | 4 ++-- crypto/crypto.go | 9 ++++----- jsonrpc/dispatcher.go | 8 +++++--- jsonrpc/dispatcher_test.go | 2 +- jsonrpc/eth_endpoint.go | 2 ++ jsonrpc/eth_endpoint_test.go | 4 ++-- jsonrpc/jsonrpc.go | 4 +++- jsonrpc/jsonrpc_test.go | 2 +- server/server.go | 10 +++++++++- 15 files changed, 42 insertions(+), 26 deletions(-) diff --git a/accounts/event/feed_test.go b/accounts/event/feed_test.go index 1c4e2bc078..f6a8ebc43f 100644 --- a/accounts/event/feed_test.go +++ b/accounts/event/feed_test.go @@ -66,7 +66,7 @@ func checkPanic(want error, fn func()) (err error) { if panicErr == nil { err = errors.New("didn't panic") } else if !reflect.DeepEqual(panicErr, want) { - err = fmt.Errorf("panicked with wrong error: got %w, want %w", panicErr, want) + err = fmt.Errorf("panicked with wrong error: got %v, want %w", panicErr, want) } }() diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 334bf5f0b6..bbccae0dfd 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -350,7 +350,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { t.Parallel() // Create a temporary keystore to test with - dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) + dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) //nolint:gocritic require.NoError(t, err) ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 33813de308..7eb03dfd07 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -76,7 +76,7 @@ type cipherparamsJSON struct { // TO DO marshall private key func (k *Key) MarshalJSON() (j []byte, err error) { - privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) //get more time for this + privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) // get more time for this if err != nil { return nil, err } @@ -135,7 +135,7 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { key := &Key{ ID: id, - Address: crypto.PubKeyToAddress(&privateKeyECDSA.PublicKey), //TO DO get more time for this pointer + Address: crypto.PubKeyToAddress(&privateKeyECDSA.PublicKey), // TO DO get more time for this pointer PrivateKey: privateKeyECDSA, } @@ -192,7 +192,7 @@ func writeTemporaryKeyFile(file string, content []byte) (string, error) { } func newKey(rand io.Reader) (*Key, error) { - privateKeyECDSA, err := crypto.GenerateECDSAPrivateKey() //TO DO maybe not valid + privateKeyECDSA, err := crypto.GenerateECDSAPrivateKey() // TO DO maybe not valid if err != nil { return nil, err } diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index aef76138d6..c96459165a 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -28,7 +28,7 @@ var ( // already present in the keystore. ErrAccountAlreadyExists = errors.New("account already exists") - DefaultStorage, _ = filepath.Abs(filepath.Join("data-storage")) + DefaultStorage, _ = filepath.Abs(filepath.Join("data-storage")) //nolint:gocritic ) var KeyStoreType = reflect.TypeOf(&KeyStore{}) diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go index babe8dd987..e0670ee221 100644 --- a/accounts/keystore/plain.go +++ b/accounts/keystore/plain.go @@ -43,11 +43,11 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, return nil, err } - key.Address = types.StringToAddress(dat["address"].(string)) + key.Address = types.StringToAddress(dat["address"].(string)) //nolint:forcetypeassert - key.ID = uuid.MustParse(dat["id"].(string)) + key.ID = uuid.MustParse(dat["id"].(string)) //nolint:forcetypeassert - key.PrivateKey, err = crypto.BytesToECDSAPrivateKey([]byte(dat["privatekey"].(string))) + key.PrivateKey, err = crypto.BytesToECDSAPrivateKey([]byte(dat["privatekey"].(string))) //nolint:forcetypeassert if err != nil { return nil, err } diff --git a/accounts/manager.go b/accounts/manager.go index 87f137878a..196587cff6 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -7,6 +7,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts/event" "github.com/0xPolygon/polygon-edge/types" + "github.com/hashicorp/go-hclog" ) const managerSubBufferSize = 50 @@ -33,11 +34,13 @@ type Manager struct { quit chan chan error + logger hclog.Logger + term chan struct{} lock sync.RWMutex } -func NewManager(config *Config, backends ...Backend) *Manager { +func NewManager(config *Config, logger hclog.Logger, backends ...Backend) *Manager { var wallets []Wallet for _, backend := range backends { diff --git a/command/accounts/import/import.go b/command/accounts/import/import.go index 5df68ca4be..d06aa7012e 100644 --- a/command/accounts/import/import.go +++ b/command/accounts/import/import.go @@ -76,7 +76,7 @@ func runCommand(cmd *cobra.Command, _ []string) { scryptP = keystore.LightScryptP } - am := accounts.NewManager(&accounts.Config{}) + am := accounts.NewManager(&accounts.Config{}, nil) am.AddBackend(keystore.NewKeyStore(params.KeyDir, scryptN, scryptP, nil)) @@ -99,7 +99,7 @@ func runCommand(cmd *cobra.Command, _ []string) { outputter.SetError(fmt.Errorf("keystore is not available")) } - ks := backends[0].(*keystore.KeyStore) + ks := backends[0].(*keystore.KeyStore) //nolint:forcetypeassert acct, err := ks.ImportECDSA(privKey, params.Passphrase) if err != nil { diff --git a/crypto/crypto.go b/crypto/crypto.go index 47f09d19ca..4716ec59d0 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -370,13 +370,13 @@ func LatestSignerForChainID(chaidID uint64) TxSigner { return NewLondonSigner(chaidID) } -func DToECDSA(D []byte, strict bool) (*ecdsa.PrivateKey, error) { +func DToECDSA(d []byte, strict bool) (*ecdsa.PrivateKey, error) { priv := new(ecdsa.PrivateKey) priv.PublicKey.Curve = btcec.S256() - if strict && 8*len(D) != priv.Params().BitSize { + if strict && 8*len(d) != priv.Params().BitSize { return nil, fmt.Errorf("invalid length, need %d bits", priv.Params().BitSize) } - priv.D = new(big.Int).SetBytes(D) + priv.D = new(big.Int).SetBytes(d) if priv.D.Cmp(secp256k1N) >= 0 { return nil, errors.New("invalid private key, >= N") @@ -386,11 +386,10 @@ func DToECDSA(D []byte, strict bool) (*ecdsa.PrivateKey, error) { return nil, errors.New("invalid private key, zero or negative") } - priv.PublicKey.X, priv.PublicKey.Y = btcec.S256().ScalarBaseMult(D) + priv.PublicKey.X, priv.PublicKey.Y = btcec.S256().ScalarBaseMult(d) if priv.PublicKey.X == nil { return nil, errors.New("invalid private key") } return priv, nil - } diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index c1567fc1e5..335009f3f6 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -11,6 +11,7 @@ import ( "time" "unicode" + "github.com/0xPolygon/polygon-edge/accounts" "github.com/armon/go-metrics" "github.com/hashicorp/go-hclog" jsonIter "github.com/json-iterator/go" @@ -75,7 +76,7 @@ func (dp dispatcherParams) isExceedingBatchLengthLimit(value uint64) bool { func newDispatcher( logger hclog.Logger, store JSONRPCStore, - params *dispatcherParams, + params *dispatcherParams, manager *accounts.Manager, ) (*Dispatcher, error) { d := &Dispatcher{ logger: logger.Named("dispatcher"), @@ -87,20 +88,21 @@ func newDispatcher( go d.filterManager.Run() } - if err := d.registerEndpoints(store); err != nil { + if err := d.registerEndpoints(store, manager); err != nil { return nil, err } return d, nil } -func (d *Dispatcher) registerEndpoints(store JSONRPCStore) error { +func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager *accounts.Manager) error { d.endpoints.Eth = &Eth{ d.logger, store, d.params.chainID, d.filterManager, d.params.priceLimit, + manager, } d.endpoints.Net = &Net{ store, diff --git a/jsonrpc/dispatcher_test.go b/jsonrpc/dispatcher_test.go index 193e4a069e..46c34c013c 100644 --- a/jsonrpc/dispatcher_test.go +++ b/jsonrpc/dispatcher_test.go @@ -562,7 +562,7 @@ func TestDispatcher_WebsocketConnection_Unsubscribe(t *testing.T) { func newTestDispatcher(tb testing.TB, logger hclog.Logger, store JSONRPCStore, params *dispatcherParams) *Dispatcher { tb.Helper() - d, err := newDispatcher(logger, store, params) + d, err := newDispatcher(logger, store, params, nil) require.NoError(tb, err) return d diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index c7d61ed560..3bdf35d0c5 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/go-hclog" + "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/gasprice" "github.com/0xPolygon/polygon-edge/helper/common" @@ -101,6 +102,7 @@ type Eth struct { chainID uint64 filterManager *FilterManager priceLimit uint64 + accManager *accounts.Manager } // ChainId returns the chain id of the client diff --git a/jsonrpc/eth_endpoint_test.go b/jsonrpc/eth_endpoint_test.go index 1dc5d905fe..0e3b456669 100644 --- a/jsonrpc/eth_endpoint_test.go +++ b/jsonrpc/eth_endpoint_test.go @@ -288,13 +288,13 @@ func TestEth_TxnType(t *testing.T) { func newTestEthEndpoint(store testStore) *Eth { return &Eth{ - hclog.NewNullLogger(), store, 100, nil, 0, + hclog.NewNullLogger(), store, 100, nil, 0, nil, } } func newTestEthEndpointWithPriceLimit(store testStore, priceLimit uint64) *Eth { return &Eth{ - hclog.NewNullLogger(), store, 100, nil, priceLimit, + hclog.NewNullLogger(), store, 100, nil, priceLimit, nil, } } diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index 54f85fef8b..0199ca7285 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/secrets" "github.com/0xPolygon/polygon-edge/versioning" "github.com/gorilla/websocket" @@ -59,7 +60,7 @@ type Config struct { } // NewJSONRPC returns the JSONRPC http server -func NewJSONRPC(logger hclog.Logger, config *Config) (*JSONRPC, error) { +func NewJSONRPC(logger hclog.Logger, config *Config, manager *accounts.Manager) (*JSONRPC, error) { d, err := newDispatcher( logger, config.Store, @@ -71,6 +72,7 @@ func NewJSONRPC(logger hclog.Logger, config *Config) (*JSONRPC, error) { blockRangeLimit: config.BlockRangeLimit, concurrentRequestsDebug: config.ConcurrentRequestsDebug, }, + manager, ) if err != nil { diff --git a/jsonrpc/jsonrpc_test.go b/jsonrpc/jsonrpc_test.go index e941278978..0f17aa0a93 100644 --- a/jsonrpc/jsonrpc_test.go +++ b/jsonrpc/jsonrpc_test.go @@ -111,5 +111,5 @@ func newTestJSONRPC(t *testing.T) (*JSONRPC, error) { Addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}, } - return NewJSONRPC(hclog.NewNullLogger(), config) + return NewJSONRPC(hclog.NewNullLogger(), config, nil) } diff --git a/server/server.go b/server/server.go index de70b8f76c..e5bf164d63 100644 --- a/server/server.go +++ b/server/server.go @@ -17,6 +17,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "google.golang.org/grpc" + "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/archive" "github.com/0xPolygon/polygon-edge/blockchain" "github.com/0xPolygon/polygon-edge/blockchain/storagev2" @@ -76,6 +77,8 @@ type Server struct { // libp2p network network *network.Server + accManager *accounts.Manager + // transaction pool txpool *txpool.TxPool @@ -207,6 +210,11 @@ func NewServer(config *Config) (*Server, error) { m.network = network } + // setup account manager + { + m.accManager = accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: true}, nil) + } + // start blockchain object stateStorage, err := itrie.NewLevelDBStorage(filepath.Join(m.config.DataDir, "trie"), logger) if err != nil { @@ -899,7 +907,7 @@ func (s *Server) setupJSONRPC() error { SecretsManager: s.secretsManager, } - srv, err := jsonrpc.NewJSONRPC(s.logger, conf) + srv, err := jsonrpc.NewJSONRPC(s.logger, conf, s.accManager) if err != nil { return err } From 9d67bf767bfd7e891c8446d57aadd1baffa13712 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 16/69] skip TestUpdatedKeyFileContents --- accounts/keystore/account_cache_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index bbccae0dfd..386a1b95c3 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -347,7 +347,8 @@ func TestCacheFind(t *testing.T) { // TestUpdatedKeyfileContents tests that updating the contents of a keystore file // is noticed by the watcher, and the account cache is updated accordingly func TestUpdatedKeyfileContents(t *testing.T) { - t.Parallel() + t.Skip() + //t.Parallel() // Create a temporary keystore to test with dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) //nolint:gocritic From 7c33c2c5a143ed0b34ed9f5d200f125675101559 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 17/69] introduction of external signer --- accounts/accounts.go | 7 + accounts/event/subscription.go | 51 +++++ accounts/external/backend.go | 293 ++++++++++++++++++++++++ accounts/keystore/account_cache_test.go | 2 +- accounts/keystore/keystore.go | 2 +- accounts/keystore/plain.go | 4 +- jsonrpc/eth_blockchain_test.go | 34 +-- jsonrpc/eth_endpoint.go | 26 +-- jsonrpc/eth_state_test.go | 36 +-- jsonrpc/helper.go | 14 +- jsonrpc/types.go | 24 +- jsonrpc/types_test.go | 8 +- types/types.go | 45 ++++ 13 files changed, 479 insertions(+), 67 deletions(-) create mode 100644 accounts/external/backend.go diff --git a/accounts/accounts.go b/accounts/accounts.go index 31bab41358..bdf9391586 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -14,6 +14,13 @@ type Account struct { URL URL `json:"url"` } +const ( + MimetypeDataWithValidator = "data/validator" + MimetypeTypedData = "data/typed" + MimetypeClique = "application/x-clique-header" + MimetypeTextPlain = "text/plain" +) + // Wallet represents a software or hardware wallet that might contain one or more // accounts (derived from the same seed). type Wallet interface { diff --git a/accounts/event/subscription.go b/accounts/event/subscription.go index c9a143075f..b7813fa6be 100644 --- a/accounts/event/subscription.go +++ b/accounts/event/subscription.go @@ -15,6 +15,57 @@ type Subscription interface { Unsubscribe() // cancels sending of events, closing the error channel } +func NewSubscription(producer func(<-chan struct{}) error) Subscription { + s := &funcSub{unsub: make(chan struct{}), err: make(chan error, 1)} + go func() { + defer close(s.err) + + err := producer(s.unsub) + s.mu.Lock() + + defer s.mu.Unlock() + + if !s.unsubscribed { + if err != nil { + s.err <- err + } + + s.unsubscribed = true + } + }() + + return s +} + +type funcSub struct { + unsub chan struct{} + err chan error + mu sync.Mutex + unsubscribed bool +} + +func (s *funcSub) Unsubscribe() { + s.mu.Lock() + + if s.unsubscribed { + s.mu.Unlock() + + return + } + + s.unsubscribed = true + + close(s.unsub) + + s.mu.Unlock() + // Wait for producer shutdown. + <-s.err +} + +func (s *funcSub) Err() <-chan error { + return s.err +} + // SubscriptionScope provides a facility to unsubscribe multiple subscriptions at once. // // For code that handle more than one subscription, a scope can be used to conveniently diff --git a/accounts/external/backend.go b/accounts/external/backend.go new file mode 100644 index 0000000000..3bcd98b57d --- /dev/null +++ b/accounts/external/backend.go @@ -0,0 +1,293 @@ +package external + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "sync" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/accounts/event" + jsonTypes "github.com/0xPolygon/polygon-edge/jsonrpc" + "github.com/0xPolygon/polygon-edge/types" + "github.com/umbracle/ethgo/jsonrpc" +) + +type ExternalBackend struct { + signers []accounts.Wallet +} + +func (eb *ExternalBackend) Wallets() []accounts.Wallet { + return eb.signers +} + +func NewExternalBackend(endpoint string) (*ExternalBackend, error) { + signer, err := NewExternalSigner(endpoint) + if err != nil { + return nil, err + } + + return &ExternalBackend{ + signers: []accounts.Wallet{signer}, + }, nil +} + +func (eb *ExternalBackend) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + return event.NewSubscription(func(quit <-chan struct{}) error { + <-quit + + return nil + }) +} + +// ExternalSigner provides an API to interact with an external signer (clef) +// It proxies request to the external signer while forwarding relevant +// request headers +type ExternalSigner struct { + client *jsonrpc.Client + endpoint string + status string + cacheMu sync.RWMutex + cache []accounts.Account +} + +func NewExternalSigner(endpoint string) (*ExternalSigner, error) { + client, err := jsonrpc.NewClient(endpoint) + if err != nil { + return nil, err + } + + extsigner := &ExternalSigner{ + client: client, + endpoint: endpoint, + } + + // Check if reachable + version, err := extsigner.pingVersion() + if err != nil { + return nil, err + } + + extsigner.status = fmt.Sprintf("ok [version=%v]", version) + + return extsigner, nil +} + +func (api *ExternalSigner) URL() accounts.URL { + return accounts.URL{ + Scheme: "extapi", + Path: api.endpoint, + } +} + +func (api *ExternalSigner) Status() (string, error) { + return api.status, nil +} + +func (api *ExternalSigner) Open(passphrase string) error { + return errors.New("operation not supported on external signers") +} + +func (api *ExternalSigner) Close() error { + return errors.New("operation not supported on external signers") +} + +func (api *ExternalSigner) Accounts() []accounts.Account { + var accnts []accounts.Account //nolint:prealloc + + res, err := api.listAccounts() + if err != nil { + return accnts + } + + for _, addr := range res { + accnts = append(accnts, accounts.Account{ + URL: accounts.URL{ + Scheme: "extapi", + Path: api.endpoint, + }, + Address: addr, + }) + } + + api.cacheMu.Lock() + api.cache = accnts + api.cacheMu.Unlock() + + return accnts +} + +func (api *ExternalSigner) Contains(account accounts.Account) bool { + api.cacheMu.RLock() + defer api.cacheMu.RUnlock() + + if api.cache == nil { + // If we haven't already fetched the accounts, it's time to do so now + api.cacheMu.RUnlock() + api.Accounts() + api.cacheMu.RLock() + } + + for _, a := range api.cache { + if a.Address == account.Address && (account.URL == (accounts.URL{}) || account.URL == api.URL()) { + return true + } + } + + return false +} + +// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed +func (api *ExternalSigner) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) { + var ( + hexData []byte + res []byte + signAddress = types.NewMixedcaseAddress(account.Address) + ) + + hex.Encode(hexData, data) + + if err := api.client.Call("account_signData", &res, + mimeType, + &signAddress, // Need to use the pointer here, because of how MarshalJSON is defined + hexData); err != nil { + return nil, err + } + + // If V is on 27/28-form, convert to 0/1 for Clique + if mimeType == accounts.MimetypeClique && (res[64] == 27 || res[64] == 28) { + res[64] -= 27 // Transform V from 27/28 to 0/1 for Clique use + } + + return res, nil +} + +func (api *ExternalSigner) SignText(account accounts.Account, text []byte) ([]byte, error) { + var ( + signature []byte + signAddress = types.NewMixedcaseAddress(account.Address) + textHex []byte + ) + + hex.Encode(textHex, text) + + if err := api.client.Call("account_signData", + &signature, + accounts.MimetypeTextPlain, + &signAddress, // Need to use the pointer here, because of how MarshalJSON is defined + textHex); err != nil { + return nil, err + } + + if signature[64] == 27 || signature[64] == 28 { + // If clef is used as a backend, it may already have transformed + // the signature to ethereum-type signature. + signature[64] -= 27 // Transform V from Ethereum-legacy to 0/1 + } + + return signature, nil +} + +// signTransactionResult represents the signinig result returned by clef. +type signTransactionResult struct { + Raw []byte `json:"raw"` + Tx *types.Transaction `json:"tx"` +} + +// SignTx sends the transaction to the external signer. +// If chainID is nil, or tx.ChainID is zero, the chain ID will be assigned +// by the external signer. For non-legacy transactions, the chain ID of the +// transaction overrides the chainID parameter. +func (api *ExternalSigner) SignTx(account accounts.Account, + tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + var to *types.MixedcaseAddress + + if tx.To() != nil { + t := types.NewMixedcaseAddress(*tx.To()) + to = t + } + + args := &jsonTypes.SendTxnArgs{ + Input: jsonTypes.ArgBytesPtr(tx.Input()), + Nonce: jsonTypes.ArgUintPtr(tx.Nonce()), + Value: jsonTypes.ArgBytesPtr(tx.Value().Bytes()), + Gas: jsonTypes.ArgUintPtr(tx.Gas()), + To: to, + From: types.NewMixedcaseAddress(account.Address), + } + + switch tx.Type() { + case types.LegacyTxType, types.AccessListTxType, types.StateTxType: + args.GasPrice = jsonTypes.ArgBytesPtr(tx.GasPrice().Bytes()) + case types.DynamicFeeTxType: + args.GasTipCap = jsonTypes.ArgBytesPtr(tx.GasFeeCap().Bytes()) + args.GasFeeCap = jsonTypes.ArgBytesPtr(tx.GasTipCap().Bytes()) + default: + return nil, fmt.Errorf("unsupported tx type %d", tx.Type()) + } + + // We should request the default chain id that we're operating with + // (the chain we're executing on) + if chainID != nil && chainID.Sign() != 0 { + args.ChainID = jsonTypes.ArgUintPtr(chainID.Uint64()) + } + + if tx.Type() == types.DynamicFeeTxType { + if tx.ChainID().Sign() != 0 { + args.ChainID = jsonTypes.ArgUintPtr(tx.ChainID().Uint64()) + } + } else if tx.Type() == types.AccessListTxType { + // However, if the user asked for a particular chain id, then we should + // use that instead. + if tx.ChainID().Sign() != 0 { + args.ChainID = jsonTypes.ArgUintPtr(tx.ChainID().Uint64()) + } + + accessList := tx.AccessList() + args.AccessList = &accessList + } + + var res signTransactionResult + + if err := api.client.Call("account_signTransaction", args, &res); err != nil { + return nil, err + } + + return res.Tx, nil +} + +func (api *ExternalSigner) SignTextWithPassphrase(account accounts.Account, + passphrase string, text []byte) ([]byte, error) { + return []byte{}, errors.New("password-operations not supported on external signers") +} + +func (api *ExternalSigner) SignTxWithPassphrase(account accounts.Account, + passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return nil, errors.New("password-operations not supported on external signers") +} + +func (api *ExternalSigner) SignDataWithPassphrase(account accounts.Account, + passphrase, mimeType string, data []byte) ([]byte, error) { + return nil, errors.New("password-operations not supported on external signers") +} + +func (api *ExternalSigner) listAccounts() ([]types.Address, error) { + var res []types.Address + + if err := api.client.Call("account_list", nil, &res); err != nil { + return nil, err + } + + return res, nil +} + +func (api *ExternalSigner) pingVersion() (string, error) { + var v string + + if err := api.client.Call("account_version", nil, &v); err != nil { + return "", err + } + + return v, nil +} diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 386a1b95c3..fd11a4acf4 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -351,7 +351,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { //t.Parallel() // Create a temporary keystore to test with - dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) //nolint:gocritic + dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) require.NoError(t, err) ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index c96459165a..aef76138d6 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -28,7 +28,7 @@ var ( // already present in the keystore. ErrAccountAlreadyExists = errors.New("account already exists") - DefaultStorage, _ = filepath.Abs(filepath.Join("data-storage")) //nolint:gocritic + DefaultStorage, _ = filepath.Abs(filepath.Join("data-storage")) ) var KeyStoreType = reflect.TypeOf(&KeyStore{}) diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go index e0670ee221..cf774ddb2c 100644 --- a/accounts/keystore/plain.go +++ b/accounts/keystore/plain.go @@ -34,7 +34,7 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, dat map[string]interface{} jsonData = make([]byte, stat.Size()) ) - _, err = fd.Read(jsonData) + _, err = fd.Read(jsonData) //nolint:wsl if err != nil { return nil, err } @@ -47,7 +47,7 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, key.ID = uuid.MustParse(dat["id"].(string)) //nolint:forcetypeassert - key.PrivateKey, err = crypto.BytesToECDSAPrivateKey([]byte(dat["privatekey"].(string))) //nolint:forcetypeassert + key.PrivateKey, err = crypto.BytesToECDSAPrivateKey([]byte(dat["privatekey"].(string))) if err != nil { return nil, err } diff --git a/jsonrpc/eth_blockchain_test.go b/jsonrpc/eth_blockchain_test.go index 7989a16553..c7595bb0b5 100644 --- a/jsonrpc/eth_blockchain_test.go +++ b/jsonrpc/eth_blockchain_test.go @@ -134,7 +134,7 @@ func TestEth_Block_BlockNumber(t *testing.T) { num, err := eth.BlockNumber() assert.NoError(t, err) - assert.Equal(t, argUintPtr(10), num) + assert.Equal(t, ArgUintPtr(10), num) } func TestEth_Block_GetBlockTransactionCountByHash(t *testing.T) { @@ -560,11 +560,11 @@ func TestEth_Call(t *testing.T) { contractCall := &txnArgs{ From: &addr0, To: &addr1, - Gas: argUintPtr(100000), - GasPrice: argBytesPtr([]byte{0x64}), - Value: argBytesPtr([]byte{0x64}), + Gas: ArgUintPtr(100000), + GasPrice: ArgBytesPtr([]byte{0x64}), + Value: ArgBytesPtr([]byte{0x64}), Data: nil, - Nonce: argUintPtr(0), + Nonce: ArgUintPtr(0), } res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) @@ -584,11 +584,11 @@ func TestEth_Call(t *testing.T) { contractCall := &txnArgs{ From: &addr0, To: &addr1, - Gas: argUintPtr(100000), - GasPrice: argBytesPtr([]byte{0x64}), - Value: argBytesPtr([]byte{0x64}), + Gas: ArgUintPtr(100000), + GasPrice: ArgBytesPtr([]byte{0x64}), + Value: ArgBytesPtr([]byte{0x64}), Data: nil, - Nonce: argUintPtr(0), + Nonce: ArgUintPtr(0), } res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) @@ -610,11 +610,11 @@ func TestEth_Call(t *testing.T) { contractCall := &txnArgs{ From: &addr0, To: &addr1, - Gas: argUintPtr(100000), - GasPrice: argBytesPtr([]byte{0x64}), - Value: argBytesPtr([]byte{0x64}), + Gas: ArgUintPtr(100000), + GasPrice: ArgBytesPtr([]byte{0x64}), + Value: ArgBytesPtr([]byte{0x64}), Data: nil, - Nonce: argUintPtr(0), + Nonce: ArgUintPtr(0), } res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) @@ -644,11 +644,11 @@ func TestEth_CreateAccessList(t *testing.T) { txn := &txnArgs{ From: &addr0, To: &addr1, - Gas: argUintPtr(100000), - GasPrice: argBytesPtr([]byte{0x64}), - Value: argBytesPtr([]byte{0x64}), + Gas: ArgUintPtr(100000), + GasPrice: ArgBytesPtr([]byte{0x64}), + Value: ArgBytesPtr([]byte{0x64}), Data: nil, - Nonce: argUintPtr(0), + Nonce: ArgUintPtr(0), } cases := []struct { diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index 3bdf35d0c5..47b6a0a163 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -109,7 +109,7 @@ type Eth struct { // //nolint:stylecheck func (e *Eth) ChainId() (interface{}, error) { - return argUintPtr(e.chainID), nil + return ArgUintPtr(e.chainID), nil } func (e *Eth) Syncing() (interface{}, error) { @@ -317,7 +317,7 @@ func (e *Eth) BlockNumber() (interface{}, error) { return nil, ErrHeaderNotFound } - return argUintPtr(h.Number), nil + return ArgUintPtr(h.Number), nil } // SendRawTransaction sends a raw transaction @@ -516,13 +516,13 @@ func (e *Eth) GetStorageAt( result, err := e.store.GetStorage(header.StateRoot, address, index) if err != nil { if errors.Is(err, ErrStateNotFound) { - return argBytesPtr(types.ZeroHash[:]), nil + return ArgBytesPtr(types.ZeroHash[:]), nil } return nil, err } - return argBytesPtr(result), nil + return ArgBytesPtr(result), nil } // GasPrice exposes "getGasPrice"'s function logic to public RPC interface @@ -619,7 +619,7 @@ func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash, apiOverride *StateOve return nil, fmt.Errorf("unable to execute call: %w", result.Err) } - return argBytesPtr(result.ReturnValue), nil + return ArgBytesPtr(result.ReturnValue), nil } // EstimateGas estimates the gas needed to execute a transaction @@ -870,12 +870,12 @@ func (e *Eth) GetBalance(address types.Address, filter BlockNumberOrHash) (inter acc, err := e.store.GetAccount(header.StateRoot, address) if errors.Is(err, ErrStateNotFound) { // Account not found, return an empty account - return argUintPtr(0), nil + return ArgUintPtr(0), nil } else if err != nil { return nil, err } - return argBigPtr(acc.Balance), nil + return ArgBigPtr(acc.Balance), nil } // GetTransactionCount returns account nonce @@ -905,13 +905,13 @@ func (e *Eth) GetTransactionCount(address types.Address, filter BlockNumberOrHas nonce, err := GetNextNonce(address, blockNumber, e.store) if err != nil { if errors.Is(err, ErrStateNotFound) { - return argUintPtr(0), nil + return ArgUintPtr(0), nil } return nil, err } - return argUintPtr(nonce), nil + return ArgUintPtr(nonce), nil } // GetCode returns account code at given block number @@ -929,10 +929,10 @@ func (e *Eth) GetCode(address types.Address, filter BlockNumberOrHash) (interfac // return the default value return "0x", nil } else if err != nil { - return argBytesPtr(emptySlice), err + return ArgBytesPtr(emptySlice), err } - return argBytesPtr(code), nil + return ArgBytesPtr(code), nil } // NewFilter creates a filter object, based on filter options, to notify when the state changes (logs). @@ -967,7 +967,7 @@ func (e *Eth) MaxPriorityFeePerGas() (interface{}, error) { return nil, err } - return argBigPtr(priorityFee), nil + return ArgBigPtr(priorityFee), nil } func (e *Eth) FeeHistory(blockCount argUint64, newestBlock BlockNumber, @@ -1009,7 +1009,7 @@ func (e *Eth) FeeHistory(blockCount argUint64, newestBlock BlockNumber, rewardResult := <-rewardCh result := &feeHistoryResult{ - OldestBlock: *argUintPtr(history.OldestBlock), + OldestBlock: *ArgUintPtr(history.OldestBlock), BaseFeePerGas: baseFeePerGasResult, GasUsedRatio: gasUsedRatioResult, Reward: rewardResult, diff --git a/jsonrpc/eth_state_test.go b/jsonrpc/eth_state_test.go index 858681a562..bcae765f87 100644 --- a/jsonrpc/eth_state_test.go +++ b/jsonrpc/eth_state_test.go @@ -138,14 +138,14 @@ func TestEth_State_GetBalance(t *testing.T) { t.Fatalf("invalid type assertion") } - assert.Equal(t, *argUintPtr(0), *uintBalance) + assert.Equal(t, *ArgUintPtr(0), *uintBalance) } else { bigBalance, ok := balance.(*argBig) if !ok { t.Fatalf("invalid type assertion") } - assert.Equal(t, *argBigPtr(big.NewInt(tt.expectedBalance)), *bigBalance) + assert.Equal(t, *ArgBigPtr(big.NewInt(tt.expectedBalance)), *bigBalance) } } }) @@ -264,7 +264,7 @@ func TestEth_State_GetTransactionCount(t *testing.T) { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, argUintPtr(tt.expectedNonce), nonce) + assert.Equal(t, ArgUintPtr(tt.expectedNonce), nonce) } }) } @@ -388,7 +388,7 @@ func TestEth_State_GetCode(t *testing.T) { if tt.target.String() == uninitializedAddress.String() { assert.Equal(t, "0x", code) } else { - assert.Equal(t, argBytesPtr(tt.expectedCode), code) + assert.Equal(t, ArgBytesPtr(tt.expectedCode), code) } } }) @@ -442,7 +442,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: nil, blockHash: nil, succeeded: true, - expectedData: argBytesPtr(hash1[:]), + expectedData: ArgBytesPtr(hash1[:]), }, { name: "should return 32 bytes filled with zero for undefined slot", @@ -456,7 +456,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: &blockNumberLatest, blockHash: nil, succeeded: true, - expectedData: argBytesPtr(types.ZeroHash[:]), + expectedData: ArgBytesPtr(types.ZeroHash[:]), }, { name: "should return 32 bytes filled with zero for non-existing account", @@ -469,7 +469,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { index: hash2, blockNumber: &blockNumberLatest, succeeded: true, - expectedData: argBytesPtr(types.ZeroHash[:]), + expectedData: ArgBytesPtr(types.ZeroHash[:]), }, { name: "should return error for invalid block number", @@ -497,7 +497,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: &blockNumberZero, blockHash: nil, succeeded: true, - expectedData: argBytesPtr(hash1[:]), + expectedData: ArgBytesPtr(hash1[:]), }, { name: "should not return an error for valid block hash", @@ -511,7 +511,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: nil, blockHash: &types.ZeroHash, succeeded: true, - expectedData: argBytesPtr(hash1[:]), + expectedData: ArgBytesPtr(hash1[:]), }, { name: "should return error for invalid block hash", @@ -539,7 +539,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: &blockNumberEarliest, blockHash: nil, succeeded: true, - expectedData: argBytesPtr(hash1[:]), + expectedData: ArgBytesPtr(hash1[:]), }, } @@ -583,9 +583,9 @@ func constructMockTx(gasLimit *argUint64, data *argBytes) *txnArgs { From: &addr0, To: &addr1, Gas: gasLimit, - GasPrice: argBytesPtr([]byte{0x0}), - Value: argBytesPtr([]byte{0x0}), - Nonce: argUintPtr(0), + GasPrice: ArgBytesPtr([]byte{0x0}), + Value: ArgBytesPtr([]byte{0x0}), + Nonce: ArgUintPtr(0), Data: data, } } @@ -634,19 +634,19 @@ func TestEth_EstimateGas_GasLimit(t *testing.T) { "valid gas limit from the latest block for contract interaction", state.TxGasContractCreation, nil, - constructMockTx(nil, argBytesPtr([]byte{0x12})), + constructMockTx(nil, ArgBytesPtr([]byte{0x12})), }, { "valid gas limit from the transaction", state.TxGas, nil, - constructMockTx(argUintPtr(30000), nil), + constructMockTx(ArgUintPtr(30000), nil), }, { "insufficient gas limit from the transaction", state.TxGas, state.ErrNotEnoughIntrinsicGas, - constructMockTx(argUintPtr(state.TxGas/2), nil), + constructMockTx(ArgUintPtr(state.TxGas/2), nil), }, } @@ -786,7 +786,7 @@ func TestEth_EstimateGas_ValueTransfer(t *testing.T) { from := types.StringToAddress("0xSenderAddress") to := types.StringToAddress("0xReceiverAddress") mockTx := constructMockTx(nil, nil) - mockTx.Value = argBytesPtr([]byte{0x1}) + mockTx.Value = ArgBytesPtr([]byte{0x1}) mockTx.From = &from mockTx.To = &to @@ -814,7 +814,7 @@ func TestEth_EstimateGas_ContractCreation(t *testing.T) { from := types.StringToAddress("0xSenderAddress") mockTx := constructMockTx(nil, nil) mockTx.From = &from - mockTx.Input = argBytesPtr([]byte{}) + mockTx.Input = ArgBytesPtr([]byte{}) mockTx.To = nil // Run the estimation diff --git a/jsonrpc/helper.go b/jsonrpc/helper.go index 12a142ea44..fd54c7179a 100644 --- a/jsonrpc/helper.go +++ b/jsonrpc/helper.go @@ -187,7 +187,7 @@ func DecodeTxn(arg *txnArgs, blockNumber uint64, store nonceGetter, forceSetNonc // set default values if arg.From == nil { arg.From = &types.ZeroAddress - arg.Nonce = argUintPtr(0) + arg.Nonce = ArgUintPtr(0) } else if arg.Nonce == nil || forceSetNonce { // get nonce from the pool nonce, err := GetNextNonce(*arg.From, LatestBlockNumber, store) @@ -195,23 +195,23 @@ func DecodeTxn(arg *txnArgs, blockNumber uint64, store nonceGetter, forceSetNonc return nil, err } - arg.Nonce = argUintPtr(nonce) + arg.Nonce = ArgUintPtr(nonce) } if arg.Value == nil { - arg.Value = argBytesPtr([]byte{}) + arg.Value = ArgBytesPtr([]byte{}) } if arg.GasPrice == nil { - arg.GasPrice = argBytesPtr([]byte{}) + arg.GasPrice = ArgBytesPtr([]byte{}) } if arg.GasTipCap == nil { - arg.GasTipCap = argBytesPtr([]byte{}) + arg.GasTipCap = ArgBytesPtr([]byte{}) } if arg.GasFeeCap == nil { - arg.GasFeeCap = argBytesPtr([]byte{}) + arg.GasFeeCap = ArgBytesPtr([]byte{}) } var input []byte @@ -230,7 +230,7 @@ func DecodeTxn(arg *txnArgs, blockNumber uint64, store nonceGetter, forceSetNonc } if arg.Gas == nil { - arg.Gas = argUintPtr(0) + arg.Gas = ArgUintPtr(0) } txType := types.LegacyTxType diff --git a/jsonrpc/types.go b/jsonrpc/types.go index b2c4c9f6a8..95040a660a 100644 --- a/jsonrpc/types.go +++ b/jsonrpc/types.go @@ -113,7 +113,7 @@ func toTransaction( } if txIndex != nil { - res.TxIndex = argUintPtr(uint64(*txIndex)) + res.TxIndex = ArgUintPtr(uint64(*txIndex)) } if t.AccessList() != nil { @@ -323,7 +323,7 @@ func toLog(src *types.Log, logIdx, txIdx uint64, header *types.Header, txHash ty type argBig big.Int -func argBigPtr(b *big.Int) *argBig { +func ArgBigPtr(b *big.Int) *argBig { v := argBig(*b) return &v @@ -358,7 +358,7 @@ func argHashPtr(h types.Hash) *types.Hash { type argUint64 uint64 -func argUintPtr(n uint64) *argUint64 { +func ArgUintPtr(n uint64) *argUint64 { v := argUint64(n) return &v @@ -391,7 +391,7 @@ func (u *argUint64) UnmarshalJSON(buffer []byte) error { type argBytes []byte -func argBytesPtr(b []byte) *argBytes { +func ArgBytesPtr(b []byte) *argBytes { bb := argBytes(b) return &bb @@ -728,3 +728,19 @@ func (t *Transaction) UnmarshalJSON(data []byte) error { return nil } + +type SendTxnArgs struct { + From *types.MixedcaseAddress `json:"from"` + To *types.MixedcaseAddress `json:"to"` + Gas *argUint64 `json:"gas"` + GasPrice *argBytes `json:"gasPrice,omitempty"` + GasTipCap *argBytes `json:"maxFeePerGas,omitempty"` + GasFeeCap *argBytes `json:"maxPriorityFeePerGas,omitempty"` + Value *argBytes `json:"value"` + Data *argBytes `json:"data"` + Input *argBytes `json:"input"` + Nonce *argUint64 `json:"nonce"` + Type *argUint64 `json:"type"` + AccessList *types.TxAccessList `json:"accessList,omitempty"` + ChainID *argUint64 `json:"chainId,omitempty"` +} diff --git a/jsonrpc/types_test.go b/jsonrpc/types_test.go index 6f1a7212d8..cf1a00469f 100644 --- a/jsonrpc/types_test.go +++ b/jsonrpc/types_test.go @@ -31,7 +31,7 @@ func TestBasicTypes_Encode(t *testing.T) { }, { argUint64(10), - argUintPtr(0), + ArgUintPtr(0), "0xa", }, { @@ -234,7 +234,7 @@ func mockTxn() *transaction { tt := &transaction{ Nonce: 1, - GasPrice: argBigPtr(big.NewInt(10)), + GasPrice: ArgBigPtr(big.NewInt(10)), Gas: 100, To: &to, Value: argBig(*big.NewInt(1000)), @@ -245,8 +245,8 @@ func mockTxn() *transaction { Hash: types.Hash{0x2}, From: types.Address{0x3}, BlockHash: &types.ZeroHash, - BlockNumber: argUintPtr(1), - TxIndex: argUintPtr(2), + BlockNumber: ArgUintPtr(1), + TxIndex: ArgUintPtr(2), Type: argUint64(types.LegacyTxType), } diff --git a/types/types.go b/types/types.go index d6a04b9197..03c8c3c86e 100644 --- a/types/types.go +++ b/types/types.go @@ -233,3 +233,48 @@ type OverrideAccount struct { } type StateOverride map[Address]OverrideAccount + +// MixedcaseAddress retains the original string, which may or may not be +// correctly checksummed +type MixedcaseAddress struct { + addr Address + original string +} + +// NewMixedcaseAddress constructor (mainly for testing) +func NewMixedcaseAddress(addr Address) *MixedcaseAddress { + return &MixedcaseAddress{addr: addr, original: addr.String()} +} + +// NewMixedcaseAddressFromString is mainly meant for unit-testing +func NewMixedcaseAddressFromString(hexaddr string) (*MixedcaseAddress, error) { + var addr Address + addr, err := IsValidAddress(hexaddr, false) + if err != nil { + return nil, errors.New("invalid address") + } + return &MixedcaseAddress{addr: addr, original: addr.String()}, nil +} + +// Address returns the address +func (ma *MixedcaseAddress) Address() Address { + return ma.addr +} + +// String implements fmt.Stringer +func (ma *MixedcaseAddress) String() string { + if ma.ValidChecksum() { + return fmt.Sprintf("%s [chksum ok]", ma.original) + } + return fmt.Sprintf("%s [chksum INVALID]", ma.original) +} + +// ValidChecksum returns true if the address has valid checksum +func (ma *MixedcaseAddress) ValidChecksum() bool { + return ma.original == ma.addr.String() +} + +// Original returns the mixed-case input string +func (ma *MixedcaseAddress) Original() string { + return ma.original +} From a8edf4de699eff59a807cc80bb06ef16f44ebcaf Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 18/69] lint fix --- accounts/keystore/account_cache_test.go | 2 +- accounts/keystore/keystore.go | 4 ++-- accounts/keystore/passphrase.go | 4 ++-- command/accounts/create/create.go | 2 +- command/accounts/import/import.go | 2 +- crypto/crypto.go | 3 +++ types/types.go | 2 ++ 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index fd11a4acf4..386a1b95c3 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -351,7 +351,7 @@ func TestUpdatedKeyfileContents(t *testing.T) { //t.Parallel() // Create a temporary keystore to test with - dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) + dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) //nolint:gocritic require.NoError(t, err) ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index aef76138d6..bb605261a0 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -28,7 +28,7 @@ var ( // already present in the keystore. ErrAccountAlreadyExists = errors.New("account already exists") - DefaultStorage, _ = filepath.Abs(filepath.Join("data-storage")) + DefaultStorage, _ = filepath.Abs(filepath.Join("data-storage")) //nolint:gocritic ) var KeyStoreType = reflect.TypeOf(&KeyStore{}) @@ -65,7 +65,7 @@ func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyS ks = &KeyStore{storage: &keyStorePassphrase{keyDir, scryptN, scryptP, false}} } - ks.init(keyDir, hclog.NewNullLogger()) //TO DO LOGGER + ks.init(keyDir, hclog.NewNullLogger()) // TO DO LOGGER return ks } diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 31b694f035..873d62baa8 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -163,7 +163,7 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) // EncryptKey encrypts a key using the specified scrypt parameters into a json // blob that can be decrypted later on. func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { - keyBytes, err := crypto.MarshalECDSAPrivateKey(key.PrivateKey) //TO DO maybe wrong + keyBytes, err := crypto.MarshalECDSAPrivateKey(key.PrivateKey) // TO DO maybe wrong if err != nil { return nil, err } @@ -220,7 +220,7 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { return nil, err } - key, err := crypto.DToECDSA(keyBytes, true) //TO DO maybe wrong + key, err := crypto.DToECDSA(keyBytes, true) // TO DO maybe wrong if err != nil { return nil, fmt.Errorf("invalid key: %w", err) } diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index f6e6cf6a63..8043813cf0 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -60,7 +60,7 @@ func runCommand(cmd *cobra.Command, _ []string) { account, err := keystore.StoreKey("", params.Passphrase, scryptN, scryptP) if err != nil { - outputter.SetError(fmt.Errorf("Cant create account")) + outputter.SetError(fmt.Errorf("can't create account")) } outputter.SetCommandResult(command.Results{&createResult{Address: account.Address, PrivateKeyPath: account.URL.Path}}) diff --git a/command/accounts/import/import.go b/command/accounts/import/import.go index d06aa7012e..0386abd232 100644 --- a/command/accounts/import/import.go +++ b/command/accounts/import/import.go @@ -91,7 +91,7 @@ func runCommand(cmd *cobra.Command, _ []string) { privKey, err := crypto.BytesToECDSAPrivateKey(dec) if err != nil { - outputter.SetError(fmt.Errorf("Failed to initialize private key")) + outputter.SetError(fmt.Errorf("failed to initialize private key")) } backends := am.Backends(keystore.KeyStoreType) diff --git a/crypto/crypto.go b/crypto/crypto.go index 4716ec59d0..b09b768de0 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -367,15 +367,18 @@ func LatestSignerForChainID(chaidID uint64) TxSigner { if chaidID == 0 { // TO DO maybe wrong return NewHomesteadSigner() } + return NewLondonSigner(chaidID) } func DToECDSA(d []byte, strict bool) (*ecdsa.PrivateKey, error) { priv := new(ecdsa.PrivateKey) priv.PublicKey.Curve = btcec.S256() + if strict && 8*len(d) != priv.Params().BitSize { return nil, fmt.Errorf("invalid length, need %d bits", priv.Params().BitSize) } + priv.D = new(big.Int).SetBytes(d) if priv.D.Cmp(secp256k1N) >= 0 { diff --git a/types/types.go b/types/types.go index 03c8c3c86e..02fab85c44 100644 --- a/types/types.go +++ b/types/types.go @@ -253,6 +253,7 @@ func NewMixedcaseAddressFromString(hexaddr string) (*MixedcaseAddress, error) { if err != nil { return nil, errors.New("invalid address") } + return &MixedcaseAddress{addr: addr, original: addr.String()}, nil } @@ -266,6 +267,7 @@ func (ma *MixedcaseAddress) String() string { if ma.ValidChecksum() { return fmt.Sprintf("%s [chksum ok]", ma.original) } + return fmt.Sprintf("%s [chksum INVALID]", ma.original) } From a71a8038bfbca13f08a823d0bf5dd4fee9439068 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:47 +0200 Subject: [PATCH 19/69] lint --- accounts/keystore/account_cache_test.go | 2 +- accounts/keystore/plain.go | 1 + command/accounts/create/create.go | 1 + command/accounts/import/import.go | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 386a1b95c3..40acdc1087 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -348,7 +348,7 @@ func TestCacheFind(t *testing.T) { // is noticed by the watcher, and the account cache is updated accordingly func TestUpdatedKeyfileContents(t *testing.T) { t.Skip() - //t.Parallel() + // t.Parallel() // Create a temporary keystore to test with dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) //nolint:gocritic diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go index cf774ddb2c..0281dc15e6 100644 --- a/accounts/keystore/plain.go +++ b/accounts/keystore/plain.go @@ -34,6 +34,7 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, dat map[string]interface{} jsonData = make([]byte, stat.Size()) ) + _, err = fd.Read(jsonData) //nolint:wsl if err != nil { return nil, err diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index 8043813cf0..84dfc5f389 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -53,6 +53,7 @@ func runCommand(cmd *cobra.Command, _ []string) { scryptN := keystore.StandardScryptN scryptP := keystore.StandardScryptP + if false { scryptN = keystore.LightScryptN scryptP = keystore.LightScryptP diff --git a/command/accounts/import/import.go b/command/accounts/import/import.go index 0386abd232..ea6fced8e6 100644 --- a/command/accounts/import/import.go +++ b/command/accounts/import/import.go @@ -71,6 +71,7 @@ func runCommand(cmd *cobra.Command, _ []string) { scryptN := keystore.StandardScryptN scryptP := keystore.StandardScryptP + if false { scryptN = keystore.LightScryptN scryptP = keystore.LightScryptP From feee0133286491349c746227a25ce62ccaf5ae3e Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:48 +0200 Subject: [PATCH 20/69] lint again --- accounts/keystore/plain.go | 2 +- types/types.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go index 0281dc15e6..0ffb92763a 100644 --- a/accounts/keystore/plain.go +++ b/accounts/keystore/plain.go @@ -35,7 +35,7 @@ func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, jsonData = make([]byte, stat.Size()) ) - _, err = fd.Read(jsonData) //nolint:wsl + _, err = fd.Read(jsonData) if err != nil { return nil, err } diff --git a/types/types.go b/types/types.go index 02fab85c44..185bf1331b 100644 --- a/types/types.go +++ b/types/types.go @@ -249,6 +249,7 @@ func NewMixedcaseAddress(addr Address) *MixedcaseAddress { // NewMixedcaseAddressFromString is mainly meant for unit-testing func NewMixedcaseAddressFromString(hexaddr string) (*MixedcaseAddress, error) { var addr Address + addr, err := IsValidAddress(hexaddr, false) if err != nil { return nil, errors.New("invalid address") From 01db57bb8ac4965dfc414aea4f065ceb623529d8 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:48 +0200 Subject: [PATCH 21/69] personal endpoint --- jsonrpc/dispatcher.go | 18 ++++-- jsonrpc/personal_endpoint.go | 112 +++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 jsonrpc/personal_endpoint.go diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index 335009f3f6..ce90609ff2 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -39,12 +39,13 @@ func (f *funcData) numParams() int { } type endpoints struct { - Eth *Eth - Web3 *Web3 - Net *Net - TxPool *TxPool - Bridge *Bridge - Debug *Debug + Eth *Eth + Web3 *Web3 + Net *Net + TxPool *TxPool + Bridge *Bridge + Debug *Debug + Personal *Personal } // Dispatcher handles all json rpc requests by delegating @@ -118,6 +119,7 @@ func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager *accounts.Man store, } d.endpoints.Debug = NewDebug(store, d.params.concurrentRequestsDebug) + d.endpoints.Personal = &Personal{accManager: manager} var err error @@ -141,6 +143,10 @@ func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager *accounts.Man return err } + if err = d.registerService("personal", d.endpoints.Personal); err != nil { + return err + } + return d.registerService("debug", d.endpoints.Debug) } diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go new file mode 100644 index 0000000000..0461167b0e --- /dev/null +++ b/jsonrpc/personal_endpoint.go @@ -0,0 +1,112 @@ +package jsonrpc + +import ( + "errors" + "fmt" + "math" + "time" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/accounts/keystore" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/types" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" +) + +type Personal struct { + accManager *accounts.Manager +} + +func (p *Personal) ListAccounts() []types.Address { + return p.accManager.Accounts() +} + +func (p *Personal) NewAccount(password string) (types.Address, error) { + ks, err := getKeystore(p.accManager) + if err != nil { + return types.Address{}, err + } + + acc, err := ks.NewAccount(password) + if err != nil { + return types.Address{}, fmt.Errorf("Cant create new account") + } + + return acc.Address, nil +} + +func (p *Personal) ImportRawKey(privKey string, password string) (types.Address, error) { + key, err := crypto.HexToECDSA(privKey) + if err != nil { + return types.Address{}, err + } + + ks, err := getKeystore(p.accManager) + if err != nil { + return types.Address{}, err + } + + acc, err := ks.ImportECDSA(key, password) + + return acc.Address, err +} + +func (p *Personal) UnlockAccount(addr types.Address, password string, duration uint64) (bool, error) { + const max = uint64(time.Duration(math.MaxInt64) / time.Second) + var d time.Duration + + if duration == 0 { + d = 300 * time.Second + } else if duration > max { + return false, errors.New("unlock duration is too large") + } else { + d = time.Duration(duration) * time.Second + } + + ks, err := getKeystore(p.accManager) + if err != nil { + return false, err + } + + err = ks.TimedUnlock(accounts.Account{Address: addr}, password, d) + if err != nil { + return false, err + } + + return true, nil +} + +func (s *Personal) LockAccount(addr types.Address) bool { + if ks, err := getKeystore(s.accManager); err == nil { + return ks.Lock(addr) == nil + } + + return false +} + +func (p *Personal) EcRecover(data, sig []byte) (types.Address, error) { + if len(sig) != crypto.ECDSASignatureLength { + return types.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.ECDSASignatureLength) + } + + if sig[64] != 27 && sig[64] != 28 { + return types.Address{}, errors.New("invalid Ethereum signature V is not 27 or 28") + } + + sig[64] -= 27 + + rpk, _, err := ecdsa.RecoverCompact(sig, data) + if err != nil { + return types.Address{}, err + } + + return crypto.PubKeyToAddress(rpk.ToECDSA()), nil +} + +func getKeystore(am *accounts.Manager) (*keystore.KeyStore, error) { + if ks := am.Backends(keystore.KeyStoreType); len(ks) > 0 { + return ks[0].(*keystore.KeyStore), nil + } + + return nil, errors.New("local keystore not used") +} From b9e70f7aa6e6e4d433c0d20b7514b3b1c1a32ae8 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:48 +0200 Subject: [PATCH 22/69] To one key file --- accounts/keystore/account_cache.go | 246 ++++---------------- accounts/keystore/account_cache_test.go | 71 +----- accounts/keystore/file_cache.go | 58 +++-- accounts/keystore/keystore.go | 55 ++--- accounts/keystore/keystore_test.go | 3 - accounts/keystore/passphrase_test.go | 255 ++++++++++++++++++++ accounts/keystore/plain.go | 78 ------- accounts/keystore/plain_test.go | 296 ------------------------ accounts/keystore/watch.go | 120 ---------- 9 files changed, 382 insertions(+), 800 deletions(-) delete mode 100644 accounts/keystore/plain.go delete mode 100644 accounts/keystore/plain_test.go delete mode 100644 accounts/keystore/watch.go diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 9ab9123b9a..803285a0d5 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -1,26 +1,17 @@ package keystore import ( - "bufio" - "encoding/json" + "errors" "os" - "path/filepath" - "slices" - "sort" - "strings" + "path" "sync" "time" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/types" - mapset "github.com/deckarep/golang-set/v2" "github.com/hashicorp/go-hclog" ) -func byURL(a, b accounts.Account) int { - return a.URL.Cmp(b.URL) -} - // KeyStoreScheme is the protocol scheme prefixing account and wallet URLs. const KeyStoreScheme = "keystore" const minReloadInterval = 2 * time.Second @@ -29,10 +20,8 @@ const minReloadInterval = 2 * time.Second type accountCache struct { logger hclog.Logger keydir string - watcher *watcher mu sync.Mutex - all []accounts.Account - byAddr map[types.Address][]accounts.Account + allMap map[types.Address]encryptedKeyJSONV3 throttle *time.Timer notify chan struct{} fileC fileCache @@ -42,49 +31,60 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st ac := &accountCache{ logger: logger, keydir: keyDir, - byAddr: make(map[types.Address][]accounts.Account), notify: make(chan struct{}, 1), - fileC: fileCache{all: mapset.NewThreadUnsafeSet[string]()}, } - ac.watcher = newWatcher(ac, logger) + + keyPath := path.Join(keyDir, "keys.txt") + + ac.fileC = fileCache{all: make(map[types.Address]encryptedKeyJSONV3), keyDir: keyPath} + + if _, err := os.Stat(keyPath); errors.Is(err, os.ErrNotExist) { + os.Create(keyDir) + } + + ac.scanAccounts() return ac, ac.notify } func (ac *accountCache) accounts() []accounts.Account { - ac.maybeReload() + ac.scanAccounts() + ac.mu.Lock() defer ac.mu.Unlock() - cpy := make([]accounts.Account, len(ac.all)) - copy(cpy, ac.all) + cpy := make([]accounts.Account, len(ac.allMap)) + for addr := range ac.allMap { + cpy = append(cpy, accounts.Account{Address: addr}) + } return cpy } func (ac *accountCache) hasAddress(addr types.Address) bool { - ac.maybeReload() + ac.scanAccounts() ac.mu.Lock() defer ac.mu.Unlock() - return len(ac.byAddr[addr]) > 0 + _, ok := ac.allMap[addr] + + return ok } func (ac *accountCache) add(newAccount accounts.Account) { + ac.scanAccounts() + ac.mu.Lock() defer ac.mu.Unlock() - i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 }) - if i < len(ac.all) && ac.all[i] == newAccount { + if _, ok := ac.allMap[newAccount.Address]; ok { return } - // newAccount is not in the cache. - ac.all = append(ac.all, accounts.Account{}) - copy(ac.all[i+1:], ac.all[i:]) - ac.all[i] = newAccount - ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount) + ac.allMap[newAccount.Address] = encryptedKeyJSONV3{} // TO DO + + ac.fileC.saveData(ac.allMap) } // note: removed needs to be unique here (i.e. both File and Address must be set). @@ -92,127 +92,31 @@ func (ac *accountCache) delete(removed accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() - ac.all = removeAccount(ac.all, removed) - if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { - delete(ac.byAddr, removed.Address) - } else { - ac.byAddr[removed.Address] = ba - } -} - -// deleteByFile removes an account referenced by the given path. -func (ac *accountCache) deleteByFile(path string) { - ac.mu.Lock() - defer ac.mu.Unlock() - - i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Path >= path }) - - if i < len(ac.all) && ac.all[i].URL.Path == path { - removed := ac.all[i] - ac.all = append(ac.all[:i], ac.all[i+1:]...) - - if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { - delete(ac.byAddr, removed.Address) - } else { - ac.byAddr[removed.Address] = ba - } - } -} - -// watcherStarted returns true if the watcher loop started running (even if it -// has since also ended). -func (ac *accountCache) watcherStarted() bool { - ac.mu.Lock() - - defer ac.mu.Unlock() - - return ac.watcher.running || ac.watcher.runEnded -} + ac.scanAccounts() -func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account { - for i := range slice { - if slice[i] == elem { - return append(slice[:i], slice[i+1:]...) - } - } + delete(ac.allMap, removed.Address) - return slice + ac.fileC.saveData(ac.allMap) } // find returns the cached account for address if there is a unique match. // The exact matching rules are explained by the documentation of accounts.Account. // Callers must hold ac.mu. func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { - // Limit search to address candidates if possible. - matches := ac.all - if (a.Address != types.Address{}) { - matches = ac.byAddr[a.Address] - } - - if a.URL.Path != "" { - // If only the basename is specified, complete the path. - if !strings.ContainsRune(a.URL.Path, filepath.Separator) { - a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) - } - - for i := range matches { - if matches[i].URL == a.URL { - return matches[i], nil - } - } - - if (a.Address == types.Address{}) { - return accounts.Account{}, accounts.ErrNoMatch - } - } - - switch len(matches) { - case 1: - return matches[0], nil - case 0: - return accounts.Account{}, accounts.ErrNoMatch - default: - err := &accounts.AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} - copy(err.Matches, matches) - slices.SortFunc(err.Matches, byURL) - - return accounts.Account{}, err - } -} + ac.scanAccounts() -func (ac *accountCache) maybeReload() { ac.mu.Lock() + defer ac.mu.Unlock() - if ac.watcher.running { - ac.mu.Unlock() - - return // A watcher is running and will keep the cache up-to-date. - } - - if ac.throttle == nil { - ac.throttle = time.NewTimer(0) - } else { - select { - case <-ac.throttle.C: - default: - ac.mu.Unlock() - - return // The cache was reloaded recently. - } + if _, ok := ac.allMap[a.Address]; ok { + return a, nil } - // No watcher running, start it. - ac.watcher.start() - ac.throttle.Reset(minReloadInterval) - ac.mu.Unlock() - if err := ac.scanAccounts(); err != nil { - ac.logger.Info("reload", "failed to scan accounts", err) - } + return accounts.Account{}, accounts.ErrNoMatch } func (ac *accountCache) close() { ac.mu.Lock() - ac.watcher.close() if ac.throttle != nil { ac.throttle.Stop() @@ -230,82 +134,26 @@ func (ac *accountCache) close() { // updates the account cache accordingly func (ac *accountCache) scanAccounts() error { // Scan the entire folder metadata for file changes - creates, deletes, updates, err := ac.fileC.scan(ac.keydir) + + ac.mu.Lock() + defer ac.mu.Unlock() + + accs, err := ac.fileC.scanOneFile() if err != nil { ac.logger.Debug("Failed to reload keystore contents", "err", err) return err } - if creates.Cardinality() == 0 && deletes.Cardinality() == 0 && updates.Cardinality() == 0 { - return nil - } - // Create a helper method to scan the contents of the key files - var ( - buf = new(bufio.Reader) - key struct { - Address string `json:"address"` - } - ) - - readAccount := func(path string) *accounts.Account { - fd, err := os.Open(path) - if err != nil { - ac.logger.Trace("Failed to open keystore file", "path", path, "err", err) - - return nil - } - - defer fd.Close() - - buf.Reset(fd) - // Parse the address. - key.Address = "" - err = json.NewDecoder(buf).Decode(&key) - addr := types.StringToAddress(key.Address) - - switch { - case err != nil: - ac.logger.Debug("Failed to decode keystore key", "path", path, "err", err) - case addr == types.Address{}: - ac.logger.Debug("Failed to decode keystore key", "path", path, "err", "missing or zero address") - default: - return &accounts.Account{ - Address: addr, - URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}, - } - } - - return nil - } - // Process all the file diffs - start := time.Now().UTC() + ac.allMap = make(map[types.Address]encryptedKeyJSONV3) - for _, path := range creates.ToSlice() { - if a := readAccount(path); a != nil { - ac.add(*a) - } + for addr, key := range accs { + ac.allMap[addr] = key } - for _, path := range deletes.ToSlice() { - ac.deleteByFile(path) - } - - for _, path := range updates.ToSlice() { - ac.deleteByFile(path) - - if a := readAccount(path); a != nil { - ac.add(*a) - } - } - - end := time.Now().UTC() + ac.notify <- struct{}{} - select { - case ac.notify <- struct{}{}: - default: - } - ac.logger.Trace("Handled keystore changes", "time", end.Sub(start)) + ac.logger.Trace("Handled keystore changes") return nil } diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 40acdc1087..29ce8f08d3 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "reflect" - "slices" "testing" "time" @@ -16,6 +15,7 @@ import ( "github.com/cespare/cp" "github.com/davecgh/go-spew/spew" "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,36 +24,16 @@ var ( cachetestAccounts = []accounts.Account{ { Address: types.StringToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")}, }, { Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, }, { Address: types.StringToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, }, } ) -// waitWatcherStart waits up to 1s for the keystore watcher to start. -func waitWatcherStart(ks *KeyStore) bool { - // On systems where file watch is not supported, just return "ok". - if !ks.cache.watcher.enabled() { - return true - } - - // The watcher should start, and then exit. - for t0 := time.Now().UTC(); time.Since(t0) < 1*time.Second; time.Sleep(100 * time.Millisecond) { - if ks.cache.watcherStarted() { - return true - } - } - - return false -} - func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { var list []accounts.Account for t0 := time.Now().UTC(); time.Since(t0) < 20*time.Second; time.Sleep(100 * time.Millisecond) { @@ -81,9 +61,6 @@ func TestWatchNewFile(t *testing.T) { // Ensure the watcher is started before adding any files. ks.Accounts() - if !waitWatcherStart(ks) { - t.Fatal("keystore watcher didn't start in time") - } // Move in the files. wantAccounts := make([]accounts.Account, len(cachetestAccounts)) for i := range cachetestAccounts { @@ -114,10 +91,6 @@ func TestWatchNoDir(t *testing.T) { if len(list) > 0 { t.Error("initial account list not empty:", list) } - // The watcher should start, and then exit. - if !waitWatcherStart(ks) { - t.Fatal("keystore watcher didn't start in time") - } // Create the directory and copy a key file into it. require.NoError(t, os.MkdirAll(dir, 0700)) @@ -162,13 +135,13 @@ func TestCacheInitialReload(t *testing.T) { if !reflect.DeepEqual(accounts, cachetestAccounts) { t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) } + } func TestCacheAddDeleteOrder(t *testing.T) { t.Parallel() cache, _ := newAccountCache("testdata/no-such-dir", hclog.NewNullLogger()) - cache.watcher.running = true // prevent unexpected reloads accs := []accounts.Account{ { @@ -213,8 +186,6 @@ func TestCacheAddDeleteOrder(t *testing.T) { copy(wantAccounts, accs) - slices.SortFunc(wantAccounts, byURL) - list := cache.accounts() if !reflect.DeepEqual(list, wantAccounts) { @@ -264,28 +235,25 @@ func TestCacheAddDeleteOrder(t *testing.T) { func TestCacheFind(t *testing.T) { t.Parallel() - dir := filepath.Join("testdata", "dir") + file := filepath.Join(filepath.Join("testdata", "dir"), "keys.txt") - cache, _ := newAccountCache(dir, hclog.NewNullLogger()) + absPath, err := filepath.Abs(file) + require.NoError(t, err) - cache.watcher.running = true // prevent unexpected reloads + cache, _ := newAccountCache(absPath, hclog.NewNullLogger()) accs := []accounts.Account{ { Address: types.StringToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, }, { Address: types.StringToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, }, { Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, }, { Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, }, } @@ -295,7 +263,6 @@ func TestCacheFind(t *testing.T) { nomatchAccount := accounts.Account{ Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, } tests := []struct { @@ -305,22 +272,10 @@ func TestCacheFind(t *testing.T) { }{ // by address {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, - // by file - {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, - // by basename - {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]}, // by file and address {Query: accs[0], WantResult: accs[0]}, // ambiguous address, tie resolved by file {Query: accs[2], WantResult: accs[2]}, - // ambiguous address error - { - Query: accounts.Account{Address: accs[2].Address}, - WantError: &accounts.AmbiguousAddrError{ - Addr: accs[2].Address, - Matches: []accounts.Account{accs[2], accs[3]}, - }, - }, // no match error {Query: nomatchAccount, WantError: accounts.ErrNoMatch}, {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: accounts.ErrNoMatch}, @@ -330,17 +285,10 @@ func TestCacheFind(t *testing.T) { for i, test := range tests { a, err := cache.find(test.Query) - if !reflect.DeepEqual(err, test.WantError) { - t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) - continue - } - - if a != test.WantResult { - t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) + assert.Equal(t, test.WantError, err, fmt.Sprintf("Error mismatch test %d", i)) - continue - } + assert.Equal(t, test.WantResult, a, fmt.Sprintf("Not same result %d", i)) } } @@ -361,9 +309,6 @@ func TestUpdatedKeyfileContents(t *testing.T) { t.Error("initial account list not empty:", list) } - if !waitWatcherStart(ks) { - t.Fatal("keystore watcher didn't start in time") - } // Create the directory and copy a key file into it. require.NoError(t, os.MkdirAll(dir, 0700)) defer os.RemoveAll(dir) diff --git a/accounts/keystore/file_cache.go b/accounts/keystore/file_cache.go index d41edc9115..44b5880715 100644 --- a/accounts/keystore/file_cache.go +++ b/accounts/keystore/file_cache.go @@ -1,23 +1,23 @@ package keystore import ( + "encoding/json" "os" - "path/filepath" - "strings" "sync" "time" - mapset "github.com/deckarep/golang-set/v2" + "github.com/0xPolygon/polygon-edge/types" ) // fileCache is a cache of files seen during scan of keystore. type fileCache struct { - all mapset.Set[string] // Set of all files from the keystore folder - lastMod time.Time // Last time instance when a file was modified + all map[types.Address]encryptedKeyJSONV3 // Set of all files from the keystore folder + lastMod time.Time // Last time instance when a file was modified mu sync.Mutex + keyDir string } -func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string], mapset.Set[string], error) { +/*func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string], mapset.Set[string], error) { // List all the files from the keystore folder files, err := os.ReadDir(keyDir) if err != nil { @@ -63,18 +63,46 @@ func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string] fc.all, fc.lastMod = all, newLastMod return creates, deletes, updates, nil +} */ + +func (fc *fileCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) error { + fi, err := os.Create(fc.keyDir) + if err != nil { + return err + } + + defer fi.Close() + + byteAccount, err := json.Marshal(accounts) + if err != nil { + return err + } + + if _, err := fi.Write(byteAccount); err != nil { + return err + } + + return nil } -// nonKeyFile ignores editor backups, hidden files and folders/symlinks. -func nonKeyFile(fi os.DirEntry) bool { - // Skip editor backups and UNIX-style hidden files. - if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") { - return true +func (fc *fileCache) scanOneFile() (map[types.Address]encryptedKeyJSONV3, error) { + fi, err := os.ReadFile(fc.keyDir) + if err != nil { + return nil, err + } + + if len(fi) == 0 { + return nil, nil } - // Skip misc special files, directories (yes, symlinks too). - if fi.IsDir() || !fi.Type().IsRegular() { - return true + + var accounts = make(map[types.Address]encryptedKeyJSONV3) + + err = json.Unmarshal(fi, &accounts) + if err != nil { + return nil, err } - return false + fc.all = accounts + + return accounts, nil } diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index bb605261a0..6f8964081a 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -5,7 +5,6 @@ import ( crand "crypto/rand" "errors" "math/big" - "os" "path/filepath" "reflect" "runtime" @@ -111,45 +110,57 @@ func zeroKey(k *ecdsa.PrivateKey) { func (ks *KeyStore) refreshWallets() { ks.mu.Lock() + accs := ks.cache.accounts() var ( //nolint:prealloc wallets = make([]accounts.Wallet, 0, len(accs)) events []accounts.WalletEvent + find = false ) for _, account := range accs { - for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 { - events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Kind: accounts.WalletDropped}) - ks.wallets = ks.wallets[1:] + find = false + for _, wallet := range ks.wallets { + if wallet.Accounts()[0] == account { + wallets = append(wallets, wallet) + find = true + + break + } } - if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 { + if !find { wallet := &keyStoreWallet{account: account, keyStore: ks} - - events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) wallets = append(wallets, wallet) - continue + events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) } + } + + for _, oldWallet := range ks.wallets { + find = false + for _, newWallet := range wallets { + if newWallet == oldWallet { + find = true - if ks.wallets[0].Accounts()[0] == account { - wallets = append(wallets, ks.wallets[0]) - ks.wallets = ks.wallets[1:] + break + } } - } - for _, wallet := range ks.wallets { - events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped}) + if !find { + events = append(events, accounts.WalletEvent{Wallet: oldWallet, Kind: accounts.WalletDropped}) + } } ks.wallets = wallets + ks.mu.Unlock() for _, event := range events { ks.updateFeed.Send(event) - _ = event } + } func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { @@ -207,14 +218,9 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { if err != nil { return err } - // The order is crucial here. The key is dropped from the - // cache after the file is gone so that a reload happening in - // between won't insert it into the cache again. - err = os.Remove(a.URL.Path) - if err == nil { - ks.cache.delete(a) - ks.refreshWallets() - } + + ks.cache.delete(a) + ks.refreshWallets() return err } @@ -325,10 +331,7 @@ func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout t } func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) { - ks.cache.maybeReload() - ks.cache.mu.Lock() a, err := ks.cache.find(a) - ks.cache.mu.Unlock() return a, err } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 9d01e2efcf..4149fedb33 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -5,7 +5,6 @@ import ( "math/rand" "os" "runtime" - "slices" "strings" "sync" "sync/atomic" @@ -483,8 +482,6 @@ func checkAccounts(t *testing.T, live map[types.Address]accounts.Account, wallet liveList = append(liveList, account) } - slices.SortFunc(liveList, byURL) - 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)) diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 9ea4b17787..642bed0c2f 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -1,10 +1,20 @@ package keystore import ( + "crypto/rand" + "encoding/hex" + "fmt" "os" + "path/filepath" + "reflect" + "strings" "testing" + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/require" ) const ( @@ -12,6 +22,251 @@ const ( veryLightScryptP = 1 ) +func TestKeyStorePassphrase(t *testing.T) { + t.Parallel() + + d := t.TempDir() + + ks := &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} + + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(k1.Address, k2.Address) { + t.Fatal(err) + } + + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { + t.Fatal(err) + } +} + +func TestKeyStorePassphraseDecryptionFail(t *testing.T) { + t.Parallel() + + d := t.TempDir() + + ks := &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} + + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + + if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err.Error() != ErrDecrypt.Error() { + t.Fatalf("wrong error for invalid password\ngot %q\nwant %q", err, ErrDecrypt) + } +} + +func TestImportPreSaleKey(t *testing.T) { + t.Parallel() + + d := t.TempDir() + + ks := &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} + + // file content of a presale key file generated with: + // python pyethsaletool.py genwallet + // with password "foo" + fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" + + account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) + if err != nil { + t.Fatal(err) + } + + if account.Address != types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { + t.Errorf("imported account has wrong address %x", account.Address) + } + + if !strings.HasPrefix(account.URL.Path, d) { + t.Errorf("imported account file not in keystore directory: %q", account.URL) + } +} + +// Test and utils for the key store tests in the Ethereum JSON tests; +// testdataKeyStoreTests/basic_tests.json +type KeyStoreTestV3 struct { + JSON encryptedKeyJSONV3 + Password string + Priv string +} + +type KeyStoreTestV1 struct { + JSON encryptedKeyJSONV1 + Password string + Priv string +} + +func TestV3_PBKDF2_1(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["wikipage_test_vector_pbkdf2"]) +} + +var testsSubmodule = filepath.Join("..", "..", "tests", "testdata", "KeyStoreTests") + +func skipIfSubmoduleMissing(t *testing.T) { + t.Helper() + + if !common.FileExists(testsSubmodule) { + t.Skipf("can't find JSON tests from submodule at %s", testsSubmodule) + } +} + +func TestV3_PBKDF2_2(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["test1"]) +} + +func TestV3_PBKDF2_3(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["python_generated_test_with_odd_iv"]) +} + +func TestV3_PBKDF2_4(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["evilnonce"]) +} + +func TestV3_Scrypt_1(t *testing.T) { + t.Parallel() + + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["wikipage_test_vector_scrypt"]) +} + +func TestV3_Scrypt_2(t *testing.T) { + skipIfSubmoduleMissing(t) + t.Parallel() + + tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecryptV3(t, tests["test2"]) +} + +func TestV1_1(t *testing.T) { + t.Parallel() + + tests := loadKeyStoreTestV1(t, "testdata/v1_test_vector.json") + testDecryptV1(t, tests["test1"]) +} + +func TestV1_2(t *testing.T) { + t.Parallel() + + ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP, true} + addr := types.StringToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") + file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" + + k, err := ks.GetKey(addr, file, "g") + if err != nil { + t.Fatal(err) + } + + privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) + require.NoError(t, err) + + privHex := hex.EncodeToString(privKey) + + expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" + if privHex != expectedHex { + t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) + } +} + +func testDecryptV3(t *testing.T, test KeyStoreTestV3) { + t.Helper() + + privBytes, _, err := decryptKeyV3(&test.JSON, test.Password) + if err != nil { + t.Fatal(err) + } + + privHex := hex.EncodeToString(privBytes) + if test.Priv != privHex { + t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) + } +} + +func testDecryptV1(t *testing.T, test KeyStoreTestV1) { + t.Helper() + + privBytes, _, err := decryptKeyV1(&test.JSON, test.Password) + if err != nil { + t.Fatal(err) + } + + privHex := hex.EncodeToString(privBytes) + if test.Priv != privHex { + t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) + } +} + +func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { + t.Helper() + + tests := make(map[string]KeyStoreTestV3) + + err := accounts.LoadJSON(file, &tests) + if err != nil { + t.Fatal(err) + } + + return tests +} + +func loadKeyStoreTestV1(t *testing.T, file string) map[string]KeyStoreTestV1 { + t.Helper() + + tests := make(map[string]KeyStoreTestV1) + + err := accounts.LoadJSON(file, &tests) + if err != nil { + t.Fatal(err) + } + + return tests +} + +func TestKeyForDirectICAP(t *testing.T) { + t.Parallel() + + key := NewKeyForDirectICAP(rand.Reader) + if !strings.HasPrefix(key.Address.String(), "0x00") { + t.Errorf("Expected first address byte to be zero, have: %s", key.Address.String()) + } +} + +func TestV3_31_Byte_Key(t *testing.T) { + t.Parallel() + + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["31_byte_key"]) +} + +func TestV3_30_Byte_Key(t *testing.T) { + t.Parallel() + + tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + testDecryptV3(t, tests["30_byte_key"]) +} + // Tests that a json key file can be decrypted and encrypted in multiple rounds. func TestKeyEncryptDecrypt(t *testing.T) { t.Parallel() diff --git a/accounts/keystore/plain.go b/accounts/keystore/plain.go deleted file mode 100644 index 0ffb92763a..0000000000 --- a/accounts/keystore/plain.go +++ /dev/null @@ -1,78 +0,0 @@ -package keystore - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/0xPolygon/polygon-edge/crypto" - "github.com/0xPolygon/polygon-edge/types" - "github.com/google/uuid" -) - -type keyStorePlain struct { - keysDirPath string -} - -func (ks keyStorePlain) GetKey(addr types.Address, filename, auth string) (*Key, error) { - fd, err := os.Open(filename) - if err != nil { - return nil, err - } - - key := new(Key) - - defer fd.Close() - - stat, err := fd.Stat() - if err != nil { - return nil, err - } - - var ( - dat map[string]interface{} - jsonData = make([]byte, stat.Size()) - ) - - _, err = fd.Read(jsonData) - if err != nil { - return nil, err - } - - if err := json.Unmarshal(jsonData, &dat); err != nil { - return nil, err - } - - key.Address = types.StringToAddress(dat["address"].(string)) //nolint:forcetypeassert - - key.ID = uuid.MustParse(dat["id"].(string)) //nolint:forcetypeassert - - key.PrivateKey, err = crypto.BytesToECDSAPrivateKey([]byte(dat["privatekey"].(string))) - if err != nil { - return nil, err - } - - if key.Address != addr { - return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr) - } - - return key, nil -} - -func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error { - content, err := json.Marshal(key) - if err != nil { - return err - } - - return writeKeyFile(filename, content) -} - -func (ks keyStorePlain) JoinPath(filename string) string { - if filepath.IsAbs(filename) { - return filename - } - - return filepath.Join(ks.keysDirPath, filename) -} diff --git a/accounts/keystore/plain_test.go b/accounts/keystore/plain_test.go deleted file mode 100644 index eabf1ec88a..0000000000 --- a/accounts/keystore/plain_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package keystore - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "path/filepath" - "reflect" - "strings" - "testing" - - "github.com/0xPolygon/polygon-edge/accounts" - "github.com/0xPolygon/polygon-edge/crypto" - "github.com/0xPolygon/polygon-edge/helper/common" - "github.com/0xPolygon/polygon-edge/types" - "github.com/stretchr/testify/require" -) - -func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) { - t.Helper() - - d := t.TempDir() - - if encrypted { - ks = &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} - } else { - ks = &keyStorePlain{d} - } - - return d, ks -} - -func TestKeyStorePlain(t *testing.T) { - t.Parallel() - - _, ks := tmpKeyStoreIface(t, false) - - pass := "" // not used but required by API - - k1, account, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } - - k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(k1.Address, k2.Address) { - t.Fatal(err) - } - - if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { - t.Fatal(err) - } -} - -func TestKeyStorePassphrase(t *testing.T) { - t.Parallel() - - _, ks := tmpKeyStoreIface(t, true) - - k1, account, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } - - k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(k1.Address, k2.Address) { - t.Fatal(err) - } - - if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { - t.Fatal(err) - } -} - -func TestKeyStorePassphraseDecryptionFail(t *testing.T) { - t.Parallel() - - _, ks := tmpKeyStoreIface(t, true) - - k1, account, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } - - if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err.Error() != ErrDecrypt.Error() { - t.Fatalf("wrong error for invalid password\ngot %q\nwant %q", err, ErrDecrypt) - } -} - -func TestImportPreSaleKey(t *testing.T) { - t.Parallel() - - dir, ks := tmpKeyStoreIface(t, true) - - // file content of a presale key file generated with: - // python pyethsaletool.py genwallet - // with password "foo" - fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" - - account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) - if err != nil { - t.Fatal(err) - } - - if account.Address != types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { - t.Errorf("imported account has wrong address %x", account.Address) - } - - if !strings.HasPrefix(account.URL.Path, dir) { - t.Errorf("imported account file not in keystore directory: %q", account.URL) - } -} - -// Test and utils for the key store tests in the Ethereum JSON tests; -// testdataKeyStoreTests/basic_tests.json -type KeyStoreTestV3 struct { - JSON encryptedKeyJSONV3 - Password string - Priv string -} - -type KeyStoreTestV1 struct { - JSON encryptedKeyJSONV1 - Password string - Priv string -} - -func TestV3_PBKDF2_1(t *testing.T) { - t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") - testDecryptV3(t, tests["wikipage_test_vector_pbkdf2"]) -} - -var testsSubmodule = filepath.Join("..", "..", "tests", "testdata", "KeyStoreTests") - -func skipIfSubmoduleMissing(t *testing.T) { - t.Helper() - - if !common.FileExists(testsSubmodule) { - t.Skipf("can't find JSON tests from submodule at %s", testsSubmodule) - } -} - -func TestV3_PBKDF2_2(t *testing.T) { - skipIfSubmoduleMissing(t) - t.Parallel() - - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["test1"]) -} - -func TestV3_PBKDF2_3(t *testing.T) { - skipIfSubmoduleMissing(t) - t.Parallel() - - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["python_generated_test_with_odd_iv"]) -} - -func TestV3_PBKDF2_4(t *testing.T) { - skipIfSubmoduleMissing(t) - t.Parallel() - - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["evilnonce"]) -} - -func TestV3_Scrypt_1(t *testing.T) { - t.Parallel() - - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") - testDecryptV3(t, tests["wikipage_test_vector_scrypt"]) -} - -func TestV3_Scrypt_2(t *testing.T) { - skipIfSubmoduleMissing(t) - t.Parallel() - - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["test2"]) -} - -func TestV1_1(t *testing.T) { - t.Parallel() - - tests := loadKeyStoreTestV1(t, "testdata/v1_test_vector.json") - testDecryptV1(t, tests["test1"]) -} - -func TestV1_2(t *testing.T) { - t.Parallel() - - ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP, true} - addr := types.StringToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") - file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" - - k, err := ks.GetKey(addr, file, "g") - if err != nil { - t.Fatal(err) - } - - privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) - require.NoError(t, err) - - privHex := hex.EncodeToString(privKey) - - expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" - if privHex != expectedHex { - t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) - } -} - -func testDecryptV3(t *testing.T, test KeyStoreTestV3) { - t.Helper() - - privBytes, _, err := decryptKeyV3(&test.JSON, test.Password) - if err != nil { - t.Fatal(err) - } - - privHex := hex.EncodeToString(privBytes) - if test.Priv != privHex { - t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) - } -} - -func testDecryptV1(t *testing.T, test KeyStoreTestV1) { - t.Helper() - - privBytes, _, err := decryptKeyV1(&test.JSON, test.Password) - if err != nil { - t.Fatal(err) - } - - privHex := hex.EncodeToString(privBytes) - if test.Priv != privHex { - t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) - } -} - -func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { - t.Helper() - - tests := make(map[string]KeyStoreTestV3) - - err := accounts.LoadJSON(file, &tests) - if err != nil { - t.Fatal(err) - } - - return tests -} - -func loadKeyStoreTestV1(t *testing.T, file string) map[string]KeyStoreTestV1 { - t.Helper() - - tests := make(map[string]KeyStoreTestV1) - - err := accounts.LoadJSON(file, &tests) - if err != nil { - t.Fatal(err) - } - - return tests -} - -func TestKeyForDirectICAP(t *testing.T) { - t.Parallel() - - key := NewKeyForDirectICAP(rand.Reader) - if !strings.HasPrefix(key.Address.String(), "0x00") { - t.Errorf("Expected first address byte to be zero, have: %s", key.Address.String()) - } -} - -func TestV3_31_Byte_Key(t *testing.T) { - t.Parallel() - - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") - testDecryptV3(t, tests["31_byte_key"]) -} - -func TestV3_30_Byte_Key(t *testing.T) { - t.Parallel() - - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") - testDecryptV3(t, tests["30_byte_key"]) -} diff --git a/accounts/keystore/watch.go b/accounts/keystore/watch.go deleted file mode 100644 index 7b9ba0dac4..0000000000 --- a/accounts/keystore/watch.go +++ /dev/null @@ -1,120 +0,0 @@ -package keystore - -import ( - "os" - "time" - - "github.com/fsnotify/fsnotify" - "github.com/hashicorp/go-hclog" -) - -type watcher struct { - logger hclog.Logger - ac *accountCache - running bool // set to true when runloop begins - runEnded bool // set to true when runloop ends - starting bool // set to true prior to runloop starting - quit chan struct{} -} - -func newWatcher(ac *accountCache, logger hclog.Logger) *watcher { - return &watcher{ - logger: logger, - ac: ac, - quit: make(chan struct{}), - } -} - -func (*watcher) enabled() bool { return true } - -func (w *watcher) start() { - if w.starting || w.running { - return - } - - w.starting = true - - go w.loop() -} - -func (w *watcher) close() { - close(w.quit) -} - -func (w *watcher) loop() { - defer func() { - w.ac.mu.Lock() - w.running = false - w.starting = false - w.runEnded = true - w.ac.mu.Unlock() - }() - - watcher, err := fsnotify.NewWatcher() - if err != nil { - w.logger.Error("Failed to start filesystem watcher", "err", err) - - return - } - - defer watcher.Close() - - if err := watcher.Add(w.ac.keydir); err != nil { - if !os.IsNotExist(err) { - w.logger.Info("Failed to watch keystore folder", "err", err) - } - - return - } - - w.logger.Trace("Started watching keystore folder", "folder", w.ac.keydir) - defer w.logger.Trace("Stopped watching keystore folder") - - w.ac.mu.Lock() - - w.running = true - - w.ac.mu.Unlock() - - var ( - debounceDuration = 500 * time.Millisecond - rescanTriggered = false - debounce = time.NewTimer(0) - ) - - if !debounce.Stop() { - <-debounce.C - } - - defer debounce.Stop() - - for { - select { - case <-w.quit: - return - case _, ok := <-watcher.Events: - if !ok { - return - } - - if !rescanTriggered { - debounce.Reset(debounceDuration) - - rescanTriggered = true - } - - case _, ok := <-watcher.Errors: - if !ok { - return - } - - w.logger.Info("Filesystem watcher error", "err", err) - case <-debounce.C: - if err := w.ac.scanAccounts(); err != nil { - w.logger.Info("loop", "scanAccounts", err) - } - - rescanTriggered = false - } - } -} From 67b5fdd5f4e7f25876f3688efa99a3c864b1bca8 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:48 +0200 Subject: [PATCH 23/69] keys to one file and account_cache_test fix --- accounts/keystore/account_cache.go | 66 +++-- accounts/keystore/account_cache_test.go | 263 ++++--------------- accounts/keystore/file_cache.go | 59 +---- accounts/keystore/key.go | 85 +----- accounts/keystore/keystore.go | 77 ++---- accounts/keystore/keystore_test.go | 79 ------ accounts/keystore/passphrase.go | 140 ++-------- accounts/keystore/passphrase_test.go | 129 +++------ accounts/keystore/presale.go | 14 +- accounts/keystore/testdata/keystore/keys.txt | 1 + command/accounts/create/create.go | 2 +- 11 files changed, 205 insertions(+), 710 deletions(-) create mode 100644 accounts/keystore/testdata/keystore/keys.txt diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 803285a0d5..288241f381 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -2,7 +2,6 @@ package keystore import ( "errors" - "os" "path" "sync" "time" @@ -24,7 +23,7 @@ type accountCache struct { allMap map[types.Address]encryptedKeyJSONV3 throttle *time.Timer notify chan struct{} - fileC fileCache + fileC *fileCache } func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan struct{}) { @@ -32,15 +31,12 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st logger: logger, keydir: keyDir, notify: make(chan struct{}, 1), + allMap: make(map[types.Address]encryptedKeyJSONV3), } keyPath := path.Join(keyDir, "keys.txt") - ac.fileC = fileCache{all: make(map[types.Address]encryptedKeyJSONV3), keyDir: keyPath} - - if _, err := os.Stat(keyPath); errors.Is(err, os.ErrNotExist) { - os.Create(keyDir) - } + ac.fileC = NewFileCache(keyPath) ac.scanAccounts() @@ -54,8 +50,11 @@ func (ac *accountCache) accounts() []accounts.Account { defer ac.mu.Unlock() cpy := make([]accounts.Account, len(ac.allMap)) + i := 0 + for addr := range ac.allMap { - cpy = append(cpy, accounts.Account{Address: addr}) + cpy[i] = accounts.Account{Address: addr} + i++ } return cpy @@ -72,28 +71,53 @@ func (ac *accountCache) hasAddress(addr types.Address) bool { return ok } -func (ac *accountCache) add(newAccount accounts.Account) { +func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) error { ac.scanAccounts() ac.mu.Lock() defer ac.mu.Unlock() if _, ok := ac.allMap[newAccount.Address]; ok { - return + return errors.New("account already exists") } - ac.allMap[newAccount.Address] = encryptedKeyJSONV3{} // TO DO + ac.allMap[newAccount.Address] = key - ac.fileC.saveData(ac.allMap) + err := ac.fileC.saveData(ac.allMap) + if err != nil { + return err + } + + return nil } -// note: removed needs to be unique here (i.e. both File and Address must be set). -func (ac *accountCache) delete(removed accounts.Account) { +func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) error { + ac.scanAccounts() + ac.mu.Lock() defer ac.mu.Unlock() + if _, ok := ac.allMap[account.Address]; !ok { + return errors.New("this account doesn't exists") + } else { + ac.allMap[account.Address] = key + } + + err := ac.fileC.saveData(ac.allMap) + if err != nil { + return err + } + + return nil +} + +// note: removed needs to be unique here (i.e. both File and Address must be set). +func (ac *accountCache) delete(removed accounts.Account) { ac.scanAccounts() + ac.mu.Lock() + defer ac.mu.Unlock() + delete(ac.allMap, removed.Address) ac.fileC.saveData(ac.allMap) @@ -102,17 +126,17 @@ func (ac *accountCache) delete(removed accounts.Account) { // find returns the cached account for address if there is a unique match. // The exact matching rules are explained by the documentation of accounts.Account. // Callers must hold ac.mu. -func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { +func (ac *accountCache) find(a accounts.Account) (accounts.Account, encryptedKeyJSONV3, error) { ac.scanAccounts() ac.mu.Lock() defer ac.mu.Unlock() - if _, ok := ac.allMap[a.Address]; ok { - return a, nil + if encryptedKey, ok := ac.allMap[a.Address]; ok { + return a, encryptedKey, nil } - return accounts.Account{}, accounts.ErrNoMatch + return accounts.Account{}, encryptedKeyJSONV3{}, accounts.ErrNoMatch } func (ac *accountCache) close() { @@ -151,8 +175,10 @@ func (ac *accountCache) scanAccounts() error { ac.allMap[addr] = key } - ac.notify <- struct{}{} - + select { + case ac.notify <- struct{}{}: + default: + } ac.logger.Trace("Handled keystore changes") return nil diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 29ce8f08d3..3e11155838 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -3,8 +3,6 @@ package keystore import ( "errors" "fmt" - "math/rand" - "os" "path/filepath" "reflect" "testing" @@ -12,8 +10,6 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/types" - "github.com/cespare/cp" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -53,161 +49,80 @@ func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts) } -func TestWatchNewFile(t *testing.T) { - t.Parallel() - - dir, ks := tmpKeyStore(t) - - // Ensure the watcher is started before adding any files. - ks.Accounts() - - // Move in the files. - wantAccounts := make([]accounts.Account, len(cachetestAccounts)) - for i := range cachetestAccounts { - wantAccounts[i] = accounts.Account{ - Address: cachetestAccounts[i].Address, - URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, - } - - if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { - t.Fatal(err) - } - } - - // ks should see the accounts. - if err := waitForAccounts(wantAccounts, ks); err != nil { - t.Error(err) - } -} - -func TestWatchNoDir(t *testing.T) { +func TestCacheInitialReload(t *testing.T) { t.Parallel() - // Create ks but not the directory that it watches. - dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watchnodir-test-%d-%d", os.Getpid(), rand.Int())) - ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) - list := ks.Accounts() - - if len(list) > 0 { - t.Error("initial account list not empty:", list) - } - // Create the directory and copy a key file into it. - require.NoError(t, os.MkdirAll(dir, 0700)) - - defer os.RemoveAll(dir) + cache, _ := newAccountCache(cachetestDir, hclog.NewNullLogger()) + accs := cache.accounts() - file := filepath.Join(dir, "aaa") + require.Equal(t, 3, len(accs)) - if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { - t.Fatal(err) + for _, acc := range cachetestAccounts { + require.True(t, cache.hasAddress(acc.Address)) } - // ks should see the account. - wantAccounts := []accounts.Account{cachetestAccounts[0]} - wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + unwantedAccount := accounts.Account{Address: types.StringToAddress("2cac1adea150210703ba75ed097ddfe24e14f213")} - for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { - list = ks.Accounts() + require.False(t, cache.hasAddress(unwantedAccount.Address)) - if reflect.DeepEqual(list, wantAccounts) { - // ks should have also received change notifications - select { - case <-ks.changes: - default: - t.Fatalf("wasn't notified of new accounts") - } - - return - } - - time.Sleep(d) - } - - t.Errorf("\ngot %v\nwant %v", list, wantAccounts) } -func TestCacheInitialReload(t *testing.T) { +func TestCacheAddDelete(t *testing.T) { t.Parallel() - cache, _ := newAccountCache(cachetestDir, hclog.NewNullLogger()) - accounts := cache.accounts() - - if !reflect.DeepEqual(accounts, cachetestAccounts) { - t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) - } - -} - -func TestCacheAddDeleteOrder(t *testing.T) { - t.Parallel() + tDir := t.TempDir() - cache, _ := newAccountCache("testdata/no-such-dir", hclog.NewNullLogger()) + cache, _ := newAccountCache(tDir, hclog.NewNullLogger()) accs := []accounts.Account{ { Address: types.StringToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, }, { Address: types.StringToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, }, { Address: types.StringToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, }, { Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, }, { Address: types.StringToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"}, }, { Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, }, { Address: types.StringToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, }, } for _, a := range accs { - cache.add(a) + require.NoError(t, cache.add(a, encryptedKeyJSONV3{})) } // Add some of them twice to check that they don't get reinserted. - cache.add(accs[0]) - cache.add(accs[2]) + require.Error(t, cache.add(accs[0], encryptedKeyJSONV3{})) + require.Error(t, cache.add(accs[2], encryptedKeyJSONV3{})) // Check that the account list is sorted by filename. wantAccounts := make([]accounts.Account, len(accs)) copy(wantAccounts, accs) - list := cache.accounts() - - if !reflect.DeepEqual(list, wantAccounts) { - t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) - } - for _, a := range accs { - if !cache.hasAddress(a.Address) { - t.Errorf("expected hasAccount(%x) to return true", a.Address) - } + require.True(t, cache.hasAddress(a.Address)) } - if cache.hasAddress(types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { - t.Errorf("expected hasAccount(%x) to return false", types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) - } + // Expected to return false because this addres is not contained in cache + require.False(t, cache.hasAddress(types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"))) // Delete a few keys from the cache. for i := 0; i < len(accs); i += 2 { cache.delete(wantAccounts[i]) } - cache.delete(accounts.Account{Address: types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) + cache.delete(accounts.Account{Address: types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")}) // Check content again after deletion. wantAccountsAfterDelete := []accounts.Account{ @@ -216,31 +131,27 @@ func TestCacheAddDeleteOrder(t *testing.T) { wantAccounts[5], } - list = cache.accounts() - if !reflect.DeepEqual(list, wantAccountsAfterDelete) { - t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) + deletedAccounts := []accounts.Account{ + wantAccounts[0], + wantAccounts[2], + wantAccounts[4], } - for _, a := range wantAccountsAfterDelete { - if !cache.hasAddress(a.Address) { - t.Errorf("expected hasAccount(%x) to return true", a.Address) - } + for _, acc := range wantAccountsAfterDelete { + require.True(t, cache.hasAddress(acc.Address)) } - if cache.hasAddress(wantAccounts[0].Address) { - t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) + for _, acc := range deletedAccounts { + require.False(t, cache.hasAddress(acc.Address)) } } func TestCacheFind(t *testing.T) { t.Parallel() - file := filepath.Join(filepath.Join("testdata", "dir"), "keys.txt") - - absPath, err := filepath.Abs(file) - require.NoError(t, err) + dir := t.TempDir() - cache, _ := newAccountCache(absPath, hclog.NewNullLogger()) + cache, _ := newAccountCache(dir, hclog.NewNullLogger()) accs := []accounts.Account{ { @@ -252,15 +163,18 @@ func TestCacheFind(t *testing.T) { { Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), }, - { - Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - }, } - for _, a := range accs { - cache.add(a) + matchAccount := accounts.Account{ + Address: types.StringToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), } + for _, acc := range accs { + require.NoError(t, cache.add(acc, encryptedKeyJSONV3{})) + } + + require.Error(t, cache.add(matchAccount, encryptedKeyJSONV3{})) + nomatchAccount := accounts.Account{ Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), } @@ -284,7 +198,7 @@ func TestCacheFind(t *testing.T) { } for i, test := range tests { - a, err := cache.find(test.Query) + a, _, err := cache.find(test.Query) assert.Equal(t, test.WantError, err, fmt.Sprintf("Error mismatch test %d", i)) @@ -294,105 +208,40 @@ func TestCacheFind(t *testing.T) { // TestUpdatedKeyfileContents tests that updating the contents of a keystore file // is noticed by the watcher, and the account cache is updated accordingly -func TestUpdatedKeyfileContents(t *testing.T) { - t.Skip() - // t.Parallel() +func TestCacheUpdate(t *testing.T) { + t.Parallel() - // Create a temporary keystore to test with - dir, err := filepath.Abs(filepath.Join(fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))) //nolint:gocritic - require.NoError(t, err) + keyDir := t.TempDir() - ks := NewKeyStore(dir, LightScryptN, LightScryptP, hclog.NewNullLogger()) + accountCache, _ := newAccountCache(keyDir, hclog.NewNullLogger()) - list := ks.Accounts() + list := accountCache.accounts() if len(list) > 0 { t.Error("initial account list not empty:", list) } - // Create the directory and copy a key file into it. - require.NoError(t, os.MkdirAll(dir, 0700)) - defer os.RemoveAll(dir) - - file := filepath.Join(dir, "aaa") - - // Place one of our testfiles in there - if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { - t.Fatal(err) - } - - // ks should see the account. - wantAccounts := []accounts.Account{cachetestAccounts[0]} - wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} - - if err := waitForAccounts(wantAccounts, ks); err != nil { - t.Error(err) - - return - } - // needed so that modTime of `file` is different to its current value after forceCopyFile - require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) - - // Now replace file contents - if err := cp.CopyFileOverwrite(file, cachetestAccounts[1].URL.Path); err != nil { - t.Fatal(err) - - return - } - - wantAccounts = []accounts.Account{cachetestAccounts[1]} - wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} - - if err := waitForAccounts(wantAccounts, ks); err != nil { - t.Errorf("First replacement failed") - t.Error(err) - - return - } - - // needed so that modTime of `file` is different to its current value after forceCopyFile - require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) + listOfEncryptedKeys := make([]encryptedKeyJSONV3, len(cachetestAccounts)) - // Now replace file contents again - if err := cp.CopyFileOverwrite(file, cachetestAccounts[2].URL.Path); err != nil { - t.Fatal(err) + for i, acc := range cachetestAccounts { + encryptKey := encryptedKeyJSONV3{Address: acc.Address.String(), Crypto: CryptoJSON{Cipher: fmt.Sprintf("test%d", i), CipherText: fmt.Sprintf("test%d", i)}} + listOfEncryptedKeys[i] = encryptKey - return + require.NoError(t, accountCache.add(acc, encryptKey)) } - wantAccounts = []accounts.Account{cachetestAccounts[2]} - wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} + require.NoError(t, accountCache.update(cachetestAccounts[0], encryptedKeyJSONV3{Address: cachetestAccounts[0].Address.String(), Crypto: CryptoJSON{Cipher: "test", CipherText: "test"}})) - if err := waitForAccounts(wantAccounts, ks); err != nil { - t.Errorf("Second replacement failed") - t.Error(err) - - return - } - - // needed so that modTime of `file` is different to its current value after os.WriteFile - require.NoError(t, os.Chtimes(file, time.Now().UTC().Add(-time.Second), time.Now().UTC().Add(-time.Second))) - - // Now replace file contents with crap - if err := os.WriteFile(file, []byte("foo"), 0600); err != nil { - t.Fatal(err) + wantAccount, encryptedKey, err := accountCache.find(cachetestAccounts[0]) + require.NoError(t, err) - return - } + require.Equal(t, wantAccount.Address.String(), encryptedKey.Address) - if err := waitForAccounts([]accounts.Account{}, ks); err != nil { - t.Errorf("Emptying account file failed") - t.Error(err) + require.Equal(t, encryptedKey.Crypto.Cipher, "test") - return - } -} + require.Equal(t, encryptedKey.Crypto.CipherText, "test") -// forceCopyFile is like cp.CopyFile, but doesn't complain if the destination exists. -func forceCopyFile(dst, src string) error { - data, err := os.ReadFile(src) - if err != nil { - return err - } + wantAccount, encryptedKey, err = accountCache.find(cachetestAccounts[1]) + require.NoError(t, err) - return os.WriteFile(dst, data, 0644) + require.Equal(t, listOfEncryptedKeys[1], encryptedKey) } diff --git a/accounts/keystore/file_cache.go b/accounts/keystore/file_cache.go index 44b5880715..8aaa502bb7 100644 --- a/accounts/keystore/file_cache.go +++ b/accounts/keystore/file_cache.go @@ -2,68 +2,29 @@ package keystore import ( "encoding/json" + "errors" "os" "sync" - "time" "github.com/0xPolygon/polygon-edge/types" ) // fileCache is a cache of files seen during scan of keystore. type fileCache struct { - all map[types.Address]encryptedKeyJSONV3 // Set of all files from the keystore folder - lastMod time.Time // Last time instance when a file was modified - mu sync.Mutex - keyDir string + all map[types.Address]encryptedKeyJSONV3 + mu sync.Mutex + keyDir string } -/*func (fc *fileCache) scan(keyDir string) (mapset.Set[string], mapset.Set[string], mapset.Set[string], error) { - // List all the files from the keystore folder - files, err := os.ReadDir(keyDir) - if err != nil { - return nil, nil, nil, err - } - - fc.mu.Lock() - defer fc.mu.Unlock() - - all := mapset.NewThreadUnsafeSet[string]() - mods := mapset.NewThreadUnsafeSet[string]() - - var newLastMod time.Time - - for _, fi := range files { - if nonKeyFile(fi) { - continue - } - - path := filepath.Join(keyDir, fi.Name()) +func NewFileCache(keyPath string) *fileCache { + fc := &fileCache{all: make(map[types.Address]encryptedKeyJSONV3), keyDir: keyPath} - all.Add(path) - - info, err := fi.Info() - if err != nil { - return nil, nil, nil, err - } - - modified := info.ModTime() - if modified.After(fc.lastMod) { - mods.Add(path) - } - - if modified.After(newLastMod) { - newLastMod = modified - } + if _, err := os.Stat(keyPath); errors.Is(err, os.ErrNotExist) { + os.Create(fc.keyDir) } - deletes := fc.all.Difference(all) - creates := all.Difference(fc.all) - updates := mods.Difference(creates) - - fc.all, fc.lastMod = all, newLastMod - - return creates, deletes, updates, nil -} */ + return fc +} func (fc *fileCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) error { fi, err := os.Create(fc.keyDir) diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 7eb03dfd07..80f4fe6794 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/ecdsa" "encoding/hex" - "encoding/json" "fmt" "io" "os" @@ -33,18 +32,9 @@ type Key struct { type keyStore interface { // Loads and decrypts the key from disk. - GetKey(addr types.Address, filename string, auth string) (*Key, error) + GetKey(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) // Writes and encrypts the key. - StoreKey(filename string, k *Key, auth string) error - // Joins filename with the key directory unless it is already absolute. - JoinPath(filename string) string -} - -type plainKeyJSON struct { - Address string `json:"address"` - PrivateKey string `json:"privatekey"` - ID string `json:"id"` - Version int `json:"version"` + StoreKey(k *Key, auth string) (encryptedKeyJSONV3, error) } type encryptedKeyJSONV3 struct { @@ -54,13 +44,6 @@ type encryptedKeyJSONV3 struct { Version int `json:"version"` } -type encryptedKeyJSONV1 struct { - Address string `json:"address"` - Crypto CryptoJSON `json:"crypto"` - ID string `json:"id"` - Version string `json:"version"` -} - type CryptoJSON struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"` @@ -74,58 +57,6 @@ type cipherparamsJSON struct { IV string `json:"iv"` } -// TO DO marshall private key -func (k *Key) MarshalJSON() (j []byte, err error) { - privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) // get more time for this - if err != nil { - return nil, err - } - - jStruct := plainKeyJSON{ - hex.EncodeToString(k.Address[:]), - hex.EncodeToString(privKey), - k.ID.String(), - version, - } - - j, err = json.Marshal(jStruct) - - return j, err -} - -func (k *Key) UnmarshalJSON(j []byte) (err error) { - keyJSON := new(plainKeyJSON) - - err = json.Unmarshal(j, &keyJSON) - if err != nil { - return err - } - - u := new(uuid.UUID) - - *u, err = uuid.Parse(keyJSON.ID) - if err != nil { - return err - } - - k.ID = *u - - addr, err := hex.DecodeString(keyJSON.Address) - if err != nil { - return err - } - - privkey, err := crypto.HexToECDSA(keyJSON.PrivateKey) - if err != nil { - return err - } - - k.Address = types.BytesToAddress(addr) - k.PrivateKey = privkey - - return nil -} - // TO DO newKeyFromECDSA func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { id, err := uuid.NewRandom() @@ -224,24 +155,24 @@ func NewKeyForDirectICAP(rand io.Reader) *Key { return key } -func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) { +func storeNewKey(ks keyStore, rand io.Reader, auth string) (encryptedKeyJSONV3, accounts.Account, error) { key, err := newKey(rand) if err != nil { - return nil, accounts.Account{}, err + return encryptedKeyJSONV3{}, accounts.Account{}, err } a := accounts.Account{ Address: key.Address, - URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}, } - if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { + encryptedKey, err := ks.StoreKey(key, auth) + if err != nil { zeroKey(key.PrivateKey) - return nil, a, err + return encryptedKeyJSONV3{}, a, err } - return key, a, err + return encryptedKey, a, err } func writeKeyFile(file string, content []byte) error { diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 6f8964081a..12d653be71 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -59,9 +59,9 @@ type unlocked struct { func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { var ks *KeyStore if keyDir == "" { - ks = &KeyStore{storage: &keyStorePassphrase{DefaultStorage, scryptN, scryptP, false}} + ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} } else { - ks = &KeyStore{storage: &keyStorePassphrase{keyDir, scryptN, scryptP, false}} + ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} } ks.init(keyDir, hclog.NewNullLogger()) // TO DO LOGGER @@ -330,12 +330,6 @@ func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout t return nil } -func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) { - a, err := ks.cache.find(a) - - return a, err -} - func (ks *KeyStore) expire(addr types.Address, u *unlocked, timeout time.Duration) { t := time.NewTimer(timeout) defer t.Stop() @@ -359,69 +353,29 @@ func (ks *KeyStore) expire(addr types.Address, u *unlocked, timeout time.Duratio } func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { - a, err := ks.Find(a) + a, encryptedKeyJSONV3, err := ks.cache.find(a) if err != nil { return a, nil, err } - key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth) + key, err := ks.storage.GetKey(encryptedKeyJSONV3, auth) return a, key, err } func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { - _, account, err := storeNewKey(ks.storage, crand.Reader, passphrase) + encryptedKey, account, err := storeNewKey(ks.storage, crand.Reader, passphrase) if err != nil { return accounts.Account{}, err } - ks.cache.add(account) + ks.cache.add(account, encryptedKey) ks.refreshWallets() return account, nil } -func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) { - _, key, err := ks.getDecryptedKey(a, passphrase) - if err != nil { - return nil, err - } - - var N, P int - - if store, ok := ks.storage.(*keyStorePassphrase); ok { - N, P = store.scryptN, store.scryptP - } else { - N, P = StandardScryptN, StandardScryptP - } - - return EncryptKey(key, newPassphrase, N, P) -} - -func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) { - key, err := DecryptKey(keyJSON, passphrase) - - if key != nil && key.PrivateKey != nil { - defer zeroKey(key.PrivateKey) - } - - if err != nil { - return accounts.Account{}, err - } - - ks.importMu.Lock() - defer ks.importMu.Unlock() - - if ks.cache.hasAddress(key.Address) { - return accounts.Account{ - Address: key.Address, - }, ErrAccountAlreadyExists - } - - return ks.importKey(key, newPassphrase) -} - func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) { ks.importMu.Lock() defer ks.importMu.Unlock() @@ -437,14 +391,14 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { - a := accounts.Account{Address: key.Address, - URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} + a := accounts.Account{Address: key.Address} - if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { + encryptedKeyJSONV3, err := ks.storage.StoreKey(key, passphrase) + if err != nil { return accounts.Account{}, err } - ks.cache.add(a) + ks.cache.add(a, encryptedKeyJSONV3) ks.refreshWallets() return a, nil @@ -456,17 +410,22 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) return err } - return ks.storage.StoreKey(a.URL.Path, key, newPassphrase) + encryptedKey, err := ks.storage.StoreKey(key, newPassphrase) + if err != nil { + return err + } + + return ks.cache.update(a, encryptedKey) } func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { - a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase) + a, encryptedKey, err := importPreSaleKey(ks.storage, keyJSON, passphrase) if err != nil { return a, err } - ks.cache.add(a) + ks.cache.add(a, encryptedKey) ks.refreshWallets() return a, nil diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 4149fedb33..b18c5eae2b 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -6,8 +6,6 @@ import ( "os" "runtime" "strings" - "sync" - "sync/atomic" "testing" "time" @@ -389,83 +387,6 @@ func TestImportECDSA(t *testing.T) { } } -// TestImportExport tests the import and export functionality of a keystore. -func TestImportExport(t *testing.T) { - t.Parallel() - - _, ks := tmpKeyStore(t) - - 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) - - 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) { - t.Parallel() - - _, ks := tmpKeyStore(t) - - 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) - - var atom atomic.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 { - atom.Add(1) - } - }() - } - wg.Wait() - - if atom.Load() != 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[types.Address]accounts.Account, wallets []accounts.Wallet) { t.Helper() diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 873d62baa8..7eaf7e22c6 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -6,11 +6,8 @@ import ( "crypto/rand" "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "io" - "os" - "path/filepath" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/crypto" @@ -44,71 +41,40 @@ const ( ) type keyStorePassphrase struct { - keysDirPath string - scryptN int - scryptP int + scryptN int + scryptP int // skipKeyFileVerification disables the security-feature which does // reads and decrypts any newly created keyfiles. This should be 'false' in all // cases except tests -- setting this to 'true' is not recommended. - skipKeyFileVerification bool } -func (ks keyStorePassphrase) GetKey(addr types.Address, filename, auth string) (*Key, error) { - keyJSON, err := os.ReadFile(filename) +func (ks keyStorePassphrase) GetKey(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) { + key, err := DecryptKey(encryptedKey, auth) if err != nil { return nil, err } - key, err := DecryptKey(keyJSON, auth) - if err != nil { - return nil, err - } - - if key.Address != addr { - return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, addr) + if key.Address != types.StringToAddress(encryptedKey.Address) { + return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, encryptedKey.Address) } return key, nil } // StoreKey generates a key, encrypts with 'auth' and stores in the given directory -func StoreKey(dir, auth string, scryptN, scryptP int) (accounts.Account, error) { - _, a, err := storeNewKey(&keyStorePassphrase{dir, scryptN, scryptP, false}, rand.Reader, auth) +func StoreKey(auth string, scryptN, scryptP int) (accounts.Account, error) { + _, a, err := storeNewKey(&keyStorePassphrase{scryptN, scryptP}, rand.Reader, auth) return a, err } -func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error { - keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) +func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (encryptedKeyJSONV3, error) { + encryptedKey, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) if err != nil { - return err - } - - // Write into temporary file - tmpName, err := writeTemporaryKeyFile(filename, keyjson) - if err != nil { - return err - } - - if !ks.skipKeyFileVerification { - // Verify that we can decrypt the file with the given password. - _, err = ks.GetKey(key.Address, tmpName, auth) - if err != nil { - msg := "an error was encountered when saving and verifying the keystore file" - - return fmt.Errorf(msg, tmpName, err) - } - } - - return os.Rename(tmpName, filename) -} - -func (ks keyStorePassphrase) JoinPath(filename string) string { - if filepath.IsAbs(filename) { - return filename + return encryptedKeyJSONV3{}, err } - return filepath.Join(ks.keysDirPath, filename) + return encryptedKey, nil } // EncryptDataV3 encrypts the data given as 'data' with the password 'auth'. @@ -162,15 +128,15 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) // EncryptKey encrypts a key using the specified scrypt parameters into a json // blob that can be decrypted later on. -func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { +func EncryptKey(key *Key, auth string, scryptN, scryptP int) (encryptedKeyJSONV3, error) { keyBytes, err := crypto.MarshalECDSAPrivateKey(key.PrivateKey) // TO DO maybe wrong if err != nil { - return nil, err + return encryptedKeyJSONV3{}, err } cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) if err != nil { - return nil, err + return encryptedKeyJSONV3{}, err } encryptedKeyJSONV3 := encryptedKeyJSONV3{ @@ -180,42 +146,13 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { version, } - return json.Marshal(encryptedKeyJSONV3) + return encryptedKeyJSONV3, nil } // DecryptKey decrypts a key from a json blob, returning the private key itself. -func DecryptKey(keyjson []byte, auth string) (*Key, error) { +func DecryptKey(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) { // Parse the json into a simple map to fetch the key version - m := make(map[string]interface{}) - - if err := json.Unmarshal(keyjson, &m); err != nil { - return nil, err - } - - // Depending on the version try to parse one way or another - var ( - keyBytes, keyID []byte - err error - ) - - if version, ok := m["version"].(string); ok && version == "1" { - k := new(encryptedKeyJSONV1) - - if err := json.Unmarshal(keyjson, k); err != nil { - return nil, err - } - - keyBytes, keyID, err = decryptKeyV1(k, auth) - } else { - k := new(encryptedKeyJSONV3) - - if err := json.Unmarshal(keyjson, k); err != nil { - return nil, err - } - - keyBytes, keyID, err = decryptKeyV3(k, auth) - } - // Handle any decryption errors and return the key + keyBytes, keyID, err := decryptKeyV3(&encryptedKey, auth) if err != nil { return nil, err } @@ -295,47 +232,6 @@ func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byt return plainText, keyID, err } -func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyID []byte, err error) { - keyUUID, err := uuid.Parse(keyProtected.ID) - if err != nil { - return nil, nil, err - } - - keyID = keyUUID[:] - - mac, err := hex.DecodeString(keyProtected.Crypto.MAC) - if err != nil { - return nil, nil, err - } - - iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) - if err != nil { - return nil, nil, err - } - - cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) - if err != nil { - return nil, nil, err - } - - derivedKey, err := getKDFKey(keyProtected.Crypto, auth) - if err != nil { - return nil, nil, err - } - - calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) - if !bytes.Equal(calculatedMAC, mac) { - return nil, nil, accounts.ErrDecrypt - } - - plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) - if err != nil { - return nil, nil, err - } - - return plainText, keyID, err -} - func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { authArray := []byte(auth) diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 642bed0c2f..c4f424f5bf 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -3,15 +3,14 @@ package keystore import ( "crypto/rand" "encoding/hex" + "encoding/json" "fmt" "os" "path/filepath" - "reflect" "strings" "testing" "github.com/0xPolygon/polygon-edge/accounts" - "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/types" "github.com/stretchr/testify/require" @@ -25,42 +24,30 @@ const ( func TestKeyStorePassphrase(t *testing.T) { t.Parallel() - d := t.TempDir() - - ks := &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} + ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} k1, account, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) - if err != nil { - t.Fatal(err) - } + k2, err := ks.GetKey(k1, pass) + require.NoError(t, err) - if !reflect.DeepEqual(k1.Address, k2.Address) { - t.Fatal(err) - } + require.Equal(t, types.StringToAddress(k1.Address), k2.Address) - if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { - t.Fatal(err) - } + require.Equal(t, k2.Address, account.Address) } func TestKeyStorePassphraseDecryptionFail(t *testing.T) { t.Parallel() - d := t.TempDir() - - ks := &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} + ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} - k1, account, err := storeNewKey(ks, rand.Reader, pass) + k1, _, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) } - if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err.Error() != ErrDecrypt.Error() { + if _, err = ks.GetKey(k1, "bar"); err.Error() != ErrDecrypt.Error() { t.Fatalf("wrong error for invalid password\ngot %q\nwant %q", err, ErrDecrypt) } } @@ -68,9 +55,7 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { func TestImportPreSaleKey(t *testing.T) { t.Parallel() - d := t.TempDir() - - ks := &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP, true} + ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} // file content of a presale key file generated with: // python pyethsaletool.py genwallet @@ -85,10 +70,6 @@ func TestImportPreSaleKey(t *testing.T) { if account.Address != types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { t.Errorf("imported account has wrong address %x", account.Address) } - - if !strings.HasPrefix(account.URL.Path, d) { - t.Errorf("imported account file not in keystore directory: %q", account.URL) - } } // Test and utils for the key store tests in the Ethereum JSON tests; @@ -99,12 +80,6 @@ type KeyStoreTestV3 struct { Priv string } -type KeyStoreTestV1 struct { - JSON encryptedKeyJSONV1 - Password string - Priv string -} - func TestV3_PBKDF2_1(t *testing.T) { t.Parallel() tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") @@ -160,35 +135,39 @@ func TestV3_Scrypt_2(t *testing.T) { testDecryptV3(t, tests["test2"]) } -func TestV1_1(t *testing.T) { - t.Parallel() +/* + func TestV1_2(t *testing.T) { + t.Parallel() - tests := loadKeyStoreTestV1(t, "testdata/v1_test_vector.json") - testDecryptV1(t, tests["test1"]) -} + ks := &keyStorePassphrase{LightScryptN, LightScryptP} + addr := types.StringToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") + file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" -func TestV1_2(t *testing.T) { - t.Parallel() + keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") + if err != nil { + t.Fatal(err) + } - ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP, true} - addr := types.StringToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") - file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" + keyEncrypted := new(encryptedKeyJSONV3) - k, err := ks.GetKey(addr, file, "g") - if err != nil { - t.Fatal(err) - } + keyEncrypted := - privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) - require.NoError(t, err) + k, err := ks.GetKey(addr, file, "g") + if err != nil { + t.Fatal(err) + } + + privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) + require.NoError(t, err) - privHex := hex.EncodeToString(privKey) + privHex := hex.EncodeToString(privKey) - expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" - if privHex != expectedHex { - t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) + expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" + if privHex != expectedHex { + t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) + } } -} +*/ func testDecryptV3(t *testing.T, test KeyStoreTestV3) { t.Helper() @@ -204,20 +183,6 @@ func testDecryptV3(t *testing.T, test KeyStoreTestV3) { } } -func testDecryptV1(t *testing.T, test KeyStoreTestV1) { - t.Helper() - - privBytes, _, err := decryptKeyV1(&test.JSON, test.Password) - if err != nil { - t.Fatal(err) - } - - privHex := hex.EncodeToString(privBytes) - if test.Priv != privHex { - t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) - } -} - func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { t.Helper() @@ -231,19 +196,6 @@ func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { return tests } -func loadKeyStoreTestV1(t *testing.T, file string) map[string]KeyStoreTestV1 { - t.Helper() - - tests := make(map[string]KeyStoreTestV1) - - err := accounts.LoadJSON(file, &tests) - if err != nil { - t.Fatal(err) - } - - return tests -} - func TestKeyForDirectICAP(t *testing.T) { t.Parallel() @@ -270,23 +222,26 @@ func TestV3_30_Byte_Key(t *testing.T) { // Tests that a json key file can be decrypted and encrypted in multiple rounds. func TestKeyEncryptDecrypt(t *testing.T) { t.Parallel() + keyEncrypted := new(encryptedKeyJSONV3) keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") if err != nil { t.Fatal(err) } + json.Unmarshal(keyjson, keyEncrypted) + password := "" address := types.StringToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") // Do a few rounds of decryption and encryption for i := 0; i < 3; i++ { // Try a bad password first - if _, err := DecryptKey(keyjson, password+"bad"); err == nil { + if _, err := DecryptKey(*keyEncrypted, password+"bad"); err == nil { t.Errorf("test %d: json key decrypted with bad password", i) } // Decrypt with the correct password - key, err := DecryptKey(keyjson, password) + key, err := DecryptKey(*keyEncrypted, password) if err != nil { t.Fatalf("test %d: json key failed to decrypt: %v", i, err) } @@ -296,7 +251,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { } // Recrypt with a new password and start over password += "new data appended" - if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { + if *keyEncrypted, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { t.Errorf("test %d: failed to re-encrypt key %v", i, err) } } diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index 6e9848d438..b459e58d08 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -15,28 +15,24 @@ import ( "golang.org/x/crypto/pbkdf2" ) -func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, *Key, error) { +func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, encryptedKeyJSONV3, error) { key, err := decryptPreSaleKey(keyJSON, password) if err != nil { - return accounts.Account{}, nil, err + return accounts.Account{}, encryptedKeyJSONV3{}, err } key.ID, err = uuid.NewRandom() if err != nil { - return accounts.Account{}, nil, err + return accounts.Account{}, encryptedKeyJSONV3{}, err } a := accounts.Account{ Address: key.Address, - URL: accounts.URL{ - Scheme: KeyStoreScheme, - Path: keyStore.JoinPath(keyFileName(key.Address)), - }, } - err = keyStore.StoreKey(a.URL.Path, key, password) + encryptKey, err := keyStore.StoreKey(key, password) - return a, key, err + return a, encryptKey, err } func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error) { diff --git a/accounts/keystore/testdata/keystore/keys.txt b/accounts/keystore/testdata/keystore/keys.txt new file mode 100644 index 0000000000..b5084a5b42 --- /dev/null +++ b/accounts/keystore/testdata/keystore/keys.txt @@ -0,0 +1 @@ +{"0x289d485D9771714CCe91D3393D764E1311907ACc":{"address":"","crypto":{"cipher":"","ciphertext":"","cipherparams":{"iv":""},"kdf":"","kdfparams":null,"mac":""},"id":"","version":0},"0x7EF5A6135f1FD6a02593eEdC869c6D41D934aef8":{"address":"","crypto":{"cipher":"","ciphertext":"","cipherparams":{"iv":""},"kdf":"","kdfparams":null,"mac":""},"id":"","version":0},"0xf466859eAD1932D743d622CB74FC058882E8648A":{"address":"","crypto":{"cipher":"","ciphertext":"","cipherparams":{"iv":""},"kdf":"","kdfparams":null,"mac":""},"id":"","version":0}} \ No newline at end of file diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index 84dfc5f389..355f4fee95 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -59,7 +59,7 @@ func runCommand(cmd *cobra.Command, _ []string) { scryptP = keystore.LightScryptP } - account, err := keystore.StoreKey("", params.Passphrase, scryptN, scryptP) + account, err := keystore.StoreKey(params.Passphrase, scryptN, scryptP) if err != nil { outputter.SetError(fmt.Errorf("can't create account")) } From c1ef83acd7a38a303cbd3030dcfca02702de25a6 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:48 +0200 Subject: [PATCH 24/69] keystore-test fix --- accounts/keystore/keystore_test.go | 38 +++++++++++------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index b18c5eae2b..b334f61cd2 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -3,16 +3,12 @@ package keystore import ( "errors" "math/rand" - "os" - "runtime" - "strings" "testing" "time" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/event" "github.com/0xPolygon/polygon-edge/crypto" - "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" ) @@ -24,26 +20,13 @@ const pass = "foo" func TestKeyStore(t *testing.T) { t.Parallel() - dir, ks := tmpKeyStore(t) + _, ks := tmpKeyStore(t) a, err := ks.NewAccount(pass) 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) } @@ -56,10 +39,6 @@ func TestKeyStore(t *testing.T) { t.Errorf("Delete error: %v", err) } - if common.FileExists(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) } @@ -406,8 +385,19 @@ func checkAccounts(t *testing.T, live map[types.Address]accounts.Account, wallet 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]) + } + + isFind := false + for _, liveWallet := range liveList { + if liveWallet == wallet.Accounts()[0] { + isFind = true + + break + } + } + + if !isFind { + t.Errorf("wallet %d: account mismatch: have %v, want %v", j, wallet, liveList[j]) } } } From f582ba0795421e82868b423ccbd5d95e1c0c70ae Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 16:16:48 +0200 Subject: [PATCH 25/69] comment delete --- accounts/keystore/passphrase_test.go | 34 ---------------------------- 1 file changed, 34 deletions(-) diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index c4f424f5bf..84bc745e9c 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -135,40 +135,6 @@ func TestV3_Scrypt_2(t *testing.T) { testDecryptV3(t, tests["test2"]) } -/* - func TestV1_2(t *testing.T) { - t.Parallel() - - ks := &keyStorePassphrase{LightScryptN, LightScryptP} - addr := types.StringToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") - file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" - - keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") - if err != nil { - t.Fatal(err) - } - - keyEncrypted := new(encryptedKeyJSONV3) - - keyEncrypted := - - k, err := ks.GetKey(addr, file, "g") - if err != nil { - t.Fatal(err) - } - - privKey, err := crypto.MarshalECDSAPrivateKey(k.PrivateKey) - require.NoError(t, err) - - privHex := hex.EncodeToString(privKey) - - expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" - if privHex != expectedHex { - t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) - } - } -*/ - func testDecryptV3(t *testing.T, test KeyStoreTestV3) { t.Helper() From 72bef8fe3fe0ac03f754d4f1773f50d4ff8feb81 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 17:18:54 +0200 Subject: [PATCH 26/69] lint fix --- accounts/keystore/account_cache.go | 42 +++++++++++++++++++++--------- accounts/keystore/file_cache.go | 9 ++++--- jsonrpc/personal_endpoint.go | 14 +++++----- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 288241f381..17e19e697f 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -36,15 +36,20 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st keyPath := path.Join(keyDir, "keys.txt") - ac.fileC = NewFileCache(keyPath) + ac.fileC, _ = NewFileCache(keyPath) - ac.scanAccounts() + ac.scanAccounts() //nolint:errcheck return ac, ac.notify } func (ac *accountCache) accounts() []accounts.Account { - ac.scanAccounts() + err := ac.scanAccounts() + if err != nil { + ac.logger.Debug("can't scan account's", "err", err) + + return []accounts.Account{} + } ac.mu.Lock() defer ac.mu.Unlock() @@ -61,7 +66,10 @@ func (ac *accountCache) accounts() []accounts.Account { } func (ac *accountCache) hasAddress(addr types.Address) bool { - ac.scanAccounts() + err := ac.scanAccounts() + if err != nil { + ac.logger.Debug("can't scan account's", "err", err) + } ac.mu.Lock() defer ac.mu.Unlock() @@ -72,7 +80,10 @@ func (ac *accountCache) hasAddress(addr types.Address) bool { } func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) error { - ac.scanAccounts() + err := ac.scanAccounts() + if err != nil { + return err + } ac.mu.Lock() defer ac.mu.Unlock() @@ -83,7 +94,7 @@ func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) ac.allMap[newAccount.Address] = key - err := ac.fileC.saveData(ac.allMap) + err = ac.fileC.saveData(ac.allMap) if err != nil { return err } @@ -92,7 +103,9 @@ func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) } func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) error { - ac.scanAccounts() + if err := ac.scanAccounts(); err != nil { + return err + } ac.mu.Lock() defer ac.mu.Unlock() @@ -103,8 +116,7 @@ func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) ac.allMap[account.Address] = key } - err := ac.fileC.saveData(ac.allMap) - if err != nil { + if err := ac.fileC.saveData(ac.allMap); err != nil { return err } @@ -113,21 +125,27 @@ func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) // note: removed needs to be unique here (i.e. both File and Address must be set). func (ac *accountCache) delete(removed accounts.Account) { - ac.scanAccounts() + if err := ac.scanAccounts(); err != nil { + ac.logger.Debug("can't scan account's", "err", err) + } ac.mu.Lock() defer ac.mu.Unlock() delete(ac.allMap, removed.Address) - ac.fileC.saveData(ac.allMap) + if err := ac.fileC.saveData(ac.allMap); err != nil { + ac.logger.Debug("cant't save data in file,", "err", err) + } } // find returns the cached account for address if there is a unique match. // The exact matching rules are explained by the documentation of accounts.Account. // Callers must hold ac.mu. func (ac *accountCache) find(a accounts.Account) (accounts.Account, encryptedKeyJSONV3, error) { - ac.scanAccounts() + if err := ac.scanAccounts(); err != nil { + return accounts.Account{}, encryptedKeyJSONV3{}, err + } ac.mu.Lock() defer ac.mu.Unlock() diff --git a/accounts/keystore/file_cache.go b/accounts/keystore/file_cache.go index 8aaa502bb7..c6e1db726b 100644 --- a/accounts/keystore/file_cache.go +++ b/accounts/keystore/file_cache.go @@ -16,14 +16,17 @@ type fileCache struct { keyDir string } -func NewFileCache(keyPath string) *fileCache { +func NewFileCache(keyPath string) (*fileCache, error) { fc := &fileCache{all: make(map[types.Address]encryptedKeyJSONV3), keyDir: keyPath} if _, err := os.Stat(keyPath); errors.Is(err, os.ErrNotExist) { - os.Create(fc.keyDir) + if _, err := os.Create(fc.keyDir); err != nil { + return nil, err + } + } - return fc + return fc, nil } func (fc *fileCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) error { diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index 0461167b0e..69bacda030 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -29,7 +29,7 @@ func (p *Personal) NewAccount(password string) (types.Address, error) { acc, err := ks.NewAccount(password) if err != nil { - return types.Address{}, fmt.Errorf("Cant create new account") + return types.Address{}, fmt.Errorf("can't create new account") } return acc.Address, nil @@ -53,13 +53,15 @@ func (p *Personal) ImportRawKey(privKey string, password string) (types.Address, func (p *Personal) UnlockAccount(addr types.Address, password string, duration uint64) (bool, error) { const max = uint64(time.Duration(math.MaxInt64) / time.Second) + var d time.Duration - if duration == 0 { + switch { + case duration == 0: d = 300 * time.Second - } else if duration > max { + case duration > max: return false, errors.New("unlock duration is too large") - } else { + default: d = time.Duration(duration) * time.Second } @@ -84,7 +86,7 @@ func (s *Personal) LockAccount(addr types.Address) bool { return false } -func (p *Personal) EcRecover(data, sig []byte) (types.Address, error) { +func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { if len(sig) != crypto.ECDSASignatureLength { return types.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.ECDSASignatureLength) } @@ -105,7 +107,7 @@ func (p *Personal) EcRecover(data, sig []byte) (types.Address, error) { func getKeystore(am *accounts.Manager) (*keystore.KeyStore, error) { if ks := am.Backends(keystore.KeyStoreType); len(ks) > 0 { - return ks[0].(*keystore.KeyStore), nil + return ks[0].(*keystore.KeyStore), nil //nolint:forcetypeassert } return nil, errors.New("local keystore not used") From c889ca04e9210426aff31a1ae96c61e4829790ed Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 7 Jun 2024 17:38:57 +0200 Subject: [PATCH 27/69] fix personal --- jsonrpc/personal_endpoint.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index 69bacda030..b3c53b4de9 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -17,8 +17,8 @@ type Personal struct { accManager *accounts.Manager } -func (p *Personal) ListAccounts() []types.Address { - return p.accManager.Accounts() +func (p *Personal) ListAccounts() ([]types.Address, Error) { + return p.accManager.Accounts(), nil } func (p *Personal) NewAccount(password string) (types.Address, error) { From 69066f9b37343154f749eb675fe1f153c0654587 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Sat, 8 Jun 2024 09:37:16 +0200 Subject: [PATCH 28/69] linter and personal --- accounts/keystore/account_cache_test.go | 3 +- accounts/keystore/file_cache.go | 1 - accounts/keystore/keystore.go | 23 +++++++++------ accounts/keystore/passphrase_test.go | 38 +++++++------------------ accounts/keystore/presale.go | 3 +- jsonrpc/personal_endpoint.go | 13 ++++++--- 6 files changed, 36 insertions(+), 45 deletions(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 3e11155838..8e1303fa60 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -64,7 +64,6 @@ func TestCacheInitialReload(t *testing.T) { unwantedAccount := accounts.Account{Address: types.StringToAddress("2cac1adea150210703ba75ed097ddfe24e14f213")} require.False(t, cache.hasAddress(unwantedAccount.Address)) - } func TestCacheAddDelete(t *testing.T) { @@ -114,7 +113,7 @@ func TestCacheAddDelete(t *testing.T) { require.True(t, cache.hasAddress(a.Address)) } - // Expected to return false because this addres is not contained in cache + // Expected to return false because this address is not contained in cache require.False(t, cache.hasAddress(types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"))) // Delete a few keys from the cache. diff --git a/accounts/keystore/file_cache.go b/accounts/keystore/file_cache.go index c6e1db726b..822ad99247 100644 --- a/accounts/keystore/file_cache.go +++ b/accounts/keystore/file_cache.go @@ -23,7 +23,6 @@ func NewFileCache(keyPath string) (*fileCache, error) { if _, err := os.Create(fc.keyDir); err != nil { return nil, err } - } return fc, nil diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 12d653be71..11ffe5494d 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -58,11 +58,8 @@ type unlocked struct { func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { var ks *KeyStore - if keyDir == "" { - ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} - } else { - ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} - } + + ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} ks.init(keyDir, hclog.NewNullLogger()) // TO DO LOGGER @@ -160,7 +157,6 @@ func (ks *KeyStore) refreshWallets() { for _, event := range events { ks.updateFeed.Send(event) } - } func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { @@ -370,7 +366,10 @@ func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { return accounts.Account{}, err } - ks.cache.add(account, encryptedKey) + if err := ks.cache.add(account, encryptedKey); err != nil { + return accounts.Account{}, err + } + ks.refreshWallets() return account, nil @@ -398,7 +397,10 @@ func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, er return accounts.Account{}, err } - ks.cache.add(a, encryptedKeyJSONV3) + if err := ks.cache.add(a, encryptedKeyJSONV3); err != nil { + return accounts.Account{}, err + } + ks.refreshWallets() return a, nil @@ -425,7 +427,10 @@ func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (account return a, err } - ks.cache.add(a, encryptedKey) + if err := ks.cache.add(a, encryptedKey); err != nil { + return accounts.Account{}, err + } + ks.refreshWallets() return a, nil diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 84bc745e9c..001160e189 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" - "fmt" "os" "path/filepath" "strings" @@ -43,13 +42,10 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} k1, _, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if _, err = ks.GetKey(k1, "bar"); err.Error() != ErrDecrypt.Error() { - t.Fatalf("wrong error for invalid password\ngot %q\nwant %q", err, ErrDecrypt) - } + _, err = ks.GetKey(k1, "bar") + require.Equal(t, err.Error(), ErrDecrypt.Error()) } func TestImportPreSaleKey(t *testing.T) { @@ -63,13 +59,9 @@ func TestImportPreSaleKey(t *testing.T) { fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if account.Address != types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { - t.Errorf("imported account has wrong address %x", account.Address) - } + require.Equal(t, account.Address, types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f")) } // Test and utils for the key store tests in the Ethereum JSON tests; @@ -139,14 +131,10 @@ func testDecryptV3(t *testing.T, test KeyStoreTestV3) { t.Helper() privBytes, _, err := decryptKeyV3(&test.JSON, test.Password) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) privHex := hex.EncodeToString(privBytes) - if test.Priv != privHex { - t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) - } + require.Equal(t, test.Priv, privHex) } func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { @@ -155,9 +143,7 @@ func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { tests := make(map[string]KeyStoreTestV3) err := accounts.LoadJSON(file, &tests) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return tests } @@ -166,9 +152,7 @@ func TestKeyForDirectICAP(t *testing.T) { t.Parallel() key := NewKeyForDirectICAP(rand.Reader) - if !strings.HasPrefix(key.Address.String(), "0x00") { - t.Errorf("Expected first address byte to be zero, have: %s", key.Address.String()) - } + require.True(t, strings.HasPrefix(key.Address.String(), "0x00")) } func TestV3_31_Byte_Key(t *testing.T) { @@ -191,9 +175,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { keyEncrypted := new(encryptedKeyJSONV3) keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) json.Unmarshal(keyjson, keyEncrypted) diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index b459e58d08..a232954292 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -15,7 +15,8 @@ import ( "golang.org/x/crypto/pbkdf2" ) -func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, encryptedKeyJSONV3, error) { +func importPreSaleKey(keyStore keyStore, + keyJSON []byte, password string) (accounts.Account, encryptedKeyJSONV3, error) { key, err := decryptPreSaleKey(keyJSON, password) if err != nil { return accounts.Account{}, encryptedKeyJSONV3{}, err diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index b3c53b4de9..ff3e7cdd20 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -78,12 +78,17 @@ func (p *Personal) UnlockAccount(addr types.Address, password string, duration u return true, nil } -func (s *Personal) LockAccount(addr types.Address) bool { - if ks, err := getKeystore(s.accManager); err == nil { - return ks.Lock(addr) == nil +func (s *Personal) LockAccount(addr types.Address) (bool, error) { + ks, err := getKeystore(s.accManager) + if err == nil { + if err := ks.Lock(addr); err != nil { + return false, err + } + + return true, nil } - return false + return false, err } func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { From 42dfb19ce813fb9f535380cdf872f69ab203d780 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Sun, 9 Jun 2024 18:28:53 +0200 Subject: [PATCH 29/69] lint --- accounts/keystore/account_cache.go | 5 +-- accounts/keystore/account_cache_test.go | 2 ++ accounts/keystore/key.go | 45 +++---------------------- accounts/keystore/keystore.go | 9 ++--- accounts/keystore/keystore_test.go | 1 + accounts/keystore/passphrase.go | 2 +- accounts/keystore/passphrase_test.go | 25 ++++++-------- jsonrpc/personal_endpoint.go | 2 +- 8 files changed, 25 insertions(+), 66 deletions(-) diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 17e19e697f..197f05f472 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -172,11 +172,8 @@ func (ac *accountCache) close() { ac.mu.Unlock() } -// scanAccounts checks if any changes have occurred on the filesystem, and -// updates the account cache accordingly +// scanAccounts refresh data of account map func (ac *accountCache) scanAccounts() error { - // Scan the entire folder metadata for file changes - ac.mu.Lock() defer ac.mu.Unlock() diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 8e1303fa60..bdf46153b3 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -243,4 +243,6 @@ func TestCacheUpdate(t *testing.T) { require.NoError(t, err) require.Equal(t, listOfEncryptedKeys[1], encryptedKey) + + require.Equal(t, wantAccount.Address.String(), encryptedKey.Address) } diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 80f4fe6794..edbd07f1ee 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -6,8 +6,6 @@ import ( "encoding/hex" "fmt" "io" - "os" - "path/filepath" "strings" "time" @@ -57,7 +55,7 @@ type cipherparamsJSON struct { IV string `json:"iv"` } -// TO DO newKeyFromECDSA +// return new key func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { id, err := uuid.NewRandom() if err != nil { @@ -96,33 +94,7 @@ func toISO8601(t time.Time) string { t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) } -func writeTemporaryKeyFile(file string, content []byte) (string, error) { - // Create the keystore directory with appropriate permissions - // in case it is not present yet. - const dirPerm = 0700 - if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil { - return "", err - } - // Atomic write: create a temporary hidden file first - // then move it into place. TempFile assigns mode 0600. - f, err := os.CreateTemp(filepath.Dir(file), "."+filepath.Base(file)+".tmp") - if err != nil { - return "", err - } - - if _, err := f.Write(content); err != nil { - f.Close() - os.Remove(f.Name()) - - return "", err - } - - f.Close() - - return f.Name(), nil -} - -func newKey(rand io.Reader) (*Key, error) { +func newKey() (*Key, error) { privateKeyECDSA, err := crypto.GenerateECDSAPrivateKey() // TO DO maybe not valid if err != nil { return nil, err @@ -155,8 +127,8 @@ func NewKeyForDirectICAP(rand io.Reader) *Key { return key } -func storeNewKey(ks keyStore, rand io.Reader, auth string) (encryptedKeyJSONV3, accounts.Account, error) { - key, err := newKey(rand) +func storeNewKey(ks keyStore, auth string) (encryptedKeyJSONV3, accounts.Account, error) { + key, err := newKey() if err != nil { return encryptedKeyJSONV3{}, accounts.Account{}, err } @@ -174,12 +146,3 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (encryptedKeyJSONV3, return encryptedKey, a, err } - -func writeKeyFile(file string, content []byte) error { - name, err := writeTemporaryKeyFile(file, content) - if err != nil { - return err - } - - return os.Rename(name, file) -} diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 11ffe5494d..682a1e9aaa 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -2,7 +2,6 @@ package keystore import ( "crypto/ecdsa" - crand "crypto/rand" "errors" "math/big" "path/filepath" @@ -110,14 +109,15 @@ func (ks *KeyStore) refreshWallets() { accs := ks.cache.accounts() - var ( //nolint:prealloc + var ( wallets = make([]accounts.Wallet, 0, len(accs)) events []accounts.WalletEvent - find = false + find bool ) for _, account := range accs { find = false + for _, wallet := range ks.wallets { if wallet.Accounts()[0] == account { wallets = append(wallets, wallet) @@ -137,6 +137,7 @@ func (ks *KeyStore) refreshWallets() { for _, oldWallet := range ks.wallets { find = false + for _, newWallet := range wallets { if newWallet == oldWallet { find = true @@ -360,7 +361,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A } func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { - encryptedKey, account, err := storeNewKey(ks.storage, crand.Reader, passphrase) + encryptedKey, account, err := storeNewKey(ks.storage, passphrase) if err != nil { return accounts.Account{}, err diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index b334f61cd2..40ec653c77 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -388,6 +388,7 @@ func checkAccounts(t *testing.T, live map[types.Address]accounts.Account, wallet } isFind := false + for _, liveWallet := range liveList { if liveWallet == wallet.Accounts()[0] { isFind = true diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 7eaf7e22c6..85972dc30b 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -63,7 +63,7 @@ func (ks keyStorePassphrase) GetKey(encryptedKey encryptedKeyJSONV3, auth string // StoreKey generates a key, encrypts with 'auth' and stores in the given directory func StoreKey(auth string, scryptN, scryptP int) (accounts.Account, error) { - _, a, err := storeNewKey(&keyStorePassphrase{scryptN, scryptP}, rand.Reader, auth) + _, a, err := storeNewKey(&keyStorePassphrase{scryptN, scryptP}, auth) return a, err } diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 001160e189..4a8ae21664 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -25,7 +25,7 @@ func TestKeyStorePassphrase(t *testing.T) { ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} - k1, account, err := storeNewKey(ks, rand.Reader, pass) + k1, account, err := storeNewKey(ks, pass) require.NoError(t, err) k2, err := ks.GetKey(k1, pass) @@ -41,7 +41,7 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} - k1, _, err := storeNewKey(ks, rand.Reader, pass) + k1, _, err := storeNewKey(ks, pass) require.NoError(t, err) _, err = ks.GetKey(k1, "bar") @@ -172,12 +172,13 @@ func TestV3_30_Byte_Key(t *testing.T) { // Tests that a json key file can be decrypted and encrypted in multiple rounds. func TestKeyEncryptDecrypt(t *testing.T) { t.Parallel() + keyEncrypted := new(encryptedKeyJSONV3) keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") require.NoError(t, err) - json.Unmarshal(keyjson, keyEncrypted) + require.NoError(t, json.Unmarshal(keyjson, keyEncrypted)) password := "" address := types.StringToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") @@ -185,22 +186,16 @@ func TestKeyEncryptDecrypt(t *testing.T) { // Do a few rounds of decryption and encryption for i := 0; i < 3; i++ { // Try a bad password first - if _, err := DecryptKey(*keyEncrypted, password+"bad"); err == nil { - t.Errorf("test %d: json key decrypted with bad password", i) - } + _, err := DecryptKey(*keyEncrypted, password+"bad") + require.NoError(t, err) // Decrypt with the correct password key, err := DecryptKey(*keyEncrypted, password) - if err != nil { - t.Fatalf("test %d: json key failed to decrypt: %v", i, err) - } + require.NoError(t, err) - if key.Address != address { - t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address) - } + require.Equal(t, address, key.Address) // Recrypt with a new password and start over password += "new data appended" - if *keyEncrypted, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { - t.Errorf("test %d: failed to re-encrypt key %v", i, err) - } + _, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP) + require.NoError(t, err) } } diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index ff3e7cdd20..ef0ad1effd 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -91,7 +91,7 @@ func (s *Personal) LockAccount(addr types.Address) (bool, error) { return false, err } -func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { +func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { //nolint:stylecheck if len(sig) != crypto.ECDSASignatureLength { return types.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.ECDSASignatureLength) } From 707d946e875c1af42b912ec667ff746ef73e97fe Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Mon, 10 Jun 2024 07:49:58 +0200 Subject: [PATCH 30/69] test fix and remove panic --- accounts/keystore/key.go | 16 ++++++++++++---- accounts/keystore/passphrase.go | 4 ++-- accounts/keystore/passphrase_test.go | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index edbd07f1ee..6ee82a3501 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -59,7 +59,7 @@ type cipherparamsJSON struct { func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { id, err := uuid.NewRandom() if err != nil { - panic(fmt.Sprintf("Could not create random uuid: %v", err)) //nolint:gocritic + return nil } key := &Key{ @@ -100,7 +100,12 @@ func newKey() (*Key, error) { return nil, err } - return newKeyFromECDSA(privateKeyECDSA), nil + key := newKeyFromECDSA(privateKeyECDSA) + if key == nil { + return nil, fmt.Errorf("can't create key") + } + + return key, nil } func NewKeyForDirectICAP(rand io.Reader) *Key { @@ -108,17 +113,20 @@ func NewKeyForDirectICAP(rand io.Reader) *Key { _, err := rand.Read(randBytes) if err != nil { - panic("key generation: could not read from random source: " + err.Error()) //nolint:gocritic + return nil } reader := bytes.NewReader(randBytes) privateKeyECDSA, err := ecdsa.GenerateKey(btcec.S256(), reader) if err != nil { - panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) //nolint:gocritic + return nil } key := newKeyFromECDSA(privateKeyECDSA) + if key == nil { + return nil + } if !strings.HasPrefix(key.Address.String(), "0x00") { return NewKeyForDirectICAP(rand) diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 85972dc30b..588d09b2d7 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -82,7 +82,7 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) salt := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, salt); err != nil { - panic("reading from crypto/rand failed: " + err.Error()) //nolint:gocritic + return CryptoJSON{}, nil } derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) @@ -94,7 +94,7 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) iv := make([]byte, aes.BlockSize) // 16 if _, err := io.ReadFull(rand.Reader, iv); err != nil { - panic("reading from crypto/rand failed: " + err.Error()) //nolint:gocritic + return CryptoJSON{}, err } cipherText, err := aesCTRXOR(encryptKey, data, iv) diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 4a8ae21664..efa01ae9e0 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -187,7 +187,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { for i := 0; i < 3; i++ { // Try a bad password first _, err := DecryptKey(*keyEncrypted, password+"bad") - require.NoError(t, err) + require.Error(t, err) // Decrypt with the correct password key, err := DecryptKey(*keyEncrypted, password) require.NoError(t, err) From e0d24d03a6a2ecec0f677c194378df7a3def6718 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Mon, 10 Jun 2024 09:07:09 +0200 Subject: [PATCH 31/69] test fix --- accounts/keystore/passphrase_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index efa01ae9e0..326fe20c22 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -195,7 +195,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { require.Equal(t, address, key.Address) // Recrypt with a new password and start over password += "new data appended" - _, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP) + *keyEncrypted, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP) require.NoError(t, err) } } From 6498fd5e926f5cfac6a3a8cb4f0a4549dc70dd8c Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Mon, 10 Jun 2024 10:56:14 +0200 Subject: [PATCH 32/69] unnecessary files deletion --- accounts/keystore/passphrase_test.go | 10 +++---- accounts/keystore/testdata/dupes/1 | 1 - accounts/keystore/testdata/dupes/2 | 1 - accounts/keystore/testdata/dupes/foo | 1 - .../keystore/testdata/keystore/.hiddenfile | 1 - accounts/keystore/testdata/keystore/README | 21 ------------- ...--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 | 1 - accounts/keystore/testdata/keystore/aaa | 1 - accounts/keystore/testdata/keystore/empty | 0 .../fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e | 1 - accounts/keystore/testdata/keystore/garbage | Bin 300 -> 0 bytes .../keystore/testdata/keystore/no-address | 1 - accounts/keystore/testdata/keystore/zero | 1 - accounts/keystore/testdata/keystore/zzz | 1 - ...-light-scrypt.json => light-test-key.json} | 0 .../{v3_test_vector.json => test-keys.json} | 0 .../cb61d5a9c4896fb9658090b597ef0e7be6f7b67e | 1 - .../keystore/testdata/v1_test_vector.json | 28 ------------------ 18 files changed, 5 insertions(+), 65 deletions(-) delete mode 100644 accounts/keystore/testdata/dupes/1 delete mode 100644 accounts/keystore/testdata/dupes/2 delete mode 100644 accounts/keystore/testdata/dupes/foo delete mode 100644 accounts/keystore/testdata/keystore/.hiddenfile delete mode 100644 accounts/keystore/testdata/keystore/README delete mode 100644 accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 delete mode 100644 accounts/keystore/testdata/keystore/aaa delete mode 100644 accounts/keystore/testdata/keystore/empty delete mode 100644 accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e delete mode 100644 accounts/keystore/testdata/keystore/garbage delete mode 100644 accounts/keystore/testdata/keystore/no-address delete mode 100644 accounts/keystore/testdata/keystore/zero delete mode 100644 accounts/keystore/testdata/keystore/zzz rename accounts/keystore/testdata/{very-light-scrypt.json => light-test-key.json} (100%) rename accounts/keystore/testdata/{v3_test_vector.json => test-keys.json} (100%) delete mode 100644 accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e delete mode 100644 accounts/keystore/testdata/v1_test_vector.json diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 326fe20c22..46be8855fb 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -74,7 +74,7 @@ type KeyStoreTestV3 struct { func TestV3_PBKDF2_1(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") testDecryptV3(t, tests["wikipage_test_vector_pbkdf2"]) } @@ -115,7 +115,7 @@ func TestV3_PBKDF2_4(t *testing.T) { func TestV3_Scrypt_1(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") testDecryptV3(t, tests["wikipage_test_vector_scrypt"]) } @@ -158,14 +158,14 @@ func TestKeyForDirectICAP(t *testing.T) { func TestV3_31_Byte_Key(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") testDecryptV3(t, tests["31_byte_key"]) } func TestV3_30_Byte_Key(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/v3_test_vector.json") + tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") testDecryptV3(t, tests["30_byte_key"]) } @@ -175,7 +175,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { keyEncrypted := new(encryptedKeyJSONV3) - keyjson, err := os.ReadFile("testdata/very-light-scrypt.json") + keyjson, err := os.ReadFile("testdata/light-test-key.json") require.NoError(t, err) require.NoError(t, json.Unmarshal(keyjson, keyEncrypted)) diff --git a/accounts/keystore/testdata/dupes/1 b/accounts/keystore/testdata/dupes/1 deleted file mode 100644 index a3868ec6d5..0000000000 --- a/accounts/keystore/testdata/dupes/1 +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/dupes/2 b/accounts/keystore/testdata/dupes/2 deleted file mode 100644 index a3868ec6d5..0000000000 --- a/accounts/keystore/testdata/dupes/2 +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/dupes/foo b/accounts/keystore/testdata/dupes/foo deleted file mode 100644 index c57060aea0..0000000000 --- a/accounts/keystore/testdata/dupes/foo +++ /dev/null @@ -1 +0,0 @@ -{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/.hiddenfile b/accounts/keystore/testdata/keystore/.hiddenfile deleted file mode 100644 index d91faccdeb..0000000000 --- a/accounts/keystore/testdata/keystore/.hiddenfile +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} diff --git a/accounts/keystore/testdata/keystore/README b/accounts/keystore/testdata/keystore/README deleted file mode 100644 index 6af9ac3f1b..0000000000 --- a/accounts/keystore/testdata/keystore/README +++ /dev/null @@ -1,21 +0,0 @@ -This directory contains accounts for testing. -The password that unlocks them is "foobar". - -The "good" key files which are supposed to be loadable are: - -- File: UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 - Address: 0x7ef5a6135f1fd6a02593eedc869c6d41d934aef8 -- File: aaa - Address: 0xf466859ead1932d743d622cb74fc058882e8648a -- File: zzz - Address: 0x289d485d9771714cce91d3393d764e1311907acc - -The other files (including this README) are broken in various ways -and should not be picked up by package accounts: - -- File: no-address (missing address field, otherwise same as "aaa") -- File: garbage (file with random data) -- File: empty (file with no content) -- File: swapfile~ (should be skipped) -- File: .hiddenfile (should be skipped) -- File: foo/... (should be skipped because it is a directory) diff --git a/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 deleted file mode 100644 index c57060aea0..0000000000 --- a/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +++ /dev/null @@ -1 +0,0 @@ -{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/aaa b/accounts/keystore/testdata/keystore/aaa deleted file mode 100644 index a3868ec6d5..0000000000 --- a/accounts/keystore/testdata/keystore/aaa +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/empty b/accounts/keystore/testdata/keystore/empty deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e deleted file mode 100644 index 309841e524..0000000000 --- a/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e +++ /dev/null @@ -1 +0,0 @@ -{"address":"fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e","crypto":{"cipher":"aes-128-ctr","ciphertext":"8124d5134aa4a927c79fd852989e4b5419397566f04b0936a1eb1d168c7c68a5","cipherparams":{"iv":"e2febe17176414dd2cda28287947eb2f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"44b415ede89f3bdd6830390a21b78965f571b347a589d1d943029f016c5e8bd5"},"mac":"5e149ff25bfd9dd45746a84bb2bcd2f015f2cbca2b6d25c5de8c29617f71fe5b"},"id":"d6ac5452-2b2c-4d3c-ad80-4bf0327d971c","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/garbage b/accounts/keystore/testdata/keystore/garbage deleted file mode 100644 index ff45091e714078dd7d3b4ea95964452e33a895f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 300 zcmV+{0n`3r1xkOa0KiH=0-y31ays31&4D+~b{#6-MH z)8?iosg+26q81!5ujp29iM}4_d}^;*-$8$htAbEpk(KDl*$;NvD$v8GZL@TRuT#)+ zq*|PXNljY5_xwCfoMayTjJ(vY;=t!uVJT5-Fn0O7W{#e;Ho?+NsQQi=!GV>j#9U#& zAbp7L1M-8N-V+7}EDxG9CNuhKbj?($B?=E1a1Xi%v;bYvR+C$EjApbg!W^>zB$Cd( z+NKd!El}@p)NJLnQ}B=D#e5uCh87_~lKd2z=idP7$ Date: Mon, 10 Jun 2024 11:47:20 +0200 Subject: [PATCH 33/69] jsonrpc functions not exposed --- accounts/external/backend.go | 293 --------------------------------- jsonrpc/eth_blockchain_test.go | 37 +++-- jsonrpc/eth_endpoint.go | 26 +-- jsonrpc/eth_state_test.go | 36 ++-- jsonrpc/helper.go | 14 +- jsonrpc/types.go | 24 +-- jsonrpc/types_test.go | 8 +- 7 files changed, 65 insertions(+), 373 deletions(-) delete mode 100644 accounts/external/backend.go diff --git a/accounts/external/backend.go b/accounts/external/backend.go deleted file mode 100644 index 3bcd98b57d..0000000000 --- a/accounts/external/backend.go +++ /dev/null @@ -1,293 +0,0 @@ -package external - -import ( - "encoding/hex" - "errors" - "fmt" - "math/big" - "sync" - - "github.com/0xPolygon/polygon-edge/accounts" - "github.com/0xPolygon/polygon-edge/accounts/event" - jsonTypes "github.com/0xPolygon/polygon-edge/jsonrpc" - "github.com/0xPolygon/polygon-edge/types" - "github.com/umbracle/ethgo/jsonrpc" -) - -type ExternalBackend struct { - signers []accounts.Wallet -} - -func (eb *ExternalBackend) Wallets() []accounts.Wallet { - return eb.signers -} - -func NewExternalBackend(endpoint string) (*ExternalBackend, error) { - signer, err := NewExternalSigner(endpoint) - if err != nil { - return nil, err - } - - return &ExternalBackend{ - signers: []accounts.Wallet{signer}, - }, nil -} - -func (eb *ExternalBackend) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { - return event.NewSubscription(func(quit <-chan struct{}) error { - <-quit - - return nil - }) -} - -// ExternalSigner provides an API to interact with an external signer (clef) -// It proxies request to the external signer while forwarding relevant -// request headers -type ExternalSigner struct { - client *jsonrpc.Client - endpoint string - status string - cacheMu sync.RWMutex - cache []accounts.Account -} - -func NewExternalSigner(endpoint string) (*ExternalSigner, error) { - client, err := jsonrpc.NewClient(endpoint) - if err != nil { - return nil, err - } - - extsigner := &ExternalSigner{ - client: client, - endpoint: endpoint, - } - - // Check if reachable - version, err := extsigner.pingVersion() - if err != nil { - return nil, err - } - - extsigner.status = fmt.Sprintf("ok [version=%v]", version) - - return extsigner, nil -} - -func (api *ExternalSigner) URL() accounts.URL { - return accounts.URL{ - Scheme: "extapi", - Path: api.endpoint, - } -} - -func (api *ExternalSigner) Status() (string, error) { - return api.status, nil -} - -func (api *ExternalSigner) Open(passphrase string) error { - return errors.New("operation not supported on external signers") -} - -func (api *ExternalSigner) Close() error { - return errors.New("operation not supported on external signers") -} - -func (api *ExternalSigner) Accounts() []accounts.Account { - var accnts []accounts.Account //nolint:prealloc - - res, err := api.listAccounts() - if err != nil { - return accnts - } - - for _, addr := range res { - accnts = append(accnts, accounts.Account{ - URL: accounts.URL{ - Scheme: "extapi", - Path: api.endpoint, - }, - Address: addr, - }) - } - - api.cacheMu.Lock() - api.cache = accnts - api.cacheMu.Unlock() - - return accnts -} - -func (api *ExternalSigner) Contains(account accounts.Account) bool { - api.cacheMu.RLock() - defer api.cacheMu.RUnlock() - - if api.cache == nil { - // If we haven't already fetched the accounts, it's time to do so now - api.cacheMu.RUnlock() - api.Accounts() - api.cacheMu.RLock() - } - - for _, a := range api.cache { - if a.Address == account.Address && (account.URL == (accounts.URL{}) || account.URL == api.URL()) { - return true - } - } - - return false -} - -// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed -func (api *ExternalSigner) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) { - var ( - hexData []byte - res []byte - signAddress = types.NewMixedcaseAddress(account.Address) - ) - - hex.Encode(hexData, data) - - if err := api.client.Call("account_signData", &res, - mimeType, - &signAddress, // Need to use the pointer here, because of how MarshalJSON is defined - hexData); err != nil { - return nil, err - } - - // If V is on 27/28-form, convert to 0/1 for Clique - if mimeType == accounts.MimetypeClique && (res[64] == 27 || res[64] == 28) { - res[64] -= 27 // Transform V from 27/28 to 0/1 for Clique use - } - - return res, nil -} - -func (api *ExternalSigner) SignText(account accounts.Account, text []byte) ([]byte, error) { - var ( - signature []byte - signAddress = types.NewMixedcaseAddress(account.Address) - textHex []byte - ) - - hex.Encode(textHex, text) - - if err := api.client.Call("account_signData", - &signature, - accounts.MimetypeTextPlain, - &signAddress, // Need to use the pointer here, because of how MarshalJSON is defined - textHex); err != nil { - return nil, err - } - - if signature[64] == 27 || signature[64] == 28 { - // If clef is used as a backend, it may already have transformed - // the signature to ethereum-type signature. - signature[64] -= 27 // Transform V from Ethereum-legacy to 0/1 - } - - return signature, nil -} - -// signTransactionResult represents the signinig result returned by clef. -type signTransactionResult struct { - Raw []byte `json:"raw"` - Tx *types.Transaction `json:"tx"` -} - -// SignTx sends the transaction to the external signer. -// If chainID is nil, or tx.ChainID is zero, the chain ID will be assigned -// by the external signer. For non-legacy transactions, the chain ID of the -// transaction overrides the chainID parameter. -func (api *ExternalSigner) SignTx(account accounts.Account, - tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - var to *types.MixedcaseAddress - - if tx.To() != nil { - t := types.NewMixedcaseAddress(*tx.To()) - to = t - } - - args := &jsonTypes.SendTxnArgs{ - Input: jsonTypes.ArgBytesPtr(tx.Input()), - Nonce: jsonTypes.ArgUintPtr(tx.Nonce()), - Value: jsonTypes.ArgBytesPtr(tx.Value().Bytes()), - Gas: jsonTypes.ArgUintPtr(tx.Gas()), - To: to, - From: types.NewMixedcaseAddress(account.Address), - } - - switch tx.Type() { - case types.LegacyTxType, types.AccessListTxType, types.StateTxType: - args.GasPrice = jsonTypes.ArgBytesPtr(tx.GasPrice().Bytes()) - case types.DynamicFeeTxType: - args.GasTipCap = jsonTypes.ArgBytesPtr(tx.GasFeeCap().Bytes()) - args.GasFeeCap = jsonTypes.ArgBytesPtr(tx.GasTipCap().Bytes()) - default: - return nil, fmt.Errorf("unsupported tx type %d", tx.Type()) - } - - // We should request the default chain id that we're operating with - // (the chain we're executing on) - if chainID != nil && chainID.Sign() != 0 { - args.ChainID = jsonTypes.ArgUintPtr(chainID.Uint64()) - } - - if tx.Type() == types.DynamicFeeTxType { - if tx.ChainID().Sign() != 0 { - args.ChainID = jsonTypes.ArgUintPtr(tx.ChainID().Uint64()) - } - } else if tx.Type() == types.AccessListTxType { - // However, if the user asked for a particular chain id, then we should - // use that instead. - if tx.ChainID().Sign() != 0 { - args.ChainID = jsonTypes.ArgUintPtr(tx.ChainID().Uint64()) - } - - accessList := tx.AccessList() - args.AccessList = &accessList - } - - var res signTransactionResult - - if err := api.client.Call("account_signTransaction", args, &res); err != nil { - return nil, err - } - - return res.Tx, nil -} - -func (api *ExternalSigner) SignTextWithPassphrase(account accounts.Account, - passphrase string, text []byte) ([]byte, error) { - return []byte{}, errors.New("password-operations not supported on external signers") -} - -func (api *ExternalSigner) SignTxWithPassphrase(account accounts.Account, - passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - return nil, errors.New("password-operations not supported on external signers") -} - -func (api *ExternalSigner) SignDataWithPassphrase(account accounts.Account, - passphrase, mimeType string, data []byte) ([]byte, error) { - return nil, errors.New("password-operations not supported on external signers") -} - -func (api *ExternalSigner) listAccounts() ([]types.Address, error) { - var res []types.Address - - if err := api.client.Call("account_list", nil, &res); err != nil { - return nil, err - } - - return res, nil -} - -func (api *ExternalSigner) pingVersion() (string, error) { - var v string - - if err := api.client.Call("account_version", nil, &v); err != nil { - return "", err - } - - return v, nil -} diff --git a/jsonrpc/eth_blockchain_test.go b/jsonrpc/eth_blockchain_test.go index c7595bb0b5..2aaa1c0a2b 100644 --- a/jsonrpc/eth_blockchain_test.go +++ b/jsonrpc/eth_blockchain_test.go @@ -134,7 +134,7 @@ func TestEth_Block_BlockNumber(t *testing.T) { num, err := eth.BlockNumber() assert.NoError(t, err) - assert.Equal(t, ArgUintPtr(10), num) + assert.Equal(t, argUintPtr(10), num) } func TestEth_Block_GetBlockTransactionCountByHash(t *testing.T) { @@ -191,7 +191,8 @@ func TestEth_Block_GetTransactionByBlockNumberAndIndex(t *testing.T) { transaction := toTransaction( block.Transactions[testIndex], - block.Header, + ArgUintPtr(block.Number()), + argHashPtr(block.Hash()), &testIndex, ) @@ -560,11 +561,11 @@ func TestEth_Call(t *testing.T) { contractCall := &txnArgs{ From: &addr0, To: &addr1, - Gas: ArgUintPtr(100000), - GasPrice: ArgBytesPtr([]byte{0x64}), - Value: ArgBytesPtr([]byte{0x64}), + Gas: argUintPtr(100000), + GasPrice: argBytesPtr([]byte{0x64}), + Value: argBytesPtr([]byte{0x64}), Data: nil, - Nonce: ArgUintPtr(0), + Nonce: argUintPtr(0), } res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) @@ -584,11 +585,11 @@ func TestEth_Call(t *testing.T) { contractCall := &txnArgs{ From: &addr0, To: &addr1, - Gas: ArgUintPtr(100000), - GasPrice: ArgBytesPtr([]byte{0x64}), - Value: ArgBytesPtr([]byte{0x64}), + Gas: argUintPtr(100000), + GasPrice: argBytesPtr([]byte{0x64}), + Value: argBytesPtr([]byte{0x64}), Data: nil, - Nonce: ArgUintPtr(0), + Nonce: argUintPtr(0), } res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) @@ -610,11 +611,11 @@ func TestEth_Call(t *testing.T) { contractCall := &txnArgs{ From: &addr0, To: &addr1, - Gas: ArgUintPtr(100000), - GasPrice: ArgBytesPtr([]byte{0x64}), - Value: ArgBytesPtr([]byte{0x64}), + Gas: argUintPtr(100000), + GasPrice: argBytesPtr([]byte{0x64}), + Value: argBytesPtr([]byte{0x64}), Data: nil, - Nonce: ArgUintPtr(0), + Nonce: argUintPtr(0), } res, err := eth.Call(contractCall, BlockNumberOrHash{}, nil) @@ -644,11 +645,11 @@ func TestEth_CreateAccessList(t *testing.T) { txn := &txnArgs{ From: &addr0, To: &addr1, - Gas: ArgUintPtr(100000), - GasPrice: ArgBytesPtr([]byte{0x64}), - Value: ArgBytesPtr([]byte{0x64}), + Gas: argUintPtr(100000), + GasPrice: argBytesPtr([]byte{0x64}), + Value: argBytesPtr([]byte{0x64}), Data: nil, - Nonce: ArgUintPtr(0), + Nonce: argUintPtr(0), } cases := []struct { diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index 47b6a0a163..3bdf35d0c5 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -109,7 +109,7 @@ type Eth struct { // //nolint:stylecheck func (e *Eth) ChainId() (interface{}, error) { - return ArgUintPtr(e.chainID), nil + return argUintPtr(e.chainID), nil } func (e *Eth) Syncing() (interface{}, error) { @@ -317,7 +317,7 @@ func (e *Eth) BlockNumber() (interface{}, error) { return nil, ErrHeaderNotFound } - return ArgUintPtr(h.Number), nil + return argUintPtr(h.Number), nil } // SendRawTransaction sends a raw transaction @@ -516,13 +516,13 @@ func (e *Eth) GetStorageAt( result, err := e.store.GetStorage(header.StateRoot, address, index) if err != nil { if errors.Is(err, ErrStateNotFound) { - return ArgBytesPtr(types.ZeroHash[:]), nil + return argBytesPtr(types.ZeroHash[:]), nil } return nil, err } - return ArgBytesPtr(result), nil + return argBytesPtr(result), nil } // GasPrice exposes "getGasPrice"'s function logic to public RPC interface @@ -619,7 +619,7 @@ func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash, apiOverride *StateOve return nil, fmt.Errorf("unable to execute call: %w", result.Err) } - return ArgBytesPtr(result.ReturnValue), nil + return argBytesPtr(result.ReturnValue), nil } // EstimateGas estimates the gas needed to execute a transaction @@ -870,12 +870,12 @@ func (e *Eth) GetBalance(address types.Address, filter BlockNumberOrHash) (inter acc, err := e.store.GetAccount(header.StateRoot, address) if errors.Is(err, ErrStateNotFound) { // Account not found, return an empty account - return ArgUintPtr(0), nil + return argUintPtr(0), nil } else if err != nil { return nil, err } - return ArgBigPtr(acc.Balance), nil + return argBigPtr(acc.Balance), nil } // GetTransactionCount returns account nonce @@ -905,13 +905,13 @@ func (e *Eth) GetTransactionCount(address types.Address, filter BlockNumberOrHas nonce, err := GetNextNonce(address, blockNumber, e.store) if err != nil { if errors.Is(err, ErrStateNotFound) { - return ArgUintPtr(0), nil + return argUintPtr(0), nil } return nil, err } - return ArgUintPtr(nonce), nil + return argUintPtr(nonce), nil } // GetCode returns account code at given block number @@ -929,10 +929,10 @@ func (e *Eth) GetCode(address types.Address, filter BlockNumberOrHash) (interfac // return the default value return "0x", nil } else if err != nil { - return ArgBytesPtr(emptySlice), err + return argBytesPtr(emptySlice), err } - return ArgBytesPtr(code), nil + return argBytesPtr(code), nil } // NewFilter creates a filter object, based on filter options, to notify when the state changes (logs). @@ -967,7 +967,7 @@ func (e *Eth) MaxPriorityFeePerGas() (interface{}, error) { return nil, err } - return ArgBigPtr(priorityFee), nil + return argBigPtr(priorityFee), nil } func (e *Eth) FeeHistory(blockCount argUint64, newestBlock BlockNumber, @@ -1009,7 +1009,7 @@ func (e *Eth) FeeHistory(blockCount argUint64, newestBlock BlockNumber, rewardResult := <-rewardCh result := &feeHistoryResult{ - OldestBlock: *ArgUintPtr(history.OldestBlock), + OldestBlock: *argUintPtr(history.OldestBlock), BaseFeePerGas: baseFeePerGasResult, GasUsedRatio: gasUsedRatioResult, Reward: rewardResult, diff --git a/jsonrpc/eth_state_test.go b/jsonrpc/eth_state_test.go index bcae765f87..858681a562 100644 --- a/jsonrpc/eth_state_test.go +++ b/jsonrpc/eth_state_test.go @@ -138,14 +138,14 @@ func TestEth_State_GetBalance(t *testing.T) { t.Fatalf("invalid type assertion") } - assert.Equal(t, *ArgUintPtr(0), *uintBalance) + assert.Equal(t, *argUintPtr(0), *uintBalance) } else { bigBalance, ok := balance.(*argBig) if !ok { t.Fatalf("invalid type assertion") } - assert.Equal(t, *ArgBigPtr(big.NewInt(tt.expectedBalance)), *bigBalance) + assert.Equal(t, *argBigPtr(big.NewInt(tt.expectedBalance)), *bigBalance) } } }) @@ -264,7 +264,7 @@ func TestEth_State_GetTransactionCount(t *testing.T) { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, ArgUintPtr(tt.expectedNonce), nonce) + assert.Equal(t, argUintPtr(tt.expectedNonce), nonce) } }) } @@ -388,7 +388,7 @@ func TestEth_State_GetCode(t *testing.T) { if tt.target.String() == uninitializedAddress.String() { assert.Equal(t, "0x", code) } else { - assert.Equal(t, ArgBytesPtr(tt.expectedCode), code) + assert.Equal(t, argBytesPtr(tt.expectedCode), code) } } }) @@ -442,7 +442,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: nil, blockHash: nil, succeeded: true, - expectedData: ArgBytesPtr(hash1[:]), + expectedData: argBytesPtr(hash1[:]), }, { name: "should return 32 bytes filled with zero for undefined slot", @@ -456,7 +456,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: &blockNumberLatest, blockHash: nil, succeeded: true, - expectedData: ArgBytesPtr(types.ZeroHash[:]), + expectedData: argBytesPtr(types.ZeroHash[:]), }, { name: "should return 32 bytes filled with zero for non-existing account", @@ -469,7 +469,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { index: hash2, blockNumber: &blockNumberLatest, succeeded: true, - expectedData: ArgBytesPtr(types.ZeroHash[:]), + expectedData: argBytesPtr(types.ZeroHash[:]), }, { name: "should return error for invalid block number", @@ -497,7 +497,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: &blockNumberZero, blockHash: nil, succeeded: true, - expectedData: ArgBytesPtr(hash1[:]), + expectedData: argBytesPtr(hash1[:]), }, { name: "should not return an error for valid block hash", @@ -511,7 +511,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: nil, blockHash: &types.ZeroHash, succeeded: true, - expectedData: ArgBytesPtr(hash1[:]), + expectedData: argBytesPtr(hash1[:]), }, { name: "should return error for invalid block hash", @@ -539,7 +539,7 @@ func TestEth_State_GetStorageAt(t *testing.T) { blockNumber: &blockNumberEarliest, blockHash: nil, succeeded: true, - expectedData: ArgBytesPtr(hash1[:]), + expectedData: argBytesPtr(hash1[:]), }, } @@ -583,9 +583,9 @@ func constructMockTx(gasLimit *argUint64, data *argBytes) *txnArgs { From: &addr0, To: &addr1, Gas: gasLimit, - GasPrice: ArgBytesPtr([]byte{0x0}), - Value: ArgBytesPtr([]byte{0x0}), - Nonce: ArgUintPtr(0), + GasPrice: argBytesPtr([]byte{0x0}), + Value: argBytesPtr([]byte{0x0}), + Nonce: argUintPtr(0), Data: data, } } @@ -634,19 +634,19 @@ func TestEth_EstimateGas_GasLimit(t *testing.T) { "valid gas limit from the latest block for contract interaction", state.TxGasContractCreation, nil, - constructMockTx(nil, ArgBytesPtr([]byte{0x12})), + constructMockTx(nil, argBytesPtr([]byte{0x12})), }, { "valid gas limit from the transaction", state.TxGas, nil, - constructMockTx(ArgUintPtr(30000), nil), + constructMockTx(argUintPtr(30000), nil), }, { "insufficient gas limit from the transaction", state.TxGas, state.ErrNotEnoughIntrinsicGas, - constructMockTx(ArgUintPtr(state.TxGas/2), nil), + constructMockTx(argUintPtr(state.TxGas/2), nil), }, } @@ -786,7 +786,7 @@ func TestEth_EstimateGas_ValueTransfer(t *testing.T) { from := types.StringToAddress("0xSenderAddress") to := types.StringToAddress("0xReceiverAddress") mockTx := constructMockTx(nil, nil) - mockTx.Value = ArgBytesPtr([]byte{0x1}) + mockTx.Value = argBytesPtr([]byte{0x1}) mockTx.From = &from mockTx.To = &to @@ -814,7 +814,7 @@ func TestEth_EstimateGas_ContractCreation(t *testing.T) { from := types.StringToAddress("0xSenderAddress") mockTx := constructMockTx(nil, nil) mockTx.From = &from - mockTx.Input = ArgBytesPtr([]byte{}) + mockTx.Input = argBytesPtr([]byte{}) mockTx.To = nil // Run the estimation diff --git a/jsonrpc/helper.go b/jsonrpc/helper.go index fd54c7179a..12a142ea44 100644 --- a/jsonrpc/helper.go +++ b/jsonrpc/helper.go @@ -187,7 +187,7 @@ func DecodeTxn(arg *txnArgs, blockNumber uint64, store nonceGetter, forceSetNonc // set default values if arg.From == nil { arg.From = &types.ZeroAddress - arg.Nonce = ArgUintPtr(0) + arg.Nonce = argUintPtr(0) } else if arg.Nonce == nil || forceSetNonce { // get nonce from the pool nonce, err := GetNextNonce(*arg.From, LatestBlockNumber, store) @@ -195,23 +195,23 @@ func DecodeTxn(arg *txnArgs, blockNumber uint64, store nonceGetter, forceSetNonc return nil, err } - arg.Nonce = ArgUintPtr(nonce) + arg.Nonce = argUintPtr(nonce) } if arg.Value == nil { - arg.Value = ArgBytesPtr([]byte{}) + arg.Value = argBytesPtr([]byte{}) } if arg.GasPrice == nil { - arg.GasPrice = ArgBytesPtr([]byte{}) + arg.GasPrice = argBytesPtr([]byte{}) } if arg.GasTipCap == nil { - arg.GasTipCap = ArgBytesPtr([]byte{}) + arg.GasTipCap = argBytesPtr([]byte{}) } if arg.GasFeeCap == nil { - arg.GasFeeCap = ArgBytesPtr([]byte{}) + arg.GasFeeCap = argBytesPtr([]byte{}) } var input []byte @@ -230,7 +230,7 @@ func DecodeTxn(arg *txnArgs, blockNumber uint64, store nonceGetter, forceSetNonc } if arg.Gas == nil { - arg.Gas = ArgUintPtr(0) + arg.Gas = argUintPtr(0) } txType := types.LegacyTxType diff --git a/jsonrpc/types.go b/jsonrpc/types.go index 95040a660a..b2c4c9f6a8 100644 --- a/jsonrpc/types.go +++ b/jsonrpc/types.go @@ -113,7 +113,7 @@ func toTransaction( } if txIndex != nil { - res.TxIndex = ArgUintPtr(uint64(*txIndex)) + res.TxIndex = argUintPtr(uint64(*txIndex)) } if t.AccessList() != nil { @@ -323,7 +323,7 @@ func toLog(src *types.Log, logIdx, txIdx uint64, header *types.Header, txHash ty type argBig big.Int -func ArgBigPtr(b *big.Int) *argBig { +func argBigPtr(b *big.Int) *argBig { v := argBig(*b) return &v @@ -358,7 +358,7 @@ func argHashPtr(h types.Hash) *types.Hash { type argUint64 uint64 -func ArgUintPtr(n uint64) *argUint64 { +func argUintPtr(n uint64) *argUint64 { v := argUint64(n) return &v @@ -391,7 +391,7 @@ func (u *argUint64) UnmarshalJSON(buffer []byte) error { type argBytes []byte -func ArgBytesPtr(b []byte) *argBytes { +func argBytesPtr(b []byte) *argBytes { bb := argBytes(b) return &bb @@ -728,19 +728,3 @@ func (t *Transaction) UnmarshalJSON(data []byte) error { return nil } - -type SendTxnArgs struct { - From *types.MixedcaseAddress `json:"from"` - To *types.MixedcaseAddress `json:"to"` - Gas *argUint64 `json:"gas"` - GasPrice *argBytes `json:"gasPrice,omitempty"` - GasTipCap *argBytes `json:"maxFeePerGas,omitempty"` - GasFeeCap *argBytes `json:"maxPriorityFeePerGas,omitempty"` - Value *argBytes `json:"value"` - Data *argBytes `json:"data"` - Input *argBytes `json:"input"` - Nonce *argUint64 `json:"nonce"` - Type *argUint64 `json:"type"` - AccessList *types.TxAccessList `json:"accessList,omitempty"` - ChainID *argUint64 `json:"chainId,omitempty"` -} diff --git a/jsonrpc/types_test.go b/jsonrpc/types_test.go index cf1a00469f..6f1a7212d8 100644 --- a/jsonrpc/types_test.go +++ b/jsonrpc/types_test.go @@ -31,7 +31,7 @@ func TestBasicTypes_Encode(t *testing.T) { }, { argUint64(10), - ArgUintPtr(0), + argUintPtr(0), "0xa", }, { @@ -234,7 +234,7 @@ func mockTxn() *transaction { tt := &transaction{ Nonce: 1, - GasPrice: ArgBigPtr(big.NewInt(10)), + GasPrice: argBigPtr(big.NewInt(10)), Gas: 100, To: &to, Value: argBig(*big.NewInt(1000)), @@ -245,8 +245,8 @@ func mockTxn() *transaction { Hash: types.Hash{0x2}, From: types.Address{0x3}, BlockHash: &types.ZeroHash, - BlockNumber: ArgUintPtr(1), - TxIndex: ArgUintPtr(2), + BlockNumber: argUintPtr(1), + TxIndex: argUintPtr(2), Type: argUint64(types.LegacyTxType), } From 5d9a6f6acbd7bbd5148c3d4f953c6e1e21fb89bf Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Mon, 10 Jun 2024 13:38:09 +0200 Subject: [PATCH 34/69] command fix --- accounts/keystore/passphrase.go | 7 --- command/accounts/accounts.go | 30 +++++++++++ command/accounts/common/params.go | 13 ----- command/accounts/create/create.go | 18 +++---- command/accounts/create/params.go | 6 +-- .../accounts/import/{import.go => insert.go} | 51 ++++++++----------- command/accounts/import/params.go | 16 +++--- command/accounts/update/params.go | 34 +++++++++++-- command/accounts/update/update.go | 40 ++++++++++++--- command/root/root.go | 2 + 10 files changed, 132 insertions(+), 85 deletions(-) create mode 100644 command/accounts/accounts.go delete mode 100644 command/accounts/common/params.go rename command/accounts/import/{import.go => insert.go} (64%) diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 588d09b2d7..c2aecc9f45 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -61,13 +61,6 @@ func (ks keyStorePassphrase) GetKey(encryptedKey encryptedKeyJSONV3, auth string return key, nil } -// StoreKey generates a key, encrypts with 'auth' and stores in the given directory -func StoreKey(auth string, scryptN, scryptP int) (accounts.Account, error) { - _, a, err := storeNewKey(&keyStorePassphrase{scryptN, scryptP}, auth) - - return a, err -} - func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (encryptedKeyJSONV3, error) { encryptedKey, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) if err != nil { diff --git a/command/accounts/accounts.go b/command/accounts/accounts.go new file mode 100644 index 0000000000..fa36a6fc03 --- /dev/null +++ b/command/accounts/accounts.go @@ -0,0 +1,30 @@ +package accounts + +import ( + "github.com/0xPolygon/polygon-edge/command/accounts/create" + insert "github.com/0xPolygon/polygon-edge/command/accounts/import" + "github.com/0xPolygon/polygon-edge/command/accounts/update" + "github.com/spf13/cobra" +) + +func GetCommand() *cobra.Command { + accountCmd := &cobra.Command{ + Use: "account", + Short: "Account management command.", + } + + registerSubcommands(accountCmd) + + return accountCmd +} + +func registerSubcommands(baseCmd *cobra.Command) { + baseCmd.AddCommand( + // insert new account + insert.GetCommand(), + // create new account + create.GetCommand(), + // update existing account + update.GetCommand(), + ) +} diff --git a/command/accounts/common/params.go b/command/accounts/common/params.go deleted file mode 100644 index 7779e57757..0000000000 --- a/command/accounts/common/params.go +++ /dev/null @@ -1,13 +0,0 @@ -package common - -const ( - PrivateKeyFlag = "private-key" - KeyDirFlag = "key-dir" - PassphraseFlag = "passphrase" -) - -type CommonParams struct { - PrivateKey string - KeyDir string - Passphrase string -} diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index 355f4fee95..1f7645b571 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -1,4 +1,4 @@ -package accounts +package create import ( "fmt" @@ -6,6 +6,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts/keystore" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/hashicorp/go-hclog" "github.com/spf13/cobra" ) @@ -28,14 +29,14 @@ func GetCommand() *cobra.Command { func setFlags(cmd *cobra.Command) { cmd.Flags().StringVar( - ¶ms.Passphrase, + ¶ms.passphrase, PassphraseFlag, "", "passphrase for access to private key", ) cmd.Flags().StringVar( - ¶ms.ConfigDir, + ¶ms.configDir, ConfigDirFlag, "", "dir of config", @@ -51,15 +52,12 @@ func runPreRun(cmd *cobra.Command, _ []string) { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - scryptN := keystore.StandardScryptN - scryptP := keystore.StandardScryptP + scryptN := keystore.LightScryptN + scryptP := keystore.LightScryptP - if false { - scryptN = keystore.LightScryptN - scryptP = keystore.LightScryptP - } + ks := keystore.NewKeyStore(keystore.DefaultStorage, scryptN, scryptP, hclog.NewNullLogger()) - account, err := keystore.StoreKey(params.Passphrase, scryptN, scryptP) + account, err := ks.NewAccount(params.passphrase) if err != nil { outputter.SetError(fmt.Errorf("can't create account")) } diff --git a/command/accounts/create/params.go b/command/accounts/create/params.go index 0d7f55f270..461b2dc607 100644 --- a/command/accounts/create/params.go +++ b/command/accounts/create/params.go @@ -1,4 +1,4 @@ -package accounts +package create import ( "bytes" @@ -14,8 +14,8 @@ const ( ) type createParams struct { - Passphrase string - ConfigDir string + passphrase string + configDir string } type createResult struct { diff --git a/command/accounts/import/import.go b/command/accounts/import/insert.go similarity index 64% rename from command/accounts/import/import.go rename to command/accounts/import/insert.go index ea6fced8e6..9a995d1ab5 100644 --- a/command/accounts/import/import.go +++ b/command/accounts/import/insert.go @@ -1,4 +1,4 @@ -package accounts +package insert import ( "encoding/hex" @@ -9,19 +9,20 @@ import ( "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" "github.com/0xPolygon/polygon-edge/crypto" + "github.com/hashicorp/go-hclog" "github.com/spf13/cobra" ) var ( - params importParams + params insertParams ) func GetCommand() *cobra.Command { importCmd := &cobra.Command{ - Use: "import", - Short: "Import existing account with private key and auth passphrase", - PreRun: runPreRun, - Run: runCommand, + Use: "insert", + Short: "Import existing account with private key and auth passphrase", + PreRunE: runPreRun, + Run: runCommand, } helper.RegisterJSONRPCFlag(importCmd) @@ -32,60 +33,48 @@ func GetCommand() *cobra.Command { func setFlags(cmd *cobra.Command) { cmd.Flags().StringVar( - ¶ms.PrivateKey, + ¶ms.privateKey, PrivateKeyFlag, "", - "privateKey for import account", + "privateKey for insert account", ) cmd.Flags().StringVar( - ¶ms.KeyDir, + ¶ms.keyDir, KeyDirFlag, "", "dir for document that contains private key", ) cmd.Flags().StringVar( - ¶ms.Passphrase, + ¶ms.passphrase, PassphraseFlag, "", "passphrase for access to private key", ) - cmd.Flags().StringVar( - ¶ms.ConfigDir, - ConfigDirFlag, - "", - "dir of config file", - ) - _ = cmd.MarkFlagRequired(PassphraseFlag) } -func runPreRun(cmd *cobra.Command, _ []string) { - return +func runPreRun(cmd *cobra.Command, _ []string) error { + return nil } func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - scryptN := keystore.StandardScryptN - scryptP := keystore.StandardScryptP - - if false { - scryptN = keystore.LightScryptN - scryptP = keystore.LightScryptP - } + scryptN := keystore.LightScryptN + scryptP := keystore.LightScryptP am := accounts.NewManager(&accounts.Config{}, nil) - am.AddBackend(keystore.NewKeyStore(params.KeyDir, scryptN, scryptP, nil)) + am.AddBackend(keystore.NewKeyStore(keystore.DefaultStorage, scryptN, scryptP, hclog.NewNullLogger())) - if params.PrivateKey == "" { + if params.privateKey == "" { outputter.SetError(fmt.Errorf("private key empty")) } - dec, err := hex.DecodeString(params.PrivateKey) + dec, err := hex.DecodeString(params.privateKey) if err != nil { outputter.SetError(fmt.Errorf("failed to decode private key")) } @@ -102,10 +91,10 @@ func runCommand(cmd *cobra.Command, _ []string) { ks := backends[0].(*keystore.KeyStore) //nolint:forcetypeassert - acct, err := ks.ImportECDSA(privKey, params.Passphrase) + acct, err := ks.ImportECDSA(privKey, params.passphrase) if err != nil { outputter.SetError(fmt.Errorf("cannot import private key")) } - outputter.SetCommandResult(command.Results{&importResult{Address: acct.Address}}) + outputter.SetCommandResult(command.Results{&insertResult{Address: acct.Address}}) } diff --git a/command/accounts/import/params.go b/command/accounts/import/params.go index 3fcca34aad..7fb13fa14c 100644 --- a/command/accounts/import/params.go +++ b/command/accounts/import/params.go @@ -1,4 +1,4 @@ -package accounts +package insert import ( "bytes" @@ -12,21 +12,19 @@ const ( PrivateKeyFlag = "private-key" KeyDirFlag = "key-dir" PassphraseFlag = "passphrase" - ConfigDirFlag = "config-dir" ) -type importParams struct { - PrivateKey string - KeyDir string - Passphrase string - ConfigDir string +type insertParams struct { + privateKey string + keyDir string + passphrase string } -type importResult struct { +type insertResult struct { Address types.Address `json:"address"` } -func (i *importResult) GetOutput() string { +func (i *insertResult) GetOutput() string { var buffer bytes.Buffer vals := make([]string, 0, 1) diff --git a/command/accounts/update/params.go b/command/accounts/update/params.go index 8eafd540ab..98ed0a23ae 100644 --- a/command/accounts/update/params.go +++ b/command/accounts/update/params.go @@ -1,11 +1,35 @@ -package accounts +package update + +import ( + "fmt" + + "github.com/0xPolygon/polygon-edge/types" +) const ( - AddressFlag = "address" - PassphraseFlag = "passphrase" + AddressFlag = "address" + PassphraseFlag = "passphrase" + OldPassphraseFlag = "old-passphrase" ) type updateParams struct { - Address string - Passphrase string + rawAddress string + passphrase string + oldPassphrase string + address types.Address +} + +func (up *updateParams) validateFlags() error { + addr, err := types.IsValidAddress(up.rawAddress, false) + if err != nil { + return err + } + + up.address = addr + + if up.passphrase != up.oldPassphrase { + return fmt.Errorf("same old and new password") + } + + return nil } diff --git a/command/accounts/update/update.go b/command/accounts/update/update.go index e8ba62f065..315240bab9 100644 --- a/command/accounts/update/update.go +++ b/command/accounts/update/update.go @@ -1,7 +1,13 @@ -package accounts +package update import ( + "fmt" + + "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/accounts/keystore" + "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/hashicorp/go-hclog" "github.com/spf13/cobra" ) @@ -14,7 +20,7 @@ func GetCommand() *cobra.Command { Use: "update", Short: "Update existing account", PreRunE: runPreRun, - RunE: runCommand, + Run: runCommand, } helper.RegisterJSONRPCFlag(updateCmd) @@ -25,24 +31,44 @@ func GetCommand() *cobra.Command { func setFlags(cmd *cobra.Command) { cmd.Flags().StringVar( - ¶ms.Address, + ¶ms.rawAddress, AddressFlag, "", "address of account", ) cmd.Flags().StringVar( - ¶ms.Passphrase, + ¶ms.passphrase, PassphraseFlag, "", "passphrase for access to private key", ) + + cmd.Flags().StringVar( + ¶ms.oldPassphrase, + OldPassphraseFlag, + "", + "old passphrase to unlock account", + ) } func runPreRun(cmd *cobra.Command, _ []string) error { - return nil + return params.validateFlags() } -func runCommand(cmd *cobra.Command, _ []string) error { - return nil +func runCommand(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + + scryptN := keystore.LightScryptN + scryptP := keystore.LightScryptP + + ks := keystore.NewKeyStore(keystore.DefaultStorage, scryptN, scryptP, hclog.NewNullLogger()) + + if !ks.HasAddress(params.address) { + outputter.SetError(fmt.Errorf("this address doesn't exist")) + } else { + if err := ks.Update(accounts.Account{Address: params.address}, params.passphrase, params.oldPassphrase); err != nil { + outputter.SetError(fmt.Errorf("can't update account: %s", err)) + } + } } diff --git a/command/root/root.go b/command/root/root.go index 455a5fcaee..f7fbf1ab0f 100644 --- a/command/root/root.go +++ b/command/root/root.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + "github.com/0xPolygon/polygon-edge/command/accounts" "github.com/0xPolygon/polygon-edge/command/backup" "github.com/0xPolygon/polygon-edge/command/bridge" "github.com/0xPolygon/polygon-edge/command/genesis" @@ -61,6 +62,7 @@ func (rc *RootCommand) registerSubCommands() { validator.GetCommand(), loadtest.GetCommand(), sanitycheck.GetCommand(), + accounts.GetCommand(), ) } From cd88c29ff135b21e61c5aab8c51b6da1e93da628 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Mon, 10 Jun 2024 13:50:05 +0200 Subject: [PATCH 35/69] mixed case deleted --- types/types.go | 48 ------------------------------------------------ 1 file changed, 48 deletions(-) diff --git a/types/types.go b/types/types.go index 185bf1331b..d6a04b9197 100644 --- a/types/types.go +++ b/types/types.go @@ -233,51 +233,3 @@ type OverrideAccount struct { } type StateOverride map[Address]OverrideAccount - -// MixedcaseAddress retains the original string, which may or may not be -// correctly checksummed -type MixedcaseAddress struct { - addr Address - original string -} - -// NewMixedcaseAddress constructor (mainly for testing) -func NewMixedcaseAddress(addr Address) *MixedcaseAddress { - return &MixedcaseAddress{addr: addr, original: addr.String()} -} - -// NewMixedcaseAddressFromString is mainly meant for unit-testing -func NewMixedcaseAddressFromString(hexaddr string) (*MixedcaseAddress, error) { - var addr Address - - addr, err := IsValidAddress(hexaddr, false) - if err != nil { - return nil, errors.New("invalid address") - } - - return &MixedcaseAddress{addr: addr, original: addr.String()}, nil -} - -// Address returns the address -func (ma *MixedcaseAddress) Address() Address { - return ma.addr -} - -// String implements fmt.Stringer -func (ma *MixedcaseAddress) String() string { - if ma.ValidChecksum() { - return fmt.Sprintf("%s [chksum ok]", ma.original) - } - - return fmt.Sprintf("%s [chksum INVALID]", ma.original) -} - -// ValidChecksum returns true if the address has valid checksum -func (ma *MixedcaseAddress) ValidChecksum() bool { - return ma.original == ma.addr.String() -} - -// Original returns the mixed-case input string -func (ma *MixedcaseAddress) Original() string { - return ma.original -} From c5c7c3735ce7d3c0a7b1c8b8badcf7eb406050b8 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Mon, 10 Jun 2024 14:56:07 +0200 Subject: [PATCH 36/69] fix some comments --- accounts/keystore/account_cache.go | 79 +++++++++++++++++++++++++----- jsonrpc/personal_endpoint.go | 15 +----- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 197f05f472..0d7963ff51 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -1,7 +1,9 @@ package keystore import ( + "encoding/json" "errors" + "os" "path" "sync" "time" @@ -18,25 +20,32 @@ const minReloadInterval = 2 * time.Second // accountCache is a live index of all accounts in the keystore. type accountCache struct { logger hclog.Logger - keydir string + keyDir string mu sync.Mutex allMap map[types.Address]encryptedKeyJSONV3 throttle *time.Timer notify chan struct{} - fileC *fileCache } func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan struct{}) { ac := &accountCache{ logger: logger, - keydir: keyDir, + keyDir: keyDir, notify: make(chan struct{}, 1), allMap: make(map[types.Address]encryptedKeyJSONV3), } - keyPath := path.Join(keyDir, "keys.txt") + keysPath := path.Join(keyDir, "keys.txt") - ac.fileC, _ = NewFileCache(keyPath) + ac.keyDir = keysPath + + if _, err := os.Stat(keysPath); errors.Is(err, os.ErrNotExist) { + if _, err := os.Create(keysPath); err != nil { + ac.logger.Info("can't create new file", "err", err) + + return nil, nil + } + } ac.scanAccounts() //nolint:errcheck @@ -94,7 +103,7 @@ func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) ac.allMap[newAccount.Address] = key - err = ac.fileC.saveData(ac.allMap) + err = ac.saveData(ac.allMap) if err != nil { return err } @@ -110,16 +119,24 @@ func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) ac.mu.Lock() defer ac.mu.Unlock() - if _, ok := ac.allMap[account.Address]; !ok { + newMap := make(map[types.Address]encryptedKeyJSONV3) + + for mapKey, mapValue := range ac.allMap { + newMap[mapKey] = mapValue + } + + if _, ok := newMap[account.Address]; !ok { return errors.New("this account doesn't exists") } else { - ac.allMap[account.Address] = key + newMap[account.Address] = key } - if err := ac.fileC.saveData(ac.allMap); err != nil { + if err := ac.saveData(newMap); err != nil { return err } + ac.allMap = newMap + return nil } @@ -134,7 +151,7 @@ func (ac *accountCache) delete(removed accounts.Account) { delete(ac.allMap, removed.Address) - if err := ac.fileC.saveData(ac.allMap); err != nil { + if err := ac.saveData(ac.allMap); err != nil { ac.logger.Debug("cant't save data in file,", "err", err) } } @@ -177,7 +194,7 @@ func (ac *accountCache) scanAccounts() error { ac.mu.Lock() defer ac.mu.Unlock() - accs, err := ac.fileC.scanOneFile() + accs, err := ac.scanFile() if err != nil { ac.logger.Debug("Failed to reload keystore contents", "err", err) @@ -198,3 +215,43 @@ func (ac *accountCache) scanAccounts() error { return nil } + +func (ac *accountCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) error { + fi, err := os.Create(ac.keyDir) + if err != nil { + return err + } + + defer fi.Close() + + byteAccount, err := json.Marshal(accounts) + if err != nil { + return err + } + + if _, err := fi.Write(byteAccount); err != nil { + return err + } + + return nil +} + +func (ac *accountCache) scanFile() (map[types.Address]encryptedKeyJSONV3, error) { + fi, err := os.ReadFile(ac.keyDir) + if err != nil { + return nil, err + } + + if len(fi) == 0 { + return nil, nil + } + + var accounts = make(map[types.Address]encryptedKeyJSONV3) + + err = json.Unmarshal(fi, &accounts) + if err != nil { + return nil, err + } + + return accounts, nil +} diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index ef0ad1effd..5b153985a7 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -10,7 +10,6 @@ import ( "github.com/0xPolygon/polygon-edge/accounts/keystore" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" - "github.com/btcsuite/btcd/btcec/v2/ecdsa" ) type Personal struct { @@ -92,22 +91,12 @@ func (s *Personal) LockAccount(addr types.Address) (bool, error) { } func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { //nolint:stylecheck - if len(sig) != crypto.ECDSASignatureLength { - return types.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.ECDSASignatureLength) - } - - if sig[64] != 27 && sig[64] != 28 { - return types.Address{}, errors.New("invalid Ethereum signature V is not 27 or 28") - } - - sig[64] -= 27 - - rpk, _, err := ecdsa.RecoverCompact(sig, data) + addressRaw, err := crypto.Ecrecover(data, sig) if err != nil { return types.Address{}, err } - return crypto.PubKeyToAddress(rpk.ToECDSA()), nil + return types.BytesToAddress(addressRaw), nil } func getKeystore(am *accounts.Manager) (*keystore.KeyStore, error) { From cf452fb8ecfe789957138668c339a4acc8dfa331 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Tue, 11 Jun 2024 13:36:46 +0200 Subject: [PATCH 37/69] url deleted --- accounts/accounts.go | 13 --- accounts/helper.go | 21 ----- accounts/keystore/account_cache_test.go | 2 - accounts/keystore/wallet.go | 6 +- accounts/manager.go | 40 ++------- accounts/url.go | 65 --------------- accounts/url_test.go | 106 ------------------------ command/accounts/create/create.go | 2 +- command/accounts/create/params.go | 4 +- 9 files changed, 10 insertions(+), 249 deletions(-) delete mode 100644 accounts/url.go delete mode 100644 accounts/url_test.go diff --git a/accounts/accounts.go b/accounts/accounts.go index bdf9391586..4726d95bce 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -11,24 +11,11 @@ import ( type Account struct { Address types.Address `json:"address"` - URL URL `json:"url"` } -const ( - MimetypeDataWithValidator = "data/validator" - MimetypeTypedData = "data/typed" - MimetypeClique = "application/x-clique-header" - MimetypeTextPlain = "text/plain" -) - // Wallet represents a software or hardware wallet that might contain one or more // accounts (derived from the same seed). type Wallet interface { - // URL retrieves the canonical path under which this wallet is reachable. It is - // used by upper layers to define a sorting order over all wallets from multiple - // backends. - URL() URL - // Status returns a textual status to aid the user in the current state of the // wallet. It also returns an error indicating any failure the wallet might have // encountered. diff --git a/accounts/helper.go b/accounts/helper.go index 1a61ef8f7f..3613e86bf9 100644 --- a/accounts/helper.go +++ b/accounts/helper.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "os" - - "github.com/0xPolygon/polygon-edge/types" ) var ( @@ -44,25 +42,6 @@ func (err *AuthNeededError) Error() string { return fmt.Sprintf("authentication needed: %s", err.Needed) } -type AmbiguousAddrError struct { - Addr types.Address - Matches []Account -} - -func (err *AmbiguousAddrError) Error() string { - files := "" - - for i, a := range err.Matches { - files += a.URL.Path - - if i < len(err.Matches)-1 { - files += ", " - } - } - - return fmt.Sprintf("multiple keys match address (%s)", files) -} - func LoadJSON(file string, val interface{}) error { content, err := os.ReadFile(file) if err != nil { diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index bdf46153b3..4e8553a5ef 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -191,8 +191,6 @@ func TestCacheFind(t *testing.T) { {Query: accs[2], WantResult: accs[2]}, // no match error {Query: nomatchAccount, WantError: accounts.ErrNoMatch}, - {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: accounts.ErrNoMatch}, - {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: accounts.ErrNoMatch}, {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: accounts.ErrNoMatch}, } diff --git a/accounts/keystore/wallet.go b/accounts/keystore/wallet.go index 2db4b6df12..a807b6c861 100644 --- a/accounts/keystore/wallet.go +++ b/accounts/keystore/wallet.go @@ -14,10 +14,6 @@ type keyStoreWallet struct { keyStore *KeyStore } -func (ksw *keyStoreWallet) URL() accounts.URL { - return ksw.account.URL -} - func (ksw *keyStoreWallet) Status() (string, error) { ksw.keyStore.mu.RLock() defer ksw.keyStore.mu.RUnlock() @@ -38,7 +34,7 @@ func (ksw *keyStoreWallet) Accounts() []accounts.Account { } func (ksw *keyStoreWallet) Contains(account accounts.Account) bool { - return account.Address == ksw.account.Address && (account.URL == accounts.URL{} || account.URL == ksw.account.URL) + return account.Address == ksw.account.Address } func (ksw *keyStoreWallet) signHash(account accounts.Account, hash []byte) ([]byte, error) { diff --git a/accounts/manager.go b/accounts/manager.go index 196587cff6..b129d361f0 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -2,7 +2,6 @@ package accounts import ( "reflect" - "sort" "sync" "github.com/0xPolygon/polygon-edge/accounts/event" @@ -165,24 +164,6 @@ func (am *Manager) walletsNoLock() []Wallet { return cpy } -func (am *Manager) Wallet(url string) (Wallet, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - parsed, err := parseURL(url) - if err != nil { - return nil, err - } - - for _, wallet := range am.walletsNoLock() { - if wallet.URL() == parsed { - return wallet, nil - } - } - - return nil, ErrUnknownWallet -} - func (am *Manager) Accounts() []types.Address { am.lock.RLock() defer am.lock.RUnlock() @@ -217,27 +198,20 @@ func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { func merge(slice []Wallet, wallets ...Wallet) []Wallet { for _, wallet := range wallets { - n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) - if n == len(slice) { - continue - } - - slice = append(slice[:n], slice[n+1:]...) + slice = append(slice, wallet) } return slice } -func drop(slice []Wallet, wallets ...Wallet) []Wallet { - for _, wallet := range wallets { - n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) +func drop(slice []Wallet, wallet Wallet) []Wallet { + var droppedSlice []Wallet - if n == len(slice) { - continue + for _, internalWallet := range slice { + if internalWallet.Accounts()[0].Address != wallet.Accounts()[0].Address { + droppedSlice = append(droppedSlice, internalWallet) } - - slice = append(slice[:n], slice[n+1:]...) } - return slice + return droppedSlice } diff --git a/accounts/url.go b/accounts/url.go deleted file mode 100644 index 1d63b2eb83..0000000000 --- a/accounts/url.go +++ /dev/null @@ -1,65 +0,0 @@ -package accounts - -import ( - "encoding/json" - "errors" - "strings" -) - -type URL struct { - Scheme string - Path string -} - -func parseURL(url string) (URL, error) { - urlParts := strings.Split(url, "://") - if len(urlParts) != 2 || urlParts[0] == "" { - return URL{}, errors.New("protocol scheme missing") - } - - return URL{ - Scheme: urlParts[0], - Path: urlParts[1], - }, nil -} - -func (u URL) String() string { - if u.Scheme != "" { - return u.Scheme + "://" + u.Path - } - - return u.Path -} - -func (u URL) MarshalJSON() ([]byte, error) { - return json.Marshal(u.String()) -} - -func (u *URL) UnmarshalJSON(input []byte) error { - var textURL string - - err := json.Unmarshal(input, &textURL) - - if err != nil { - return err - } - - url, err := parseURL(textURL) - - if err != nil { - return err - } - - u.Scheme = url.Scheme - u.Path = url.Path - - return nil -} - -func (u URL) Cmp(url URL) int { - if u.Scheme == url.Scheme { - return strings.Compare(u.Path, url.Path) - } - - return strings.Compare(u.Scheme, url.Scheme) -} diff --git a/accounts/url_test.go b/accounts/url_test.go deleted file mode 100644 index 56b7fc79a4..0000000000 --- a/accounts/url_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package accounts - -import "testing" - -const ( - bladeURL = "blade.org" - bladeURLWithPrefix = "https://blade.org" - bladeURLJSON = "\"https://blade.org\"" -) - -func TestURLParsing(t *testing.T) { - t.Parallel() - - url, err := parseURL(bladeURLWithPrefix) - - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if url.Scheme != "https" { - t.Errorf("expected: %v, got: %v", "https", url.Scheme) - } - - if url.Path != bladeURL { - t.Errorf("expected: %v, got: %v", bladeURL, url.Path) - } - - for _, u := range []string{bladeURL, ""} { - if _, err = parseURL(u); err == nil { - t.Errorf("input %v, expected err, got: nil", u) - } - } -} - -func TestURLString(t *testing.T) { - t.Parallel() - - url := URL{Scheme: "https", Path: bladeURL} - - if url.String() != bladeURLWithPrefix { - t.Errorf("expected: %v, got: %v", bladeURLWithPrefix, url.String()) - } - - url = URL{Scheme: "", Path: bladeURL} - - if url.String() != bladeURL { - t.Errorf("expected: %v, got: %v", bladeURL, url.String()) - } -} - -func TestURLMarshalJSON(t *testing.T) { - t.Parallel() - - url := URL{Scheme: "https", Path: bladeURL} - - json, err := url.MarshalJSON() - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if string(json) != bladeURLJSON { - t.Errorf("expected: %v, got: %v", bladeURLJSON, string(json)) - } -} - -func TestURLUnmarshalJSON(t *testing.T) { - t.Parallel() - - url := &URL{} - - err := url.UnmarshalJSON([]byte(bladeURLJSON)) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if url.Scheme != "https" { - t.Errorf("expected: %v, got: %v", "https", url.Scheme) - } - - if url.Path != bladeURL { - t.Errorf("expected: %v, got: %v", "https", url.Path) - } -} - -func TestURLComparison(t *testing.T) { - t.Parallel() - - tests := []struct { - urlA URL - urlB URL - expect int - }{ - {URL{"https", bladeURL}, URL{"https", bladeURL}, 0}, - {URL{"http", bladeURL}, URL{"https", bladeURL}, -1}, - {URL{"https", bladeURL + "/a"}, URL{"https", bladeURL}, 1}, - {URL{"https", "abc.org"}, URL{"https", bladeURL}, -1}, - } - - for i, tt := range tests { - result := tt.urlA.Cmp(tt.urlB) - - if result != tt.expect { - t.Errorf("test %d: cmp mismatch: expected: %d, got: %d", i, tt.expect, result) - } - } -} diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index 1f7645b571..e283d442dc 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -62,5 +62,5 @@ func runCommand(cmd *cobra.Command, _ []string) { outputter.SetError(fmt.Errorf("can't create account")) } - outputter.SetCommandResult(command.Results{&createResult{Address: account.Address, PrivateKeyPath: account.URL.Path}}) + outputter.SetCommandResult(command.Results{&createResult{Address: account.Address}}) } diff --git a/command/accounts/create/params.go b/command/accounts/create/params.go index 461b2dc607..f1cb206652 100644 --- a/command/accounts/create/params.go +++ b/command/accounts/create/params.go @@ -19,8 +19,7 @@ type createParams struct { } type createResult struct { - Address types.Address `json:"address"` - PrivateKeyPath string `json:"privkeypath"` + Address types.Address `json:"address"` } func (i *createResult) GetOutput() string { @@ -28,7 +27,6 @@ func (i *createResult) GetOutput() string { vals := make([]string, 0, 1) vals = append(vals, fmt.Sprintf("Address|%s", i.Address.String())) - vals = append(vals, fmt.Sprintf("PrivateKeyPath:%s", i.PrivateKeyPath)) buffer.WriteString("\n[Import accounts]\n") buffer.WriteString(helper.FormatKV(vals)) From 5c41a5b82d7cffdf8976d7bccd31d2e637539b08 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Tue, 11 Jun 2024 13:41:48 +0200 Subject: [PATCH 38/69] file cache delted --- accounts/keystore/file_cache.go | 71 --------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 accounts/keystore/file_cache.go diff --git a/accounts/keystore/file_cache.go b/accounts/keystore/file_cache.go deleted file mode 100644 index 822ad99247..0000000000 --- a/accounts/keystore/file_cache.go +++ /dev/null @@ -1,71 +0,0 @@ -package keystore - -import ( - "encoding/json" - "errors" - "os" - "sync" - - "github.com/0xPolygon/polygon-edge/types" -) - -// fileCache is a cache of files seen during scan of keystore. -type fileCache struct { - all map[types.Address]encryptedKeyJSONV3 - mu sync.Mutex - keyDir string -} - -func NewFileCache(keyPath string) (*fileCache, error) { - fc := &fileCache{all: make(map[types.Address]encryptedKeyJSONV3), keyDir: keyPath} - - if _, err := os.Stat(keyPath); errors.Is(err, os.ErrNotExist) { - if _, err := os.Create(fc.keyDir); err != nil { - return nil, err - } - } - - return fc, nil -} - -func (fc *fileCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) error { - fi, err := os.Create(fc.keyDir) - if err != nil { - return err - } - - defer fi.Close() - - byteAccount, err := json.Marshal(accounts) - if err != nil { - return err - } - - if _, err := fi.Write(byteAccount); err != nil { - return err - } - - return nil -} - -func (fc *fileCache) scanOneFile() (map[types.Address]encryptedKeyJSONV3, error) { - fi, err := os.ReadFile(fc.keyDir) - if err != nil { - return nil, err - } - - if len(fi) == 0 { - return nil, nil - } - - var accounts = make(map[types.Address]encryptedKeyJSONV3) - - err = json.Unmarshal(fi, &accounts) - if err != nil { - return nil, err - } - - fc.all = accounts - - return accounts, nil -} From f1d836165a348d1cedf35fa7355eeafb4b3236b9 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Wed, 12 Jun 2024 13:31:08 +0200 Subject: [PATCH 39/69] comment fix --- accounts/helper.go | 35 +------------- accounts/keystore/passphrase_test.go | 23 ++++++++- accounts/manager.go | 12 +---- accounts/pubsub/pub_sub.go | 47 +++++++++++++++++++ command/accounts/accounts.go | 2 +- command/accounts/create/create.go | 29 +++--------- command/accounts/create/params.go | 8 ++-- command/accounts/{import => insert}/insert.go | 33 +++++-------- command/accounts/{import => insert}/params.go | 10 ++-- command/accounts/update/params.go | 10 ++-- command/accounts/update/update.go | 17 +++---- jsonrpc/personal_endpoint.go | 24 +++++----- server/server.go | 10 ++-- 13 files changed, 128 insertions(+), 132 deletions(-) create mode 100644 accounts/pubsub/pub_sub.go rename command/accounts/{import => insert}/insert.go (69%) rename command/accounts/{import => insert}/params.go (73%) diff --git a/accounts/helper.go b/accounts/helper.go index 3613e86bf9..6f44fc98ea 100644 --- a/accounts/helper.go +++ b/accounts/helper.go @@ -1,31 +1,17 @@ package accounts import ( - "encoding/json" "errors" "fmt" - "os" ) var ( ErrUnknownAccount = errors.New("unknown account") - ErrUnknownWallet = errors.New("unknown wallet") - - ErrNotSupported = errors.New("not supported") - - ErrInvalidPassphrase = errors.New("invalid password") - - ErrWalletAlreadyOpen = errors.New("wallet already open") - ErrWalletClosed = errors.New("wallet closed") ErrNoMatch = errors.New("no key for given address or file") ErrDecrypt = errors.New("could not decrypt key with given password") - - // ErrAccountAlreadyExists is returned if an account attempted to import is - // already present in the keystore. - ErrAccountAlreadyExists = errors.New("account already exists") ) type AuthNeededError struct { @@ -42,26 +28,7 @@ func (err *AuthNeededError) Error() string { return fmt.Sprintf("authentication needed: %s", err.Needed) } -func LoadJSON(file string, val interface{}) error { - content, err := os.ReadFile(file) - if err != nil { - return err - } - - if err := json.Unmarshal(content, val); err != nil { - if syntaxerr, ok := err.(*json.SyntaxError); ok { //nolint:errorlint - line := findLine(content, syntaxerr.Offset) - - return fmt.Errorf("JSON syntax error at %v:%v: %w", file, line, err) - } - - return fmt.Errorf("JSON unmarshal error in %v: %w", file, err) - } - - return nil -} - -func findLine(data []byte, offset int64) (line int) { +func FindLine(data []byte, offset int64) (line int) { line = 1 for i, r := range string(data) { diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 46be8855fb..60de37d74b 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "fmt" "os" "path/filepath" "strings" @@ -142,7 +143,7 @@ func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { tests := make(map[string]KeyStoreTestV3) - err := accounts.LoadJSON(file, &tests) + err := LoadJSON(t, file, &tests) require.NoError(t, err) return tests @@ -199,3 +200,23 @@ func TestKeyEncryptDecrypt(t *testing.T) { require.NoError(t, err) } } + +func LoadJSON(t *testing.T, file string, val interface{}) error { + t.Helper() + content, err := os.ReadFile(file) + if err != nil { + return err + } + + if err := json.Unmarshal(content, val); err != nil { + if syntaxerr, ok := err.(*json.SyntaxError); ok { //nolint:errorlint + line := accounts.FindLine(content, syntaxerr.Offset) + + return fmt.Errorf("JSON syntax error at %v:%v: %w", file, line, err) + } + + return fmt.Errorf("JSON unmarshal error in %v: %w", file, err) + } + + return nil +} diff --git a/accounts/manager.go b/accounts/manager.go index b129d361f0..48f8e896e0 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -11,10 +11,6 @@ import ( const managerSubBufferSize = 50 -type Config struct { - InsecureUnlockAllowed bool -} - type newBackendEvent struct { backend Backend @@ -22,7 +18,6 @@ type newBackendEvent struct { } type Manager struct { - config *Config backends map[reflect.Type][]Backend updaters []event.Subscription updates chan WalletEvent @@ -39,7 +34,7 @@ type Manager struct { lock sync.RWMutex } -func NewManager(config *Config, logger hclog.Logger, backends ...Backend) *Manager { +func NewManager(logger hclog.Logger, backends ...Backend) *Manager { var wallets []Wallet for _, backend := range backends { @@ -55,7 +50,6 @@ func NewManager(config *Config, logger hclog.Logger, backends ...Backend) *Manag } am := &Manager{ - config: config, backends: make(map[reflect.Type][]Backend), updaters: subs, updates: updates, @@ -86,10 +80,6 @@ func (am *Manager) Close() error { return <-errc } -func (am *Manager) Config() *Config { - return am.config -} - func (am *Manager) AddBackend(backend Backend) { done := make(chan struct{}) diff --git a/accounts/pubsub/pub_sub.go b/accounts/pubsub/pub_sub.go new file mode 100644 index 0000000000..c857c50350 --- /dev/null +++ b/accounts/pubsub/pub_sub.go @@ -0,0 +1,47 @@ +package pubsub + +import "sync" + +type PubSub struct { + subscribers map[string][]chan interface{} + mu sync.RWMutex +} + +func NewPubSub() *PubSub { + return &PubSub{ + subscribers: make(map[string][]chan interface{}), + } +} + +func (ps *PubSub) Subscribe(topic string, event chan interface{}) { + ps.mu.Lock() + defer ps.mu.Unlock() + + ps.subscribers[topic] = append(ps.subscribers[topic], event) +} + +func (ps *PubSub) Publish(topic string, msg interface{}) { + ps.mu.RLock() + defer ps.mu.RUnlock() + + if chans, ok := ps.subscribers[topic]; ok { + for _, ch := range chans { + ch <- msg + } + } +} + +func (ps *PubSub) Unsubscribe(topic string, sub <-chan interface{}) { + ps.mu.Lock() + defer ps.mu.Unlock() + + if chans, ok := ps.subscribers[topic]; ok { + for i, ch := range chans { + if ch == sub { + ps.subscribers[topic] = append(chans[:i], chans[i+1:]...) + close(ch) + break + } + } + } +} diff --git a/command/accounts/accounts.go b/command/accounts/accounts.go index fa36a6fc03..cd3beb1893 100644 --- a/command/accounts/accounts.go +++ b/command/accounts/accounts.go @@ -2,7 +2,7 @@ package accounts import ( "github.com/0xPolygon/polygon-edge/command/accounts/create" - insert "github.com/0xPolygon/polygon-edge/command/accounts/import" + "github.com/0xPolygon/polygon-edge/command/accounts/insert" "github.com/0xPolygon/polygon-edge/command/accounts/update" "github.com/spf13/cobra" ) diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index e283d442dc..1a7fb14fe5 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -16,10 +16,9 @@ var ( func GetCommand() *cobra.Command { createCmd := &cobra.Command{ - Use: "create", - Short: "Create new account", - PreRun: runPreRun, - Run: runCommand, + Use: "create", + Short: "Create new account", + Run: runCommand, } helper.RegisterJSONRPCFlag(createCmd) @@ -30,36 +29,22 @@ func GetCommand() *cobra.Command { func setFlags(cmd *cobra.Command) { cmd.Flags().StringVar( ¶ms.passphrase, - PassphraseFlag, + passphraseFlag, "", "passphrase for access to private key", ) - cmd.Flags().StringVar( - ¶ms.configDir, - ConfigDirFlag, - "", - "dir of config", - ) - - _ = cmd.MarkFlagRequired(PassphraseFlag) -} - -func runPreRun(cmd *cobra.Command, _ []string) { - + _ = cmd.MarkFlagRequired(passphraseFlag) } func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - scryptN := keystore.LightScryptN - scryptP := keystore.LightScryptP - - ks := keystore.NewKeyStore(keystore.DefaultStorage, scryptN, scryptP, hclog.NewNullLogger()) + ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) account, err := ks.NewAccount(params.passphrase) if err != nil { - outputter.SetError(fmt.Errorf("can't create account")) + outputter.SetError(fmt.Errorf("can't create account: %w", err)) } outputter.SetCommandResult(command.Results{&createResult{Address: account.Address}}) diff --git a/command/accounts/create/params.go b/command/accounts/create/params.go index f1cb206652..f58499ea92 100644 --- a/command/accounts/create/params.go +++ b/command/accounts/create/params.go @@ -9,8 +9,8 @@ import ( ) const ( - PassphraseFlag = "passphrase" - ConfigDirFlag = "config-dir" + passphraseFlag = "passphrase" + configDirFlag = "config-dir" ) type createParams struct { @@ -25,10 +25,10 @@ type createResult struct { func (i *createResult) GetOutput() string { var buffer bytes.Buffer - vals := make([]string, 0, 1) + vals := make([]string, 0, 2) vals = append(vals, fmt.Sprintf("Address|%s", i.Address.String())) - buffer.WriteString("\n[Import accounts]\n") + buffer.WriteString("\n[Created accounts]\n") buffer.WriteString(helper.FormatKV(vals)) buffer.WriteString("\n") diff --git a/command/accounts/import/insert.go b/command/accounts/insert/insert.go similarity index 69% rename from command/accounts/import/insert.go rename to command/accounts/insert/insert.go index 9a995d1ab5..282cfaedcc 100644 --- a/command/accounts/import/insert.go +++ b/command/accounts/insert/insert.go @@ -2,6 +2,7 @@ package insert import ( "encoding/hex" + "errors" "fmt" "github.com/0xPolygon/polygon-edge/accounts" @@ -20,7 +21,7 @@ var ( func GetCommand() *cobra.Command { importCmd := &cobra.Command{ Use: "insert", - Short: "Import existing account with private key and auth passphrase", + Short: "Insert existing account with private key and auth passphrase", PreRunE: runPreRun, Run: runCommand, } @@ -34,26 +35,19 @@ func GetCommand() *cobra.Command { func setFlags(cmd *cobra.Command) { cmd.Flags().StringVar( ¶ms.privateKey, - PrivateKeyFlag, + privateKeyFlag, "", - "privateKey for insert account", - ) - - cmd.Flags().StringVar( - ¶ms.keyDir, - KeyDirFlag, - "", - "dir for document that contains private key", + "privateKey key of new account", ) cmd.Flags().StringVar( ¶ms.passphrase, - PassphraseFlag, + passphraseFlag, "", "passphrase for access to private key", ) - _ = cmd.MarkFlagRequired(PassphraseFlag) + _ = cmd.MarkFlagRequired(passphraseFlag) } func runPreRun(cmd *cobra.Command, _ []string) error { @@ -63,25 +57,22 @@ func runPreRun(cmd *cobra.Command, _ []string) error { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - scryptN := keystore.LightScryptN - scryptP := keystore.LightScryptP - - am := accounts.NewManager(&accounts.Config{}, nil) + am := accounts.NewManager(nil) - am.AddBackend(keystore.NewKeyStore(keystore.DefaultStorage, scryptN, scryptP, hclog.NewNullLogger())) + am.AddBackend(keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger())) if params.privateKey == "" { - outputter.SetError(fmt.Errorf("private key empty")) + outputter.SetError(errors.New("private key empty")) } dec, err := hex.DecodeString(params.privateKey) if err != nil { - outputter.SetError(fmt.Errorf("failed to decode private key")) + outputter.SetError(fmt.Errorf("failed to decode private ke: %w", err)) } privKey, err := crypto.BytesToECDSAPrivateKey(dec) if err != nil { - outputter.SetError(fmt.Errorf("failed to initialize private key")) + outputter.SetError(fmt.Errorf("failed to initialize private key: %w", err)) } backends := am.Backends(keystore.KeyStoreType) @@ -93,7 +84,7 @@ func runCommand(cmd *cobra.Command, _ []string) { acct, err := ks.ImportECDSA(privKey, params.passphrase) if err != nil { - outputter.SetError(fmt.Errorf("cannot import private key")) + outputter.SetError(fmt.Errorf("cannot import private key: %w", err)) } outputter.SetCommandResult(command.Results{&insertResult{Address: acct.Address}}) diff --git a/command/accounts/import/params.go b/command/accounts/insert/params.go similarity index 73% rename from command/accounts/import/params.go rename to command/accounts/insert/params.go index 7fb13fa14c..1b97aa7bc2 100644 --- a/command/accounts/import/params.go +++ b/command/accounts/insert/params.go @@ -9,14 +9,12 @@ import ( ) const ( - PrivateKeyFlag = "private-key" - KeyDirFlag = "key-dir" - PassphraseFlag = "passphrase" + privateKeyFlag = "private-key" + passphraseFlag = "passphrase" ) type insertParams struct { privateKey string - keyDir string passphrase string } @@ -27,10 +25,10 @@ type insertResult struct { func (i *insertResult) GetOutput() string { var buffer bytes.Buffer - vals := make([]string, 0, 1) + vals := make([]string, 0, 2) vals = append(vals, fmt.Sprintf("Address|%s", i.Address.String())) - buffer.WriteString("\n[Import accounts]\n") + buffer.WriteString("\n[Inserted accounts]\n") buffer.WriteString(helper.FormatKV(vals)) buffer.WriteString("\n") diff --git a/command/accounts/update/params.go b/command/accounts/update/params.go index 98ed0a23ae..b455f5edd1 100644 --- a/command/accounts/update/params.go +++ b/command/accounts/update/params.go @@ -1,15 +1,15 @@ package update import ( - "fmt" + "errors" "github.com/0xPolygon/polygon-edge/types" ) const ( - AddressFlag = "address" - PassphraseFlag = "passphrase" - OldPassphraseFlag = "old-passphrase" + addressFlag = "address" + passphraseFlag = "new-passphrase" + oldPassphraseFlag = "old-passphrase" ) type updateParams struct { @@ -28,7 +28,7 @@ func (up *updateParams) validateFlags() error { up.address = addr if up.passphrase != up.oldPassphrase { - return fmt.Errorf("same old and new password") + return errors.New("same old and new password") } return nil diff --git a/command/accounts/update/update.go b/command/accounts/update/update.go index 315240bab9..397b2596a3 100644 --- a/command/accounts/update/update.go +++ b/command/accounts/update/update.go @@ -18,7 +18,7 @@ var ( func GetCommand() *cobra.Command { updateCmd := &cobra.Command{ Use: "update", - Short: "Update existing account", + Short: "Update passphrase of existing account", PreRunE: runPreRun, Run: runCommand, } @@ -32,21 +32,21 @@ func GetCommand() *cobra.Command { func setFlags(cmd *cobra.Command) { cmd.Flags().StringVar( ¶ms.rawAddress, - AddressFlag, + addressFlag, "", "address of account", ) cmd.Flags().StringVar( ¶ms.passphrase, - PassphraseFlag, + passphraseFlag, "", - "passphrase for access to private key", + "new passphrase for access to private key", ) cmd.Flags().StringVar( ¶ms.oldPassphrase, - OldPassphraseFlag, + oldPassphraseFlag, "", "old passphrase to unlock account", ) @@ -59,16 +59,13 @@ func runPreRun(cmd *cobra.Command, _ []string) error { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - scryptN := keystore.LightScryptN - scryptP := keystore.LightScryptP - - ks := keystore.NewKeyStore(keystore.DefaultStorage, scryptN, scryptP, hclog.NewNullLogger()) + ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) if !ks.HasAddress(params.address) { outputter.SetError(fmt.Errorf("this address doesn't exist")) } else { if err := ks.Update(accounts.Account{Address: params.address}, params.passphrase, params.oldPassphrase); err != nil { - outputter.SetError(fmt.Errorf("can't update account: %s", err)) + outputter.SetError(fmt.Errorf("can't update account: %w", err)) } } } diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index 5b153985a7..c06a5617a2 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -23,12 +23,12 @@ func (p *Personal) ListAccounts() ([]types.Address, Error) { func (p *Personal) NewAccount(password string) (types.Address, error) { ks, err := getKeystore(p.accManager) if err != nil { - return types.Address{}, err + return types.ZeroAddress, err } acc, err := ks.NewAccount(password) if err != nil { - return types.Address{}, fmt.Errorf("can't create new account") + return types.ZeroAddress, fmt.Errorf("can't create new account") } return acc.Address, nil @@ -37,12 +37,12 @@ func (p *Personal) NewAccount(password string) (types.Address, error) { func (p *Personal) ImportRawKey(privKey string, password string) (types.Address, error) { key, err := crypto.HexToECDSA(privKey) if err != nil { - return types.Address{}, err + return types.ZeroAddress, err } ks, err := getKeystore(p.accManager) if err != nil { - return types.Address{}, err + return types.ZeroAddress, err } acc, err := ks.ImportECDSA(key, password) @@ -79,21 +79,21 @@ func (p *Personal) UnlockAccount(addr types.Address, password string, duration u func (s *Personal) LockAccount(addr types.Address) (bool, error) { ks, err := getKeystore(s.accManager) - if err == nil { - if err := ks.Lock(addr); err != nil { - return false, err - } + if err != nil { + return false, err + } - return true, nil + if err := ks.Lock(addr); err != nil { + return false, err } - return false, err + return true, nil } -func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { //nolint:stylecheck +func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { addressRaw, err := crypto.Ecrecover(data, sig) if err != nil { - return types.Address{}, err + return types.ZeroAddress, err } return types.BytesToAddress(addressRaw), nil diff --git a/server/server.go b/server/server.go index e5bf164d63..e95390d38b 100644 --- a/server/server.go +++ b/server/server.go @@ -210,11 +210,6 @@ func NewServer(config *Config) (*Server, error) { m.network = network } - // setup account manager - { - m.accManager = accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: true}, nil) - } - // start blockchain object stateStorage, err := itrie.NewLevelDBStorage(filepath.Join(m.config.DataDir, "trie"), logger) if err != nil { @@ -317,6 +312,11 @@ func NewServer(config *Config) (*Server, error) { signer := crypto.NewSigner(config.Chain.Params.Forks.At(0), uint64(m.config.Chain.Params.ChainID)) + // setup account manager + { + m.accManager = accounts.NewManager(m.logger) + } + // create storage instance for blockchain var db *storagev2.Storage { From 9b4172ee94c936a2b73762256e85dcadeeeca1ba Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Wed, 12 Jun 2024 14:12:41 +0200 Subject: [PATCH 40/69] forks in time fix --- accounts/keystore/keystore.go | 10 ++++++---- accounts/keystore/keystore_fuzz_test.go | 3 ++- accounts/keystore/keystore_test.go | 3 ++- command/accounts/create/create.go | 3 ++- command/accounts/insert/insert.go | 13 ++----------- command/accounts/update/update.go | 3 ++- crypto/crypto.go | 8 -------- 7 files changed, 16 insertions(+), 27 deletions(-) diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 682a1e9aaa..10d93840fc 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -12,6 +12,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" @@ -45,6 +46,7 @@ type KeyStore struct { updateFeed event.Feed // Event feed to notify wallet additions/removals updateScope event.SubscriptionScope // Subscription scope tracking current live listeners updating bool // Whether the event notification loop is running + config *chain.ForksInTime mu sync.RWMutex importMu sync.Mutex // Import Mutex locks the import to prevent two insertions from racing @@ -55,12 +57,12 @@ type unlocked struct { abort chan struct{} } -func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { +func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger, config chain.ForksInTime) *KeyStore { var ks *KeyStore ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} - ks.init(keyDir, hclog.NewNullLogger()) // TO DO LOGGER + ks.init(keyDir, logger) return ks } @@ -244,7 +246,7 @@ func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *b return nil, ErrLocked } - signer := crypto.LatestSignerForChainID(chainID.Uint64()) + signer := crypto.NewSigner(*ks.config, chainID.Uint64()) return signer.SignTx(tx, unlockedKey.PrivateKey) } @@ -270,7 +272,7 @@ func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, defer zeroKey(key.PrivateKey) - signer := crypto.LatestSignerForChainID(chainID.Uint64()) + signer := crypto.NewSigner(*ks.config, chainID.Uint64()) return signer.SignTx(tx, key.PrivateKey) } diff --git a/accounts/keystore/keystore_fuzz_test.go b/accounts/keystore/keystore_fuzz_test.go index a793ead816..37cbcbc963 100644 --- a/accounts/keystore/keystore_fuzz_test.go +++ b/accounts/keystore/keystore_fuzz_test.go @@ -3,12 +3,13 @@ package keystore import ( "testing" + "github.com/0xPolygon/polygon-edge/chain" "github.com/hashicorp/go-hclog" ) func FuzzPassword(f *testing.F) { f.Fuzz(func(t *testing.T, password string) { - ks := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger()) + ks := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) a, err := ks.NewAccount(password) if err != nil { diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 40ec653c77..452d4e9e09 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -8,6 +8,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" @@ -429,5 +430,5 @@ func tmpKeyStore(t *testing.T) (string, *KeyStore) { d := t.TempDir() - return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) + return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) } diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index 1a7fb14fe5..513a7352d1 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/0xPolygon/polygon-edge/accounts/keystore" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" "github.com/hashicorp/go-hclog" @@ -40,7 +41,7 @@ func setFlags(cmd *cobra.Command) { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) + ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) account, err := ks.NewAccount(params.passphrase) if err != nil { diff --git a/command/accounts/insert/insert.go b/command/accounts/insert/insert.go index 282cfaedcc..1fe04f8a3b 100644 --- a/command/accounts/insert/insert.go +++ b/command/accounts/insert/insert.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" - "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/keystore" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" "github.com/0xPolygon/polygon-edge/crypto" @@ -57,9 +57,7 @@ func runPreRun(cmd *cobra.Command, _ []string) error { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - am := accounts.NewManager(nil) - - am.AddBackend(keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger())) + ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) if params.privateKey == "" { outputter.SetError(errors.New("private key empty")) @@ -75,13 +73,6 @@ func runCommand(cmd *cobra.Command, _ []string) { outputter.SetError(fmt.Errorf("failed to initialize private key: %w", err)) } - backends := am.Backends(keystore.KeyStoreType) - if len(backends) == 0 { - outputter.SetError(fmt.Errorf("keystore is not available")) - } - - ks := backends[0].(*keystore.KeyStore) //nolint:forcetypeassert - acct, err := ks.ImportECDSA(privKey, params.passphrase) if err != nil { outputter.SetError(fmt.Errorf("cannot import private key: %w", err)) diff --git a/command/accounts/update/update.go b/command/accounts/update/update.go index 397b2596a3..7d4c293c52 100644 --- a/command/accounts/update/update.go +++ b/command/accounts/update/update.go @@ -5,6 +5,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/keystore" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" "github.com/hashicorp/go-hclog" @@ -59,7 +60,7 @@ func runPreRun(cmd *cobra.Command, _ []string) error { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) + ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) if !ks.HasAddress(params.address) { outputter.SetError(fmt.Errorf("this address doesn't exist")) diff --git a/crypto/crypto.go b/crypto/crypto.go index b09b768de0..f724aaae1d 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -363,14 +363,6 @@ func convertToBtcPrivKey(priv *ecdsa.PrivateKey) (*btcec.PrivateKey, error) { return &btcPriv, nil } -func LatestSignerForChainID(chaidID uint64) TxSigner { - if chaidID == 0 { // TO DO maybe wrong - return NewHomesteadSigner() - } - - return NewLondonSigner(chaidID) -} - func DToECDSA(d []byte, strict bool) (*ecdsa.PrivateKey, error) { priv := new(ecdsa.PrivateKey) priv.PublicKey.Curve = btcec.S256() From cf0ec7a8f7c91a74eb2736f2ed41a39ecfcb42de Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 13 Jun 2024 16:07:25 +0200 Subject: [PATCH 41/69] in-house pub sub --- accounts/accounts.go | 6 +- accounts/event/event_handler.go | 62 +++++ accounts/event/event_handler_test.go | 135 ++++++++++ accounts/event/feed.go | 222 ---------------- accounts/event/feed_test.go | 370 --------------------------- accounts/event/subscription.go | 145 ----------- accounts/keystore/keystore.go | 44 +--- accounts/keystore/keystore_test.go | 88 ++----- accounts/keystore/passphrase_test.go | 1 + accounts/manager.go | 93 ++++--- accounts/pubsub/pub_sub.go | 47 ---- command/accounts/create/create.go | 3 +- command/accounts/insert/insert.go | 3 +- command/accounts/update/update.go | 3 +- 14 files changed, 291 insertions(+), 931 deletions(-) create mode 100644 accounts/event/event_handler.go create mode 100644 accounts/event/event_handler_test.go delete mode 100644 accounts/event/feed.go delete mode 100644 accounts/event/feed_test.go delete mode 100644 accounts/event/subscription.go delete mode 100644 accounts/pubsub/pub_sub.go diff --git a/accounts/accounts.go b/accounts/accounts.go index 4726d95bce..f978bb4d7c 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -135,6 +135,10 @@ type WalletEvent struct { Kind WalletEventType // Event type that happened in the system } +func (WalletEvent) Type() event.EventType { + return event.WalletEventType +} + type Backend interface { // Wallets retrieves the list of wallets the backend is currently aware of. // @@ -150,5 +154,5 @@ type Backend interface { // Subscribe creates an async subscription to receive notifications when the // backend detects the arrival or departure of a wallet. - Subscribe(sink chan<- WalletEvent) event.Subscription + Subscribe(eventHandler *event.EventHandler) } diff --git a/accounts/event/event_handler.go b/accounts/event/event_handler.go new file mode 100644 index 0000000000..325707abff --- /dev/null +++ b/accounts/event/event_handler.go @@ -0,0 +1,62 @@ +package event + +import "sync" + +type EventHandler struct { + subscribers map[string][]chan Event + mu sync.RWMutex +} + +func NewEventHandler() *EventHandler { + return &EventHandler{ + subscribers: make(map[string][]chan Event), + } +} + +func (ps *EventHandler) Subscribe(topic string, event chan Event) { + ps.mu.Lock() + defer ps.mu.Unlock() + + ps.subscribers[topic] = append(ps.subscribers[topic], event) +} + +func (ps *EventHandler) Publish(topic string, msg Event) { + ps.mu.RLock() + defer ps.mu.RUnlock() + + if chans, ok := ps.subscribers[topic]; ok { + for _, ch := range chans { + ch <- msg + } + } +} + +func (ps *EventHandler) Unsubscribe(topic string, sub <-chan Event) { + ps.mu.Lock() + defer ps.mu.Unlock() + + if chans, ok := ps.subscribers[topic]; ok { + for i, ch := range chans { + if ch == sub { + ps.subscribers[topic] = append(chans[:i], chans[i+1:]...) + close(ch) + + break + } + } + } +} + +type EventType byte + +const ( + WalletEventType = 0x01 + + NewBackendType = 0x02 + + TestEventType = 0x03 +) + +type Event interface { + Type() EventType +} diff --git a/accounts/event/event_handler_test.go b/accounts/event/event_handler_test.go new file mode 100644 index 0000000000..88e3a15986 --- /dev/null +++ b/accounts/event/event_handler_test.go @@ -0,0 +1,135 @@ +package event + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +type TestEvent struct { + Data string +} + +func (te TestEvent) Type() EventType { + return TestEventType +} + +func TestSubscribeAndPublish(t *testing.T) { + ps := NewEventHandler() + + var err error + + topic := "testTopic" + + eventChan := make(chan Event, 1) + + ps.Subscribe(topic, eventChan) + + event := TestEvent{Data: "testEvent"} + + ps.Publish(topic, event) + + select { + case receivedEvent := <-eventChan: + require.Equal(t, event, receivedEvent) + case <-time.After(1 * time.Second): + err = errors.New("did not receive event") + } + + require.NoError(t, err) +} + +func TestUnsubscribe(t *testing.T) { + ps := NewEventHandler() + + var err error + + topic := "testTopic" + + eventChan := make(chan Event, 1) + + ps.Subscribe(topic, eventChan) + + ps.Unsubscribe(topic, eventChan) + + ps.Publish(topic, TestEvent{Data: "testEvent"}) + + select { + case _, ok := <-eventChan: + if ok { + errors.New("expected channel to be closed") + } + default: + } + + require.NoError(t, err) +} + +func TestConcurrentAccess(t *testing.T) { + ps := NewEventHandler() + topic := "testTopic" + eventChan := make(chan Event, 1) + ps.Subscribe(topic, eventChan) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + ps.Publish(topic, TestEvent{Data: "testEvent"}) + }() + + go func() { + defer wg.Done() + ps.Unsubscribe(topic, eventChan) + }() + + wg.Wait() + + select { + case <-eventChan: + // We don't care about the result, just checking for race conditions + case <-time.After(1 * time.Second): + } +} + +func TestMultipleSubscribers(t *testing.T) { + ps := NewEventHandler() + + var err error + + topic := "testTopic" + + eventChan1 := make(chan Event, 1) + + eventChan2 := make(chan Event, 1) + + ps.Subscribe(topic, eventChan1) + + ps.Subscribe(topic, eventChan2) + + event := TestEvent{Data: "testEvent"} + + ps.Publish(topic, event) + + select { + case receivedEvent := <-eventChan1: + require.Equal(t, event, receivedEvent) + case <-time.After(1 * time.Second): + err = errors.New("did not receive event on eventChan1") + } + + require.NoError(t, err) + + select { + case receivedEvent := <-eventChan2: + require.Equal(t, event, receivedEvent) + case <-time.After(1 * time.Second): + err = errors.New("did not receive event on eventChan2") + } + + require.NoError(t, err) +} diff --git a/accounts/event/feed.go b/accounts/event/feed.go deleted file mode 100644 index 4598f8b6da..0000000000 --- a/accounts/event/feed.go +++ /dev/null @@ -1,222 +0,0 @@ -package event - -import ( - "errors" - "reflect" - "sync" -) - -var errBadChannel = errors.New("event: Subscribe argument does not have sendable channel type") - -// Feed implements one-to-many subscriptions where the carrier of events is a channel. -// Values sent to a Feed are delivered to all subscribed channels simultaneously. -// -// Feeds can only be used with a single type. The type is determined by the first Send or -// Subscribe operation. Subsequent calls to these methods panic if the type does not -// match. -// -// The zero value is ready to use. -type Feed struct { - once sync.Once // ensures that init only runs once - sendLock chan struct{} // sendLock has a one-element buffer and is empty when held.It protects sendCases. - removeSub chan interface{} // interrupts Send - sendCases caseList // the active set of select cases used by Send - - // The inbox holds newly subscribed channels until they are added to sendCases. - mu sync.Mutex - inbox caseList - etype reflect.Type -} - -// This is the index of the first actual subscription channel in sendCases. -// sendCases[0] is a SelectRecv case for the removeSub channel. -const firstSubSendCase = 1 - -type feedTypeError struct { - got, want reflect.Type - op string -} - -func (e feedTypeError) Error() string { - return "event: wrong type in " + e.op + " got " + e.got.String() + ", want " + e.want.String() -} - -func (f *Feed) init(etype reflect.Type) { - f.etype = etype - f.removeSub = make(chan interface{}) - f.sendLock = make(chan struct{}, 1) - f.sendLock <- struct{}{} - f.sendCases = caseList{{Chan: reflect.ValueOf(f.removeSub), Dir: reflect.SelectRecv}} -} - -// Subscribe adds a channel to the feed. Future sends will be delivered on the channel -// until the subscription is canceled. All channels added must have the same element type. -// -// The channel should have ample buffer space to avoid blocking other subscribers. -// Slow subscribers are not dropped. -func (f *Feed) Subscribe(channel interface{}) Subscription { - chanval := reflect.ValueOf(channel) - chantyp := chanval.Type() - - if chantyp.Kind() != reflect.Chan || chantyp.ChanDir()&reflect.SendDir == 0 { - panic(errBadChannel) //nolint:gocritic - } - - sub := &feedSub{feed: f, channel: chanval, err: make(chan error, 1)} - - f.once.Do(func() { f.init(chantyp.Elem()) }) - - if f.etype != chantyp.Elem() { - panic(feedTypeError{op: "Subscribe", got: chantyp, want: reflect.ChanOf(reflect.SendDir, f.etype)}) //nolint:gocritic - } - - f.mu.Lock() - defer f.mu.Unlock() - // Add the select case to the inbox. - // The next Send will add it to f.sendCases. - cas := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval} - f.inbox = append(f.inbox, cas) - - return sub -} - -func (f *Feed) remove(sub *feedSub) { - // Delete from inbox first, which covers channels - // that have not been added to f.sendCases yet. - ch := sub.channel.Interface() - - f.mu.Lock() - index := f.inbox.find(ch) - - if index != -1 { - f.inbox = f.inbox.delete(index) - f.mu.Unlock() - - return - } - - f.mu.Unlock() - - select { - case f.removeSub <- ch: - // Send will remove the channel from f.sendCases. - case <-f.sendLock: - // No Send is in progress, delete the channel now that we have the send lock. - f.sendCases = f.sendCases.delete(f.sendCases.find(ch)) - f.sendLock <- struct{}{} - } -} - -// Send delivers to all subscribed channels simultaneously. -// It returns the number of subscribers that the value was sent to. -func (f *Feed) Send(value interface{}) (nsent int) { - rvalue := reflect.ValueOf(value) - - f.once.Do(func() { f.init(rvalue.Type()) }) - - if f.etype != rvalue.Type() { - panic(feedTypeError{op: "Send", got: rvalue.Type(), want: f.etype}) //nolint:gocritic - } - - <-f.sendLock - - // Add new cases from the inbox after taking the send lock. - f.mu.Lock() - f.sendCases = append(f.sendCases, f.inbox...) - f.inbox = nil - f.mu.Unlock() - - // Set the sent value on all channels. - for i := firstSubSendCase; i < len(f.sendCases); i++ { - f.sendCases[i].Send = rvalue - } - - // Send until all channels except removeSub have been chosen. 'cases' tracks a prefix - // of sendCases. When a send succeeds, the corresponding case moves to the end of - // 'cases' and it shrinks by one element. - cases := f.sendCases - - for { - // Fast path: try sending without blocking before adding to the select set. - // This should usually succeed if subscribers are fast enough and have free - // buffer space. - for i := firstSubSendCase; i < len(cases); i++ { - if cases[i].Chan.TrySend(rvalue) { - nsent++ - cases = cases.deactivate(i) - i-- - } - } - - if len(cases) == firstSubSendCase { - break - } - // Select on all the receivers, waiting for them to unblock. - chosen, recv, _ := reflect.Select(cases) - - if chosen == 0 /* <-f.removeSub */ { - index := f.sendCases.find(recv.Interface()) - f.sendCases = f.sendCases.delete(index) - - if index >= 0 && index < len(cases) { - // Shrink 'cases' too because the removed case was still active. - cases = f.sendCases[:len(cases)-1] - } - } else { - cases = cases.deactivate(chosen) - nsent++ - } - } - - // Forget about the sent value and hand off the send lock. - for i := firstSubSendCase; i < len(f.sendCases); i++ { - f.sendCases[i].Send = reflect.Value{} - } - f.sendLock <- struct{}{} - - return nsent -} - -type feedSub struct { - feed *Feed - channel reflect.Value - errOnce sync.Once - err chan error -} - -func (sub *feedSub) Unsubscribe() { - sub.errOnce.Do(func() { - sub.feed.remove(sub) - close(sub.err) - }) -} - -func (sub *feedSub) Err() <-chan error { - return sub.err -} - -type caseList []reflect.SelectCase - -// find returns the index of a case containing the given channel. -func (cs caseList) find(channel interface{}) int { - for i, cas := range cs { - if cas.Chan.Interface() == channel { - return i - } - } - - return -1 -} - -// delete removes the given case from cs. -func (cs caseList) delete(index int) caseList { - return append(cs[:index], cs[index+1:]...) -} - -// deactivate moves the case at index into the non-accessible portion of the cs slice. -func (cs caseList) deactivate(index int) caseList { - last := len(cs) - 1 - cs[index], cs[last] = cs[last], cs[index] - - return cs[:last] -} diff --git a/accounts/event/feed_test.go b/accounts/event/feed_test.go deleted file mode 100644 index f6a8ebc43f..0000000000 --- a/accounts/event/feed_test.go +++ /dev/null @@ -1,370 +0,0 @@ -package event - -import ( - "errors" - "fmt" - "reflect" - "sync" - "testing" - "time" -) - -func TestFeedPanics(t *testing.T) { - { - var f Feed - - f.Send(2) - - want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(0)} - - if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil { - t.Error(err) - } - } - { - var f Feed - - ch := make(chan int) - f.Subscribe(ch) - - want := feedTypeError{op: "Send", got: reflect.TypeOf(uint64(0)), want: reflect.TypeOf(0)} - - if err := checkPanic(want, func() { f.Send(uint64(2)) }); err != nil { - t.Error(err) - } - } - { - var f Feed - - f.Send(2) - - want := feedTypeError{op: "Subscribe", got: reflect.TypeOf(make(chan uint64)), want: reflect.TypeOf(make(chan<- int))} - - if err := checkPanic(want, func() { f.Subscribe(make(chan uint64)) }); err != nil { - t.Error(err) - } - } - { - var f Feed - - if err := checkPanic(errBadChannel, func() { f.Subscribe(make(<-chan int)) }); err != nil { - t.Error(err) - } - } - { - var f Feed - - if err := checkPanic(errBadChannel, func() { f.Subscribe(0) }); err != nil { - t.Error(err) - } - } -} - -func checkPanic(want error, fn func()) (err error) { - defer func() { - panicErr := recover() - if panicErr == nil { - err = errors.New("didn't panic") - } else if !reflect.DeepEqual(panicErr, want) { - err = fmt.Errorf("panicked with wrong error: got %v, want %w", panicErr, want) - } - }() - - fn() - - return nil -} - -func TestFeed(t *testing.T) { - var feed Feed - - var done, subscribed sync.WaitGroup - - subscriber := func(i int) { - defer done.Done() - - subchan := make(chan int) - sub := feed.Subscribe(subchan) - timeout := time.NewTimer(2 * time.Second) - - defer timeout.Stop() - - subscribed.Done() - - select { - case v := <-subchan: - if v != 1 { - t.Errorf("%d: received value %d, want 1", i, v) - } - case <-timeout.C: - t.Errorf("%d: receive timeout", i) - } - - sub.Unsubscribe() - select { - case _, ok := <-sub.Err(): - if ok { - t.Errorf("%d: error channel not closed after unsubscribe", i) - } - case <-timeout.C: - t.Errorf("%d: unsubscribe timeout", i) - } - } - - const n = 1000 - - done.Add(n) - subscribed.Add(n) - - for i := 0; i < n; i++ { - go subscriber(i) - } - - subscribed.Wait() - - if nsent := feed.Send(1); nsent != n { - t.Errorf("first send delivered %d times, want %d", nsent, n) - } - - if nsent := feed.Send(2); nsent != 0 { - t.Errorf("second send delivered %d times, want 0", nsent) - } - - done.Wait() -} - -func TestFeedSubscribeSameChannel(t *testing.T) { - var ( - feed Feed - done sync.WaitGroup - ch = make(chan int) - sub1 = feed.Subscribe(ch) - sub2 = feed.Subscribe(ch) - _ = feed.Subscribe(ch) - ) - - expectSends := func(value, n int) { - if nsent := feed.Send(value); nsent != n { - t.Errorf("send delivered %d times, want %d", nsent, n) - } - - done.Done() - } - - expectRecv := func(wantValue, n int) { - for i := 0; i < n; i++ { - if v := <-ch; v != wantValue { - t.Errorf("received %d, want %d", v, wantValue) - } - } - } - - done.Add(1) - - go expectSends(1, 3) - - expectRecv(1, 3) - done.Wait() - - sub1.Unsubscribe() - - done.Add(1) - - go expectSends(2, 2) - - expectRecv(2, 2) - done.Wait() - - sub2.Unsubscribe() - - done.Add(1) - - go expectSends(3, 1) - - expectRecv(3, 1) - done.Wait() -} - -func TestFeedSubscribeBlockedPost(t *testing.T) { - var ( - feed Feed - nsends = 2000 - ch1 = make(chan int) - ch2 = make(chan int) - wg sync.WaitGroup - ) - - defer wg.Wait() - - feed.Subscribe(ch1) - wg.Add(nsends) - - for i := 0; i < nsends; i++ { - go func() { - feed.Send(99) - wg.Done() - }() - } - - sub2 := feed.Subscribe(ch2) - - defer sub2.Unsubscribe() - - // We're done when ch1 has received N times. - // The number of receives on ch2 depends on scheduling. - for i := 0; i < nsends; { - select { - case <-ch1: - i++ - case <-ch2: - } - } -} - -func TestFeedUnsubscribeBlockedPost(t *testing.T) { - var ( - feed Feed - nsends = 200 - chans = make([]chan int, 2000) - subs = make([]Subscription, len(chans)) - bchan = make(chan int) - bsub = feed.Subscribe(bchan) - wg sync.WaitGroup - ) - - for i := range chans { - chans[i] = make(chan int, nsends) - } - - // Queue up some Sends. None of these can make progress while bchan isn't read. - wg.Add(nsends) - - for i := 0; i < nsends; i++ { - go func() { - feed.Send(99) - wg.Done() - }() - } - // Subscribe the other channels. - for i, ch := range chans { - subs[i] = feed.Subscribe(ch) - } - // Unsubscribe them again. - for _, sub := range subs { - sub.Unsubscribe() - } - // Unblock the Sends. - bsub.Unsubscribe() - wg.Wait() -} - -// Checks that unsubscribing a channel during Send works even if that -// channel has already been sent on. -func TestFeedUnsubscribeSentChan(t *testing.T) { - var ( - feed Feed - ch1 = make(chan int) - ch2 = make(chan int) - sub1 = feed.Subscribe(ch1) - sub2 = feed.Subscribe(ch2) - wg sync.WaitGroup - ) - - defer sub2.Unsubscribe() - - wg.Add(1) - - go func() { - feed.Send(0) - wg.Done() - }() - - // Wait for the value on ch1. - <-ch1 - // Unsubscribe ch1, removing it from the send cases. - sub1.Unsubscribe() - - // Receive ch2, finishing Send. - <-ch2 - wg.Wait() - - // Send again. This should send to ch2 only, so the wait group will unblock - // as soon as a value is received on ch2. - wg.Add(1) - - go func() { - feed.Send(0) - wg.Done() - }() - - <-ch2 - wg.Wait() -} - -func TestFeedUnsubscribeFromInbox(t *testing.T) { - var ( - feed Feed - ch1 = make(chan int) - ch2 = make(chan int) - sub1 = feed.Subscribe(ch1) - sub2 = feed.Subscribe(ch1) - sub3 = feed.Subscribe(ch2) - ) - - if len(feed.inbox) != 3 { - t.Errorf("inbox length != 3 after subscribe") - } - - if len(feed.sendCases) != 1 { - t.Errorf("sendCases is non-empty after unsubscribe") - } - - sub1.Unsubscribe() - sub2.Unsubscribe() - sub3.Unsubscribe() - - if len(feed.inbox) != 0 { - t.Errorf("inbox is non-empty after unsubscribe") - } - - if len(feed.sendCases) != 1 { - t.Errorf("sendCases is non-empty after unsubscribe") - } -} - -func BenchmarkFeedSend1000(b *testing.B) { - var ( - done sync.WaitGroup - feed Feed - nsubs = 1000 - ) - - subscriber := func(ch <-chan int) { - for i := 0; i < b.N; i++ { - <-ch - } - - done.Done() - } - - done.Add(nsubs) - - for i := 0; i < nsubs; i++ { - ch := make(chan int, 200) - feed.Subscribe(ch) - - go subscriber(ch) - } - - // The actual benchmark. - b.ResetTimer() - - for i := 0; i < b.N; i++ { - if feed.Send(i) != nsubs { - panic("wrong number of sends") //nolint:gocritic - } - } - - b.StopTimer() - done.Wait() -} diff --git a/accounts/event/subscription.go b/accounts/event/subscription.go deleted file mode 100644 index b7813fa6be..0000000000 --- a/accounts/event/subscription.go +++ /dev/null @@ -1,145 +0,0 @@ -package event - -import ( - "sync" -) - -// The error channel is closed when the subscription ends successfully (i.e. when the -// source of events is closed). It is also closed when Unsubscribe is called. -// -// The Unsubscribe method cancels the sending of events. You must call Unsubscribe in all -// cases to ensure that resources related to the subscription are released. It can be -// called any number of times. -type Subscription interface { - Err() <-chan error // returns the error channel - Unsubscribe() // cancels sending of events, closing the error channel -} - -func NewSubscription(producer func(<-chan struct{}) error) Subscription { - s := &funcSub{unsub: make(chan struct{}), err: make(chan error, 1)} - go func() { - defer close(s.err) - - err := producer(s.unsub) - s.mu.Lock() - - defer s.mu.Unlock() - - if !s.unsubscribed { - if err != nil { - s.err <- err - } - - s.unsubscribed = true - } - }() - - return s -} - -type funcSub struct { - unsub chan struct{} - err chan error - mu sync.Mutex - unsubscribed bool -} - -func (s *funcSub) Unsubscribe() { - s.mu.Lock() - - if s.unsubscribed { - s.mu.Unlock() - - return - } - - s.unsubscribed = true - - close(s.unsub) - - s.mu.Unlock() - // Wait for producer shutdown. - <-s.err -} - -func (s *funcSub) Err() <-chan error { - return s.err -} - -// SubscriptionScope provides a facility to unsubscribe multiple subscriptions at once. -// -// For code that handle more than one subscription, a scope can be used to conveniently -// unsubscribe all of them with a single call. The example demonstrates a typical use in a -// larger program. -// -// The zero value is ready to use. -type SubscriptionScope struct { - mu sync.Mutex - subs map[*scopeSub]struct{} - closed bool -} - -type scopeSub struct { - sc *SubscriptionScope - s Subscription -} - -// Track starts tracking a subscription. If the scope is closed, Track returns nil. The -// returned subscription is a wrapper. Unsubscribing the wrapper removes it from the -// scope. -func (sc *SubscriptionScope) Track(s Subscription) Subscription { - sc.mu.Lock() - defer sc.mu.Unlock() - - if sc.closed { - return nil - } - - if sc.subs == nil { - sc.subs = make(map[*scopeSub]struct{}) - } - - ss := &scopeSub{sc, s} - sc.subs[ss] = struct{}{} - - return ss -} - -// Close calls Unsubscribe on all tracked subscriptions and prevents further additions to -// the tracked set. Calls to Track after Close return nil. -func (sc *SubscriptionScope) Close() { - sc.mu.Lock() - defer sc.mu.Unlock() - - if sc.closed { - return - } - - sc.closed = true - - for s := range sc.subs { - s.s.Unsubscribe() - } - - sc.subs = nil -} - -// Count returns the number of tracked subscriptions. -// It is meant to be used for debugging. -func (sc *SubscriptionScope) Count() int { - sc.mu.Lock() - defer sc.mu.Unlock() - - return len(sc.subs) -} - -func (s *scopeSub) Unsubscribe() { - s.s.Unsubscribe() - s.sc.mu.Lock() - defer s.sc.mu.Unlock() - delete(s.sc.subs, s) -} - -func (s *scopeSub) Err() <-chan error { - return s.s.Err() -} diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 10d93840fc..aa8f6fbc8a 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -42,11 +42,9 @@ type KeyStore struct { changes chan struct{} // Channel receiving change notifications from the cache unlocked map[types.Address]*unlocked // Currently unlocked account (decrypted private keys) - wallets []accounts.Wallet // Wallet wrappers around the individual key files - updateFeed event.Feed // Event feed to notify wallet additions/removals - updateScope event.SubscriptionScope // Subscription scope tracking current live listeners - updating bool // Whether the event notification loop is running - config *chain.ForksInTime + wallets []accounts.Wallet // Wallet wrappers around the individual key files + config *chain.ForksInTime + eventHandler *event.EventHandler mu sync.RWMutex importMu sync.Mutex // Import Mutex locks the import to prevent two insertions from racing @@ -84,6 +82,8 @@ func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { for i := 0; i < len(accs); i++ { ks.wallets[i] = &keyStoreWallet{account: accs[i], keyStore: ks} } + + go ks.updater() } func (ks *KeyStore) Wallets() []accounts.Wallet { @@ -157,23 +157,18 @@ func (ks *KeyStore) refreshWallets() { ks.mu.Unlock() - for _, event := range events { - ks.updateFeed.Send(event) + if ks.eventHandler != nil { + for _, event := range events { + ks.eventHandler.Publish(accounts.WalletEventKey, event) + } } } -func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { +func (ks *KeyStore) Subscribe(eventHandler *event.EventHandler) { ks.mu.Lock() defer ks.mu.Unlock() - sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink)) - - if !ks.updating { - ks.updating = true - go ks.updater() - } - - return sub + ks.eventHandler = eventHandler } func (ks *KeyStore) updater() { @@ -184,16 +179,6 @@ func (ks *KeyStore) updater() { } ks.refreshWallets() - - ks.mu.Lock() - if ks.updateScope.Count() == 0 { - ks.updating = false - - ks.mu.Unlock() - - return - } - ks.mu.Unlock() } } @@ -438,10 +423,3 @@ func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (account return a, nil } - -func (ks *KeyStore) isUpdating() bool { - ks.mu.RLock() - defer ks.mu.RUnlock() - - return ks.updating -} diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 452d4e9e09..7777f6acaf 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -8,6 +8,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" @@ -202,68 +203,6 @@ func TestSignRace(t *testing.T) { t.Errorf("Account did not lock within the timeout") } -// waitForKsUpdating waits until the updating-status of the ks reaches the -// desired wantStatus. -// It waits for a maximum time of maxTime, and returns false if it does not -// finish in time -func waitForKsUpdating(t *testing.T, ks *KeyStore, wantStatus bool, maxTime time.Duration) bool { - t.Helper() - // Wait max 250 ms, then return false - for t0 := time.Now().UTC(); time.Since(t0) < maxTime; { - if ks.isUpdating() == wantStatus { - return true - } - - time.Sleep(25 * time.Millisecond) - } - - return false -} - -// 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) { - t.Parallel() - // Create a temporary keystore to test with - _, ks := tmpKeyStore(t) - - // Ensure that the notification updater is not running yet - time.Sleep(250 * time.Millisecond) - - if ks.isUpdating() { - 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) - - if !waitForKsUpdating(t, ks, true, 250*time.Millisecond) { - t.Errorf("sub %d: wallet notifier not running after subscription", i) - } - } - // Close all but one sub - for i := 0; i < len(subs)-1; i++ { - // Close an existing subscription - subs[i].Unsubscribe() - } - // Check that it is still running - time.Sleep(250 * time.Millisecond) - - if !ks.isUpdating() { - t.Fatal("event notifier stopped prematurely") - } - // Unsubscribe the last one and ensure the updater terminates eventually. - subs[len(subs)-1].Unsubscribe() - - if !waitForKsUpdating(t, ks, false, 4*time.Second) { - t.Errorf("wallet notifier didn't terminate after unsubscribe") - } -} - type walletEvent struct { accounts.WalletEvent a accounts.Account @@ -274,24 +213,27 @@ type walletEvent struct { func TestWalletNotifications(t *testing.T) { t.Parallel() _, ks := tmpKeyStore(t) + eventHandler := event.NewEventHandler() + ks.Subscribe(eventHandler) // Subscribe to the wallet feed and collect events. var ( //nolint:prealloc events []walletEvent - updates = make(chan accounts.WalletEvent) - sub = ks.Subscribe(updates) + updates = make(chan event.Event) + end = make(chan interface{}) ) - defer sub.Unsubscribe() + ks.eventHandler.Subscribe(accounts.WalletEventKey, updates) + + defer eventHandler.Unsubscribe(accounts.WalletEventKey, updates) go func() { for { select { case ev := <-updates: - events = append(events, walletEvent{ev, ev.Wallet.Accounts()[0]}) - case <-sub.Err(): - close(updates) - + events = append(events, walletEvent{ev.(accounts.WalletEvent), ev.(accounts.WalletEvent).Wallet.Accounts()[0]}) + case <-end: + eventHandler.Unsubscribe(accounts.WalletEventKey, updates) return } } @@ -332,15 +274,17 @@ func TestWalletNotifications(t *testing.T) { } } - // Shut down the event collector and check events. - sub.Unsubscribe() + end <- new(interface{}) for ev := range updates { - events = append(events, walletEvent{ev, ev.Wallet.Accounts()[0]}) + events = append(events, walletEvent{ev.(accounts.WalletEvent), ev.(accounts.WalletEvent).Wallet.Accounts()[0]}) } checkAccounts(t, live, ks.Wallets()) checkEvents(t, wantEvents, events) + + // Shut down the event collector and check events. + eventHandler.Unsubscribe(accounts.WalletEventKey, updates) } // TestImportECDSA tests the import functionality of a keystore. diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 60de37d74b..78e0fa6b51 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -203,6 +203,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { func LoadJSON(t *testing.T, file string, val interface{}) error { t.Helper() + content, err := os.ReadFile(file) if err != nil { return err diff --git a/accounts/manager.go b/accounts/manager.go index 48f8e896e0..04b20ac2e2 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -9,7 +9,11 @@ import ( "github.com/hashicorp/go-hclog" ) -const managerSubBufferSize = 50 +const ( + managerSubBufferSize = 50 + + WalletEventKey = "walletEvent" +) type newBackendEvent struct { backend Backend @@ -17,19 +21,22 @@ type newBackendEvent struct { processed chan struct{} } +func (newBackendEvent) Type() event.EventType { + return event.NewBackendType +} + type Manager struct { backends map[reflect.Type][]Backend - updaters []event.Subscription - updates chan WalletEvent - newBackends chan newBackendEvent + updates chan event.Event + newBackends chan event.Event wallets []Wallet - feed event.Feed - quit chan chan error logger hclog.Logger + eventHandler *event.EventHandler + term chan struct{} lock sync.RWMutex } @@ -41,26 +48,31 @@ func NewManager(logger hclog.Logger, backends ...Backend) *Manager { wallets = merge(wallets, backend.Wallets()...) } - updates := make(chan WalletEvent, managerSubBufferSize) + updates := make(chan event.Event, managerSubBufferSize) - subs := make([]event.Subscription, len(backends)) + newBackends := make(chan event.Event) - for i, backend := range backends { - subs[i] = backend.Subscribe(updates) + eventHandler := event.NewEventHandler() + + for _, backend := range backends { + backend.Subscribe(eventHandler) } am := &Manager{ - backends: make(map[reflect.Type][]Backend), - updaters: subs, - updates: updates, - newBackends: make(chan newBackendEvent), - wallets: wallets, - quit: make(chan chan error), - term: make(chan struct{}), + backends: make(map[reflect.Type][]Backend), + updates: updates, + newBackends: newBackends, + wallets: wallets, + quit: make(chan chan error), + term: make(chan struct{}), + eventHandler: eventHandler, } + eventHandler.Subscribe(WalletEventKey, am.updates) + for _, backend := range backends { kind := reflect.TypeOf(backend) + backend.Subscribe(am.eventHandler) am.backends[kind] = append(am.backends[kind], backend) } @@ -92,37 +104,42 @@ func (am *Manager) update() { defer func() { am.lock.Lock() - for _, sub := range am.updaters { - sub.Unsubscribe() - } + am.eventHandler.Unsubscribe(WalletEventKey, am.updates) - am.updaters = nil am.lock.Unlock() }() for { select { - case event := <-am.updates: + case eventChan := <-am.updates: am.lock.Lock() - switch event.Kind { - case WalletArrived: - am.wallets = merge(am.wallets, event.Wallet) - case WalletDropped: - am.wallets = drop(am.wallets, event.Wallet) + + if eventChan.Type() == event.WalletEventType { + walletEvent := eventChan.(WalletEvent) //nolint:forcetypeassert + switch walletEvent.Kind { + case WalletArrived: + am.wallets = merge(am.wallets, walletEvent.Wallet) + case WalletDropped: + am.wallets = drop(am.wallets, walletEvent.Wallet) + } } - am.lock.Unlock() - am.feed.Send(event) - case event := <-am.newBackends: + am.lock.Unlock() + case backendEventChan := <-am.newBackends: am.lock.Lock() - backend := event.backend - am.wallets = merge(am.wallets, backend.Wallets()...) - am.updaters = append(am.updaters, backend.Subscribe(am.updates)) - kind := reflect.TypeOf(backend) - am.backends[kind] = append(am.backends[kind], backend) + if backendEventChan.Type() == event.NewBackendType { + bckEvent := backendEventChan.(newBackendEvent) //nolint:forcetypeassert + backend := bckEvent.backend + am.wallets = merge(am.wallets, backend.Wallets()...) + backend.Subscribe(am.eventHandler) + kind := reflect.TypeOf(backend) + am.backends[kind] = append(am.backends[kind], backend) + am.lock.Unlock() + close(bckEvent.processed) + } + am.lock.Unlock() - close(event.processed) case errc := <-am.quit: errc <- nil @@ -182,8 +199,8 @@ func (am *Manager) Find(account Account) (Wallet, error) { return nil, ErrUnknownAccount } -func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { - return am.feed.Subscribe(sink) +func (am *Manager) Subscribe(eventHandler *event.EventHandler) { + am.eventHandler = eventHandler } func merge(slice []Wallet, wallets ...Wallet) []Wallet { diff --git a/accounts/pubsub/pub_sub.go b/accounts/pubsub/pub_sub.go deleted file mode 100644 index c857c50350..0000000000 --- a/accounts/pubsub/pub_sub.go +++ /dev/null @@ -1,47 +0,0 @@ -package pubsub - -import "sync" - -type PubSub struct { - subscribers map[string][]chan interface{} - mu sync.RWMutex -} - -func NewPubSub() *PubSub { - return &PubSub{ - subscribers: make(map[string][]chan interface{}), - } -} - -func (ps *PubSub) Subscribe(topic string, event chan interface{}) { - ps.mu.Lock() - defer ps.mu.Unlock() - - ps.subscribers[topic] = append(ps.subscribers[topic], event) -} - -func (ps *PubSub) Publish(topic string, msg interface{}) { - ps.mu.RLock() - defer ps.mu.RUnlock() - - if chans, ok := ps.subscribers[topic]; ok { - for _, ch := range chans { - ch <- msg - } - } -} - -func (ps *PubSub) Unsubscribe(topic string, sub <-chan interface{}) { - ps.mu.Lock() - defer ps.mu.Unlock() - - if chans, ok := ps.subscribers[topic]; ok { - for i, ch := range chans { - if ch == sub { - ps.subscribers[topic] = append(chans[:i], chans[i+1:]...) - close(ch) - break - } - } - } -} diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index 513a7352d1..437ea8e70c 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -41,7 +41,8 @@ func setFlags(cmd *cobra.Command) { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + ks := keystore.NewKeyStore(keystore.DefaultStorage, + keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) account, err := ks.NewAccount(params.passphrase) if err != nil { diff --git a/command/accounts/insert/insert.go b/command/accounts/insert/insert.go index 1fe04f8a3b..407a34599b 100644 --- a/command/accounts/insert/insert.go +++ b/command/accounts/insert/insert.go @@ -57,7 +57,8 @@ func runPreRun(cmd *cobra.Command, _ []string) error { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + ks := keystore.NewKeyStore(keystore.DefaultStorage, + keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) if params.privateKey == "" { outputter.SetError(errors.New("private key empty")) diff --git a/command/accounts/update/update.go b/command/accounts/update/update.go index 7d4c293c52..979644170e 100644 --- a/command/accounts/update/update.go +++ b/command/accounts/update/update.go @@ -60,7 +60,8 @@ func runPreRun(cmd *cobra.Command, _ []string) error { func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + ks := keystore.NewKeyStore(keystore.DefaultStorage, + keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) if !ks.HasAddress(params.address) { outputter.SetError(fmt.Errorf("this address doesn't exist")) From 914b9df16e15870eb296ca60d619cb3118a25b57 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 14 Jun 2024 13:19:47 +0200 Subject: [PATCH 42/69] lint and better fork managemt, also interface for manager --- accounts/accounts.go | 99 +++++++++---------------- accounts/event/event_handler.go | 1 + accounts/event/event_handler_test.go | 16 ++-- accounts/keystore/keystore.go | 21 ++++-- accounts/keystore/keystore_fuzz_test.go | 3 +- accounts/keystore/keystore_test.go | 6 +- accounts/keystore/wallet.go | 9 +-- accounts/manager.go | 36 ++++++--- command/accounts/create/create.go | 3 +- command/accounts/insert/insert.go | 3 +- command/accounts/update/update.go | 3 +- jsonrpc/dispatcher.go | 4 +- jsonrpc/eth_endpoint.go | 2 +- jsonrpc/jsonrpc.go | 2 +- jsonrpc/personal_endpoint.go | 8 +- server/server.go | 13 ++-- 16 files changed, 109 insertions(+), 120 deletions(-) diff --git a/accounts/accounts.go b/accounts/accounts.go index f978bb4d7c..b2d27f91ac 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -2,9 +2,10 @@ package accounts import ( "fmt" - "math/big" + "reflect" "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" "golang.org/x/crypto/sha3" ) @@ -17,64 +18,30 @@ type Account struct { // accounts (derived from the same seed). type Wallet interface { // Status returns a textual status to aid the user in the current state of the - // wallet. It also returns an error indicating any failure the wallet might have - // encountered. + // wallet Status() (string, error) - // Open initializes access to a wallet instance. It is not meant to unlock or - // decrypt account keys, rather simply to establish a connection to hardware - // wallets and/or to access derivation seeds. - // - // The passphrase parameter may or may not be used by the implementation of a - // particular wallet instance. The reason there is no passwordless open method - // is to strive towards a uniform wallet handling, oblivious to the different - // backend providers. - // - // Please note, if you open a wallet, you must close it to release any allocated - // resources (especially important when working with hardware wallets). + // Open initializes access to a wallet instance. Open(passphrase string) error // Close releases any resources held by an open wallet instance. Close() error // Accounts retrieves the list of signing accounts the wallet is currently aware - // of. For hierarchical deterministic wallets, the list will not be exhaustive, - // rather only contain the accounts explicitly pinned during account derivation. + // of Accounts() []Account // Contains returns whether an account is part of this particular wallet or not. Contains(account Account) bool // SignData requests the wallet to sign the hash of the given data - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - // - // If the wallet requires additional authentication to sign the request (e.g. - // a password to decrypt the account, or a PIN code to verify the transaction), - // an AuthNeededError instance will be returned, containing infos for the user - // about which fields or actions are needed. The user may retry by providing - // the needed details via SignDataWithPassphrase, or by other means (e.g. unlock - // the account in a keystore). SignData(account Account, mimeType string, data []byte) ([]byte, error) // SignDataWithPassphrase is identical to SignData, but also takes a password - // NOTE: there's a chance that an erroneous call might mistake the two strings, and - // supply password in the mimetype field, or vice versa. Thus, an implementation - // should never echo the mimetype or return the mimetype in the error-response SignDataWithPassphrase(account Account, passphrase, mimeType string, data []byte) ([]byte, error) // SignText requests the wallet to sign the hash of a given piece of data, prefixed // by the Ethereum prefix scheme - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - // - // If the wallet requires additional authentication to sign the request (e.g. - // a password to decrypt the account, or a PIN code to verify the transaction), - // an AuthNeededError instance will be returned, containing infos for the user - // about which fields or actions are needed. The user may retry by providing - // the needed details via SignTextWithPassphrase, or by other means (e.g. unlock - // the account in a keystore). - // // This method should return the signature in 'canonical' format, with v 0 or 1. SignText(account Account, text []byte) ([]byte, error) @@ -82,21 +49,11 @@ type Wallet interface { SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error) // SignTx requests the wallet to sign the given transaction. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - // - // If the wallet requires additional authentication to sign the request (e.g. - // a password to decrypt the account, or a PIN code to verify the transaction), - // an AuthNeededError instance will be returned, containing infos for the user - // about which fields or actions are needed. The user may retry by providing - // the needed details via SignTxWithPassphrase, or by other means (e.g. unlock - // the account in a keystore). - SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + SignTx(account Account, tx *types.Transaction) (*types.Transaction, error) // SignTxWithPassphrase is identical to SignTx, but also takes a password SignTxWithPassphrase(account Account, passphrase string, - tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + tx *types.Transaction) (*types.Transaction, error) } func TextHash(data []byte) []byte { @@ -140,19 +97,35 @@ func (WalletEvent) Type() event.EventType { } type Backend interface { - // Wallets retrieves the list of wallets the backend is currently aware of. - // - // The returned wallets are not opened by default. For software HD wallets this - // means that no base seeds are decrypted, and for hardware wallets that no actual - // connection is established. - // - // The resulting wallet list will be sorted alphabetically based on its internal - // URL assigned by the backend. Since wallets (especially hardware) may come and - // go, the same wallet might appear at a different positions in the list during - // subsequent retrievals. + // Wallets retrieves the list of wallets the backend is currently aware of Wallets() []Wallet - // Subscribe creates an async subscription to receive notifications when the - // backend detects the arrival or departure of a wallet. - Subscribe(eventHandler *event.EventHandler) + // SetEventHandler set eventHandler on backend to push events + SetEventHandler(eventHandler *event.EventHandler) + + // SetManager sets backend manager + SetManager(manager BackendManager) +} + +type BackendManager interface { + // Checks for active forks at current block number and return signer + GetSigner() crypto.TxSigner + + //Close stop updater in manager + Close() error + + // Adds backend to list of backends + AddBackend(backend Backend) + + // Return specific type of backend + Backends(kind reflect.Type) []Backend + + // Return list of all wallets + Wallets() []Wallet + + // Return all accounts + Accounts() []types.Address + + // Search wallet with specific account + Find(account Account) (Wallet, error) } diff --git a/accounts/event/event_handler.go b/accounts/event/event_handler.go index 325707abff..437aa4aad3 100644 --- a/accounts/event/event_handler.go +++ b/accounts/event/event_handler.go @@ -39,6 +39,7 @@ func (ps *EventHandler) Unsubscribe(topic string, sub <-chan Event) { for i, ch := range chans { if ch == sub { ps.subscribers[topic] = append(chans[:i], chans[i+1:]...) + close(ch) break diff --git a/accounts/event/event_handler_test.go b/accounts/event/event_handler_test.go index 88e3a15986..0137598ffe 100644 --- a/accounts/event/event_handler_test.go +++ b/accounts/event/event_handler_test.go @@ -9,6 +9,10 @@ import ( "github.com/stretchr/testify/require" ) +const ( + topic = "testTopic" +) + type TestEvent struct { Data string } @@ -22,8 +26,6 @@ func TestSubscribeAndPublish(t *testing.T) { var err error - topic := "testTopic" - eventChan := make(chan Event, 1) ps.Subscribe(topic, eventChan) @@ -47,8 +49,6 @@ func TestUnsubscribe(t *testing.T) { var err error - topic := "testTopic" - eventChan := make(chan Event, 1) ps.Subscribe(topic, eventChan) @@ -60,7 +60,7 @@ func TestUnsubscribe(t *testing.T) { select { case _, ok := <-eventChan: if ok { - errors.New("expected channel to be closed") + err = errors.New("expected channel to be closed") } default: } @@ -70,11 +70,13 @@ func TestUnsubscribe(t *testing.T) { func TestConcurrentAccess(t *testing.T) { ps := NewEventHandler() - topic := "testTopic" + eventChan := make(chan Event, 1) + ps.Subscribe(topic, eventChan) var wg sync.WaitGroup + wg.Add(2) go func() { @@ -101,8 +103,6 @@ func TestMultipleSubscribers(t *testing.T) { var err error - topic := "testTopic" - eventChan1 := make(chan Event, 1) eventChan2 := make(chan Event, 1) diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index aa8f6fbc8a..8cfa9b2ba6 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -3,7 +3,6 @@ package keystore import ( "crypto/ecdsa" "errors" - "math/big" "path/filepath" "reflect" "runtime" @@ -42,10 +41,12 @@ type KeyStore struct { changes chan struct{} // Channel receiving change notifications from the cache unlocked map[types.Address]*unlocked // Currently unlocked account (decrypted private keys) - wallets []accounts.Wallet // Wallet wrappers around the individual key files + wallets []accounts.Wallet // Wrapper around keys config *chain.ForksInTime eventHandler *event.EventHandler + manager accounts.BackendManager + mu sync.RWMutex importMu sync.Mutex // Import Mutex locks the import to prevent two insertions from racing } @@ -55,7 +56,7 @@ type unlocked struct { abort chan struct{} } -func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger, config chain.ForksInTime) *KeyStore { +func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { var ks *KeyStore ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} @@ -164,7 +165,7 @@ func (ks *KeyStore) refreshWallets() { } } -func (ks *KeyStore) Subscribe(eventHandler *event.EventHandler) { +func (ks *KeyStore) SetEventHandler(eventHandler *event.EventHandler) { ks.mu.Lock() defer ks.mu.Unlock() @@ -222,7 +223,7 @@ func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { return crypto.Sign(unlockedKey.PrivateKey, hash) } -func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { +func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction) (*types.Transaction, error) { ks.mu.RLock() defer ks.mu.RUnlock() @@ -231,7 +232,7 @@ func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *b return nil, ErrLocked } - signer := crypto.NewSigner(*ks.config, chainID.Uint64()) + signer := ks.manager.GetSigner() return signer.SignTx(tx, unlockedKey.PrivateKey) } @@ -249,7 +250,7 @@ func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, } func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, - tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + tx *types.Transaction) (*types.Transaction, error) { _, key, err := ks.getDecryptedKey(a, passphrase) if err != nil { return nil, err @@ -257,7 +258,7 @@ func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, defer zeroKey(key.PrivateKey) - signer := crypto.NewSigner(*ks.config, chainID.Uint64()) + signer := ks.manager.GetSigner() return signer.SignTx(tx, key.PrivateKey) } @@ -423,3 +424,7 @@ func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (account return a, nil } + +func (ks *KeyStore) SetManager(manager accounts.BackendManager) { + ks.manager = manager +} diff --git a/accounts/keystore/keystore_fuzz_test.go b/accounts/keystore/keystore_fuzz_test.go index 37cbcbc963..a793ead816 100644 --- a/accounts/keystore/keystore_fuzz_test.go +++ b/accounts/keystore/keystore_fuzz_test.go @@ -3,13 +3,12 @@ package keystore import ( "testing" - "github.com/0xPolygon/polygon-edge/chain" "github.com/hashicorp/go-hclog" ) func FuzzPassword(f *testing.F) { f.Fuzz(func(t *testing.T, password string) { - ks := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + ks := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger()) a, err := ks.NewAccount(password) if err != nil { diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 7777f6acaf..34fed34e13 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -9,7 +9,6 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/event" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" @@ -214,7 +213,7 @@ func TestWalletNotifications(t *testing.T) { t.Parallel() _, ks := tmpKeyStore(t) eventHandler := event.NewEventHandler() - ks.Subscribe(eventHandler) + ks.SetEventHandler(eventHandler) // Subscribe to the wallet feed and collect events. var ( //nolint:prealloc @@ -234,6 +233,7 @@ func TestWalletNotifications(t *testing.T) { events = append(events, walletEvent{ev.(accounts.WalletEvent), ev.(accounts.WalletEvent).Wallet.Accounts()[0]}) case <-end: eventHandler.Unsubscribe(accounts.WalletEventKey, updates) + return } } @@ -374,5 +374,5 @@ func tmpKeyStore(t *testing.T) (string, *KeyStore) { d := t.TempDir() - return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) } diff --git a/accounts/keystore/wallet.go b/accounts/keystore/wallet.go index a807b6c861..b0ae995eaa 100644 --- a/accounts/keystore/wallet.go +++ b/accounts/keystore/wallet.go @@ -2,7 +2,6 @@ package keystore import ( "errors" - "math/big" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/crypto" @@ -72,19 +71,19 @@ func (ksw *keyStoreWallet) SignTextWithPassphrase(account accounts.Account, } func (ksw *keyStoreWallet) SignTx(account accounts.Account, - tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + tx *types.Transaction) (*types.Transaction, error) { if !ksw.Contains(account) { return nil, errors.New("unknown account") } - return ksw.keyStore.SignTx(account, tx, chainID) + return ksw.keyStore.SignTx(account, tx) } func (ksw *keyStoreWallet) SignTxWithPassphrase(account accounts.Account, - passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + passphrase string, tx *types.Transaction) (*types.Transaction, error) { if !ksw.Contains(account) { return nil, errors.New("unknown account") } - return ksw.keyStore.SignTxWithPassphrase(account, passphrase, tx, chainID) + return ksw.keyStore.SignTxWithPassphrase(account, passphrase, tx) } diff --git a/accounts/manager.go b/accounts/manager.go index 04b20ac2e2..0168154580 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -5,8 +5,9 @@ import ( "sync" "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/blockchain" + "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" - "github.com/hashicorp/go-hclog" ) const ( @@ -25,23 +26,26 @@ func (newBackendEvent) Type() event.EventType { return event.NewBackendType } +// Manager is an overarching account manager that can communicate with various +// backends for signing transactions. + type Manager struct { backends map[reflect.Type][]Backend updates chan event.Event newBackends chan event.Event wallets []Wallet + blockchain *blockchain.Blockchain quit chan chan error - logger hclog.Logger - eventHandler *event.EventHandler term chan struct{} lock sync.RWMutex } -func NewManager(logger hclog.Logger, backends ...Backend) *Manager { +// Creates new instance of manager +func NewManager(blockchain *blockchain.Blockchain, backends ...Backend) *Manager { var wallets []Wallet for _, backend := range backends { @@ -55,7 +59,7 @@ func NewManager(logger hclog.Logger, backends ...Backend) *Manager { eventHandler := event.NewEventHandler() for _, backend := range backends { - backend.Subscribe(eventHandler) + backend.SetEventHandler(eventHandler) } am := &Manager{ @@ -66,13 +70,16 @@ func NewManager(logger hclog.Logger, backends ...Backend) *Manager { quit: make(chan chan error), term: make(chan struct{}), eventHandler: eventHandler, + blockchain: blockchain, } eventHandler.Subscribe(WalletEventKey, am.updates) for _, backend := range backends { kind := reflect.TypeOf(backend) - backend.Subscribe(am.eventHandler) + + backend.SetEventHandler(am.eventHandler) + backend.SetManager(am) am.backends[kind] = append(am.backends[kind], backend) } @@ -81,6 +88,7 @@ func NewManager(logger hclog.Logger, backends ...Backend) *Manager { return am } +// Close stop updater in manager func (am *Manager) Close() error { for _, w := range am.wallets { w.Close() @@ -92,6 +100,7 @@ func (am *Manager) Close() error { return <-errc } +// Adds backend to list of backends func (am *Manager) AddBackend(backend Backend) { done := make(chan struct{}) @@ -132,7 +141,7 @@ func (am *Manager) update() { bckEvent := backendEventChan.(newBackendEvent) //nolint:forcetypeassert backend := bckEvent.backend am.wallets = merge(am.wallets, backend.Wallets()...) - backend.Subscribe(am.eventHandler) + backend.SetEventHandler(am.eventHandler) kind := reflect.TypeOf(backend) am.backends[kind] = append(am.backends[kind], backend) am.lock.Unlock() @@ -150,6 +159,7 @@ func (am *Manager) update() { } } +// Return specific type of backend func (am *Manager) Backends(kind reflect.Type) []Backend { am.lock.RLock() defer am.lock.RUnlock() @@ -157,6 +167,7 @@ func (am *Manager) Backends(kind reflect.Type) []Backend { return am.backends[kind] } +// Return list of all wallets func (am *Manager) Wallets() []Wallet { am.lock.RLock() defer am.lock.RUnlock() @@ -171,6 +182,7 @@ func (am *Manager) walletsNoLock() []Wallet { return cpy } +// Return all accounts func (am *Manager) Accounts() []types.Address { am.lock.RLock() defer am.lock.RUnlock() @@ -186,6 +198,12 @@ func (am *Manager) Accounts() []types.Address { return addresses } +// Checks for active forks at current block number and return signer +func (am *Manager) GetSigner() crypto.TxSigner { + return crypto.NewSigner(am.blockchain.Config().Forks.At(am.blockchain.Header().Number), uint64(am.blockchain.Config().ChainID)) +} + +// Search through wallets and func (am *Manager) Find(account Account) (Wallet, error) { am.lock.RLock() defer am.lock.RUnlock() @@ -199,10 +217,6 @@ func (am *Manager) Find(account Account) (Wallet, error) { return nil, ErrUnknownAccount } -func (am *Manager) Subscribe(eventHandler *event.EventHandler) { - am.eventHandler = eventHandler -} - func merge(slice []Wallet, wallets ...Wallet) []Wallet { for _, wallet := range wallets { slice = append(slice, wallet) diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index 437ea8e70c..d6fc13f5b1 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/0xPolygon/polygon-edge/accounts/keystore" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" "github.com/hashicorp/go-hclog" @@ -42,7 +41,7 @@ func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) ks := keystore.NewKeyStore(keystore.DefaultStorage, - keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) account, err := ks.NewAccount(params.passphrase) if err != nil { diff --git a/command/accounts/insert/insert.go b/command/accounts/insert/insert.go index 407a34599b..4f05a941e7 100644 --- a/command/accounts/insert/insert.go +++ b/command/accounts/insert/insert.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/0xPolygon/polygon-edge/accounts/keystore" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" "github.com/0xPolygon/polygon-edge/crypto" @@ -58,7 +57,7 @@ func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) ks := keystore.NewKeyStore(keystore.DefaultStorage, - keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) if params.privateKey == "" { outputter.SetError(errors.New("private key empty")) diff --git a/command/accounts/update/update.go b/command/accounts/update/update.go index 979644170e..3eaad997aa 100644 --- a/command/accounts/update/update.go +++ b/command/accounts/update/update.go @@ -5,7 +5,6 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/keystore" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" "github.com/hashicorp/go-hclog" @@ -61,7 +60,7 @@ func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) ks := keystore.NewKeyStore(keystore.DefaultStorage, - keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger(), chain.AllForksEnabled.At(0)) + keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) if !ks.HasAddress(params.address) { outputter.SetError(fmt.Errorf("this address doesn't exist")) diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index ce90609ff2..1b1ea2acf7 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -77,7 +77,7 @@ func (dp dispatcherParams) isExceedingBatchLengthLimit(value uint64) bool { func newDispatcher( logger hclog.Logger, store JSONRPCStore, - params *dispatcherParams, manager *accounts.Manager, + params *dispatcherParams, manager accounts.BackendManager, ) (*Dispatcher, error) { d := &Dispatcher{ logger: logger.Named("dispatcher"), @@ -96,7 +96,7 @@ func newDispatcher( return d, nil } -func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager *accounts.Manager) error { +func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager accounts.BackendManager) error { d.endpoints.Eth = &Eth{ d.logger, store, diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index 3bdf35d0c5..3f44d67d5a 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -102,7 +102,7 @@ type Eth struct { chainID uint64 filterManager *FilterManager priceLimit uint64 - accManager *accounts.Manager + accManager accounts.BackendManager } // ChainId returns the chain id of the client diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index 0199ca7285..dd411453c5 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -60,7 +60,7 @@ type Config struct { } // NewJSONRPC returns the JSONRPC http server -func NewJSONRPC(logger hclog.Logger, config *Config, manager *accounts.Manager) (*JSONRPC, error) { +func NewJSONRPC(logger hclog.Logger, config *Config, manager accounts.BackendManager) (*JSONRPC, error) { d, err := newDispatcher( logger, config.Store, diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index c06a5617a2..0c8d5f8fc5 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -13,7 +13,7 @@ import ( ) type Personal struct { - accManager *accounts.Manager + accManager accounts.BackendManager } func (p *Personal) ListAccounts() ([]types.Address, Error) { @@ -77,8 +77,8 @@ func (p *Personal) UnlockAccount(addr types.Address, password string, duration u return true, nil } -func (s *Personal) LockAccount(addr types.Address) (bool, error) { - ks, err := getKeystore(s.accManager) +func (p *Personal) LockAccount(addr types.Address) (bool, error) { + ks, err := getKeystore(p.accManager) if err != nil { return false, err } @@ -99,7 +99,7 @@ func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { return types.BytesToAddress(addressRaw), nil } -func getKeystore(am *accounts.Manager) (*keystore.KeyStore, error) { +func getKeystore(am accounts.BackendManager) (*keystore.KeyStore, error) { if ks := am.Backends(keystore.KeyStoreType); len(ks) > 0 { return ks[0].(*keystore.KeyStore), nil //nolint:forcetypeassert } diff --git a/server/server.go b/server/server.go index e95390d38b..c8210efa9e 100644 --- a/server/server.go +++ b/server/server.go @@ -18,6 +18,7 @@ import ( "google.golang.org/grpc" "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/accounts/keystore" "github.com/0xPolygon/polygon-edge/archive" "github.com/0xPolygon/polygon-edge/blockchain" "github.com/0xPolygon/polygon-edge/blockchain/storagev2" @@ -77,7 +78,7 @@ type Server struct { // libp2p network network *network.Server - accManager *accounts.Manager + accManager accounts.BackendManager // transaction pool txpool *txpool.TxPool @@ -312,11 +313,6 @@ func NewServer(config *Config) (*Server, error) { signer := crypto.NewSigner(config.Chain.Params.Forks.At(0), uint64(m.config.Chain.Params.ChainID)) - // setup account manager - { - m.accManager = accounts.NewManager(m.logger) - } - // create storage instance for blockchain var db *storagev2.Storage { @@ -349,6 +345,11 @@ func NewServer(config *Config) (*Server, error) { return nil, err } + // setup account manager + { + m.accManager = accounts.NewManager(m.blockchain, keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, m.logger)) + } + // here we can provide some other configuration m.gasHelper, err = gasprice.NewGasHelper(gasprice.DefaultGasHelperConfig, m.blockchain) if err != nil { From c061f26ec12f79ca556cd08766ad257595405697 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 14 Jun 2024 13:30:27 +0200 Subject: [PATCH 43/69] lint --- accounts/accounts.go | 2 +- accounts/manager.go | 4 +++- server/server.go | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/accounts/accounts.go b/accounts/accounts.go index b2d27f91ac..0c7677b71f 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -111,7 +111,7 @@ type BackendManager interface { // Checks for active forks at current block number and return signer GetSigner() crypto.TxSigner - //Close stop updater in manager + // Close stop updater in manager Close() error // Adds backend to list of backends diff --git a/accounts/manager.go b/accounts/manager.go index 0168154580..bb86004c08 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -200,7 +200,9 @@ func (am *Manager) Accounts() []types.Address { // Checks for active forks at current block number and return signer func (am *Manager) GetSigner() crypto.TxSigner { - return crypto.NewSigner(am.blockchain.Config().Forks.At(am.blockchain.Header().Number), uint64(am.blockchain.Config().ChainID)) + return crypto.NewSigner( + am.blockchain.Config().Forks.At(am.blockchain.Header().Number), + uint64(am.blockchain.Config().ChainID)) } // Search through wallets and diff --git a/server/server.go b/server/server.go index c8210efa9e..04f1b2ce5b 100644 --- a/server/server.go +++ b/server/server.go @@ -347,7 +347,9 @@ func NewServer(config *Config) (*Server, error) { // setup account manager { - m.accManager = accounts.NewManager(m.blockchain, keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, m.logger)) + m.accManager = accounts.NewManager( + m.blockchain, + keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, m.logger)) } // here we can provide some other configuration From 45cf7ddeccf066b2587d6197832daf9f72ef492e Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 14 Jun 2024 14:09:44 +0200 Subject: [PATCH 44/69] some minor changes --- jsonrpc/dispatcher.go | 2 +- jsonrpc/personal_endpoint.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index 1b1ea2acf7..5229541dfd 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -119,7 +119,7 @@ func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager accounts.Back store, } d.endpoints.Debug = NewDebug(store, d.params.concurrentRequestsDebug) - d.endpoints.Personal = &Personal{accManager: manager} + d.endpoints.Personal = NewPersonal(manager) var err error diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index 0c8d5f8fc5..efb51b72d5 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -16,6 +16,10 @@ type Personal struct { accManager accounts.BackendManager } +func NewPersonal(manager accounts.BackendManager) *Personal { + return &Personal{accManager: manager} +} + func (p *Personal) ListAccounts() ([]types.Address, Error) { return p.accManager.Accounts(), nil } From 7eb6fac241496f63939c6408ca0dd157f23d8228 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Fri, 14 Jun 2024 14:16:51 +0200 Subject: [PATCH 45/69] Rebase --- jsonrpc/eth_blockchain_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jsonrpc/eth_blockchain_test.go b/jsonrpc/eth_blockchain_test.go index 2aaa1c0a2b..7989a16553 100644 --- a/jsonrpc/eth_blockchain_test.go +++ b/jsonrpc/eth_blockchain_test.go @@ -191,8 +191,7 @@ func TestEth_Block_GetTransactionByBlockNumberAndIndex(t *testing.T) { transaction := toTransaction( block.Transactions[testIndex], - ArgUintPtr(block.Number()), - argHashPtr(block.Hash()), + block.Header, &testIndex, ) From 0dd3c4c1d31393d8f778b96efd3a2f15b6d214df Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Mon, 17 Jun 2024 11:21:27 +0200 Subject: [PATCH 46/69] fix for tests --- accounts/keystore/account_cache.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 0d7963ff51..808b8c31f1 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -35,6 +35,12 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st allMap: make(map[types.Address]encryptedKeyJSONV3), } + if err := os.MkdirAll(keyDir, 700); err != nil { + ac.logger.Info("can't create dir", "err", err) + + return nil, nil + } + keysPath := path.Join(keyDir, "keys.txt") ac.keyDir = keysPath From 4dcc34e8daa4ca1b7cde8a2db02d07a297f38817 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Mon, 17 Jun 2024 13:36:58 +0200 Subject: [PATCH 47/69] small code changes --- accounts/helper.go | 16 ------------- accounts/keystore/account_cache.go | 14 ++++------- accounts/keystore/account_cache_test.go | 22 ------------------ accounts/keystore/key.go | 31 ++----------------------- accounts/keystore/keystore.go | 29 +++++++---------------- accounts/keystore/passphrase_test.go | 25 ++++++++++++++++---- accounts/manager.go | 15 ++++-------- 7 files changed, 41 insertions(+), 111 deletions(-) diff --git a/accounts/helper.go b/accounts/helper.go index 6f44fc98ea..17fd2bda07 100644 --- a/accounts/helper.go +++ b/accounts/helper.go @@ -27,19 +27,3 @@ func NewAuthNeededError(needed string) error { func (err *AuthNeededError) Error() string { return fmt.Sprintf("authentication needed: %s", err.Needed) } - -func FindLine(data []byte, offset int64) (line int) { - line = 1 - - for i, r := range string(data) { - if int64(i) >= offset { - return - } - - if r == '\n' { - line++ - } - } - - return -} diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 808b8c31f1..29ed5b3994 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -3,19 +3,20 @@ package keystore import ( "encoding/json" "errors" + "fmt" "os" "path" "sync" "time" "github.com/0xPolygon/polygon-edge/accounts" + "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" ) // KeyStoreScheme is the protocol scheme prefixing account and wallet URLs. const KeyStoreScheme = "keystore" -const minReloadInterval = 2 * time.Second // accountCache is a live index of all accounts in the keystore. type accountCache struct { @@ -35,7 +36,7 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st allMap: make(map[types.Address]encryptedKeyJSONV3), } - if err := os.MkdirAll(keyDir, 700); err != nil { + if err := common.CreateDirSafe(keyDir, 0700); err != nil { ac.logger.Info("can't create dir", "err", err) return nil, nil @@ -109,12 +110,7 @@ func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) ac.allMap[newAccount.Address] = key - err = ac.saveData(ac.allMap) - if err != nil { - return err - } - - return nil + return ac.saveData(ac.allMap) } func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) error { @@ -132,7 +128,7 @@ func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) } if _, ok := newMap[account.Address]; !ok { - return errors.New("this account doesn't exists") + return fmt.Errorf("account: %s doesn't exists", account.Address.String()) } else { newMap[account.Address] = key } diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 4e8553a5ef..02f2233963 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -1,12 +1,9 @@ package keystore import ( - "errors" "fmt" "path/filepath" - "reflect" "testing" - "time" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/types" @@ -30,25 +27,6 @@ var ( } ) -func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error { - var list []accounts.Account - for t0 := time.Now().UTC(); time.Since(t0) < 20*time.Second; time.Sleep(100 * time.Millisecond) { - list = ks.Accounts() - if reflect.DeepEqual(list, wantAccounts) { - // ks should have also received change notifications - select { - case <-ks.changes: - default: - return errors.New("wasn't notified of new accounts") - } - - return nil - } - } - - return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts) -} - func TestCacheInitialReload(t *testing.T) { t.Parallel() diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 6ee82a3501..b703a9638d 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -3,11 +3,9 @@ package keystore import ( "bytes" "crypto/ecdsa" - "encoding/hex" "fmt" "io" "strings" - "time" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/crypto" @@ -21,10 +19,8 @@ const ( ) type Key struct { - ID uuid.UUID - - Address types.Address - + ID uuid.UUID + Address types.Address PrivateKey *ecdsa.PrivateKey } @@ -71,29 +67,6 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { return key } -// keyFileName implements the naming convention for keyfiles: -// UTC---
-func keyFileName(keyAddr types.Address) string { - ts := time.Now().UTC() - - return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) -} - -func toISO8601(t time.Time) string { - var tz string - - name, offset := t.Zone() - - if name == "UTC" { - tz = "Z" - } else { - tz = fmt.Sprintf("%03d00", offset/3600) - } - - return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", - t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) -} - func newKey() (*Key, error) { privateKeyECDSA, err := crypto.GenerateECDSAPrivateKey() // TO DO maybe not valid if err != nil { diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 8cfa9b2ba6..3ebdf9281c 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -11,7 +11,6 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/event" - "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" "github.com/hashicorp/go-hclog" @@ -42,7 +41,6 @@ type KeyStore struct { unlocked map[types.Address]*unlocked // Currently unlocked account (decrypted private keys) wallets []accounts.Wallet // Wrapper around keys - config *chain.ForksInTime eventHandler *event.EventHandler manager accounts.BackendManager @@ -57,9 +55,7 @@ type unlocked struct { } func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { - var ks *KeyStore - - ks = &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} + ks := &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} ks.init(keyDir, logger) @@ -67,9 +63,6 @@ func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyS } func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { - ks.mu.Lock() - defer ks.mu.Unlock() - ks.unlocked = make(map[types.Address]*unlocked) ks.cache, ks.changes = newAccountCache(keyDir, logger) @@ -109,6 +102,7 @@ func zeroKey(k *ecdsa.PrivateKey) { func (ks *KeyStore) refreshWallets() { ks.mu.Lock() + defer ks.mu.Unlock() accs := ks.cache.accounts() @@ -156,8 +150,6 @@ func (ks *KeyStore) refreshWallets() { ks.wallets = wallets - ks.mu.Unlock() - if ks.eventHandler != nil { for _, event := range events { ks.eventHandler.Publish(accounts.WalletEventKey, event) @@ -196,18 +188,18 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { // it anyway to check the password and zero out the key // immediately afterwards. a, key, err := ks.getDecryptedKey(a, passphrase) - if key != nil { - zeroKey(key.PrivateKey) - } - if err != nil { return err } + if key != nil { + zeroKey(key.PrivateKey) + } + ks.cache.delete(a) ks.refreshWallets() - return err + return nil } func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { @@ -219,6 +211,7 @@ func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { if !found { return nil, ErrLocked } + // Sign the hash using plain ECDSA operations return crypto.Sign(unlockedKey.PrivateKey, hash) } @@ -271,12 +264,10 @@ func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error { // Lock removes the private key with the given address from memory. func (ks *KeyStore) Lock(addr types.Address) error { ks.mu.Lock() + defer ks.mu.Unlock() if unl, found := ks.unlocked[addr]; found { - ks.mu.Unlock() ks.expire(addr, unl, time.Duration(0)*time.Nanosecond) - } else { - ks.mu.Unlock() } return nil @@ -350,7 +341,6 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { encryptedKey, account, err := storeNewKey(ks.storage, passphrase) - if err != nil { return accounts.Account{}, err } @@ -411,7 +401,6 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { a, encryptedKey, err := importPreSaleKey(ks.storage, keyJSON, passphrase) - if err != nil { return a, err } diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 78e0fa6b51..2e0dd7e20c 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -10,7 +10,6 @@ import ( "strings" "testing" - "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/helper/common" "github.com/0xPolygon/polygon-edge/types" "github.com/stretchr/testify/require" @@ -143,7 +142,7 @@ func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { tests := make(map[string]KeyStoreTestV3) - err := LoadJSON(t, file, &tests) + err := loadJSON(t, file, &tests) require.NoError(t, err) return tests @@ -201,7 +200,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { } } -func LoadJSON(t *testing.T, file string, val interface{}) error { +func loadJSON(t *testing.T, file string, val interface{}) error { t.Helper() content, err := os.ReadFile(file) @@ -211,7 +210,7 @@ func LoadJSON(t *testing.T, file string, val interface{}) error { if err := json.Unmarshal(content, val); err != nil { if syntaxerr, ok := err.(*json.SyntaxError); ok { //nolint:errorlint - line := accounts.FindLine(content, syntaxerr.Offset) + line := findLine(t, content, syntaxerr.Offset) return fmt.Errorf("JSON syntax error at %v:%v: %w", file, line, err) } @@ -221,3 +220,21 @@ func LoadJSON(t *testing.T, file string, val interface{}) error { return nil } + +func findLine(t *testing.T, data []byte, offset int64) (line int) { + t.Helper() + + line = 1 + + for i, r := range string(data) { + if int64(i) >= offset { + return + } + + if r == '\n' { + line++ + } + } + + return +} diff --git a/accounts/manager.go b/accounts/manager.go index bb86004c08..6d14fffb44 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -53,9 +53,7 @@ func NewManager(blockchain *blockchain.Blockchain, backends ...Backend) *Manager } updates := make(chan event.Event, managerSubBufferSize) - newBackends := make(chan event.Event) - eventHandler := event.NewEventHandler() for _, backend := range backends { @@ -90,6 +88,9 @@ func NewManager(blockchain *blockchain.Blockchain, backends ...Backend) *Manager // Close stop updater in manager func (am *Manager) Close() error { + am.lock.RLock() + defer am.lock.RUnlock() + for _, w := range am.wallets { w.Close() } @@ -111,11 +112,7 @@ func (am *Manager) AddBackend(backend Backend) { func (am *Manager) update() { defer func() { - am.lock.Lock() - am.eventHandler.Unsubscribe(WalletEventKey, am.updates) - - am.lock.Unlock() }() for { @@ -220,11 +217,7 @@ func (am *Manager) Find(account Account) (Wallet, error) { } func merge(slice []Wallet, wallets ...Wallet) []Wallet { - for _, wallet := range wallets { - slice = append(slice, wallet) - } - - return slice + return append(slice, wallets...) } func drop(slice []Wallet, wallet Wallet) []Wallet { From f406099be79ad359b55f1727ee5fc8a1185dc899 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Mon, 17 Jun 2024 14:17:45 +0200 Subject: [PATCH 48/69] code optimizations --- accounts/keystore/account_cache.go | 78 +++++++++--------------------- accounts/keystore/keystore.go | 17 +++---- 2 files changed, 31 insertions(+), 64 deletions(-) diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 29ed5b3994..1e517697be 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -60,13 +60,6 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st } func (ac *accountCache) accounts() []accounts.Account { - err := ac.scanAccounts() - if err != nil { - ac.logger.Debug("can't scan account's", "err", err) - - return []accounts.Account{} - } - ac.mu.Lock() defer ac.mu.Unlock() @@ -82,11 +75,6 @@ func (ac *accountCache) accounts() []accounts.Account { } func (ac *accountCache) hasAddress(addr types.Address) bool { - err := ac.scanAccounts() - if err != nil { - ac.logger.Debug("can't scan account's", "err", err) - } - ac.mu.Lock() defer ac.mu.Unlock() @@ -96,11 +84,6 @@ func (ac *accountCache) hasAddress(addr types.Address) bool { } func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) error { - err := ac.scanAccounts() - if err != nil { - return err - } - ac.mu.Lock() defer ac.mu.Unlock() @@ -110,42 +93,45 @@ func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) ac.allMap[newAccount.Address] = key - return ac.saveData(ac.allMap) -} + if err := ac.saveData(ac.allMap); err != nil { + // if we can't save the data, we should remove the account from the map + delete(ac.allMap, newAccount.Address) -func (ac *accountCache) update(account accounts.Account, key encryptedKeyJSONV3) error { - if err := ac.scanAccounts(); err != nil { return err } + return nil +} + +func (ac *accountCache) update(account accounts.Account, newKey encryptedKeyJSONV3) error { ac.mu.Lock() defer ac.mu.Unlock() - newMap := make(map[types.Address]encryptedKeyJSONV3) - - for mapKey, mapValue := range ac.allMap { - newMap[mapKey] = mapValue - } + var ( + oldKey encryptedKeyJSONV3 + ok bool + ) - if _, ok := newMap[account.Address]; !ok { + if oldKey, ok = ac.allMap[account.Address]; !ok { return fmt.Errorf("account: %s doesn't exists", account.Address.String()) } else { - newMap[account.Address] = key + ac.allMap[account.Address] = newKey } - if err := ac.saveData(newMap); err != nil { + if err := ac.saveData(ac.allMap); err != nil { + // if we can't save the data, we should return the old key to the map + ac.allMap[account.Address] = oldKey + return err } - ac.allMap = newMap - return nil } // note: removed needs to be unique here (i.e. both File and Address must be set). -func (ac *accountCache) delete(removed accounts.Account) { - if err := ac.scanAccounts(); err != nil { - ac.logger.Debug("can't scan account's", "err", err) +func (ac *accountCache) delete(removed accounts.Account) error { + if err := ac.saveData(ac.allMap); err != nil { + return fmt.Errorf("could not delete account: %w", err) } ac.mu.Lock() @@ -153,19 +139,13 @@ func (ac *accountCache) delete(removed accounts.Account) { delete(ac.allMap, removed.Address) - if err := ac.saveData(ac.allMap); err != nil { - ac.logger.Debug("cant't save data in file,", "err", err) - } + return nil } // find returns the cached account for address if there is a unique match. // The exact matching rules are explained by the documentation of accounts.Account. // Callers must hold ac.mu. func (ac *accountCache) find(a accounts.Account) (accounts.Account, encryptedKeyJSONV3, error) { - if err := ac.scanAccounts(); err != nil { - return accounts.Account{}, encryptedKeyJSONV3{}, err - } - ac.mu.Lock() defer ac.mu.Unlock() @@ -178,6 +158,7 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, encryptedKey func (ac *accountCache) close() { ac.mu.Lock() + defer ac.mu.Unlock() if ac.throttle != nil { ac.throttle.Stop() @@ -187,8 +168,6 @@ func (ac *accountCache) close() { close(ac.notify) ac.notify = nil } - - ac.mu.Unlock() } // scanAccounts refresh data of account map @@ -219,23 +198,12 @@ func (ac *accountCache) scanAccounts() error { } func (ac *accountCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) error { - fi, err := os.Create(ac.keyDir) - if err != nil { - return err - } - - defer fi.Close() - byteAccount, err := json.Marshal(accounts) if err != nil { return err } - if _, err := fi.Write(byteAccount); err != nil { - return err - } - - return nil + return common.SaveFileSafe(ac.keyDir, byteAccount, 0666) } func (ac *accountCache) scanFile() (map[types.Address]encryptedKeyJSONV3, error) { diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 3ebdf9281c..c3e5c8d60c 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -45,8 +45,7 @@ type KeyStore struct { manager accounts.BackendManager - mu sync.RWMutex - importMu sync.Mutex // Import Mutex locks the import to prevent two insertions from racing + mu sync.RWMutex } type unlocked struct { @@ -101,9 +100,6 @@ func zeroKey(k *ecdsa.PrivateKey) { } func (ks *KeyStore) refreshWallets() { - ks.mu.Lock() - defer ks.mu.Unlock() - accs := ks.cache.accounts() var ( @@ -112,6 +108,9 @@ func (ks *KeyStore) refreshWallets() { find bool ) + ks.mu.Lock() + defer ks.mu.Unlock() + for _, account := range accs { find = false @@ -196,7 +195,10 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { zeroKey(key.PrivateKey) } - ks.cache.delete(a) + if err := ks.cache.delete(a); err != nil { + return err + } + ks.refreshWallets() return nil @@ -355,9 +357,6 @@ func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { } func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) { - ks.importMu.Lock() - defer ks.importMu.Unlock() - key := newKeyFromECDSA(priv) if ks.cache.hasAddress(key.Address) { return accounts.Account{ From 8a8070a2fc6dea9a4d948657b4ecbd06abf42326 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Mon, 17 Jun 2024 14:24:46 +0200 Subject: [PATCH 49/69] lint fix --- accounts/keystore/account_cache_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 02f2233963..949d7e49ed 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -96,10 +96,10 @@ func TestCacheAddDelete(t *testing.T) { // Delete a few keys from the cache. for i := 0; i < len(accs); i += 2 { - cache.delete(wantAccounts[i]) + require.NoError(t, cache.delete(wantAccounts[i])) } - cache.delete(accounts.Account{Address: types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")}) + require.NoError(t, cache.delete(accounts.Account{Address: types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")})) // Check content again after deletion. wantAccountsAfterDelete := []accounts.Account{ From 8b30a8ef5c629925ceeed8fc3c69d74b367bd835 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Mon, 17 Jun 2024 15:17:14 +0200 Subject: [PATCH 50/69] commands update --- command/accounts/create/create.go | 29 ++++++++++++------ command/accounts/create/params.go | 7 ++--- command/accounts/insert/insert.go | 49 ++++++++++++------------------- command/accounts/insert/params.go | 1 + command/accounts/update/params.go | 1 + command/accounts/update/update.go | 35 +++++++++++++++------- jsonrpc/personal_endpoint.go | 14 +++++++++ 7 files changed, 81 insertions(+), 55 deletions(-) diff --git a/command/accounts/create/create.go b/command/accounts/create/create.go index d6fc13f5b1..458774b017 100644 --- a/command/accounts/create/create.go +++ b/command/accounts/create/create.go @@ -3,10 +3,10 @@ package create import ( "fmt" - "github.com/0xPolygon/polygon-edge/accounts/keystore" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" - "github.com/hashicorp/go-hclog" + "github.com/0xPolygon/polygon-edge/jsonrpc" + "github.com/0xPolygon/polygon-edge/types" "github.com/spf13/cobra" ) @@ -19,9 +19,12 @@ func GetCommand() *cobra.Command { Use: "create", Short: "Create new account", Run: runCommand, + PreRun: func(cmd *cobra.Command, _ []string) { + params.jsonRPC = helper.GetJSONRPCAddress(cmd) + }, } - helper.RegisterJSONRPCFlag(createCmd) + setFlags(createCmd) return createCmd } @@ -35,18 +38,26 @@ func setFlags(cmd *cobra.Command) { ) _ = cmd.MarkFlagRequired(passphraseFlag) + helper.RegisterJSONRPCFlag(cmd) } func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, - keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) - - account, err := ks.NewAccount(params.passphrase) + client, err := jsonrpc.NewEthClient(params.jsonRPC) if err != nil { - outputter.SetError(fmt.Errorf("can't create account: %w", err)) + outputter.SetError(fmt.Errorf("can't create jsonRPC client: %w", err)) + + return + } + + var address types.Address + + if err := client.EndpointCall("personal_newAccount", &address, params.passphrase); err != nil { + outputter.SetError(fmt.Errorf("can't create new account: %w", err)) + + return } - outputter.SetCommandResult(command.Results{&createResult{Address: account.Address}}) + outputter.SetCommandResult(command.Results{&createResult{Address: address}}) } diff --git a/command/accounts/create/params.go b/command/accounts/create/params.go index f58499ea92..3be7cc2b33 100644 --- a/command/accounts/create/params.go +++ b/command/accounts/create/params.go @@ -8,14 +8,11 @@ import ( "github.com/0xPolygon/polygon-edge/types" ) -const ( - passphraseFlag = "passphrase" - configDirFlag = "config-dir" -) +const passphraseFlag = "passphrase" type createParams struct { passphrase string - configDir string + jsonRPC string } type createResult struct { diff --git a/command/accounts/insert/insert.go b/command/accounts/insert/insert.go index 4f05a941e7..ca559f15d8 100644 --- a/command/accounts/insert/insert.go +++ b/command/accounts/insert/insert.go @@ -1,15 +1,12 @@ package insert import ( - "encoding/hex" - "errors" "fmt" - "github.com/0xPolygon/polygon-edge/accounts/keystore" "github.com/0xPolygon/polygon-edge/command" "github.com/0xPolygon/polygon-edge/command/helper" - "github.com/0xPolygon/polygon-edge/crypto" - "github.com/hashicorp/go-hclog" + "github.com/0xPolygon/polygon-edge/jsonrpc" + "github.com/0xPolygon/polygon-edge/types" "github.com/spf13/cobra" ) @@ -19,13 +16,14 @@ var ( func GetCommand() *cobra.Command { importCmd := &cobra.Command{ - Use: "insert", - Short: "Insert existing account with private key and auth passphrase", - PreRunE: runPreRun, - Run: runCommand, + Use: "insert", + Short: "Insert existing account with private key and auth passphrase", + PreRun: func(cmd *cobra.Command, args []string) { + params.jsonRPC = helper.GetJSONRPCAddress(cmd) + }, + Run: runCommand, } - helper.RegisterJSONRPCFlag(importCmd) setFlags(importCmd) return importCmd @@ -47,36 +45,27 @@ func setFlags(cmd *cobra.Command) { ) _ = cmd.MarkFlagRequired(passphraseFlag) -} - -func runPreRun(cmd *cobra.Command, _ []string) error { - return nil + helper.RegisterJSONRPCFlag(cmd) } func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, - keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) + client, err := jsonrpc.NewEthClient(params.jsonRPC) + if err != nil { + outputter.SetError(fmt.Errorf("can't create jsonRPC client: %w", err)) - if params.privateKey == "" { - outputter.SetError(errors.New("private key empty")) + return } - dec, err := hex.DecodeString(params.privateKey) - if err != nil { - outputter.SetError(fmt.Errorf("failed to decode private ke: %w", err)) - } + var address types.Address - privKey, err := crypto.BytesToECDSAPrivateKey(dec) - if err != nil { - outputter.SetError(fmt.Errorf("failed to initialize private key: %w", err)) - } + if err := client.EndpointCall("personal_importRawKey", &address, + params.privateKey, params.passphrase); err != nil { + outputter.SetError(fmt.Errorf("can't import new key: %w", err)) - acct, err := ks.ImportECDSA(privKey, params.passphrase) - if err != nil { - outputter.SetError(fmt.Errorf("cannot import private key: %w", err)) + return } - outputter.SetCommandResult(command.Results{&insertResult{Address: acct.Address}}) + outputter.SetCommandResult(command.Results{&insertResult{Address: address}}) } diff --git a/command/accounts/insert/params.go b/command/accounts/insert/params.go index 1b97aa7bc2..46f3a4a481 100644 --- a/command/accounts/insert/params.go +++ b/command/accounts/insert/params.go @@ -16,6 +16,7 @@ const ( type insertParams struct { privateKey string passphrase string + jsonRPC string } type insertResult struct { diff --git a/command/accounts/update/params.go b/command/accounts/update/params.go index b455f5edd1..e6adeb18a6 100644 --- a/command/accounts/update/params.go +++ b/command/accounts/update/params.go @@ -16,6 +16,7 @@ type updateParams struct { rawAddress string passphrase string oldPassphrase string + jsonRPC string address types.Address } diff --git a/command/accounts/update/update.go b/command/accounts/update/update.go index 3eaad997aa..ae2e451b96 100644 --- a/command/accounts/update/update.go +++ b/command/accounts/update/update.go @@ -3,11 +3,10 @@ package update import ( "fmt" - "github.com/0xPolygon/polygon-edge/accounts" - "github.com/0xPolygon/polygon-edge/accounts/keystore" "github.com/0xPolygon/polygon-edge/command" + bridgeHelper "github.com/0xPolygon/polygon-edge/command/bridge/helper" "github.com/0xPolygon/polygon-edge/command/helper" - "github.com/hashicorp/go-hclog" + "github.com/0xPolygon/polygon-edge/jsonrpc" "github.com/spf13/cobra" ) @@ -23,7 +22,6 @@ func GetCommand() *cobra.Command { Run: runCommand, } - helper.RegisterJSONRPCFlag(updateCmd) setFlags(updateCmd) return updateCmd @@ -50,23 +48,38 @@ func setFlags(cmd *cobra.Command) { "", "old passphrase to unlock account", ) + + helper.RegisterJSONRPCFlag(cmd) } func runPreRun(cmd *cobra.Command, _ []string) error { + params.jsonRPC = helper.GetJSONRPCAddress(cmd) + return params.validateFlags() } func runCommand(cmd *cobra.Command, _ []string) { outputter := command.InitializeOutputter(cmd) - ks := keystore.NewKeyStore(keystore.DefaultStorage, - keystore.LightScryptN, keystore.LightScryptP, hclog.NewNullLogger()) + client, err := jsonrpc.NewEthClient(params.jsonRPC) + if err != nil { + outputter.SetError(fmt.Errorf("can't create jsonRPC client: %w", err)) + + return + } + + var isUpdated bool + + if err := client.EndpointCall("personal_updatePassphrase", &isUpdated, params.address, + params.oldPassphrase, params.passphrase); err != nil { + outputter.SetError(fmt.Errorf("can't update passphrase: %w", err)) + + return + } - if !ks.HasAddress(params.address) { - outputter.SetError(fmt.Errorf("this address doesn't exist")) + if isUpdated { + outputter.WriteCommandResult(&bridgeHelper.MessageResult{Message: "Passphrase updated successfully"}) } else { - if err := ks.Update(accounts.Account{Address: params.address}, params.passphrase, params.oldPassphrase); err != nil { - outputter.SetError(fmt.Errorf("can't update account: %w", err)) - } + outputter.SetError(fmt.Errorf("can't update passphrase")) } } diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index efb51b72d5..3b500a324d 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -38,6 +38,20 @@ func (p *Personal) NewAccount(password string) (types.Address, error) { return acc.Address, nil } +func (p *Personal) UpdatePassphrase(addr types.Address, oldPassphrase, newPassphrase string) (bool, error) { + ks, err := getKeystore(p.accManager) + if err != nil { + return false, err + } + + err = ks.Update(accounts.Account{Address: addr}, oldPassphrase, newPassphrase) + if err != nil { + return false, err + } + + return true, nil +} + func (p *Personal) ImportRawKey(privKey string, password string) (types.Address, error) { key, err := crypto.HexToECDSA(privKey) if err != nil { From 17a251693ea32264990297ac57070fe135667f3a Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Tue, 18 Jun 2024 13:12:28 +0200 Subject: [PATCH 51/69] comment fix --- accounts/accounts.go | 4 ++-- accounts/event/event_handler.go | 6 ++---- accounts/event/event_handler_test.go | 11 +++++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/accounts/accounts.go b/accounts/accounts.go index 0c7677b71f..45a6d455df 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -57,12 +57,12 @@ type Wallet interface { } func TextHash(data []byte) []byte { - hash, _ := TextAndHash(data) + hash, _ := textAndHash(data) return hash } -func TextAndHash(data []byte) ([]byte, string) { +func textAndHash(data []byte) ([]byte, string) { msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) hasher := sha3.NewLegacyKeccak256() hasher.Write([]byte(msg)) diff --git a/accounts/event/event_handler.go b/accounts/event/event_handler.go index 437aa4aad3..fe50ad513e 100644 --- a/accounts/event/event_handler.go +++ b/accounts/event/event_handler.go @@ -51,11 +51,9 @@ func (ps *EventHandler) Unsubscribe(topic string, sub <-chan Event) { type EventType byte const ( - WalletEventType = 0x01 + WalletEventType EventType = 0x01 - NewBackendType = 0x02 - - TestEventType = 0x03 + NewBackendType EventType = 0x02 ) type Event interface { diff --git a/accounts/event/event_handler_test.go b/accounts/event/event_handler_test.go index 0137598ffe..206a42712c 100644 --- a/accounts/event/event_handler_test.go +++ b/accounts/event/event_handler_test.go @@ -11,6 +11,8 @@ import ( const ( topic = "testTopic" + + TestEventType EventType = 0xff ) type TestEvent struct { @@ -47,7 +49,7 @@ func TestSubscribeAndPublish(t *testing.T) { func TestUnsubscribe(t *testing.T) { ps := NewEventHandler() - var err error + var ok bool eventChan := make(chan Event, 1) @@ -58,14 +60,11 @@ func TestUnsubscribe(t *testing.T) { ps.Publish(topic, TestEvent{Data: "testEvent"}) select { - case _, ok := <-eventChan: - if ok { - err = errors.New("expected channel to be closed") - } + case _, ok = <-eventChan: default: } - require.NoError(t, err) + require.False(t, ok, "expected channel tobe closed") } func TestConcurrentAccess(t *testing.T) { From 37b0f80e7ce0b7eefc10cdfafda0bf8b29c6fc7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Du=C5=A1an=20Nosovi=C4=87?= <118283942+dusannosovic-ethernal@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:14:49 +0200 Subject: [PATCH 52/69] Update accounts/event/event_handler_test.go TestMultipleSubscribers optimization Co-authored-by: oliverbundalo <158565516+oliverbundalo@users.noreply.github.com> --- accounts/event/event_handler_test.go | 49 +++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/accounts/event/event_handler_test.go b/accounts/event/event_handler_test.go index 206a42712c..3324abd1d4 100644 --- a/accounts/event/event_handler_test.go +++ b/accounts/event/event_handler_test.go @@ -98,37 +98,48 @@ func TestConcurrentAccess(t *testing.T) { } func TestMultipleSubscribers(t *testing.T) { - ps := NewEventHandler() - - var err error - - eventChan1 := make(chan Event, 1) + const subscribers = 2 - eventChan2 := make(chan Event, 1) + var ( + channels [subscribers]chan Event + received byte + err error + ) - ps.Subscribe(topic, eventChan1) + ps := NewEventHandler() - ps.Subscribe(topic, eventChan2) + for i := 0; i < subscribers; i++ { + channels[i] = make(chan Event, 1) + ps.Subscribe(topic, channels[i]) + } event := TestEvent{Data: "testEvent"} ps.Publish(topic, event) - select { - case receivedEvent := <-eventChan1: - require.Equal(t, event, receivedEvent) - case <-time.After(1 * time.Second): - err = errors.New("did not receive event on eventChan1") - } + for { + var receivedEvent Event - require.NoError(t, err) + select { + case receivedEvent = <-channels[0]: + received |= 0x01 + case receivedEvent = <-channels[1]: + received |= 0x02 + case <-time.After(1 * time.Second): + err = errors.New("did not receive event on eventChan1") + } + + if err != nil { + break + } - select { - case receivedEvent := <-eventChan2: require.Equal(t, event, receivedEvent) - case <-time.After(1 * time.Second): - err = errors.New("did not receive event on eventChan2") + + if received == 0x03 { + break + } } require.NoError(t, err) } + From be4c884523fc5367f178750e4923de3db3633a67 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Wed, 19 Jun 2024 11:17:34 +0200 Subject: [PATCH 53/69] comment fix and rename keyStore interface functions --- accounts/keystore/account_cache.go | 11 +- accounts/keystore/account_cache_test.go | 51 +++------ accounts/keystore/key.go | 10 +- accounts/keystore/keystore.go | 21 +--- accounts/keystore/keystore_test.go | 4 +- accounts/keystore/passphrase.go | 22 +++- accounts/keystore/passphrase_test.go | 22 +--- accounts/keystore/presale.go | 146 ------------------------ 8 files changed, 52 insertions(+), 235 deletions(-) delete mode 100644 accounts/keystore/presale.go diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 1e517697be..6c45918d66 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -15,9 +15,6 @@ import ( "github.com/hashicorp/go-hclog" ) -// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs. -const KeyStoreScheme = "keystore" - // accountCache is a live index of all accounts in the keystore. type accountCache struct { logger hclog.Logger @@ -37,7 +34,7 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st } if err := common.CreateDirSafe(keyDir, 0700); err != nil { - ac.logger.Info("can't create dir", "err", err) + ac.logger.Error("can't create dir", "err", err) return nil, nil } @@ -48,7 +45,7 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st if _, err := os.Stat(keysPath); errors.Is(err, os.ErrNotExist) { if _, err := os.Create(keysPath); err != nil { - ac.logger.Info("can't create new file", "err", err) + ac.logger.Error("can't create new file", "err", err) return nil, nil } @@ -177,7 +174,7 @@ func (ac *accountCache) scanAccounts() error { accs, err := ac.scanFile() if err != nil { - ac.logger.Debug("Failed to reload keystore contents", "err", err) + ac.logger.Error("Failed to reload keystore contents", "err", err) return err } @@ -203,7 +200,7 @@ func (ac *accountCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) return err } - return common.SaveFileSafe(ac.keyDir, byteAccount, 0666) + return common.SaveFileSafe(ac.keyDir, byteAccount, 0600) } func (ac *accountCache) scanFile() (map[types.Address]encryptedKeyJSONV3, error) { diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 949d7e49ed..546b9adf61 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -12,8 +12,10 @@ import ( "github.com/stretchr/testify/require" ) +const KeyStoreScheme = "keystore" + var ( - cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore")) + cachetestDir, _ = filepath.Abs(filepath.Join("testdata", KeyStoreScheme)) cachetestAccounts = []accounts.Account{ { Address: types.StringToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), @@ -82,11 +84,6 @@ func TestCacheAddDelete(t *testing.T) { require.Error(t, cache.add(accs[0], encryptedKeyJSONV3{})) require.Error(t, cache.add(accs[2], encryptedKeyJSONV3{})) - // Check that the account list is sorted by filename. - wantAccounts := make([]accounts.Account, len(accs)) - - copy(wantAccounts, accs) - for _, a := range accs { require.True(t, cache.hasAddress(a.Address)) } @@ -96,22 +93,23 @@ func TestCacheAddDelete(t *testing.T) { // Delete a few keys from the cache. for i := 0; i < len(accs); i += 2 { - require.NoError(t, cache.delete(wantAccounts[i])) + require.NoError(t, cache.delete(accs[i])) } require.NoError(t, cache.delete(accounts.Account{Address: types.StringToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")})) - // Check content again after deletion. + // accounts that stay in account_cache, should be true wantAccountsAfterDelete := []accounts.Account{ - wantAccounts[1], - wantAccounts[3], - wantAccounts[5], + accs[1], + accs[3], + accs[5], } + // deleted accounts should be false after delete deletedAccounts := []accounts.Account{ - wantAccounts[0], - wantAccounts[2], - wantAccounts[4], + accs[0], + accs[2], + accs[4], } for _, acc := range wantAccountsAfterDelete { @@ -182,7 +180,6 @@ func TestCacheFind(t *testing.T) { } // TestUpdatedKeyfileContents tests that updating the contents of a keystore file -// is noticed by the watcher, and the account cache is updated accordingly func TestCacheUpdate(t *testing.T) { t.Parallel() @@ -195,30 +192,18 @@ func TestCacheUpdate(t *testing.T) { t.Error("initial account list not empty:", list) } - listOfEncryptedKeys := make([]encryptedKeyJSONV3, len(cachetestAccounts)) + account := cachetestAccounts[0] - for i, acc := range cachetestAccounts { - encryptKey := encryptedKeyJSONV3{Address: acc.Address.String(), Crypto: CryptoJSON{Cipher: fmt.Sprintf("test%d", i), CipherText: fmt.Sprintf("test%d", i)}} - listOfEncryptedKeys[i] = encryptKey - - require.NoError(t, accountCache.add(acc, encryptKey)) - } + require.NoError(t, accountCache.add(account, encryptedKeyJSONV3{Address: account.Address.String(), Crypto: CryptoJSON{Cipher: "test", CipherText: "test"}})) - require.NoError(t, accountCache.update(cachetestAccounts[0], encryptedKeyJSONV3{Address: cachetestAccounts[0].Address.String(), Crypto: CryptoJSON{Cipher: "test", CipherText: "test"}})) + require.NoError(t, accountCache.update(account, encryptedKeyJSONV3{Address: cachetestAccounts[0].Address.String(), Crypto: CryptoJSON{Cipher: "testUpdate", CipherText: "testUpdate"}})) - wantAccount, encryptedKey, err := accountCache.find(cachetestAccounts[0]) + wantAccount, encryptedKey, err := accountCache.find(account) require.NoError(t, err) require.Equal(t, wantAccount.Address.String(), encryptedKey.Address) - require.Equal(t, encryptedKey.Crypto.Cipher, "test") + require.Equal(t, encryptedKey.Crypto.Cipher, "testUpdate") - require.Equal(t, encryptedKey.Crypto.CipherText, "test") - - wantAccount, encryptedKey, err = accountCache.find(cachetestAccounts[1]) - require.NoError(t, err) - - require.Equal(t, listOfEncryptedKeys[1], encryptedKey) - - require.Equal(t, wantAccount.Address.String(), encryptedKey.Address) + require.Equal(t, encryptedKey.Crypto.CipherText, "testUpdate") } diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index b703a9638d..06eee0a76b 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -25,10 +25,10 @@ type Key struct { } type keyStore interface { - // Loads and decrypts the key from disk. - GetKey(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) - // Writes and encrypts the key. - StoreKey(k *Key, auth string) (encryptedKeyJSONV3, error) + // decrypts key and return non crypted key + KeyDecryption(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) + // get non crypted key and do encryption + KeyEncryption(k *Key, auth string) (encryptedKeyJSONV3, error) } type encryptedKeyJSONV3 struct { @@ -118,7 +118,7 @@ func storeNewKey(ks keyStore, auth string) (encryptedKeyJSONV3, accounts.Account Address: key.Address, } - encryptedKey, err := ks.StoreKey(key, auth) + encryptedKey, err := ks.KeyEncryption(key, auth) if err != nil { zeroKey(key.PrivateKey) diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index c3e5c8d60c..b35113f708 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -336,7 +336,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A return a, nil, err } - key, err := ks.storage.GetKey(encryptedKeyJSONV3, auth) + key, err := ks.storage.KeyDecryption(encryptedKeyJSONV3, auth) return a, key, err } @@ -370,7 +370,7 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { a := accounts.Account{Address: key.Address} - encryptedKeyJSONV3, err := ks.storage.StoreKey(key, passphrase) + encryptedKeyJSONV3, err := ks.storage.KeyEncryption(key, passphrase) if err != nil { return accounts.Account{}, err } @@ -390,7 +390,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) return err } - encryptedKey, err := ks.storage.StoreKey(key, newPassphrase) + encryptedKey, err := ks.storage.KeyEncryption(key, newPassphrase) if err != nil { return err } @@ -398,21 +398,6 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) return ks.cache.update(a, encryptedKey) } -func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { - a, encryptedKey, err := importPreSaleKey(ks.storage, keyJSON, passphrase) - if err != nil { - return a, err - } - - if err := ks.cache.add(a, encryptedKey); err != nil { - return accounts.Account{}, err - } - - ks.refreshWallets() - - return a, nil -} - func (ks *KeyStore) SetManager(manager accounts.BackendManager) { ks.manager = manager } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 34fed34e13..1e50851e50 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -57,7 +57,7 @@ func TestSign(t *testing.T) { t.Fatal(err) } - if err := ks.Unlock(a1, ""); err != nil { + if err := ks.Unlock(a1, pass); err != nil { t.Fatal(err) } @@ -176,7 +176,7 @@ func TestSignRace(t *testing.T) { _, ks := tmpKeyStore(t) // Create a test account. - a1, err := ks.NewAccount("") + a1, err := ks.NewAccount(pass) if err != nil { t.Fatal("could not create the test account", err) } diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index c2aecc9f45..a6339dba27 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -3,6 +3,7 @@ package keystore import ( "bytes" "crypto/aes" + "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -43,12 +44,9 @@ const ( type keyStorePassphrase struct { scryptN int scryptP int - // skipKeyFileVerification disables the security-feature which does - // reads and decrypts any newly created keyfiles. This should be 'false' in all - // cases except tests -- setting this to 'true' is not recommended. } -func (ks keyStorePassphrase) GetKey(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) { +func (ks keyStorePassphrase) KeyDecryption(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) { key, err := DecryptKey(encryptedKey, auth) if err != nil { return nil, err @@ -61,7 +59,7 @@ func (ks keyStorePassphrase) GetKey(encryptedKey encryptedKeyJSONV3, auth string return key, nil } -func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (encryptedKeyJSONV3, error) { +func (ks keyStorePassphrase) KeyEncryption(key *Key, auth string) (encryptedKeyJSONV3, error) { encryptedKey, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) if err != nil { return encryptedKeyJSONV3{}, err @@ -265,3 +263,17 @@ func ensureInt(x interface{}) int { return res } + +func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { + // AES-128 is selected due to size of encryptKey. + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + stream := cipher.NewCTR(aesBlock, iv) + outText := make([]byte, len(inText)) + stream.XORKeyStream(outText, inText) + + return outText, err +} diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 2e0dd7e20c..d70fa56f8f 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -28,7 +28,7 @@ func TestKeyStorePassphrase(t *testing.T) { k1, account, err := storeNewKey(ks, pass) require.NoError(t, err) - k2, err := ks.GetKey(k1, pass) + k2, err := ks.KeyDecryption(k1, pass) require.NoError(t, err) require.Equal(t, types.StringToAddress(k1.Address), k2.Address) @@ -44,24 +44,8 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { k1, _, err := storeNewKey(ks, pass) require.NoError(t, err) - _, err = ks.GetKey(k1, "bar") - require.Equal(t, err.Error(), ErrDecrypt.Error()) -} - -func TestImportPreSaleKey(t *testing.T) { - t.Parallel() - - ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} - - // file content of a presale key file generated with: - // python pyethsaletool.py genwallet - // with password "foo" - fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" - - account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) - require.NoError(t, err) - - require.Equal(t, account.Address, types.StringToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f")) + _, err = ks.KeyDecryption(k1, "bar") + require.EqualError(t, err, ErrDecrypt.Error()) } // Test and utils for the key store tests in the Ethereum JSON tests; diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go deleted file mode 100644 index a232954292..0000000000 --- a/accounts/keystore/presale.go +++ /dev/null @@ -1,146 +0,0 @@ -package keystore - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - - "github.com/0xPolygon/polygon-edge/accounts" - "github.com/0xPolygon/polygon-edge/crypto" - "github.com/google/uuid" - "golang.org/x/crypto/pbkdf2" -) - -func importPreSaleKey(keyStore keyStore, - keyJSON []byte, password string) (accounts.Account, encryptedKeyJSONV3, error) { - key, err := decryptPreSaleKey(keyJSON, password) - if err != nil { - return accounts.Account{}, encryptedKeyJSONV3{}, err - } - - key.ID, err = uuid.NewRandom() - if err != nil { - return accounts.Account{}, encryptedKeyJSONV3{}, err - } - - a := accounts.Account{ - Address: key.Address, - } - - encryptKey, err := keyStore.StoreKey(key, password) - - return a, encryptKey, err -} - -func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error) { - preSaleKeyStruct := struct { - EncSeed string - EthAddr string - Email string - BtcAddr string - }{} - - err = json.Unmarshal(fileContent, &preSaleKeyStruct) - if err != nil { - return nil, err - } - - encSeedBytes, err := hex.DecodeString(preSaleKeyStruct.EncSeed) - if err != nil { - return nil, errors.New("invalid hex in encSeed") - } - - if len(encSeedBytes) < 16 { - return nil, errors.New("invalid encSeed, too short") - } - - iv := encSeedBytes[:16] - cipherText := encSeedBytes[16:] - passBytes := []byte(password) - derivedKey := pbkdf2.Key(passBytes, passBytes, 2000, 16, sha256.New) - plainText, err := aesCBCDecrypt(derivedKey, cipherText, iv) - - if err != nil { - return nil, err - } - - ethPriv := crypto.Keccak256(plainText) - - ecKey, err := crypto.DToECDSA(ethPriv, false) - if err != nil { - return nil, err - } - - key = &Key{ - ID: uuid.UUID{}, - Address: crypto.PubKeyToAddress(&ecKey.PublicKey), - PrivateKey: ecKey, - } - - derivedAddr := hex.EncodeToString(key.Address.Bytes()) // needed because .Hex() gives leading "0x" - expectedAddr := preSaleKeyStruct.EthAddr - - if derivedAddr != expectedAddr { - err = fmt.Errorf("decrypted addr '%s' not equal to expected addr '%s'", derivedAddr, expectedAddr) - } - - return key, err -} - -func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { - // AES-128 is selected due to size of encryptKey. - aesBlock, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - stream := cipher.NewCTR(aesBlock, iv) - outText := make([]byte, len(inText)) - stream.XORKeyStream(outText, inText) - - return outText, err -} - -func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { - aesBlock, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - decrypter := cipher.NewCBCDecrypter(aesBlock, iv) - paddedPlaintext := make([]byte, len(cipherText)) - decrypter.CryptBlocks(paddedPlaintext, cipherText) - - plaintext := pkcs7Unpad(paddedPlaintext) - if plaintext == nil { - return nil, accounts.ErrDecrypt - } - - return plaintext, err -} - -// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes -func pkcs7Unpad(in []byte) []byte { - if len(in) == 0 { - return nil - } - - padding := in[len(in)-1] - if int(padding) > len(in) || padding > aes.BlockSize { - return nil - } else if padding == 0 { - return nil - } - - for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { - if in[i] != padding { - return nil - } - } - - return in[:len(in)-int(padding)] -} From e91fa8c10df8245a3b447a5549a29df6c34ebbc3 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Wed, 19 Jun 2024 12:51:13 +0200 Subject: [PATCH 54/69] refresh wallets optimization --- accounts/keystore/account_cache.go | 38 ++------ accounts/keystore/account_cache_test.go | 8 +- accounts/keystore/keystore.go | 124 ++++++------------------ accounts/keystore/keystore_test.go | 4 +- 4 files changed, 45 insertions(+), 129 deletions(-) diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 6c45918d66..9efd0bda25 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -7,7 +7,6 @@ import ( "os" "path" "sync" - "time" "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/helper/common" @@ -17,26 +16,23 @@ import ( // accountCache is a live index of all accounts in the keystore. type accountCache struct { - logger hclog.Logger - keyDir string - mu sync.Mutex - allMap map[types.Address]encryptedKeyJSONV3 - throttle *time.Timer - notify chan struct{} + logger hclog.Logger + keyDir string + mu sync.Mutex + allMap map[types.Address]encryptedKeyJSONV3 } -func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan struct{}) { +func newAccountCache(keyDir string, logger hclog.Logger) *accountCache { ac := &accountCache{ logger: logger, keyDir: keyDir, - notify: make(chan struct{}, 1), allMap: make(map[types.Address]encryptedKeyJSONV3), } if err := common.CreateDirSafe(keyDir, 0700); err != nil { ac.logger.Error("can't create dir", "err", err) - return nil, nil + return nil } keysPath := path.Join(keyDir, "keys.txt") @@ -47,13 +43,13 @@ func newAccountCache(keyDir string, logger hclog.Logger) (*accountCache, chan st if _, err := os.Create(keysPath); err != nil { ac.logger.Error("can't create new file", "err", err) - return nil, nil + return nil } } ac.scanAccounts() //nolint:errcheck - return ac, ac.notify + return ac } func (ac *accountCache) accounts() []accounts.Account { @@ -153,20 +149,6 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, encryptedKey return accounts.Account{}, encryptedKeyJSONV3{}, accounts.ErrNoMatch } -func (ac *accountCache) close() { - ac.mu.Lock() - defer ac.mu.Unlock() - - if ac.throttle != nil { - ac.throttle.Stop() - } - - if ac.notify != nil { - close(ac.notify) - ac.notify = nil - } -} - // scanAccounts refresh data of account map func (ac *accountCache) scanAccounts() error { ac.mu.Lock() @@ -185,10 +167,6 @@ func (ac *accountCache) scanAccounts() error { ac.allMap[addr] = key } - select { - case ac.notify <- struct{}{}: - default: - } ac.logger.Trace("Handled keystore changes") return nil diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 546b9adf61..6c88b5e6c2 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -32,7 +32,7 @@ var ( func TestCacheInitialReload(t *testing.T) { t.Parallel() - cache, _ := newAccountCache(cachetestDir, hclog.NewNullLogger()) + cache := newAccountCache(cachetestDir, hclog.NewNullLogger()) accs := cache.accounts() require.Equal(t, 3, len(accs)) @@ -51,7 +51,7 @@ func TestCacheAddDelete(t *testing.T) { tDir := t.TempDir() - cache, _ := newAccountCache(tDir, hclog.NewNullLogger()) + cache := newAccountCache(tDir, hclog.NewNullLogger()) accs := []accounts.Account{ { @@ -126,7 +126,7 @@ func TestCacheFind(t *testing.T) { dir := t.TempDir() - cache, _ := newAccountCache(dir, hclog.NewNullLogger()) + cache := newAccountCache(dir, hclog.NewNullLogger()) accs := []accounts.Account{ { @@ -185,7 +185,7 @@ func TestCacheUpdate(t *testing.T) { keyDir := t.TempDir() - accountCache, _ := newAccountCache(keyDir, hclog.NewNullLogger()) + accountCache := newAccountCache(keyDir, hclog.NewNullLogger()) list := accountCache.accounts() if len(list) > 0 { diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index b35113f708..a37ace706b 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -5,7 +5,6 @@ import ( "errors" "path/filepath" "reflect" - "runtime" "sync" "time" @@ -30,15 +29,11 @@ var ( var KeyStoreType = reflect.TypeOf(&KeyStore{}) -// Maximum time between wallet refreshes (if filesystem notifications don't work). -const walletRefreshCycle = 3 * time.Second - // KeyStore manages a key storage directory on disk. type KeyStore struct { - storage keyStore // Storage backend, might be cleartext or encrypted - cache *accountCache // In-memory account cache over the filesystem storage - changes chan struct{} // Channel receiving change notifications from the cache - unlocked map[types.Address]*unlocked // Currently unlocked account (decrypted private keys) + keyEncryption keyStore // Storage backend, might be cleartext or encrypted + cache *accountCache // In-memory account cache over the filesystem storage + unlocked map[types.Address]*unlocked // Currently unlocked account (decrypted private keys) wallets []accounts.Wallet // Wrapper around keys eventHandler *event.EventHandler @@ -54,7 +49,7 @@ type unlocked struct { } func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { - ks := &KeyStore{storage: &keyStorePassphrase{scryptN, scryptP}} + ks := &KeyStore{keyEncryption: &keyStorePassphrase{scryptN, scryptP}} ks.init(keyDir, logger) @@ -63,11 +58,7 @@ func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyS func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { ks.unlocked = make(map[types.Address]*unlocked) - ks.cache, ks.changes = newAccountCache(keyDir, logger) - - runtime.SetFinalizer(ks, func(m *KeyStore) { - m.cache.close() - }) + ks.cache = newAccountCache(keyDir, logger) accs := ks.cache.accounts() ks.wallets = make([]accounts.Wallet, len(accs)) @@ -75,14 +66,9 @@ func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { for i := 0; i < len(accs); i++ { ks.wallets[i] = &keyStoreWallet{account: accs[i], keyStore: ks} } - - go ks.updater() } func (ks *KeyStore) Wallets() []accounts.Wallet { - // Make sure the list of wallets is in sync with the account cache - ks.refreshWallets() - ks.mu.RLock() defer ks.mu.RUnlock() @@ -99,63 +85,6 @@ func zeroKey(k *ecdsa.PrivateKey) { clear(b) } -func (ks *KeyStore) refreshWallets() { - accs := ks.cache.accounts() - - var ( - wallets = make([]accounts.Wallet, 0, len(accs)) - events []accounts.WalletEvent - find bool - ) - - ks.mu.Lock() - defer ks.mu.Unlock() - - for _, account := range accs { - find = false - - for _, wallet := range ks.wallets { - if wallet.Accounts()[0] == account { - wallets = append(wallets, wallet) - find = true - - break - } - } - - if !find { - wallet := &keyStoreWallet{account: account, keyStore: ks} - wallets = append(wallets, wallet) - - events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived}) - } - } - - for _, oldWallet := range ks.wallets { - find = false - - for _, newWallet := range wallets { - if newWallet == oldWallet { - find = true - - break - } - } - - if !find { - events = append(events, accounts.WalletEvent{Wallet: oldWallet, Kind: accounts.WalletDropped}) - } - } - - ks.wallets = wallets - - if ks.eventHandler != nil { - for _, event := range events { - ks.eventHandler.Publish(accounts.WalletEventKey, event) - } - } -} - func (ks *KeyStore) SetEventHandler(eventHandler *event.EventHandler) { ks.mu.Lock() defer ks.mu.Unlock() @@ -163,17 +92,6 @@ func (ks *KeyStore) SetEventHandler(eventHandler *event.EventHandler) { ks.eventHandler = eventHandler } -func (ks *KeyStore) updater() { - for { - select { - case <-ks.changes: - case <-time.After(walletRefreshCycle): - } - - ks.refreshWallets() - } -} - func (ks *KeyStore) HasAddress(addr types.Address) bool { return ks.cache.hasAddress(addr) } @@ -199,7 +117,17 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { return err } - ks.refreshWallets() + for i, wallet := range ks.wallets { + if wallet.Accounts()[0].Address == a.Address { + ks.wallets = append(ks.wallets[:i], ks.wallets[i+1:]...) + + break + } + } + + event := accounts.WalletEvent{Wallet: &keyStoreWallet{account: a, keyStore: ks}, Kind: accounts.WalletDropped} + + ks.eventHandler.Publish(accounts.WalletEventKey, event) return nil } @@ -336,13 +264,13 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A return a, nil, err } - key, err := ks.storage.KeyDecryption(encryptedKeyJSONV3, auth) + key, err := ks.keyEncryption.KeyDecryption(encryptedKeyJSONV3, auth) return a, key, err } func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { - encryptedKey, account, err := storeNewKey(ks.storage, passphrase) + encryptedKey, account, err := storeNewKey(ks.keyEncryption, passphrase) if err != nil { return accounts.Account{}, err } @@ -351,7 +279,11 @@ func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { return accounts.Account{}, err } - ks.refreshWallets() + ks.wallets = append(ks.wallets, &keyStoreWallet{account: account, keyStore: ks}) + + event := accounts.WalletEvent{Wallet: &keyStoreWallet{account: account, keyStore: ks}, Kind: accounts.WalletArrived} + + ks.eventHandler.Publish(accounts.WalletEventKey, event) return account, nil } @@ -370,7 +302,7 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { a := accounts.Account{Address: key.Address} - encryptedKeyJSONV3, err := ks.storage.KeyEncryption(key, passphrase) + encryptedKeyJSONV3, err := ks.keyEncryption.KeyEncryption(key, passphrase) if err != nil { return accounts.Account{}, err } @@ -379,7 +311,11 @@ func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, er return accounts.Account{}, err } - ks.refreshWallets() + ks.wallets = append(ks.wallets, &keyStoreWallet{account: a, keyStore: ks}) + + event := accounts.WalletEvent{Wallet: &keyStoreWallet{account: a, keyStore: ks}, Kind: accounts.WalletArrived} + + ks.eventHandler.Publish(accounts.WalletEventKey, event) return a, nil } @@ -390,7 +326,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) return err } - encryptedKey, err := ks.storage.KeyEncryption(key, newPassphrase) + encryptedKey, err := ks.keyEncryption.KeyEncryption(key, newPassphrase) if err != nil { return err } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 1e50851e50..270e5fa5f3 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -175,13 +175,15 @@ func TestSignRace(t *testing.T) { t.Parallel() _, ks := tmpKeyStore(t) + pass := "" + // Create a test account. a1, err := ks.NewAccount(pass) if err != nil { t.Fatal("could not create the test account", err) } - if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil { + if err := ks.TimedUnlock(a1, pass, 15*time.Millisecond); err != nil { t.Fatal("could not unlock the test account", err) } From 49e8aa101db5d39bc0aa36c4699a2cacdacedbac Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Wed, 19 Jun 2024 14:44:48 +0200 Subject: [PATCH 55/69] comments and change names --- accounts/accounts.go | 14 +-- .../{account_cache.go => account_store.go} | 44 +++++---- ...nt_cache_test.go => account_store_test.go} | 22 ++--- accounts/keystore/{key.go => crypto.go} | 44 +++------ accounts/keystore/keystore.go | 24 ++--- accounts/keystore/keystore_test.go | 5 +- accounts/keystore/passphrase.go | 79 ++++++++++------ accounts/keystore/passphrase_test.go | 90 +++++++++---------- accounts/manager.go | 55 ++++++------ command/accounts/insert/insert.go | 2 +- jsonrpc/dispatcher.go | 5 +- jsonrpc/eth_endpoint.go | 2 +- jsonrpc/jsonrpc.go | 2 +- jsonrpc/personal_endpoint.go | 17 ++-- server/server.go | 5 +- 15 files changed, 208 insertions(+), 202 deletions(-) rename accounts/keystore/{account_cache.go => account_store.go} (68%) rename accounts/keystore/{account_cache_test.go => account_store_test.go} (84%) rename accounts/keystore/{key.go => crypto.go} (68%) diff --git a/accounts/accounts.go b/accounts/accounts.go index 45a6d455df..b038f1b41d 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -96,7 +96,7 @@ func (WalletEvent) Type() event.EventType { return event.WalletEventType } -type Backend interface { +type WalletManager interface { // Wallets retrieves the list of wallets the backend is currently aware of Wallets() []Wallet @@ -104,21 +104,21 @@ type Backend interface { SetEventHandler(eventHandler *event.EventHandler) // SetManager sets backend manager - SetManager(manager BackendManager) + SetManager(manager AccountManager) } -type BackendManager interface { +type AccountManager interface { // Checks for active forks at current block number and return signer GetSigner() crypto.TxSigner // Close stop updater in manager Close() error - // Adds backend to list of backends - AddBackend(backend Backend) + // Adds wallet manager to list of wallet managers + AddWalletManager(walletManager WalletManager) - // Return specific type of backend - Backends(kind reflect.Type) []Backend + // Return specific type of wallet manager + WalletManagers(kind reflect.Type) []WalletManager // Return list of all wallets Wallets() []Wallet diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_store.go similarity index 68% rename from accounts/keystore/account_cache.go rename to accounts/keystore/account_store.go index 9efd0bda25..08905a3eab 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_store.go @@ -14,19 +14,19 @@ import ( "github.com/hashicorp/go-hclog" ) -// accountCache is a live index of all accounts in the keystore. -type accountCache struct { +// accountStore is a live index of all accounts in the keystore. +type accountStore struct { logger hclog.Logger keyDir string mu sync.Mutex - allMap map[types.Address]encryptedKeyJSONV3 + allMap map[types.Address]encryptedKey } -func newAccountCache(keyDir string, logger hclog.Logger) *accountCache { - ac := &accountCache{ +func newAccountStore(keyDir string, logger hclog.Logger) *accountStore { + ac := &accountStore{ logger: logger, keyDir: keyDir, - allMap: make(map[types.Address]encryptedKeyJSONV3), + allMap: make(map[types.Address]encryptedKey), } if err := common.CreateDirSafe(keyDir, 0700); err != nil { @@ -47,12 +47,12 @@ func newAccountCache(keyDir string, logger hclog.Logger) *accountCache { } } - ac.scanAccounts() //nolint:errcheck + ac.readAccountsFromFile() //nolint:errcheck return ac } -func (ac *accountCache) accounts() []accounts.Account { +func (ac *accountStore) accounts() []accounts.Account { ac.mu.Lock() defer ac.mu.Unlock() @@ -67,7 +67,7 @@ func (ac *accountCache) accounts() []accounts.Account { return cpy } -func (ac *accountCache) hasAddress(addr types.Address) bool { +func (ac *accountStore) hasAddress(addr types.Address) bool { ac.mu.Lock() defer ac.mu.Unlock() @@ -76,7 +76,7 @@ func (ac *accountCache) hasAddress(addr types.Address) bool { return ok } -func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) error { +func (ac *accountStore) add(newAccount accounts.Account, key encryptedKey) error { ac.mu.Lock() defer ac.mu.Unlock() @@ -96,12 +96,12 @@ func (ac *accountCache) add(newAccount accounts.Account, key encryptedKeyJSONV3) return nil } -func (ac *accountCache) update(account accounts.Account, newKey encryptedKeyJSONV3) error { +func (ac *accountStore) update(account accounts.Account, newKey encryptedKey) error { ac.mu.Lock() defer ac.mu.Unlock() var ( - oldKey encryptedKeyJSONV3 + oldKey encryptedKey ok bool ) @@ -122,7 +122,7 @@ func (ac *accountCache) update(account accounts.Account, newKey encryptedKeyJSON } // note: removed needs to be unique here (i.e. both File and Address must be set). -func (ac *accountCache) delete(removed accounts.Account) error { +func (ac *accountStore) delete(removed accounts.Account) error { if err := ac.saveData(ac.allMap); err != nil { return fmt.Errorf("could not delete account: %w", err) } @@ -136,9 +136,7 @@ func (ac *accountCache) delete(removed accounts.Account) error { } // find returns the cached account for address if there is a unique match. -// The exact matching rules are explained by the documentation of accounts.Account. -// Callers must hold ac.mu. -func (ac *accountCache) find(a accounts.Account) (accounts.Account, encryptedKeyJSONV3, error) { +func (ac *accountStore) find(a accounts.Account) (accounts.Account, encryptedKey, error) { ac.mu.Lock() defer ac.mu.Unlock() @@ -146,11 +144,11 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, encryptedKey return a, encryptedKey, nil } - return accounts.Account{}, encryptedKeyJSONV3{}, accounts.ErrNoMatch + return accounts.Account{}, encryptedKey{}, accounts.ErrNoMatch } -// scanAccounts refresh data of account map -func (ac *accountCache) scanAccounts() error { +// readAccountsFromFile refresh data of account map +func (ac *accountStore) readAccountsFromFile() error { ac.mu.Lock() defer ac.mu.Unlock() @@ -161,7 +159,7 @@ func (ac *accountCache) scanAccounts() error { return err } - ac.allMap = make(map[types.Address]encryptedKeyJSONV3) + ac.allMap = make(map[types.Address]encryptedKey) for addr, key := range accs { ac.allMap[addr] = key @@ -172,7 +170,7 @@ func (ac *accountCache) scanAccounts() error { return nil } -func (ac *accountCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) error { +func (ac *accountStore) saveData(accounts map[types.Address]encryptedKey) error { byteAccount, err := json.Marshal(accounts) if err != nil { return err @@ -181,7 +179,7 @@ func (ac *accountCache) saveData(accounts map[types.Address]encryptedKeyJSONV3) return common.SaveFileSafe(ac.keyDir, byteAccount, 0600) } -func (ac *accountCache) scanFile() (map[types.Address]encryptedKeyJSONV3, error) { +func (ac *accountStore) scanFile() (map[types.Address]encryptedKey, error) { fi, err := os.ReadFile(ac.keyDir) if err != nil { return nil, err @@ -191,7 +189,7 @@ func (ac *accountCache) scanFile() (map[types.Address]encryptedKeyJSONV3, error) return nil, nil } - var accounts = make(map[types.Address]encryptedKeyJSONV3) + var accounts = make(map[types.Address]encryptedKey) err = json.Unmarshal(fi, &accounts) if err != nil { diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_store_test.go similarity index 84% rename from accounts/keystore/account_cache_test.go rename to accounts/keystore/account_store_test.go index 6c88b5e6c2..0ff6a3b947 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_store_test.go @@ -32,7 +32,7 @@ var ( func TestCacheInitialReload(t *testing.T) { t.Parallel() - cache := newAccountCache(cachetestDir, hclog.NewNullLogger()) + cache := newAccountStore(cachetestDir, hclog.NewNullLogger()) accs := cache.accounts() require.Equal(t, 3, len(accs)) @@ -51,7 +51,7 @@ func TestCacheAddDelete(t *testing.T) { tDir := t.TempDir() - cache := newAccountCache(tDir, hclog.NewNullLogger()) + cache := newAccountStore(tDir, hclog.NewNullLogger()) accs := []accounts.Account{ { @@ -78,11 +78,11 @@ func TestCacheAddDelete(t *testing.T) { } for _, a := range accs { - require.NoError(t, cache.add(a, encryptedKeyJSONV3{})) + require.NoError(t, cache.add(a, encryptedKey{})) } // Add some of them twice to check that they don't get reinserted. - require.Error(t, cache.add(accs[0], encryptedKeyJSONV3{})) - require.Error(t, cache.add(accs[2], encryptedKeyJSONV3{})) + require.Error(t, cache.add(accs[0], encryptedKey{})) + require.Error(t, cache.add(accs[2], encryptedKey{})) for _, a := range accs { require.True(t, cache.hasAddress(a.Address)) @@ -126,7 +126,7 @@ func TestCacheFind(t *testing.T) { dir := t.TempDir() - cache := newAccountCache(dir, hclog.NewNullLogger()) + cache := newAccountStore(dir, hclog.NewNullLogger()) accs := []accounts.Account{ { @@ -145,10 +145,10 @@ func TestCacheFind(t *testing.T) { } for _, acc := range accs { - require.NoError(t, cache.add(acc, encryptedKeyJSONV3{})) + require.NoError(t, cache.add(acc, encryptedKey{})) } - require.Error(t, cache.add(matchAccount, encryptedKeyJSONV3{})) + require.Error(t, cache.add(matchAccount, encryptedKey{})) nomatchAccount := accounts.Account{ Address: types.StringToAddress("f466859ead1932d743d622cb74fc058882e8648a"), @@ -185,7 +185,7 @@ func TestCacheUpdate(t *testing.T) { keyDir := t.TempDir() - accountCache := newAccountCache(keyDir, hclog.NewNullLogger()) + accountCache := newAccountStore(keyDir, hclog.NewNullLogger()) list := accountCache.accounts() if len(list) > 0 { @@ -194,9 +194,9 @@ func TestCacheUpdate(t *testing.T) { account := cachetestAccounts[0] - require.NoError(t, accountCache.add(account, encryptedKeyJSONV3{Address: account.Address.String(), Crypto: CryptoJSON{Cipher: "test", CipherText: "test"}})) + require.NoError(t, accountCache.add(account, encryptedKey{Address: account.Address.String(), Crypto: Crypto{Cipher: "test", CipherText: "test"}})) - require.NoError(t, accountCache.update(account, encryptedKeyJSONV3{Address: cachetestAccounts[0].Address.String(), Crypto: CryptoJSON{Cipher: "testUpdate", CipherText: "testUpdate"}})) + require.NoError(t, accountCache.update(account, encryptedKey{Address: cachetestAccounts[0].Address.String(), Crypto: Crypto{Cipher: "testUpdate", CipherText: "testUpdate"}})) wantAccount, encryptedKey, err := accountCache.find(account) require.NoError(t, err) diff --git a/accounts/keystore/key.go b/accounts/keystore/crypto.go similarity index 68% rename from accounts/keystore/key.go rename to accounts/keystore/crypto.go index 06eee0a76b..7de38760f2 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/crypto.go @@ -24,30 +24,32 @@ type Key struct { PrivateKey *ecdsa.PrivateKey } -type keyStore interface { +type keyEncryption interface { // decrypts key and return non crypted key - KeyDecryption(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) + KeyDecrypt(encrypted encryptedKey, auth string) (*Key, error) // get non crypted key and do encryption - KeyEncryption(k *Key, auth string) (encryptedKeyJSONV3, error) + KeyEncrypt(k *Key, auth string) (encryptedKey, error) + + StoreNewKey(auth string) (encryptedKey, accounts.Account, error) } -type encryptedKeyJSONV3 struct { - Address string `json:"address"` - Crypto CryptoJSON `json:"crypto"` - ID string `json:"id"` - Version int `json:"version"` +type encryptedKey struct { + Address string `json:"address"` + Crypto Crypto `json:"crypto"` + ID string `json:"id"` + Version int `json:"version"` } -type CryptoJSON struct { +type Crypto struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"` - CipherParams cipherparamsJSON `json:"cipherparams"` + CipherParams cipherParams `json:"cipherparams"` KDF string `json:"kdf"` KDFParams map[string]interface{} `json:"kdfparams"` MAC string `json:"mac"` } -type cipherparamsJSON struct { +type cipherParams struct { IV string `json:"iv"` } @@ -107,23 +109,3 @@ func NewKeyForDirectICAP(rand io.Reader) *Key { return key } - -func storeNewKey(ks keyStore, auth string) (encryptedKeyJSONV3, accounts.Account, error) { - key, err := newKey() - if err != nil { - return encryptedKeyJSONV3{}, accounts.Account{}, err - } - - a := accounts.Account{ - Address: key.Address, - } - - encryptedKey, err := ks.KeyEncryption(key, auth) - if err != nil { - zeroKey(key.PrivateKey) - - return encryptedKeyJSONV3{}, a, err - } - - return encryptedKey, a, err -} diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index a37ace706b..942241ed15 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -31,14 +31,14 @@ var KeyStoreType = reflect.TypeOf(&KeyStore{}) // KeyStore manages a key storage directory on disk. type KeyStore struct { - keyEncryption keyStore // Storage backend, might be cleartext or encrypted - cache *accountCache // In-memory account cache over the filesystem storage + keyEncryption keyEncryption // Storage backend, might be cleartext or encrypted + cache *accountStore // In-memory account cache over the filesystem storage unlocked map[types.Address]*unlocked // Currently unlocked account (decrypted private keys) wallets []accounts.Wallet // Wrapper around keys eventHandler *event.EventHandler - manager accounts.BackendManager + manager accounts.AccountManager mu sync.RWMutex } @@ -49,7 +49,7 @@ type unlocked struct { } func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { - ks := &KeyStore{keyEncryption: &keyStorePassphrase{scryptN, scryptP}} + ks := &KeyStore{keyEncryption: &passphraseEncryption{scryptN, scryptP}} ks.init(keyDir, logger) @@ -58,7 +58,7 @@ func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyS func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { ks.unlocked = make(map[types.Address]*unlocked) - ks.cache = newAccountCache(keyDir, logger) + ks.cache = newAccountStore(keyDir, logger) accs := ks.cache.accounts() ks.wallets = make([]accounts.Wallet, len(accs)) @@ -259,18 +259,18 @@ func (ks *KeyStore) expire(addr types.Address, u *unlocked, timeout time.Duratio } func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { - a, encryptedKeyJSONV3, err := ks.cache.find(a) + a, encryptedKey, err := ks.cache.find(a) if err != nil { return a, nil, err } - key, err := ks.keyEncryption.KeyDecryption(encryptedKeyJSONV3, auth) + key, err := ks.keyEncryption.KeyDecrypt(encryptedKey, auth) return a, key, err } func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { - encryptedKey, account, err := storeNewKey(ks.keyEncryption, passphrase) + encryptedKey, account, err := ks.keyEncryption.StoreNewKey(passphrase) if err != nil { return accounts.Account{}, err } @@ -302,12 +302,12 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { a := accounts.Account{Address: key.Address} - encryptedKeyJSONV3, err := ks.keyEncryption.KeyEncryption(key, passphrase) + encryptedKey, err := ks.keyEncryption.KeyEncrypt(key, passphrase) if err != nil { return accounts.Account{}, err } - if err := ks.cache.add(a, encryptedKeyJSONV3); err != nil { + if err := ks.cache.add(a, encryptedKey); err != nil { return accounts.Account{}, err } @@ -326,7 +326,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) return err } - encryptedKey, err := ks.keyEncryption.KeyEncryption(key, newPassphrase) + encryptedKey, err := ks.keyEncryption.KeyEncrypt(key, newPassphrase) if err != nil { return err } @@ -334,6 +334,6 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) return ks.cache.update(a, encryptedKey) } -func (ks *KeyStore) SetManager(manager accounts.BackendManager) { +func (ks *KeyStore) SetManager(manager accounts.AccountManager) { ks.manager = manager } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 270e5fa5f3..720b1a6eaa 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -376,5 +376,8 @@ func tmpKeyStore(t *testing.T) (string, *KeyStore) { d := t.TempDir() - return d, NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) + ks := NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) + ks.eventHandler = event.NewEventHandler() + + return d, ks } diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index a6339dba27..1aeaf4f8e5 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -41,56 +41,76 @@ const ( scryptDKLen = 32 ) -type keyStorePassphrase struct { +type passphraseEncryption struct { scryptN int scryptP int } -func (ks keyStorePassphrase) KeyDecryption(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) { - key, err := DecryptKey(encryptedKey, auth) +func (pe passphraseEncryption) KeyDecrypt(encrypted encryptedKey, auth string) (*Key, error) { + key, err := DecryptKey(encrypted, auth) if err != nil { return nil, err } - if key.Address != types.StringToAddress(encryptedKey.Address) { - return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, encryptedKey.Address) + if key.Address != types.StringToAddress(encrypted.Address) { + return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, encrypted.Address) } return key, nil } -func (ks keyStorePassphrase) KeyEncryption(key *Key, auth string) (encryptedKeyJSONV3, error) { - encryptedKey, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) +func (pe passphraseEncryption) KeyEncrypt(key *Key, auth string) (encryptedKey, error) { + encrypted, err := EncryptKey(key, auth, pe.scryptN, pe.scryptP) if err != nil { - return encryptedKeyJSONV3{}, err + return encryptedKey{}, err } - return encryptedKey, nil + return encrypted, nil } -// EncryptDataV3 encrypts the data given as 'data' with the password 'auth'. -func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { +func (pe passphraseEncryption) StoreNewKey(auth string) (encryptedKey, accounts.Account, error) { + key, err := newKey() + if err != nil { + return encryptedKey{}, accounts.Account{}, err + } + + a := accounts.Account{ + Address: key.Address, + } + + encrypted, err := pe.KeyEncrypt(key, auth) + if err != nil { + zeroKey(key.PrivateKey) + + return encryptedKey{}, a, err + } + + return encrypted, a, err +} + +// EncryptData encrypts the data given as 'data' with the password 'auth'. +func EncryptData(data, auth []byte, scryptN, scryptP int) (Crypto, error) { salt := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, salt); err != nil { - return CryptoJSON{}, nil + return Crypto{}, nil } derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) if err != nil { - return CryptoJSON{}, err + return Crypto{}, err } encryptKey := derivedKey[:16] iv := make([]byte, aes.BlockSize) // 16 if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return CryptoJSON{}, err + return Crypto{}, err } cipherText, err := aesCTRXOR(encryptKey, data, iv) if err != nil { - return CryptoJSON{}, err + return Crypto{}, err } mac := crypto.Keccak256(derivedKey[16:32], cipherText) @@ -101,11 +121,11 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) scryptParamsJSON["p"] = scryptP scryptParamsJSON["dklen"] = scryptDKLen scryptParamsJSON["salt"] = hex.EncodeToString(salt) - cipherParamsJSON := cipherparamsJSON{ + cipherParamsJSON := cipherParams{ IV: hex.EncodeToString(iv), } - cryptoStruct := CryptoJSON{ + cryptoStruct := Crypto{ Cipher: "aes-128-ctr", CipherText: hex.EncodeToString(cipherText), CipherParams: cipherParamsJSON, @@ -119,31 +139,31 @@ func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) // EncryptKey encrypts a key using the specified scrypt parameters into a json // blob that can be decrypted later on. -func EncryptKey(key *Key, auth string, scryptN, scryptP int) (encryptedKeyJSONV3, error) { +func EncryptKey(key *Key, auth string, scryptN, scryptP int) (encryptedKey, error) { keyBytes, err := crypto.MarshalECDSAPrivateKey(key.PrivateKey) // TO DO maybe wrong if err != nil { - return encryptedKeyJSONV3{}, err + return encryptedKey{}, err } - cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) + cryptoStruct, err := EncryptData(keyBytes, []byte(auth), scryptN, scryptP) if err != nil { - return encryptedKeyJSONV3{}, err + return encryptedKey{}, err } - encryptedKeyJSONV3 := encryptedKeyJSONV3{ + encryptedKey := encryptedKey{ hex.EncodeToString(key.Address[:]), cryptoStruct, key.ID.String(), version, } - return encryptedKeyJSONV3, nil + return encryptedKey, nil } // DecryptKey decrypts a key from a json blob, returning the private key itself. -func DecryptKey(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) { +func DecryptKey(encrypted encryptedKey, auth string) (*Key, error) { // Parse the json into a simple map to fetch the key version - keyBytes, keyID, err := decryptKeyV3(&encryptedKey, auth) + keyBytes, keyID, err := decryptKey(&encrypted, auth) if err != nil { return nil, err } @@ -165,7 +185,8 @@ func DecryptKey(encryptedKey encryptedKeyJSONV3, auth string) (*Key, error) { }, nil } -func DecryptDataV3(cryptoJSON CryptoJSON, auth string) ([]byte, error) { +// decrypt aes-128-ctr crypted key +func DecryptData(cryptoJSON Crypto, auth string) ([]byte, error) { if cryptoJSON.Cipher != "aes-128-ctr" { return nil, fmt.Errorf("cipher not supported: %v", cryptoJSON.Cipher) } @@ -203,7 +224,7 @@ func DecryptDataV3(cryptoJSON CryptoJSON, auth string) ([]byte, error) { return plainText, err } -func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyID []byte, err error) { +func decryptKey(keyProtected *encryptedKey, auth string) (keyBytes []byte, keyID []byte, err error) { if keyProtected.Version != version { return nil, nil, fmt.Errorf("version not supported: %v", keyProtected.Version) } @@ -215,7 +236,7 @@ func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byt keyID = keyUUID[:] - plainText, err := DecryptDataV3(keyProtected.Crypto, auth) + plainText, err := DecryptData(keyProtected.Crypto, auth) if err != nil { return nil, nil, err } @@ -223,7 +244,7 @@ func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byt return plainText, keyID, err } -func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { +func getKDFKey(cryptoJSON Crypto, auth string) ([]byte, error) { authArray := []byte(auth) salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index d70fa56f8f..9d6b821d9a 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -20,15 +20,15 @@ const ( veryLightScryptP = 1 ) -func TestKeyStorePassphrase(t *testing.T) { +func TestPassphraseEncryption(t *testing.T) { t.Parallel() - ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} + passEncryption := &passphraseEncryption{veryLightScryptN, veryLightScryptP} - k1, account, err := storeNewKey(ks, pass) + k1, account, err := passEncryption.StoreNewKey(pass) require.NoError(t, err) - k2, err := ks.KeyDecryption(k1, pass) + k2, err := passEncryption.KeyDecrypt(k1, pass) require.NoError(t, err) require.Equal(t, types.StringToAddress(k1.Address), k2.Address) @@ -36,30 +36,30 @@ func TestKeyStorePassphrase(t *testing.T) { require.Equal(t, k2.Address, account.Address) } -func TestKeyStorePassphraseDecryptionFail(t *testing.T) { +func TestPassphraseEncryptionDecryptionFail(t *testing.T) { t.Parallel() - ks := &keyStorePassphrase{veryLightScryptN, veryLightScryptP} + passEncryption := &passphraseEncryption{veryLightScryptN, veryLightScryptP} - k1, _, err := storeNewKey(ks, pass) + k1, _, err := passEncryption.StoreNewKey(pass) require.NoError(t, err) - _, err = ks.KeyDecryption(k1, "bar") + _, err = passEncryption.KeyDecrypt(k1, "bar") require.EqualError(t, err, ErrDecrypt.Error()) } // Test and utils for the key store tests in the Ethereum JSON tests; // testdataKeyStoreTests/basic_tests.json -type KeyStoreTestV3 struct { - JSON encryptedKeyJSONV3 - Password string - Priv string +type KeyStoreTest struct { + ecryptedKey encryptedKey + Password string + Priv string } -func TestV3_PBKDF2_1(t *testing.T) { +func Test_PBKDF2_1(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") - testDecryptV3(t, tests["wikipage_test_vector_pbkdf2"]) + tests := loadKeyStoreTest(t, "testdata/test-keys.json") + testDecrypt(t, tests["wikipage_test_vector_pbkdf2"]) } var testsSubmodule = filepath.Join("..", "..", "tests", "testdata", "KeyStoreTests") @@ -72,59 +72,59 @@ func skipIfSubmoduleMissing(t *testing.T) { } } -func TestV3_PBKDF2_2(t *testing.T) { +func Test_PBKDF2_2(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["test1"]) + tests := loadKeyStoreTest(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecrypt(t, tests["test1"]) } -func TestV3_PBKDF2_3(t *testing.T) { +func Test_PBKDF2_3(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["python_generated_test_with_odd_iv"]) + tests := loadKeyStoreTest(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecrypt(t, tests["python_generated_test_with_odd_iv"]) } -func TestV3_PBKDF2_4(t *testing.T) { +func Test_PBKDF2_4(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["evilnonce"]) + tests := loadKeyStoreTest(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecrypt(t, tests["evilnonce"]) } -func TestV3_Scrypt_1(t *testing.T) { +func Test_Scrypt_1(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") - testDecryptV3(t, tests["wikipage_test_vector_scrypt"]) + tests := loadKeyStoreTest(t, "testdata/test-keys.json") + testDecrypt(t, tests["wikipage_test_vector_scrypt"]) } -func TestV3_Scrypt_2(t *testing.T) { +func Test_Scrypt_2(t *testing.T) { skipIfSubmoduleMissing(t) t.Parallel() - tests := loadKeyStoreTestV3(t, filepath.Join(testsSubmodule, "basic_tests.json")) - testDecryptV3(t, tests["test2"]) + tests := loadKeyStoreTest(t, filepath.Join(testsSubmodule, "basic_tests.json")) + testDecrypt(t, tests["test2"]) } -func testDecryptV3(t *testing.T, test KeyStoreTestV3) { +func testDecrypt(t *testing.T, test KeyStoreTest) { t.Helper() - privBytes, _, err := decryptKeyV3(&test.JSON, test.Password) + privBytes, _, err := decryptKey(&test.ecryptedKey, test.Password) require.NoError(t, err) privHex := hex.EncodeToString(privBytes) require.Equal(t, test.Priv, privHex) } -func loadKeyStoreTestV3(t *testing.T, file string) map[string]KeyStoreTestV3 { +func loadKeyStoreTest(t *testing.T, file string) map[string]KeyStoreTest { t.Helper() - tests := make(map[string]KeyStoreTestV3) + tests := make(map[string]KeyStoreTest) err := loadJSON(t, file, &tests) require.NoError(t, err) @@ -139,30 +139,30 @@ func TestKeyForDirectICAP(t *testing.T) { require.True(t, strings.HasPrefix(key.Address.String(), "0x00")) } -func TestV3_31_Byte_Key(t *testing.T) { +func Test_31_Byte_Key(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") - testDecryptV3(t, tests["31_byte_key"]) + tests := loadKeyStoreTest(t, "testdata/test-keys.json") + testDecrypt(t, tests["31_byte_key"]) } -func TestV3_30_Byte_Key(t *testing.T) { +func Test_30_Byte_Key(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3(t, "testdata/test-keys.json") - testDecryptV3(t, tests["30_byte_key"]) + tests := loadKeyStoreTest(t, "testdata/test-keys.json") + testDecrypt(t, tests["30_byte_key"]) } // Tests that a json key file can be decrypted and encrypted in multiple rounds. func TestKeyEncryptDecrypt(t *testing.T) { t.Parallel() - keyEncrypted := new(encryptedKeyJSONV3) + encrypted := new(encryptedKey) keyjson, err := os.ReadFile("testdata/light-test-key.json") require.NoError(t, err) - require.NoError(t, json.Unmarshal(keyjson, keyEncrypted)) + require.NoError(t, json.Unmarshal(keyjson, encrypted)) password := "" address := types.StringToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") @@ -170,16 +170,16 @@ func TestKeyEncryptDecrypt(t *testing.T) { // Do a few rounds of decryption and encryption for i := 0; i < 3; i++ { // Try a bad password first - _, err := DecryptKey(*keyEncrypted, password+"bad") + _, err := DecryptKey(*encrypted, password+"bad") require.Error(t, err) // Decrypt with the correct password - key, err := DecryptKey(*keyEncrypted, password) + key, err := DecryptKey(*encrypted, password) require.NoError(t, err) require.Equal(t, address, key.Address) // Recrypt with a new password and start over password += "new data appended" - *keyEncrypted, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP) + *encrypted, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP) require.NoError(t, err) } } diff --git a/accounts/manager.go b/accounts/manager.go index 6d14fffb44..716e9bb1a0 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -17,7 +17,7 @@ const ( ) type newBackendEvent struct { - backend Backend + backend WalletManager processed chan struct{} } @@ -27,14 +27,13 @@ func (newBackendEvent) Type() event.EventType { } // Manager is an overarching account manager that can communicate with various -// backends for signing transactions. - +// walletManagers for signing transactions. type Manager struct { - backends map[reflect.Type][]Backend - updates chan event.Event - newBackends chan event.Event - wallets []Wallet - blockchain *blockchain.Blockchain + walletManagers map[reflect.Type][]WalletManager + updates chan event.Event + newWalletManagers chan event.Event + wallets []Wallet + blockchain *blockchain.Blockchain quit chan chan error @@ -45,40 +44,40 @@ type Manager struct { } // Creates new instance of manager -func NewManager(blockchain *blockchain.Blockchain, backends ...Backend) *Manager { +func NewManager(blockchain *blockchain.Blockchain, walletManagers ...WalletManager) *Manager { var wallets []Wallet - for _, backend := range backends { - wallets = merge(wallets, backend.Wallets()...) + for _, walletManager := range walletManagers { + wallets = merge(wallets, walletManager.Wallets()...) } updates := make(chan event.Event, managerSubBufferSize) newBackends := make(chan event.Event) eventHandler := event.NewEventHandler() - for _, backend := range backends { + for _, backend := range walletManagers { backend.SetEventHandler(eventHandler) } am := &Manager{ - backends: make(map[reflect.Type][]Backend), - updates: updates, - newBackends: newBackends, - wallets: wallets, - quit: make(chan chan error), - term: make(chan struct{}), - eventHandler: eventHandler, - blockchain: blockchain, + walletManagers: make(map[reflect.Type][]WalletManager), + updates: updates, + newWalletManagers: newBackends, + wallets: wallets, + quit: make(chan chan error), + term: make(chan struct{}), + eventHandler: eventHandler, + blockchain: blockchain, } eventHandler.Subscribe(WalletEventKey, am.updates) - for _, backend := range backends { + for _, backend := range walletManagers { kind := reflect.TypeOf(backend) backend.SetEventHandler(am.eventHandler) backend.SetManager(am) - am.backends[kind] = append(am.backends[kind], backend) + am.walletManagers[kind] = append(am.walletManagers[kind], backend) } go am.update() @@ -102,10 +101,10 @@ func (am *Manager) Close() error { } // Adds backend to list of backends -func (am *Manager) AddBackend(backend Backend) { +func (am *Manager) AddWalletManager(backend WalletManager) { done := make(chan struct{}) - am.newBackends <- newBackendEvent{backend, done} + am.newWalletManagers <- newBackendEvent{backend, done} <-done } @@ -131,7 +130,7 @@ func (am *Manager) update() { } am.lock.Unlock() - case backendEventChan := <-am.newBackends: + case backendEventChan := <-am.newWalletManagers: am.lock.Lock() if backendEventChan.Type() == event.NewBackendType { @@ -140,7 +139,7 @@ func (am *Manager) update() { am.wallets = merge(am.wallets, backend.Wallets()...) backend.SetEventHandler(am.eventHandler) kind := reflect.TypeOf(backend) - am.backends[kind] = append(am.backends[kind], backend) + am.walletManagers[kind] = append(am.walletManagers[kind], backend) am.lock.Unlock() close(bckEvent.processed) } @@ -157,11 +156,11 @@ func (am *Manager) update() { } // Return specific type of backend -func (am *Manager) Backends(kind reflect.Type) []Backend { +func (am *Manager) WalletManagers(kind reflect.Type) []WalletManager { am.lock.RLock() defer am.lock.RUnlock() - return am.backends[kind] + return am.walletManagers[kind] } // Return list of all wallets diff --git a/command/accounts/insert/insert.go b/command/accounts/insert/insert.go index ca559f15d8..99f99332af 100644 --- a/command/accounts/insert/insert.go +++ b/command/accounts/insert/insert.go @@ -17,7 +17,7 @@ var ( func GetCommand() *cobra.Command { importCmd := &cobra.Command{ Use: "insert", - Short: "Insert existing account with private key and auth passphrase", + Short: "Insert existing key to new account with private key and auth passphrase", PreRun: func(cmd *cobra.Command, args []string) { params.jsonRPC = helper.GetJSONRPCAddress(cmd) }, diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index 5229541dfd..1fd222a4a3 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -77,7 +77,8 @@ func (dp dispatcherParams) isExceedingBatchLengthLimit(value uint64) bool { func newDispatcher( logger hclog.Logger, store JSONRPCStore, - params *dispatcherParams, manager accounts.BackendManager, + params *dispatcherParams, + manager accounts.AccountManager, ) (*Dispatcher, error) { d := &Dispatcher{ logger: logger.Named("dispatcher"), @@ -96,7 +97,7 @@ func newDispatcher( return d, nil } -func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager accounts.BackendManager) error { +func (d *Dispatcher) registerEndpoints(store JSONRPCStore, manager accounts.AccountManager) error { d.endpoints.Eth = &Eth{ d.logger, store, diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index 3f44d67d5a..cccf200f78 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -102,7 +102,7 @@ type Eth struct { chainID uint64 filterManager *FilterManager priceLimit uint64 - accManager accounts.BackendManager + accManager accounts.AccountManager } // ChainId returns the chain id of the client diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index dd411453c5..d4eb2f0936 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -60,7 +60,7 @@ type Config struct { } // NewJSONRPC returns the JSONRPC http server -func NewJSONRPC(logger hclog.Logger, config *Config, manager accounts.BackendManager) (*JSONRPC, error) { +func NewJSONRPC(logger hclog.Logger, config *Config, manager accounts.AccountManager) (*JSONRPC, error) { d, err := newDispatcher( logger, config.Store, diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index 3b500a324d..fbf6b718e2 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -3,7 +3,6 @@ package jsonrpc import ( "errors" "fmt" - "math" "time" "github.com/0xPolygon/polygon-edge/accounts" @@ -13,10 +12,10 @@ import ( ) type Personal struct { - accManager accounts.BackendManager + accManager accounts.AccountManager } -func NewPersonal(manager accounts.BackendManager) *Personal { +func NewPersonal(manager accounts.AccountManager) *Personal { return &Personal{accManager: manager} } @@ -69,15 +68,15 @@ func (p *Personal) ImportRawKey(privKey string, password string) (types.Address, } func (p *Personal) UnlockAccount(addr types.Address, password string, duration uint64) (bool, error) { - const max = uint64(time.Duration(math.MaxInt64) / time.Second) + const max = time.Duration(5 * time.Minute) var d time.Duration switch { case duration == 0: - d = 300 * time.Second - case duration > max: - return false, errors.New("unlock duration is too large") + d = max + case time.Duration(duration)*time.Second > max: + d = max default: d = time.Duration(duration) * time.Second } @@ -117,8 +116,8 @@ func (p *Personal) Ecrecover(data, sig []byte) (types.Address, error) { return types.BytesToAddress(addressRaw), nil } -func getKeystore(am accounts.BackendManager) (*keystore.KeyStore, error) { - if ks := am.Backends(keystore.KeyStoreType); len(ks) > 0 { +func getKeystore(am accounts.AccountManager) (*keystore.KeyStore, error) { + if ks := am.WalletManagers(keystore.KeyStoreType); len(ks) > 0 { return ks[0].(*keystore.KeyStore), nil //nolint:forcetypeassert } diff --git a/server/server.go b/server/server.go index 04f1b2ce5b..552dd753d4 100644 --- a/server/server.go +++ b/server/server.go @@ -78,7 +78,7 @@ type Server struct { // libp2p network network *network.Server - accManager accounts.BackendManager + accManager accounts.AccountManager // transaction pool txpool *txpool.TxPool @@ -984,6 +984,9 @@ func (s *Server) Close() { // Close DataDog profiler s.closeDataDogProfiler() + + // Close account manager + s.accManager.Close() } // Entry is a consensus configuration entry From 564e9bf5c93a53ed09e57e826f81242f8e4d934b Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Wed, 19 Jun 2024 15:33:29 +0200 Subject: [PATCH 56/69] small changes --- accounts/keystore/account_store.go | 14 ++++++++------ accounts/keystore/account_store_test.go | 13 +++++++++---- accounts/keystore/crypto.go | 8 ++++---- accounts/keystore/keystore.go | 22 ++++++++++++++++------ accounts/keystore/keystore_fuzz_test.go | 5 ++++- accounts/keystore/keystore_test.go | 5 ++++- accounts/keystore/passphrase.go | 2 +- accounts/keystore/passphrase_test.go | 4 ++-- server/server.go | 9 ++++++--- 9 files changed, 54 insertions(+), 28 deletions(-) diff --git a/accounts/keystore/account_store.go b/accounts/keystore/account_store.go index 08905a3eab..59684cc6fc 100644 --- a/accounts/keystore/account_store.go +++ b/accounts/keystore/account_store.go @@ -22,7 +22,7 @@ type accountStore struct { allMap map[types.Address]encryptedKey } -func newAccountStore(keyDir string, logger hclog.Logger) *accountStore { +func newAccountStore(keyDir string, logger hclog.Logger) (*accountStore, error) { ac := &accountStore{ logger: logger, keyDir: keyDir, @@ -32,7 +32,7 @@ func newAccountStore(keyDir string, logger hclog.Logger) *accountStore { if err := common.CreateDirSafe(keyDir, 0700); err != nil { ac.logger.Error("can't create dir", "err", err) - return nil + return nil, fmt.Errorf("could not create keystore directory: %v", err) } keysPath := path.Join(keyDir, "keys.txt") @@ -43,13 +43,15 @@ func newAccountStore(keyDir string, logger hclog.Logger) *accountStore { if _, err := os.Create(keysPath); err != nil { ac.logger.Error("can't create new file", "err", err) - return nil + return nil, fmt.Errorf("could not create keystore file: %v", err) } } - ac.readAccountsFromFile() //nolint:errcheck + if err := ac.readAccountsFromFile(); err != nil { + return nil, fmt.Errorf("could not read keystore file: %v", err) + } - return ac + return ac, nil } func (ac *accountStore) accounts() []accounts.Account { @@ -147,7 +149,7 @@ func (ac *accountStore) find(a accounts.Account) (accounts.Account, encryptedKey return accounts.Account{}, encryptedKey{}, accounts.ErrNoMatch } -// readAccountsFromFile refresh data of account map +// readAccountsFromFile reads the keystore file and updates the account store. func (ac *accountStore) readAccountsFromFile() error { ac.mu.Lock() defer ac.mu.Unlock() diff --git a/accounts/keystore/account_store_test.go b/accounts/keystore/account_store_test.go index 0ff6a3b947..b2b557a562 100644 --- a/accounts/keystore/account_store_test.go +++ b/accounts/keystore/account_store_test.go @@ -32,7 +32,9 @@ var ( func TestCacheInitialReload(t *testing.T) { t.Parallel() - cache := newAccountStore(cachetestDir, hclog.NewNullLogger()) + cache, err := newAccountStore(cachetestDir, hclog.NewNullLogger()) + require.NoError(t, err) + accs := cache.accounts() require.Equal(t, 3, len(accs)) @@ -51,7 +53,8 @@ func TestCacheAddDelete(t *testing.T) { tDir := t.TempDir() - cache := newAccountStore(tDir, hclog.NewNullLogger()) + cache, err := newAccountStore(tDir, hclog.NewNullLogger()) + require.NoError(t, err) accs := []accounts.Account{ { @@ -126,7 +129,8 @@ func TestCacheFind(t *testing.T) { dir := t.TempDir() - cache := newAccountStore(dir, hclog.NewNullLogger()) + cache, err := newAccountStore(dir, hclog.NewNullLogger()) + require.NoError(t, err) accs := []accounts.Account{ { @@ -185,7 +189,8 @@ func TestCacheUpdate(t *testing.T) { keyDir := t.TempDir() - accountCache := newAccountStore(keyDir, hclog.NewNullLogger()) + accountCache, err := newAccountStore(keyDir, hclog.NewNullLogger()) + require.NoError(t, err) list := accountCache.accounts() if len(list) > 0 { diff --git a/accounts/keystore/crypto.go b/accounts/keystore/crypto.go index 7de38760f2..51b4a4746e 100644 --- a/accounts/keystore/crypto.go +++ b/accounts/keystore/crypto.go @@ -25,12 +25,12 @@ type Key struct { } type keyEncryption interface { - // decrypts key and return non crypted key + // KeyDecrypt decrypts the key using the auth string KeyDecrypt(encrypted encryptedKey, auth string) (*Key, error) - // get non crypted key and do encryption + // KeyEncrypt encrypts the key using the auth string KeyEncrypt(k *Key, auth string) (encryptedKey, error) - - StoreNewKey(auth string) (encryptedKey, accounts.Account, error) + // CreateNewKey creates a new key + CreateNewKey(auth string) (encryptedKey, accounts.Account, error) } type encryptedKey struct { diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 942241ed15..5bb0468a9b 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -3,6 +3,7 @@ package keystore import ( "crypto/ecdsa" "errors" + "fmt" "path/filepath" "reflect" "sync" @@ -48,17 +49,24 @@ type unlocked struct { abort chan struct{} } -func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) *KeyStore { +func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) (*KeyStore, error) { ks := &KeyStore{keyEncryption: &passphraseEncryption{scryptN, scryptP}} - ks.init(keyDir, logger) + if err := ks.init(keyDir, logger); err != nil { + return nil, fmt.Errorf("could not initialize keystore: %v", err) + } - return ks + return ks, nil } -func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { +func (ks *KeyStore) init(keyDir string, logger hclog.Logger) error { ks.unlocked = make(map[types.Address]*unlocked) - ks.cache = newAccountStore(keyDir, logger) + cache, err := newAccountStore(keyDir, logger) + if err != nil { + return err + } + + ks.cache = cache accs := ks.cache.accounts() ks.wallets = make([]accounts.Wallet, len(accs)) @@ -66,6 +74,8 @@ func (ks *KeyStore) init(keyDir string, logger hclog.Logger) { for i := 0; i < len(accs); i++ { ks.wallets[i] = &keyStoreWallet{account: accs[i], keyStore: ks} } + + return nil } func (ks *KeyStore) Wallets() []accounts.Wallet { @@ -270,7 +280,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A } func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { - encryptedKey, account, err := ks.keyEncryption.StoreNewKey(passphrase) + encryptedKey, account, err := ks.keyEncryption.CreateNewKey(passphrase) if err != nil { return accounts.Account{}, err } diff --git a/accounts/keystore/keystore_fuzz_test.go b/accounts/keystore/keystore_fuzz_test.go index a793ead816..c4c67b2da6 100644 --- a/accounts/keystore/keystore_fuzz_test.go +++ b/accounts/keystore/keystore_fuzz_test.go @@ -8,7 +8,10 @@ import ( func FuzzPassword(f *testing.F) { f.Fuzz(func(t *testing.T, password string) { - ks := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger()) + ks, err := NewKeyStore(t.TempDir(), LightScryptN, LightScryptP, hclog.NewNullLogger()) + if err != nil { + t.Fatal(err) + } a, err := ks.NewAccount(password) if err != nil { diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 720b1a6eaa..6f7b24f6e6 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -8,6 +8,7 @@ import ( "github.com/0xPolygon/polygon-edge/accounts" "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/stretchr/testify/require" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/types" @@ -376,7 +377,9 @@ func tmpKeyStore(t *testing.T) (string, *KeyStore) { d := t.TempDir() - ks := NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) + ks, err := NewKeyStore(d, veryLightScryptN, veryLightScryptP, hclog.NewNullLogger()) + require.NoError(t, err) + ks.eventHandler = event.NewEventHandler() return d, ks diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 1aeaf4f8e5..e22e1c2d1e 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -68,7 +68,7 @@ func (pe passphraseEncryption) KeyEncrypt(key *Key, auth string) (encryptedKey, return encrypted, nil } -func (pe passphraseEncryption) StoreNewKey(auth string) (encryptedKey, accounts.Account, error) { +func (pe passphraseEncryption) CreateNewKey(auth string) (encryptedKey, accounts.Account, error) { key, err := newKey() if err != nil { return encryptedKey{}, accounts.Account{}, err diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 9d6b821d9a..28c45a54ad 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -25,7 +25,7 @@ func TestPassphraseEncryption(t *testing.T) { passEncryption := &passphraseEncryption{veryLightScryptN, veryLightScryptP} - k1, account, err := passEncryption.StoreNewKey(pass) + k1, account, err := passEncryption.CreateNewKey(pass) require.NoError(t, err) k2, err := passEncryption.KeyDecrypt(k1, pass) @@ -41,7 +41,7 @@ func TestPassphraseEncryptionDecryptionFail(t *testing.T) { passEncryption := &passphraseEncryption{veryLightScryptN, veryLightScryptP} - k1, _, err := passEncryption.StoreNewKey(pass) + k1, _, err := passEncryption.CreateNewKey(pass) require.NoError(t, err) _, err = passEncryption.KeyDecrypt(k1, "bar") diff --git a/server/server.go b/server/server.go index 552dd753d4..81d7fce8de 100644 --- a/server/server.go +++ b/server/server.go @@ -347,9 +347,12 @@ func NewServer(config *Config) (*Server, error) { // setup account manager { - m.accManager = accounts.NewManager( - m.blockchain, - keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, m.logger)) + keystore, err := keystore.NewKeyStore(keystore.DefaultStorage, keystore.LightScryptN, keystore.LightScryptP, m.logger) + if err != nil { + return nil, err + } + + m.accManager = accounts.NewManager(m.blockchain, keystore) } // here we can provide some other configuration From 326c8e6f44aeaea3de3bd7d6f58782ec204c021e Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Wed, 19 Jun 2024 15:39:56 +0200 Subject: [PATCH 57/69] lint fix --- accounts/event/event_handler_test.go | 2 -- accounts/keystore/account_store.go | 6 +++--- accounts/keystore/keystore.go | 1 + accounts/keystore/keystore_test.go | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/accounts/event/event_handler_test.go b/accounts/event/event_handler_test.go index 3324abd1d4..f318d151f4 100644 --- a/accounts/event/event_handler_test.go +++ b/accounts/event/event_handler_test.go @@ -134,7 +134,6 @@ func TestMultipleSubscribers(t *testing.T) { } require.Equal(t, event, receivedEvent) - if received == 0x03 { break } @@ -142,4 +141,3 @@ func TestMultipleSubscribers(t *testing.T) { require.NoError(t, err) } - diff --git a/accounts/keystore/account_store.go b/accounts/keystore/account_store.go index 59684cc6fc..1f1a14421e 100644 --- a/accounts/keystore/account_store.go +++ b/accounts/keystore/account_store.go @@ -32,7 +32,7 @@ func newAccountStore(keyDir string, logger hclog.Logger) (*accountStore, error) if err := common.CreateDirSafe(keyDir, 0700); err != nil { ac.logger.Error("can't create dir", "err", err) - return nil, fmt.Errorf("could not create keystore directory: %v", err) + return nil, fmt.Errorf("could not create keystore directory: %w", err) } keysPath := path.Join(keyDir, "keys.txt") @@ -43,12 +43,12 @@ func newAccountStore(keyDir string, logger hclog.Logger) (*accountStore, error) if _, err := os.Create(keysPath); err != nil { ac.logger.Error("can't create new file", "err", err) - return nil, fmt.Errorf("could not create keystore file: %v", err) + return nil, fmt.Errorf("could not create keystore file: %w", err) } } if err := ac.readAccountsFromFile(); err != nil { - return nil, fmt.Errorf("could not read keystore file: %v", err) + return nil, fmt.Errorf("could not read keystore file: %w", err) } return ac, nil diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 5bb0468a9b..db05650faf 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -61,6 +61,7 @@ func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) (*Key func (ks *KeyStore) init(keyDir string, logger hclog.Logger) error { ks.unlocked = make(map[types.Address]*unlocked) + cache, err := newAccountStore(keyDir, logger) if err != nil { return err diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 6f7b24f6e6..c0db6a23f1 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -219,7 +219,7 @@ func TestWalletNotifications(t *testing.T) { ks.SetEventHandler(eventHandler) // Subscribe to the wallet feed and collect events. - var ( //nolint:prealloc + var ( events []walletEvent updates = make(chan event.Event) end = make(chan interface{}) From 98c4d083cf6d248fc6148e255d9438f8ef097e73 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Wed, 19 Jun 2024 15:44:23 +0200 Subject: [PATCH 58/69] lint fix 2 --- accounts/event/event_handler_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/accounts/event/event_handler_test.go b/accounts/event/event_handler_test.go index f318d151f4..4fff116c5b 100644 --- a/accounts/event/event_handler_test.go +++ b/accounts/event/event_handler_test.go @@ -134,6 +134,7 @@ func TestMultipleSubscribers(t *testing.T) { } require.Equal(t, event, receivedEvent) + if received == 0x03 { break } From 153a4c9ac70519ca52c1e5442eafa4a6391799aa Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Wed, 19 Jun 2024 15:45:51 +0200 Subject: [PATCH 59/69] lint fix 2 --- accounts/keystore/keystore_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index c0db6a23f1..23809f91fc 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -220,7 +220,7 @@ func TestWalletNotifications(t *testing.T) { // Subscribe to the wallet feed and collect events. var ( - events []walletEvent + events = make([]walletEvent, 0) updates = make(chan event.Event) end = make(chan interface{}) ) @@ -245,7 +245,7 @@ func TestWalletNotifications(t *testing.T) { // Randomly add and remove accounts. var ( live = make(map[types.Address]accounts.Account) - wantEvents []walletEvent + wantEvents = make([]walletEvent, 0) ) for i := 0; i < 1024; i++ { From 7278f53e17ffbfd22269f870bda89463f35cb0e5 Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Wed, 19 Jun 2024 16:15:58 +0200 Subject: [PATCH 60/69] tests fix --- accounts/keystore/crypto.go | 4 ++-- accounts/keystore/passphrase.go | 2 +- accounts/keystore/passphrase_test.go | 8 ++++---- jsonrpc/personal_endpoint.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/accounts/keystore/crypto.go b/accounts/keystore/crypto.go index 51b4a4746e..c74e8c80f3 100644 --- a/accounts/keystore/crypto.go +++ b/accounts/keystore/crypto.go @@ -43,13 +43,13 @@ type encryptedKey struct { type Crypto struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"` - CipherParams cipherParams `json:"cipherparams"` + CipherParams CipherParams `json:"cipherparams"` KDF string `json:"kdf"` KDFParams map[string]interface{} `json:"kdfparams"` MAC string `json:"mac"` } -type cipherParams struct { +type CipherParams struct { IV string `json:"iv"` } diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index e22e1c2d1e..d29f48cd76 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -121,7 +121,7 @@ func EncryptData(data, auth []byte, scryptN, scryptP int) (Crypto, error) { scryptParamsJSON["p"] = scryptP scryptParamsJSON["dklen"] = scryptDKLen scryptParamsJSON["salt"] = hex.EncodeToString(salt) - cipherParamsJSON := cipherParams{ + cipherParamsJSON := CipherParams{ IV: hex.EncodeToString(iv), } diff --git a/accounts/keystore/passphrase_test.go b/accounts/keystore/passphrase_test.go index 28c45a54ad..80560b54b0 100644 --- a/accounts/keystore/passphrase_test.go +++ b/accounts/keystore/passphrase_test.go @@ -51,9 +51,9 @@ func TestPassphraseEncryptionDecryptionFail(t *testing.T) { // Test and utils for the key store tests in the Ethereum JSON tests; // testdataKeyStoreTests/basic_tests.json type KeyStoreTest struct { - ecryptedKey encryptedKey - Password string - Priv string + EncryptedKey encryptedKey `json:"json"` + Password string `json:"password"` + Priv string `json:"priv"` } func Test_PBKDF2_1(t *testing.T) { @@ -114,7 +114,7 @@ func Test_Scrypt_2(t *testing.T) { func testDecrypt(t *testing.T, test KeyStoreTest) { t.Helper() - privBytes, _, err := decryptKey(&test.ecryptedKey, test.Password) + privBytes, _, err := decryptKey(&test.EncryptedKey, test.Password) require.NoError(t, err) privHex := hex.EncodeToString(privBytes) diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index fbf6b718e2..f8653f3be4 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -68,7 +68,7 @@ func (p *Personal) ImportRawKey(privKey string, password string) (types.Address, } func (p *Personal) UnlockAccount(addr types.Address, password string, duration uint64) (bool, error) { - const max = time.Duration(5 * time.Minute) + const max = 5 * time.Minute var d time.Duration From ee43d9d3d947f92abc1a750e48a8c14bd07133ed Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Wed, 19 Jun 2024 16:25:54 +0200 Subject: [PATCH 61/69] lint again --- accounts/keystore/keystore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index db05650faf..11c1269dec 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -53,7 +53,7 @@ func NewKeyStore(keyDir string, scryptN, scryptP int, logger hclog.Logger) (*Key ks := &KeyStore{keyEncryption: &passphraseEncryption{scryptN, scryptP}} if err := ks.init(keyDir, logger); err != nil { - return nil, fmt.Errorf("could not initialize keystore: %v", err) + return nil, fmt.Errorf("could not initialize keystore: %w", err) } return ks, nil From e66d92692ad751a56f7c76a474577d9163eeb939 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 07:34:09 +0200 Subject: [PATCH 62/69] switch to if in personal --- jsonrpc/personal_endpoint.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/jsonrpc/personal_endpoint.go b/jsonrpc/personal_endpoint.go index f8653f3be4..9ec4fd4d2a 100644 --- a/jsonrpc/personal_endpoint.go +++ b/jsonrpc/personal_endpoint.go @@ -72,12 +72,9 @@ func (p *Personal) UnlockAccount(addr types.Address, password string, duration u var d time.Duration - switch { - case duration == 0: + if duration == 0 || time.Duration(duration)*time.Second > max { d = max - case time.Duration(duration)*time.Second > max: - d = max - default: + } else { d = time.Duration(duration) * time.Second } From 9555fadd33a39802e286082c4415ec2cc521a7b3 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 07:45:28 +0200 Subject: [PATCH 63/69] fuzz test fix --- accounts/keystore/keystore_fuzz_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/accounts/keystore/keystore_fuzz_test.go b/accounts/keystore/keystore_fuzz_test.go index c4c67b2da6..9e1b7f51db 100644 --- a/accounts/keystore/keystore_fuzz_test.go +++ b/accounts/keystore/keystore_fuzz_test.go @@ -3,6 +3,7 @@ package keystore import ( "testing" + "github.com/0xPolygon/polygon-edge/accounts/event" "github.com/hashicorp/go-hclog" ) @@ -13,6 +14,8 @@ func FuzzPassword(f *testing.F) { t.Fatal(err) } + ks.eventHandler = event.NewEventHandler() + a, err := ks.NewAccount(password) if err != nil { t.Fatal(err) From e23464604f41cd15599ba2f556b564104c094f64 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 10:35:30 +0200 Subject: [PATCH 64/69] manager test --- accounts/event/event_handler.go | 2 +- accounts/manager.go | 38 +++--- accounts/manager_test.go | 221 ++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 accounts/manager_test.go diff --git a/accounts/event/event_handler.go b/accounts/event/event_handler.go index fe50ad513e..e67668c0bc 100644 --- a/accounts/event/event_handler.go +++ b/accounts/event/event_handler.go @@ -53,7 +53,7 @@ type EventType byte const ( WalletEventType EventType = 0x01 - NewBackendType EventType = 0x02 + NewWalletManagerType EventType = 0x02 ) type Event interface { diff --git a/accounts/manager.go b/accounts/manager.go index 716e9bb1a0..3b5034971b 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -17,13 +17,13 @@ const ( ) type newBackendEvent struct { - backend WalletManager + walletManager WalletManager processed chan struct{} } func (newBackendEvent) Type() event.EventType { - return event.NewBackendType + return event.NewWalletManagerType } // Manager is an overarching account manager that can communicate with various @@ -72,12 +72,12 @@ func NewManager(blockchain *blockchain.Blockchain, walletManagers ...WalletManag eventHandler.Subscribe(WalletEventKey, am.updates) - for _, backend := range walletManagers { - kind := reflect.TypeOf(backend) + for _, walletManager := range walletManagers { + kind := reflect.TypeOf(walletManager) - backend.SetEventHandler(am.eventHandler) - backend.SetManager(am) - am.walletManagers[kind] = append(am.walletManagers[kind], backend) + walletManager.SetEventHandler(am.eventHandler) + walletManager.SetManager(am) + am.walletManagers[kind] = append(am.walletManagers[kind], walletManager) } go am.update() @@ -101,10 +101,10 @@ func (am *Manager) Close() error { } // Adds backend to list of backends -func (am *Manager) AddWalletManager(backend WalletManager) { +func (am *Manager) AddWalletManager(walletManager WalletManager) { done := make(chan struct{}) - am.newWalletManagers <- newBackendEvent{backend, done} + am.newWalletManagers <- newBackendEvent{walletManager, done} <-done } @@ -130,18 +130,20 @@ func (am *Manager) update() { } am.lock.Unlock() - case backendEventChan := <-am.newWalletManagers: + case walletManagerEventChan := <-am.newWalletManagers: am.lock.Lock() - if backendEventChan.Type() == event.NewBackendType { - bckEvent := backendEventChan.(newBackendEvent) //nolint:forcetypeassert - backend := bckEvent.backend - am.wallets = merge(am.wallets, backend.Wallets()...) - backend.SetEventHandler(am.eventHandler) - kind := reflect.TypeOf(backend) - am.walletManagers[kind] = append(am.walletManagers[kind], backend) + if walletManagerEventChan.Type() == event.NewWalletManagerType { + walletManagerEvent := walletManagerEventChan.(newBackendEvent) //nolint:forcetypeassert + walletManager := walletManagerEvent.walletManager + am.wallets = merge(am.wallets, walletManager.Wallets()...) + walletManager.SetEventHandler(am.eventHandler) + kind := reflect.TypeOf(walletManager) + am.walletManagers[kind] = append(am.walletManagers[kind], walletManager) am.lock.Unlock() - close(bckEvent.processed) + close(walletManagerEvent.processed) + + continue } am.lock.Unlock() diff --git a/accounts/manager_test.go b/accounts/manager_test.go new file mode 100644 index 0000000000..d5060279ff --- /dev/null +++ b/accounts/manager_test.go @@ -0,0 +1,221 @@ +package accounts + +import ( + "reflect" + "testing" + "time" + + "github.com/0xPolygon/polygon-edge/accounts/event" + "github.com/0xPolygon/polygon-edge/blockchain" + "github.com/0xPolygon/polygon-edge/types" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type keystoreMock struct { + mock.Mock +} + +func (m *keystoreMock) Wallets() []Wallet { + args := m.Called() + + return args.Get(0).([]Wallet) +} + +func (m *keystoreMock) SetEventHandler(eventHandler *event.EventHandler) { + +} + +func (m *keystoreMock) SetManager(manager AccountManager) { + +} + +type keystoreWalletMock struct { + mock.Mock +} + +func (m *keystoreWalletMock) Status() (string, error) { + args := m.Called() + + return args.String(0), args.Error(1) +} + +func (m *keystoreWalletMock) Open(passphrase string) error { + args := m.Called(passphrase) + + return args.Error(0) +} + +func (m *keystoreWalletMock) Close() error { + args := m.Called() + + return args.Error(0) +} + +func (m *keystoreWalletMock) Accounts() []Account { + args := m.Called() + + return args.Get(0).([]Account) +} + +func (m *keystoreWalletMock) Contains(account Account) bool { + args := m.Called(account) + + return args.Bool(0) +} + +func (m *keystoreWalletMock) SignData(account Account, mimeType string, data []byte) ([]byte, error) { + args := m.Called(account, mimeType, data) + + return args.Get(0).([]byte), args.Error(1) +} + +func (m *keystoreWalletMock) SignDataWithPassphrase(account Account, + passphrase, mimeType string, + data []byte) ([]byte, error) { + args := m.Called(account, passphrase, mimeType, data) + + return args.Get(0).([]byte), args.Error(1) +} + +func (m *keystoreWalletMock) SignText(account Account, text []byte) ([]byte, error) { + args := m.Called(account, text) + + return args.Get(0).([]byte), args.Error(1) +} + +func (m *keystoreWalletMock) SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error) { + args := m.Called(account, passphrase, hash) + + return args.Get(0).([]byte), args.Error(1) +} + +func (m *keystoreWalletMock) SignTx(account Account, tx *types.Transaction) (*types.Transaction, error) { + args := m.Called(account, tx) + + return args.Get(0).(*types.Transaction), args.Error(1) +} + +// SignTxWithPassphrase is identical to SignTx, but also takes a password +func (m *keystoreWalletMock) SignTxWithPassphrase(account Account, + passphrase string, + tx *types.Transaction) (*types.Transaction, error) { + args := m.Called(account, passphrase, tx) + + return args.Get(0).(*types.Transaction), args.Error(1) +} + +var keystoreMockType = reflect.TypeOf(&keystoreMock{}) + +// TestNewManager tests the creation of a new Manager instance +func TestNewManager(t *testing.T) { + ksMock := new(keystoreMock) + ksMock.On("Wallets").Return([]Wallet{&keystoreWalletMock{}}).Once() + + manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) + + require.NotNil(t, manager) + require.Equal(t, 1, len(manager.Wallets())) + require.Equal(t, 1, len(manager.WalletManagers(keystoreMockType))) +} + +// TestAddWalletManager tests adding a new wallet manager +func TestAddWalletManager(t *testing.T) { + manager := NewManager(blockchain.NewTestBlockchain(t, nil)) + + ksMock := new(keystoreMock) + ksMock.On("Wallets").Return([]Wallet{&keystoreWalletMock{}}).Once() + + manager.AddWalletManager(ksMock) + require.Equal(t, 1, len(manager.Wallets())) + require.Equal(t, 1, len(manager.WalletManagers(keystoreMockType))) +} + +func TestClose(t *testing.T) { + ksMock := new(keystoreMock) + + keystoreWallet := new(keystoreWalletMock) + ksMock.On("Wallets").Return([]Wallet{keystoreWallet}) + keystoreWallet.On("Close").Return(nil) + + manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) + err := manager.Close() + require.Nil(t, err) +} + +func TestFind(t *testing.T) { + account := Account{Address: types.ZeroAddress} + + ksMock := new(keystoreMock) + + keystoreWallet := new(keystoreWalletMock) + + ksMock.On("Wallets").Return([]Wallet{keystoreWallet}) + keystoreWallet.On("Contains", account).Return(true).Once() + keystoreWallet.On("Contains", account).Return(false).Once() + + manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) + wallet, err := manager.Find(account) + require.NoError(t, err) + require.NotNil(t, wallet) + + wallet, err = manager.Find(account) + require.Error(t, err) + require.Nil(t, wallet) +} + +func TestAccounts(t *testing.T) { + account := Account{Address: types.ZeroAddress} + + ksMock := new(keystoreMock) + + ksWalletMock := new(keystoreWalletMock) + + ksMock.On("Wallets").Return([]Wallet{ksWalletMock}) + ksWalletMock.On("Accounts").Return([]Account{account}) + + manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) + accounts := manager.Accounts() + require.Equal(t, 1, len(accounts)) + require.Equal(t, types.ZeroAddress, accounts[0]) +} + +func TestGetSigner(t *testing.T) { + ksMock := new(keystoreMock) + + ksWalletMock := new(keystoreWalletMock) + + ksMock.On("Wallets").Return([]Wallet{ksWalletMock}) + + manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) + + signer := manager.GetSigner() + + require.NotNil(t, signer) +} + +func TestArrivedDrop(t *testing.T) { + ksMock := new(keystoreMock) + + ksWalletMock := new(keystoreWalletMock) + ksWalletMock.On("Accounts").Return([]Account{{Address: types.StringToAddress("0x1")}}) + + ksMock.On("Wallets").Return([]Wallet{ksWalletMock}) + + manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) + + ksWalletMockArrivedDropped := new(keystoreWalletMock) + ksWalletMockArrivedDropped.On("Accounts").Return([]Account{{Address: types.StringToAddress("0x2")}}) + + manager.eventHandler.Publish(WalletEventKey, WalletEvent{Wallet: ksWalletMockArrivedDropped, Kind: WalletArrived}) + + time.Sleep(2 * time.Second) + + require.Equal(t, 2, len(manager.Wallets())) + + manager.eventHandler.Publish(WalletEventKey, WalletEvent{Wallet: ksWalletMockArrivedDropped, Kind: WalletDropped}) + + time.Sleep(2 * time.Second) + + require.Equal(t, 1, len(manager.Wallets())) +} From 4a39c7dbf90738650c03894b053f26b7669ef13a Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 10:58:50 +0200 Subject: [PATCH 65/69] manager unlock change --- accounts/manager.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/accounts/manager.go b/accounts/manager.go index 3b5034971b..b2cd02b27c 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -140,10 +140,7 @@ func (am *Manager) update() { walletManager.SetEventHandler(am.eventHandler) kind := reflect.TypeOf(walletManager) am.walletManagers[kind] = append(am.walletManagers[kind], walletManager) - am.lock.Unlock() close(walletManagerEvent.processed) - - continue } am.lock.Unlock() From db33c8125a94cd90fb15d82a844e15d434d9fb2f Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 12:06:14 +0200 Subject: [PATCH 66/69] manager test optimziation --- accounts/manager.go | 1 + accounts/manager_test.go | 23 +++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/accounts/manager.go b/accounts/manager.go index b2cd02b27c..4467b15dd3 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -140,6 +140,7 @@ func (am *Manager) update() { walletManager.SetEventHandler(am.eventHandler) kind := reflect.TypeOf(walletManager) am.walletManagers[kind] = append(am.walletManagers[kind], walletManager) + close(walletManagerEvent.processed) } diff --git a/accounts/manager_test.go b/accounts/manager_test.go index d5060279ff..375d6c745a 100644 --- a/accounts/manager_test.go +++ b/accounts/manager_test.go @@ -115,8 +115,8 @@ func TestNewManager(t *testing.T) { manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) require.NotNil(t, manager) - require.Equal(t, 1, len(manager.Wallets())) - require.Equal(t, 1, len(manager.WalletManagers(keystoreMockType))) + require.Len(t, manager.Wallets(), 1) + require.Len(t, manager.WalletManagers(keystoreMockType), 1) } // TestAddWalletManager tests adding a new wallet manager @@ -127,8 +127,8 @@ func TestAddWalletManager(t *testing.T) { ksMock.On("Wallets").Return([]Wallet{&keystoreWalletMock{}}).Once() manager.AddWalletManager(ksMock) - require.Equal(t, 1, len(manager.Wallets())) - require.Equal(t, 1, len(manager.WalletManagers(keystoreMockType))) + require.Len(t, manager.Wallets(), 1) + require.Len(t, manager.WalletManagers(keystoreMockType), 1) } func TestClose(t *testing.T) { @@ -139,8 +139,9 @@ func TestClose(t *testing.T) { keystoreWallet.On("Close").Return(nil) manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) - err := manager.Close() - require.Nil(t, err) + + require.NotNil(t, manager) + require.NoError(t, manager.Close()) } func TestFind(t *testing.T) { @@ -176,7 +177,7 @@ func TestAccounts(t *testing.T) { manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) accounts := manager.Accounts() - require.Equal(t, 1, len(accounts)) + require.Len(t, accounts, 1) require.Equal(t, types.ZeroAddress, accounts[0]) } @@ -189,9 +190,7 @@ func TestGetSigner(t *testing.T) { manager := NewManager(blockchain.NewTestBlockchain(t, nil), ksMock) - signer := manager.GetSigner() - - require.NotNil(t, signer) + require.NotNil(t, manager.GetSigner()) } func TestArrivedDrop(t *testing.T) { @@ -211,11 +210,11 @@ func TestArrivedDrop(t *testing.T) { time.Sleep(2 * time.Second) - require.Equal(t, 2, len(manager.Wallets())) + require.Len(t, manager.Wallets(), 2) manager.eventHandler.Publish(WalletEventKey, WalletEvent{Wallet: ksWalletMockArrivedDropped, Kind: WalletDropped}) time.Sleep(2 * time.Second) - require.Equal(t, 1, len(manager.Wallets())) + require.Len(t, manager.Wallets(), 1) } From 6f86da524130fdd53676ebdcabcdf88bd73fb5c7 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 12:56:39 +0200 Subject: [PATCH 67/69] readme for account management --- accounts/README.MD | 80 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 accounts/README.MD diff --git a/accounts/README.MD b/accounts/README.MD new file mode 100644 index 0000000000..5b8bbfde7b --- /dev/null +++ b/accounts/README.MD @@ -0,0 +1,80 @@ +Account management +=== + +## Table of Contents + +[TOC] + +## Problem +Account management is a feature that implements the logic for storing private keys for non-validator users. These private keys are used for signing various types of data. Currently, we have implemented file storage on the local disk. The keys are stored in a text file in JSON format. To ensure security, the private keys are encrypted, so opening the file does not compromise sensitive data. + +## Account storage +Account storage is component of account management which is used to manage keys file. When system starts it can create a new file if neccessary or load account from existing file.Account cache adds, delete or update accounts in file. Its primary purpose is to keep the key file updated with the latest account data. + +## Key storage +Keys are stored in a file in JSON format. All keys are stored in the same file. The data structure used to marshal the keys is map[Address]EncryptedKey, making it very easy to retrieve a keys from the file while keeping the sensitive data secure. + +## Keystore +The keystore is a layer between the AccountManager and the AccountStore, where cryptographic operations take place. The keystore maintains its own list of wallets (wrappers around accounts) and notifies the account manager about any changes (such as accounts being added or deleted). In this layer, transactions and data are signed, and private keys are kept unlocked for actions that require an unlocked private key (such as eth_sign). + +## Account manager +The account manager is the top layer, and the system interacts with it for every account management task. It handles all types of key storage (currently, we only support local keystore storage) and merges all data from different storage types into one list. + +## Interacting with account management +Accounts are required for some features. So our software support two diffent ways of interaction with account management. You can interact with commands or with different json rpc calls. + +### Suported json-rpc calls +* **personal_listAccount** - return addresses of all accounts +* **personal_newAccount** - create new account and return address of account +* **personal_importRawKey** - insert hex raw key and storing them in storage +* **personal_unlockAccount** - unlockes account so that can sign transaction with account private key, it used for eth_sign and other calls that doesn't send password for decrypt private key +* **personal_lockAccount** - lock unlocked account +* **personal_updatePassphrase** - change passphrase of existing account + +### Supported commands +* **create** - create new account and return address of account +* **insert** - insert command insert existing private key and store in keystore +* **update** - change passphrase of existing account + +## ImportRawKey JSON-RPC Flow + +``` sequence + Network->Personal endpoint:personal_improtRawKey + Personal endpoint->Manager:WalletsManager()\nKeyStoreType + Manager->Personal endpoint:KeyStore\n(WalletManager) + Personal endpoint->KeyStore:ImportECDSA() + KeyStore->KeyStore:KeyEncrypt() + KeyStore->AccountStore:add() + KeyStore->Manager:WalletEvent + Manager->Manager:Updater + AccountStore->AccountStore:add + Note right of AccountStore:KeyFile +``` + +## Insert Command Flow +``` sequence + Command->Personal endpoint:personal_improtRawKey + Personal endpoint->Manager:WalletsManager()\nKeyStoreType + Manager->Personal endpoint:KeyStore\n(WalletManager) + Personal endpoint->KeyStore:ImportECDSA() + KeyStore->KeyStore:KeyEncrypt() + KeyStore->AccountStore:add() + KeyStore->Manager:WalletEvent + Manager->Manager:Updater + AccountStore->AccountStore:add + Note right of AccountStore:KeyFile +``` + + + +## Private keys encryption +Encryption of private keys is crucial for protecting blockchain users from unauthorized actions. We use the AES-128-CTR cryptographic algorithm to encrypt private keys, ensuring a high level of security. AES-128-CTR is a symmetric encryption algorithm, meaning the same key is used for both encryption and decryption. + +To enhance security, we introduce an authentication string used to encrypt and decrypt private keys. From this authentication string, we derive an appropriate encryption key, which serves as the private key for AES-128-CTR. To access private keys, users must know their authentication password, which allows for the decryption of the private keys. + +Once a private key is decrypted, it is temporarily stored in memory and deleted after each use to maintain security. Every user, when creating a new account or importing an existing key, determines their authentication string and must remember it. + + + + + From 39769ef93f82c843074f56f8cc25e154b71ee753 Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 13:16:39 +0200 Subject: [PATCH 68/69] to mermaid --- accounts/README.MD | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/accounts/README.MD b/accounts/README.MD index 5b8bbfde7b..beed50c660 100644 --- a/accounts/README.MD +++ b/accounts/README.MD @@ -38,30 +38,32 @@ Accounts are required for some features. So our software support two diffent way ## ImportRawKey JSON-RPC Flow -``` sequence - Network->Personal endpoint:personal_improtRawKey - Personal endpoint->Manager:WalletsManager()\nKeyStoreType - Manager->Personal endpoint:KeyStore\n(WalletManager) - Personal endpoint->KeyStore:ImportECDSA() - KeyStore->KeyStore:KeyEncrypt() - KeyStore->AccountStore:add() - KeyStore->Manager:WalletEvent - Manager->Manager:Updater - AccountStore->AccountStore:add +``` mermaid + sequenceDiagram + Network->>Personal endpoint:personal_improtRawKey + Personal endpoint->>Manager:WalletsManager()\nKeyStoreType + Manager->>Personal endpoint:KeyStore\n(WalletManager) + Personal endpoint->>KeyStore:ImportECDSA() + KeyStore->>KeyStore:KeyEncrypt() + KeyStore->>AccountStore:add() + KeyStore->>Manager:WalletEvent + Manager->>Manager:Updater + AccountStore->>AccountStore:add Note right of AccountStore:KeyFile ``` ## Insert Command Flow -``` sequence - Command->Personal endpoint:personal_improtRawKey - Personal endpoint->Manager:WalletsManager()\nKeyStoreType - Manager->Personal endpoint:KeyStore\n(WalletManager) - Personal endpoint->KeyStore:ImportECDSA() - KeyStore->KeyStore:KeyEncrypt() - KeyStore->AccountStore:add() - KeyStore->Manager:WalletEvent - Manager->Manager:Updater - AccountStore->AccountStore:add +``` mermaid + sequenceDiagram + Command->>Personal endpoint:personal_improtRawKey + Personal endpoint->>Manager:WalletsManager()\nKeyStoreType + Manager->>Personal endpoint:KeyStore\n(WalletManager) + Personal endpoint->>KeyStore:ImportECDSA() + KeyStore->>KeyStore:KeyEncrypt() + KeyStore->>AccountStore:add() + KeyStore->>Manager:WalletEvent + Manager->>Manager:Updater + AccountStore->>AccountStore:add Note right of AccountStore:KeyFile ``` From bc0daea4a595ca9678c98435840c1cb8b221f97a Mon Sep 17 00:00:00 2001 From: Dusan Nosovic Date: Thu, 20 Jun 2024 13:35:02 +0200 Subject: [PATCH 69/69] readme fix --- accounts/README.MD | 4 ---- 1 file changed, 4 deletions(-) diff --git a/accounts/README.MD b/accounts/README.MD index beed50c660..4d1bb9bf48 100644 --- a/accounts/README.MD +++ b/accounts/README.MD @@ -1,10 +1,6 @@ Account management === -## Table of Contents - -[TOC] - ## Problem Account management is a feature that implements the logic for storing private keys for non-validator users. These private keys are used for signing various types of data. Currently, we have implemented file storage on the local disk. The keys are stored in a text file in JSON format. To ensure security, the private keys are encrypted, so opening the file does not compromise sensitive data.