github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/accounts/usbwallet/wallet.go (about) 1 // Copyright 2017 The Spectrum Authors 2 // This file is part of the Spectrum library. 3 // 4 // The Spectrum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The Spectrum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the Spectrum library. If not, see <http://www.gnu.org/licenses/>. 16 17 // Package usbwallet implements support for USB hardware wallets. 18 package usbwallet 19 20 import ( 21 "context" 22 "fmt" 23 "io" 24 "math/big" 25 "sync" 26 "time" 27 28 ethereum "github.com/SmartMeshFoundation/Spectrum" 29 "github.com/SmartMeshFoundation/Spectrum/accounts" 30 "github.com/SmartMeshFoundation/Spectrum/common" 31 "github.com/SmartMeshFoundation/Spectrum/core/types" 32 "github.com/SmartMeshFoundation/Spectrum/log" 33 "github.com/karalabe/hid" 34 ) 35 36 // Maximum time between wallet health checks to detect USB unplugs. 37 const heartbeatCycle = time.Second 38 39 // Minimum time to wait between self derivation attempts, even it the user is 40 // requesting accounts like crazy. 41 const selfDeriveThrottling = time.Second 42 43 // driver defines the vendor specific functionality hardware wallets instances 44 // must implement to allow using them with the wallet lifecycle management. 45 type driver interface { 46 // Status returns a textual status to aid the user in the current state of the 47 // wallet. It also returns an error indicating any failure the wallet might have 48 // encountered. 49 Status() (string, error) 50 51 // Open initializes access to a wallet instance. The passphrase parameter may 52 // or may not be used by the implementation of a particular wallet instance. 53 Open(device io.ReadWriter, passphrase string) error 54 55 // Close releases any resources held by an open wallet instance. 56 Close() error 57 58 // Heartbeat performs a sanity check against the hardware wallet to see if it 59 // is still online and healthy. 60 Heartbeat() error 61 62 // Derive sends a derivation request to the USB device and returns the Ethereum 63 // address located on that path. 64 Derive(path accounts.DerivationPath) (common.Address, error) 65 66 // SignTx sends the transaction to the USB device and waits for the user to confirm 67 // or deny the transaction. 68 SignTx(path accounts.DerivationPath, tx *types.Transaction, chainID *big.Int) (common.Address, *types.Transaction, error) 69 } 70 71 // wallet represents the common functionality shared by all USB hardware 72 // wallets to prevent reimplementing the same complex maintenance mechanisms 73 // for different vendors. 74 type wallet struct { 75 hub *Hub // USB hub scanning 76 driver driver // Hardware implementation of the low level device operations 77 url *accounts.URL // Textual URL uniquely identifying this wallet 78 79 info hid.DeviceInfo // Known USB device infos about the wallet 80 device *hid.Device // USB device advertising itself as a hardware wallet 81 82 accounts []accounts.Account // List of derive accounts pinned on the hardware wallet 83 paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations 84 85 deriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery 86 deriveNextAddr common.Address // Next derived account address for auto-discovery 87 deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with 88 deriveReq chan chan struct{} // Channel to request a self-derivation on 89 deriveQuit chan chan error // Channel to terminate the self-deriver with 90 91 healthQuit chan chan error 92 93 // Locking a hardware wallet is a bit special. Since hardware devices are lower 94 // performing, any communication with them might take a non negligible amount of 95 // time. Worse still, waiting for user confirmation can take arbitrarily long, 96 // but exclusive communication must be upheld during. Locking the entire wallet 97 // in the mean time however would stall any parts of the system that don't want 98 // to communicate, just read some state (e.g. list the accounts). 99 // 100 // As such, a hardware wallet needs two locks to function correctly. A state 101 // lock can be used to protect the wallet's software-side internal state, which 102 // must not be held exlusively during hardware communication. A communication 103 // lock can be used to achieve exclusive access to the device itself, this one 104 // however should allow "skipping" waiting for operations that might want to 105 // use the device, but can live without too (e.g. account self-derivation). 106 // 107 // Since we have two locks, it's important to know how to properly use them: 108 // - Communication requires the `device` to not change, so obtaining the 109 // commsLock should be done after having a stateLock. 110 // - Communication must not disable read access to the wallet state, so it 111 // must only ever hold a *read* lock to stateLock. 112 commsLock chan struct{} // Mutex (buf=1) for the USB comms without keeping the state locked 113 stateLock sync.RWMutex // Protects read and write access to the wallet struct fields 114 115 log log.Logger // Contextual logger to tag the base with its id 116 } 117 118 // URL implements accounts.Wallet, returning the URL of the USB hardware device. 119 func (w *wallet) URL() accounts.URL { 120 return *w.url // Immutable, no need for a lock 121 } 122 123 // Status implements accounts.Wallet, returning a custom status message from the 124 // underlying vendor-specific hardware wallet implementation. 125 func (w *wallet) Status() (string, error) { 126 w.stateLock.RLock() // No device communication, state lock is enough 127 defer w.stateLock.RUnlock() 128 129 status, failure := w.driver.Status() 130 if w.device == nil { 131 return "Closed", failure 132 } 133 return status, failure 134 } 135 136 // Open implements accounts.Wallet, attempting to open a USB connection to the 137 // hardware wallet. 138 func (w *wallet) Open(passphrase string) error { 139 w.stateLock.Lock() // State lock is enough since there's no connection yet at this point 140 defer w.stateLock.Unlock() 141 142 // If the device was already opened once, refuse to try again 143 if w.paths != nil { 144 return accounts.ErrWalletAlreadyOpen 145 } 146 // Make sure the actual device connection is done only once 147 if w.device == nil { 148 device, err := w.info.Open() 149 if err != nil { 150 return err 151 } 152 w.device = device 153 w.commsLock = make(chan struct{}, 1) 154 w.commsLock <- struct{}{} // Enable lock 155 } 156 // Delegate device initialization to the underlying driver 157 if err := w.driver.Open(w.device, passphrase); err != nil { 158 return err 159 } 160 // Connection successful, start life-cycle management 161 w.paths = make(map[common.Address]accounts.DerivationPath) 162 163 w.deriveReq = make(chan chan struct{}) 164 w.deriveQuit = make(chan chan error) 165 w.healthQuit = make(chan chan error) 166 167 go w.heartbeat() 168 go w.selfDerive() 169 170 // Notify anyone listening for wallet events that a new device is accessible 171 go w.hub.updateFeed.Send(accounts.WalletEvent{Wallet: w, Kind: accounts.WalletOpened}) 172 173 return nil 174 } 175 176 // heartbeat is a health check loop for the USB wallets to periodically verify 177 // whether they are still present or if they malfunctioned. 178 func (w *wallet) heartbeat() { 179 w.log.Debug("USB wallet health-check started") 180 defer w.log.Debug("USB wallet health-check stopped") 181 182 // Execute heartbeat checks until termination or error 183 var ( 184 errc chan error 185 err error 186 ) 187 for errc == nil && err == nil { 188 // Wait until termination is requested or the heartbeat cycle arrives 189 select { 190 case errc = <-w.healthQuit: 191 // Termination requested 192 continue 193 case <-time.After(heartbeatCycle): 194 // Heartbeat time 195 } 196 // Execute a tiny data exchange to see responsiveness 197 w.stateLock.RLock() 198 if w.device == nil { 199 // Terminated while waiting for the lock 200 w.stateLock.RUnlock() 201 continue 202 } 203 <-w.commsLock // Don't lock state while resolving version 204 err = w.driver.Heartbeat() 205 w.commsLock <- struct{}{} 206 w.stateLock.RUnlock() 207 208 if err != nil { 209 w.stateLock.Lock() // Lock state to tear the wallet down 210 w.close() 211 w.stateLock.Unlock() 212 } 213 // Ignore non hardware related errors 214 err = nil 215 } 216 // In case of error, wait for termination 217 if err != nil { 218 w.log.Debug("USB wallet health-check failed", "err", err) 219 errc = <-w.healthQuit 220 } 221 errc <- err 222 } 223 224 // Close implements accounts.Wallet, closing the USB connection to the device. 225 func (w *wallet) Close() error { 226 // Ensure the wallet was opened 227 w.stateLock.RLock() 228 hQuit, dQuit := w.healthQuit, w.deriveQuit 229 w.stateLock.RUnlock() 230 231 // Terminate the health checks 232 var herr error 233 if hQuit != nil { 234 errc := make(chan error) 235 hQuit <- errc 236 herr = <-errc // Save for later, we *must* close the USB 237 } 238 // Terminate the self-derivations 239 var derr error 240 if dQuit != nil { 241 errc := make(chan error) 242 dQuit <- errc 243 derr = <-errc // Save for later, we *must* close the USB 244 } 245 // Terminate the device connection 246 w.stateLock.Lock() 247 defer w.stateLock.Unlock() 248 249 w.healthQuit = nil 250 w.deriveQuit = nil 251 w.deriveReq = nil 252 253 if err := w.close(); err != nil { 254 return err 255 } 256 if herr != nil { 257 return herr 258 } 259 return derr 260 } 261 262 // close is the internal wallet closer that terminates the USB connection and 263 // resets all the fields to their defaults. 264 // 265 // Note, close assumes the state lock is held! 266 func (w *wallet) close() error { 267 // Allow duplicate closes, especially for health-check failures 268 if w.device == nil { 269 return nil 270 } 271 // Close the device, clear everything, then return 272 w.device.Close() 273 w.device = nil 274 275 w.accounts, w.paths = nil, nil 276 w.driver.Close() 277 278 return nil 279 } 280 281 // Accounts implements accounts.Wallet, returning the list of accounts pinned to 282 // the USB hardware wallet. If self-derivation was enabled, the account list is 283 // periodically expanded based on current chain state. 284 func (w *wallet) Accounts() []accounts.Account { 285 // Attempt self-derivation if it's running 286 reqc := make(chan struct{}, 1) 287 select { 288 case w.deriveReq <- reqc: 289 // Self-derivation request accepted, wait for it 290 <-reqc 291 default: 292 // Self-derivation offline, throttled or busy, skip 293 } 294 // Return whatever account list we ended up with 295 w.stateLock.RLock() 296 defer w.stateLock.RUnlock() 297 298 cpy := make([]accounts.Account, len(w.accounts)) 299 copy(cpy, w.accounts) 300 return cpy 301 } 302 303 // selfDerive is an account derivation loop that upon request attempts to find 304 // new non-zero accounts. 305 func (w *wallet) selfDerive() { 306 w.log.Debug("USB wallet self-derivation started") 307 defer w.log.Debug("USB wallet self-derivation stopped") 308 309 // Execute self-derivations until termination or error 310 var ( 311 reqc chan struct{} 312 errc chan error 313 err error 314 ) 315 for errc == nil && err == nil { 316 // Wait until either derivation or termination is requested 317 select { 318 case errc = <-w.deriveQuit: 319 // Termination requested 320 continue 321 case reqc = <-w.deriveReq: 322 // Account discovery requested 323 } 324 // Derivation needs a chain and device access, skip if either unavailable 325 w.stateLock.RLock() 326 if w.device == nil || w.deriveChain == nil { 327 w.stateLock.RUnlock() 328 reqc <- struct{}{} 329 continue 330 } 331 select { 332 case <-w.commsLock: 333 default: 334 w.stateLock.RUnlock() 335 reqc <- struct{}{} 336 continue 337 } 338 // Device lock obtained, derive the next batch of accounts 339 var ( 340 accs []accounts.Account 341 paths []accounts.DerivationPath 342 343 nextAddr = w.deriveNextAddr 344 nextPath = w.deriveNextPath 345 346 context = context.Background() 347 ) 348 for empty := false; !empty; { 349 // Retrieve the next derived Ethereum account 350 if nextAddr == (common.Address{}) { 351 if nextAddr, err = w.driver.Derive(nextPath); err != nil { 352 w.log.Warn("USB wallet account derivation failed", "err", err) 353 break 354 } 355 } 356 // Check the account's status against the current chain state 357 var ( 358 balance *big.Int 359 nonce uint64 360 ) 361 balance, err = w.deriveChain.BalanceAt(context, nextAddr, nil) 362 if err != nil { 363 w.log.Warn("USB wallet balance retrieval failed", "err", err) 364 break 365 } 366 nonce, err = w.deriveChain.NonceAt(context, nextAddr, nil) 367 if err != nil { 368 w.log.Warn("USB wallet nonce retrieval failed", "err", err) 369 break 370 } 371 // If the next account is empty, stop self-derivation, but add it nonetheless 372 if balance.Sign() == 0 && nonce == 0 { 373 empty = true 374 } 375 // We've just self-derived a new account, start tracking it locally 376 path := make(accounts.DerivationPath, len(nextPath)) 377 copy(path[:], nextPath[:]) 378 paths = append(paths, path) 379 380 account := accounts.Account{ 381 Address: nextAddr, 382 URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, 383 } 384 accs = append(accs, account) 385 386 // Display a log message to the user for new (or previously empty accounts) 387 if _, known := w.paths[nextAddr]; !known || (!empty && nextAddr == w.deriveNextAddr) { 388 w.log.Info("USB wallet discovered new account", "address", nextAddr, "path", path, "balance", balance, "nonce", nonce) 389 } 390 // Fetch the next potential account 391 if !empty { 392 nextAddr = common.Address{} 393 nextPath[len(nextPath)-1]++ 394 } 395 } 396 // Self derivation complete, release device lock 397 w.commsLock <- struct{}{} 398 w.stateLock.RUnlock() 399 400 // Insert any accounts successfully derived 401 w.stateLock.Lock() 402 for i := 0; i < len(accs); i++ { 403 if _, ok := w.paths[accs[i].Address]; !ok { 404 w.accounts = append(w.accounts, accs[i]) 405 w.paths[accs[i].Address] = paths[i] 406 } 407 } 408 // Shift the self-derivation forward 409 // TODO(karalabe): don't overwrite changes from wallet.SelfDerive 410 w.deriveNextAddr = nextAddr 411 w.deriveNextPath = nextPath 412 w.stateLock.Unlock() 413 414 // Notify the user of termination and loop after a bit of time (to avoid trashing) 415 reqc <- struct{}{} 416 if err == nil { 417 select { 418 case errc = <-w.deriveQuit: 419 // Termination requested, abort 420 case <-time.After(selfDeriveThrottling): 421 // Waited enough, willing to self-derive again 422 } 423 } 424 } 425 // In case of error, wait for termination 426 if err != nil { 427 w.log.Debug("USB wallet self-derivation failed", "err", err) 428 errc = <-w.deriveQuit 429 } 430 errc <- err 431 } 432 433 // Contains implements accounts.Wallet, returning whether a particular account is 434 // or is not pinned into this wallet instance. Although we could attempt to resolve 435 // unpinned accounts, that would be an non-negligible hardware operation. 436 func (w *wallet) Contains(account accounts.Account) bool { 437 w.stateLock.RLock() 438 defer w.stateLock.RUnlock() 439 440 _, exists := w.paths[account.Address] 441 return exists 442 } 443 444 // Derive implements accounts.Wallet, deriving a new account at the specific 445 // derivation path. If pin is set to true, the account will be added to the list 446 // of tracked accounts. 447 func (w *wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { 448 // Try to derive the actual account and update its URL if successful 449 w.stateLock.RLock() // Avoid device disappearing during derivation 450 451 if w.device == nil { 452 w.stateLock.RUnlock() 453 return accounts.Account{}, accounts.ErrWalletClosed 454 } 455 <-w.commsLock // Avoid concurrent hardware access 456 address, err := w.driver.Derive(path) 457 w.commsLock <- struct{}{} 458 459 w.stateLock.RUnlock() 460 461 // If an error occurred or no pinning was requested, return 462 if err != nil { 463 return accounts.Account{}, err 464 } 465 account := accounts.Account{ 466 Address: address, 467 URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, 468 } 469 if !pin { 470 return account, nil 471 } 472 // Pinning needs to modify the state 473 w.stateLock.Lock() 474 defer w.stateLock.Unlock() 475 476 if _, ok := w.paths[address]; !ok { 477 w.accounts = append(w.accounts, account) 478 w.paths[address] = path 479 } 480 return account, nil 481 } 482 483 // SelfDerive implements accounts.Wallet, trying to discover accounts that the 484 // user used previously (based on the chain state), but ones that he/she did not 485 // explicitly pin to the wallet manually. To avoid chain head monitoring, self 486 // derivation only runs during account listing (and even then throttled). 487 func (w *wallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) { 488 w.stateLock.Lock() 489 defer w.stateLock.Unlock() 490 491 w.deriveNextPath = make(accounts.DerivationPath, len(base)) 492 copy(w.deriveNextPath[:], base[:]) 493 494 w.deriveNextAddr = common.Address{} 495 w.deriveChain = chain 496 } 497 498 // SignHash implements accounts.Wallet, however signing arbitrary data is not 499 // supported for hardware wallets, so this method will always return an error. 500 func (w *wallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { 501 return nil, accounts.ErrNotSupported 502 } 503 504 // SignTx implements accounts.Wallet. It sends the transaction over to the Ledger 505 // wallet to request a confirmation from the user. It returns either the signed 506 // transaction or a failure if the user denied the transaction. 507 // 508 // Note, if the version of the Ethereum application running on the Ledger wallet is 509 // too old to sign EIP-155 transactions, but such is requested nonetheless, an error 510 // will be returned opposed to silently signing in Homestead mode. 511 func (w *wallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { 512 w.stateLock.RLock() // Comms have own mutex, this is for the state fields 513 defer w.stateLock.RUnlock() 514 515 // If the wallet is closed, abort 516 if w.device == nil { 517 return nil, accounts.ErrWalletClosed 518 } 519 // Make sure the requested account is contained within 520 path, ok := w.paths[account.Address] 521 if !ok { 522 return nil, accounts.ErrUnknownAccount 523 } 524 // All infos gathered and metadata checks out, request signing 525 <-w.commsLock 526 defer func() { w.commsLock <- struct{}{} }() 527 528 // Ensure the device isn't screwed with while user confirmation is pending 529 // TODO(karalabe): remove if hotplug lands on Windows 530 w.hub.commsLock.Lock() 531 w.hub.commsPend++ 532 w.hub.commsLock.Unlock() 533 534 defer func() { 535 w.hub.commsLock.Lock() 536 w.hub.commsPend-- 537 w.hub.commsLock.Unlock() 538 }() 539 // Sign the transaction and verify the sender to avoid hardware fault surprises 540 sender, signed, err := w.driver.SignTx(path, tx, chainID) 541 if err != nil { 542 return nil, err 543 } 544 if sender != account.Address { 545 return nil, fmt.Errorf("signer mismatch: expected %s, got %s", account.Address.Hex(), sender.Hex()) 546 } 547 return signed, nil 548 } 549 550 // SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary 551 // data is not supported for Ledger wallets, so this method will always return 552 // an error. 553 func (w *wallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { 554 return w.SignHash(account, hash) 555 } 556 557 // SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given 558 // transaction with the given account using passphrase as extra authentication. 559 // Since USB wallets don't rely on passphrases, these are silently ignored. 560 func (w *wallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { 561 return w.SignTx(account, tx, chainID) 562 }