/
wallet.go
408 lines (325 loc) · 9.83 KB
/
wallet.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
package wallet
import (
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
"github.com/tyler-smith/go-bip39"
eth2types "github.com/wealdtech/go-eth2-types/v2"
eth2ks "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
"github.com/Seb369888/smartnode/shared/services/passwords"
"github.com/Seb369888/smartnode/shared/services/wallet/keystore"
)
// Config
const (
EntropyBits = 256
FileMode = 0600
DefaultNodeKeyPath = "m/44'/60'/0'/0/%d"
LedgerLiveNodeKeyPath = "m/44'/60'/%d/0/0"
MyEtherWalletNodeKeyPath = "m/44'/60'/0'/%d"
)
// Wallet
type Wallet struct {
// Core
walletPath string
pm *passwords.PasswordManager
encryptor *eth2ks.Encryptor
chainID *big.Int
// Encrypted store
ws *walletStore
// Seed & master key
seed []byte
mk *hdkeychain.ExtendedKey
// Node key cache
nodeKey *ecdsa.PrivateKey
nodeKeyPath string
// Validator key caches
validatorKeys map[uint]*eth2types.BLSPrivateKey
// Keystores
keystores map[string]keystore.Keystore
// Desired gas price & limit from config
maxFee *big.Int
maxPriorityFee *big.Int
gasLimit uint64
}
// Encrypted wallet store
type walletStore struct {
Crypto map[string]interface{} `json:"crypto"`
Name string `json:"name"`
Version uint `json:"version"`
UUID uuid.UUID `json:"uuid"`
DerivationPath string `json:"derivationPath,omitempty"`
WalletIndex uint `json:"walletIndex,omitempty"`
NextAccount uint `json:"next_account"`
}
// Create new wallet
func NewWallet(walletPath string, chainId uint, maxFee *big.Int, maxPriorityFee *big.Int, gasLimit uint64, passwordManager *passwords.PasswordManager) (*Wallet, error) {
// Initialize wallet
w := &Wallet{
walletPath: walletPath,
pm: passwordManager,
encryptor: eth2ks.New(),
chainID: big.NewInt(int64(chainId)),
validatorKeys: map[uint]*eth2types.BLSPrivateKey{},
keystores: map[string]keystore.Keystore{},
maxFee: maxFee,
maxPriorityFee: maxPriorityFee,
gasLimit: gasLimit,
}
// Load & decrypt wallet store
if _, err := w.loadStore(); err != nil {
return nil, err
}
// Return
return w, nil
}
// Gets the wallet's chain ID
func (w *Wallet) GetChainID() *big.Int {
copy := big.NewInt(0).Set(w.chainID)
return copy
}
// Add a keystore to the wallet
func (w *Wallet) AddKeystore(name string, ks keystore.Keystore) {
w.keystores[name] = ks
}
// Check if the wallet has been initialized
func (w *Wallet) IsInitialized() bool {
return (w.ws != nil && w.seed != nil && w.mk != nil)
}
// Attempt to initialize the wallet if not initialized and return status
func (w *Wallet) GetInitialized() (bool, error) {
if w.IsInitialized() {
return true, nil
}
return w.loadStore()
}
// Serialize the wallet to a JSON string
func (w *Wallet) String() (string, error) {
// Check wallet is initialized
if !w.IsInitialized() {
return "", errors.New("Wallet is not initialized")
}
// Encode wallet store
wsBytes, err := json.Marshal(w.ws)
if err != nil {
return "", fmt.Errorf("Could not encode wallet: %w", err)
}
// Return
return string(wsBytes), nil
}
// Initialize the wallet from a random seed
func (w *Wallet) Initialize(derivationPath string, walletIndex uint) (string, error) {
// Check wallet is not initialized
if w.IsInitialized() {
return "", errors.New("Wallet is already initialized")
}
// Generate mnemonic entropy
entropy, err := bip39.NewEntropy(EntropyBits)
if err != nil {
return "", fmt.Errorf("Could not generate wallet mnemonic entropy bytes: %w", err)
}
// Generate mnemonic
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
return "", fmt.Errorf("Could not generate wallet mnemonic: %w", err)
}
// Initialize wallet store
if err := w.initializeStore(derivationPath, walletIndex, mnemonic); err != nil {
return "", err
}
// Return
return mnemonic, nil
}
// Recover a wallet from a mnemonic
func (w *Wallet) Recover(derivationPath string, walletIndex uint, mnemonic string) error {
// Check wallet is not initialized
if w.IsInitialized() {
return errors.New("Wallet is already initialized")
}
// Check mnemonic
if !bip39.IsMnemonicValid(mnemonic) {
return fmt.Errorf("Invalid mnemonic '%s'", mnemonic)
}
// Initialize wallet store
if err := w.initializeStore(derivationPath, walletIndex, mnemonic); err != nil {
return err
}
// Return
return nil
}
// Recover a wallet from a mnemonic - only used for testing mnemonics
func (w *Wallet) TestRecovery(derivationPath string, walletIndex uint, mnemonic string) error {
// Check mnemonic
if !bip39.IsMnemonicValid(mnemonic) {
return fmt.Errorf("Invalid mnemonic '%s'", mnemonic)
}
// Generate seed
w.seed = bip39.NewSeed(mnemonic, "")
// Create master key
var err error
w.mk, err = hdkeychain.NewMaster(w.seed, &chaincfg.MainNetParams)
if err != nil {
return fmt.Errorf("Could not create wallet master key: %w", err)
}
// Create wallet store
w.ws = &walletStore{
Name: w.encryptor.Name(),
Version: w.encryptor.Version(),
UUID: uuid.New(),
DerivationPath: derivationPath,
WalletIndex: walletIndex,
NextAccount: 0,
}
// Return
return nil
}
// Save the wallet store to disk
func (w *Wallet) Save() error {
// Check wallet is initialized
if !w.IsInitialized() {
return errors.New("Wallet is not initialized")
}
// Encode wallet store
wsBytes, err := json.Marshal(w.ws)
if err != nil {
return fmt.Errorf("Could not encode wallet: %w", err)
}
// Write wallet store to disk
if err := os.WriteFile(w.walletPath, wsBytes, FileMode); err != nil {
return fmt.Errorf("Could not write wallet to disk: %w", err)
}
// Return
return nil
}
// Delete the wallet store from disk
func (w *Wallet) Delete() error {
// Check if it exists
_, err := os.Stat(w.walletPath)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("error checking wallet file path: %w", err)
}
// Write wallet store to disk
err = os.Remove(w.walletPath)
return err
}
// Signs a serialized TX using the wallet's private key
func (w *Wallet) Sign(serializedTx []byte) ([]byte, error) {
// Get private key
privateKey, _, err := w.getNodePrivateKey()
if err != nil {
return nil, err
}
tx := types.Transaction{}
err = tx.UnmarshalBinary(serializedTx)
if err != nil {
return nil, fmt.Errorf("Error unmarshalling TX: %w", err)
}
signer := types.NewLondonSigner(w.chainID)
signedTx, err := types.SignTx(&tx, signer, privateKey)
if err != nil {
return nil, fmt.Errorf("Error signing TX: %w", err)
}
signedData, err := signedTx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("Error marshalling signed TX to binary: %w", err)
}
return signedData, nil
}
// Signs an arbitrary message using the wallet's private key
func (w *Wallet) SignMessage(message string) ([]byte, error) {
// Get the wallet's private key
privateKey, _, err := w.getNodePrivateKey()
if err != nil {
return nil, err
}
messageHash := accounts.TextHash([]byte(message))
signedMessage, err := crypto.Sign(messageHash, privateKey)
if err != nil {
return nil, fmt.Errorf("Error signing message: %w", err)
}
// fix the ECDSA 'v' (see https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7#:~:text=The%20version%20number,2%E2%80%9D%20was%20introduced)
signedMessage[crypto.RecoveryIDOffset] += 27
return signedMessage, nil
}
// Reloads wallet from disk
func (w *Wallet) Reload() error {
_, err := w.loadStore()
return err
}
// Load the wallet store from disk and decrypt it
func (w *Wallet) loadStore() (bool, error) {
// Read wallet store from disk; cancel if not found
wsBytes, err := os.ReadFile(w.walletPath)
if err != nil {
return false, nil
}
// Decode wallet store
w.ws = new(walletStore)
if err = json.Unmarshal(wsBytes, w.ws); err != nil {
return false, fmt.Errorf("Could not decode wallet: %w", err)
}
// Upgrade legacy wallets to include derivation paths
if w.ws.DerivationPath == "" {
w.ws.DerivationPath = DefaultNodeKeyPath
}
// Get wallet password
password, err := w.pm.GetPassword()
if err != nil {
return false, fmt.Errorf("Could not get wallet password: %w", err)
}
// Decrypt seed
w.seed, err = w.encryptor.Decrypt(w.ws.Crypto, password)
if err != nil {
return false, fmt.Errorf("Could not decrypt wallet seed: %w", err)
}
// Create master key
w.mk, err = hdkeychain.NewMaster(w.seed, &chaincfg.MainNetParams)
if err != nil {
return false, fmt.Errorf("Could not create wallet master key: %w", err)
}
// Return
return true, nil
}
// Initialize the encrypted wallet store from a mnemonic
func (w *Wallet) initializeStore(derivationPath string, walletIndex uint, mnemonic string) error {
// Generate seed
w.seed = bip39.NewSeed(mnemonic, "")
// Create master key
var err error
w.mk, err = hdkeychain.NewMaster(w.seed, &chaincfg.MainNetParams)
if err != nil {
return fmt.Errorf("Could not create wallet master key: %w", err)
}
// Get wallet password
password, err := w.pm.GetPassword()
if err != nil {
return fmt.Errorf("Could not get wallet password: %w", err)
}
// Encrypt seed
encryptedSeed, err := w.encryptor.Encrypt(w.seed, password)
if err != nil {
return fmt.Errorf("Could not encrypt wallet seed: %w", err)
}
// Create wallet store
w.ws = &walletStore{
Crypto: encryptedSeed,
Name: w.encryptor.Name(),
Version: w.encryptor.Version(),
UUID: uuid.New(),
DerivationPath: derivationPath,
WalletIndex: walletIndex,
NextAccount: 0,
}
// Return
return nil
}