github.com/ylsGit/go-ethereum@v1.6.5/accounts/usbwallet/ledger_hub.go (about)

     1  // Copyright 2017 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum 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 go-ethereum 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 go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  // This file contains the implementation for interacting with the Ledger hardware
    18  // wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo:
    19  // https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc
    20  
    21  package usbwallet
    22  
    23  import (
    24  	"errors"
    25  	"runtime"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/ethereum/go-ethereum/accounts"
    30  	"github.com/ethereum/go-ethereum/event"
    31  	"github.com/ethereum/go-ethereum/log"
    32  	"github.com/karalabe/hid"
    33  )
    34  
    35  // LedgerScheme is the protocol scheme prefixing account and wallet URLs.
    36  var LedgerScheme = "ledger"
    37  
    38  // ledgerDeviceIDs are the known device IDs that Ledger wallets use.
    39  var ledgerDeviceIDs = []deviceID{
    40  	{Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue
    41  	{Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S
    42  }
    43  
    44  // Maximum time between wallet refreshes (if USB hotplug notifications don't work).
    45  const ledgerRefreshCycle = time.Second
    46  
    47  // Minimum time between wallet refreshes to avoid USB trashing.
    48  const ledgerRefreshThrottling = 500 * time.Millisecond
    49  
    50  // LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets.
    51  type LedgerHub struct {
    52  	refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    53  	wallets     []accounts.Wallet       // List of Ledger devices currently tracking
    54  	updateFeed  event.Feed              // Event feed to notify wallet additions/removals
    55  	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
    56  	updating    bool                    // Whether the event notification loop is running
    57  
    58  	quit chan chan error
    59  
    60  	stateLock sync.RWMutex // Protects the internals of the hub from racey access
    61  
    62  	// TODO(karalabe): remove if hotplug lands on Windows
    63  	commsPend int        // Number of operations blocking enumeration
    64  	commsLock sync.Mutex // Lock protecting the pending counter and enumeration
    65  }
    66  
    67  // NewLedgerHub creates a new hardware wallet manager for Ledger devices.
    68  func NewLedgerHub() (*LedgerHub, error) {
    69  	if !hid.Supported() {
    70  		return nil, errors.New("unsupported platform")
    71  	}
    72  	hub := &LedgerHub{
    73  		quit: make(chan chan error),
    74  	}
    75  	hub.refreshWallets()
    76  	return hub, nil
    77  }
    78  
    79  // Wallets implements accounts.Backend, returning all the currently tracked USB
    80  // devices that appear to be Ledger hardware wallets.
    81  func (hub *LedgerHub) Wallets() []accounts.Wallet {
    82  	// Make sure the list of wallets is up to date
    83  	hub.refreshWallets()
    84  
    85  	hub.stateLock.RLock()
    86  	defer hub.stateLock.RUnlock()
    87  
    88  	cpy := make([]accounts.Wallet, len(hub.wallets))
    89  	copy(cpy, hub.wallets)
    90  	return cpy
    91  }
    92  
    93  // refreshWallets scans the USB devices attached to the machine and updates the
    94  // list of wallets based on the found devices.
    95  func (hub *LedgerHub) refreshWallets() {
    96  	// Don't scan the USB like crazy it the user fetches wallets in a loop
    97  	hub.stateLock.RLock()
    98  	elapsed := time.Since(hub.refreshed)
    99  	hub.stateLock.RUnlock()
   100  
   101  	if elapsed < ledgerRefreshThrottling {
   102  		return
   103  	}
   104  	// Retrieve the current list of Ledger devices
   105  	var ledgers []hid.DeviceInfo
   106  
   107  	if runtime.GOOS == "linux" {
   108  		// hidapi on Linux opens the device during enumeration to retrieve some infos,
   109  		// breaking the Ledger protocol if that is waiting for user confirmation. This
   110  		// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
   111  		// need to prevent concurrent comms ourselves. The more elegant solution would
   112  		// be to ditch enumeration in favor of hutplug events, but that don't work yet
   113  		// on Windows so if we need to hack it anyway, this is more elegant for now.
   114  		hub.commsLock.Lock()
   115  		if hub.commsPend > 0 { // A confirmation is pending, don't refresh
   116  			hub.commsLock.Unlock()
   117  			return
   118  		}
   119  	}
   120  	for _, info := range hid.Enumerate(0, 0) { // Can't enumerate directly, one valid ID is the 0 wildcard
   121  		for _, id := range ledgerDeviceIDs {
   122  			if info.VendorID == id.Vendor && info.ProductID == id.Product {
   123  				ledgers = append(ledgers, info)
   124  				break
   125  			}
   126  		}
   127  	}
   128  	if runtime.GOOS == "linux" {
   129  		// See rationale before the enumeration why this is needed and only on Linux.
   130  		hub.commsLock.Unlock()
   131  	}
   132  	// Transform the current list of wallets into the new one
   133  	hub.stateLock.Lock()
   134  
   135  	wallets := make([]accounts.Wallet, 0, len(ledgers))
   136  	events := []accounts.WalletEvent{}
   137  
   138  	for _, ledger := range ledgers {
   139  		url := accounts.URL{Scheme: LedgerScheme, Path: ledger.Path}
   140  
   141  		// Drop wallets in front of the next device or those that failed for some reason
   142  		for len(hub.wallets) > 0 && (hub.wallets[0].URL().Cmp(url) < 0 || hub.wallets[0].(*ledgerWallet).failed()) {
   143  			events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false})
   144  			hub.wallets = hub.wallets[1:]
   145  		}
   146  		// If there are no more wallets or the device is before the next, wrap new wallet
   147  		if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
   148  			wallet := &ledgerWallet{hub: hub, url: &url, info: ledger, log: log.New("url", url)}
   149  
   150  			events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
   151  			wallets = append(wallets, wallet)
   152  			continue
   153  		}
   154  		// If the device is the same as the first wallet, keep it
   155  		if hub.wallets[0].URL().Cmp(url) == 0 {
   156  			wallets = append(wallets, hub.wallets[0])
   157  			hub.wallets = hub.wallets[1:]
   158  			continue
   159  		}
   160  	}
   161  	// Drop any leftover wallets and set the new batch
   162  	for _, wallet := range hub.wallets {
   163  		events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false})
   164  	}
   165  	hub.refreshed = time.Now()
   166  	hub.wallets = wallets
   167  	hub.stateLock.Unlock()
   168  
   169  	// Fire all wallet events and return
   170  	for _, event := range events {
   171  		hub.updateFeed.Send(event)
   172  	}
   173  }
   174  
   175  // Subscribe implements accounts.Backend, creating an async subscription to
   176  // receive notifications on the addition or removal of Ledger wallets.
   177  func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
   178  	// We need the mutex to reliably start/stop the update loop
   179  	hub.stateLock.Lock()
   180  	defer hub.stateLock.Unlock()
   181  
   182  	// Subscribe the caller and track the subscriber count
   183  	sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
   184  
   185  	// Subscribers require an active notification loop, start it
   186  	if !hub.updating {
   187  		hub.updating = true
   188  		go hub.updater()
   189  	}
   190  	return sub
   191  }
   192  
   193  // updater is responsible for maintaining an up-to-date list of wallets stored in
   194  // the keystore, and for firing wallet addition/removal events. It listens for
   195  // account change events from the underlying account cache, and also periodically
   196  // forces a manual refresh (only triggers for systems where the filesystem notifier
   197  // is not running).
   198  func (hub *LedgerHub) updater() {
   199  	for {
   200  		// Wait for a USB hotplug event (not supported yet) or a refresh timeout
   201  		select {
   202  		//case <-hub.changes: // reenable on hutplug implementation
   203  		case <-time.After(ledgerRefreshCycle):
   204  		}
   205  		// Run the wallet refresher
   206  		hub.refreshWallets()
   207  
   208  		// If all our subscribers left, stop the updater
   209  		hub.stateLock.Lock()
   210  		if hub.updateScope.Count() == 0 {
   211  			hub.updating = false
   212  			hub.stateLock.Unlock()
   213  			return
   214  		}
   215  		hub.stateLock.Unlock()
   216  	}
   217  }