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