github.com/m3shine/gochain@v2.2.26+incompatible/accounts/usbwallet/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  package usbwallet
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"runtime"
    23  	"sync"
    24  	"time"
    25  
    26  	"go.opencensus.io/trace"
    27  
    28  	"github.com/gochain-io/gochain/accounts"
    29  	"github.com/gochain-io/gochain/event"
    30  	"github.com/gochain-io/gochain/log"
    31  	"github.com/karalabe/hid"
    32  )
    33  
    34  // LedgerScheme is the protocol scheme prefixing account and wallet URLs.
    35  const LedgerScheme = "ledger"
    36  
    37  // TrezorScheme is the protocol scheme prefixing account and wallet URLs.
    38  const TrezorScheme = "trezor"
    39  
    40  // refreshCycle is the maximum time between wallet refreshes (if USB hotplug
    41  // notifications don't work).
    42  const refreshCycle = time.Second
    43  
    44  // refreshThrottling is the minimum time between wallet refreshes to avoid USB
    45  // trashing.
    46  const refreshThrottling = 500 * time.Millisecond
    47  
    48  // Hub is a accounts.Backend that can find and handle generic USB hardware wallets.
    49  type Hub struct {
    50  	scheme     string                  // Protocol scheme prefixing account and wallet URLs.
    51  	vendorID   uint16                  // USB vendor identifier used for device discovery
    52  	productIDs []uint16                // USB product identifiers used for device discovery
    53  	usageID    uint16                  // USB usage page identifier used for macOS device discovery
    54  	endpointID int                     // USB endpoint identifier used for non-macOS device discovery
    55  	makeDriver func(log.Logger) driver // Factory method to construct a vendor specific driver
    56  
    57  	refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    58  	wallets     []accounts.Wallet       // List of USB wallet devices currently tracking
    59  	updateFeed  event.Feed              // Event feed to notify wallet additions/removals
    60  	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
    61  	updating    bool                    // Whether the event notification loop is running
    62  
    63  	quit chan chan error
    64  
    65  	stateLock sync.RWMutex // Protects the internals of the hub from racey access
    66  
    67  	// TODO(karalabe): remove if hotplug lands on Windows
    68  	commsPend int        // Number of operations blocking enumeration
    69  	commsLock sync.Mutex // Lock protecting the pending counter and enumeration
    70  }
    71  
    72  // NewLedgerHub creates a new hardware wallet manager for Ledger devices.
    73  func NewLedgerHub() (*Hub, error) {
    74  	return newHub(LedgerScheme, 0x2c97, []uint16{0x0000 /* Ledger Blue */, 0x0001 /* Ledger Nano S */}, 0xffa0, 0, newLedgerDriver)
    75  }
    76  
    77  // NewTrezorHub creates a new hardware wallet manager for Trezor devices.
    78  func NewTrezorHub() (*Hub, error) {
    79  	return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor 1 */}, 0xff00, 0, newTrezorDriver)
    80  }
    81  
    82  // newHub creates a new hardware wallet manager for generic USB devices.
    83  func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) {
    84  	if !hid.Supported() {
    85  		return nil, errors.New("unsupported platform")
    86  	}
    87  	hub := &Hub{
    88  		scheme:     scheme,
    89  		vendorID:   vendorID,
    90  		productIDs: productIDs,
    91  		usageID:    usageID,
    92  		endpointID: endpointID,
    93  		makeDriver: makeDriver,
    94  		quit:       make(chan chan error),
    95  	}
    96  	hub.refreshWallets()
    97  	return hub, nil
    98  }
    99  
   100  // Wallets implements accounts.Backend, returning all the currently tracked USB
   101  // devices that appear to be hardware wallets.
   102  func (hub *Hub) Wallets() []accounts.Wallet {
   103  	// Make sure the list of wallets is up to date
   104  	hub.refreshWallets()
   105  
   106  	hub.stateLock.RLock()
   107  	defer hub.stateLock.RUnlock()
   108  
   109  	cpy := make([]accounts.Wallet, len(hub.wallets))
   110  	copy(cpy, hub.wallets)
   111  	return cpy
   112  }
   113  
   114  // refreshWallets scans the USB devices attached to the machine and updates the
   115  // list of wallets based on the found devices.
   116  func (hub *Hub) refreshWallets() {
   117  	ctx, span := trace.StartSpan(context.Background(), "Hub.refreshWallets")
   118  	defer span.End()
   119  
   120  	// Don't scan the USB like crazy it the user fetches wallets in a loop
   121  	hub.stateLock.RLock()
   122  	elapsed := time.Since(hub.refreshed)
   123  	hub.stateLock.RUnlock()
   124  
   125  	if elapsed < refreshThrottling {
   126  		return
   127  	}
   128  	// Retrieve the current list of USB wallet devices
   129  	var devices []hid.DeviceInfo
   130  
   131  	if runtime.GOOS == "linux" {
   132  		// hidapi on Linux opens the device during enumeration to retrieve some infos,
   133  		// breaking the Ledger protocol if that is waiting for user confirmation. This
   134  		// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
   135  		// need to prevent concurrent comms ourselves. The more elegant solution would
   136  		// be to ditch enumeration in favor of hutplug events, but that don't work yet
   137  		// on Windows so if we need to hack it anyway, this is more elegant for now.
   138  		hub.commsLock.Lock()
   139  		if hub.commsPend > 0 { // A confirmation is pending, don't refresh
   140  			hub.commsLock.Unlock()
   141  			return
   142  		}
   143  	}
   144  	for _, info := range hid.Enumerate(hub.vendorID, 0) {
   145  		for _, id := range hub.productIDs {
   146  			if info.ProductID == id && (info.UsagePage == hub.usageID || info.Interface == hub.endpointID) {
   147  				devices = append(devices, info)
   148  				break
   149  			}
   150  		}
   151  	}
   152  	if runtime.GOOS == "linux" {
   153  		// See rationale before the enumeration why this is needed and only on Linux.
   154  		hub.commsLock.Unlock()
   155  	}
   156  	// Transform the current list of wallets into the new one
   157  	hub.stateLock.Lock()
   158  
   159  	wallets := make([]accounts.Wallet, 0, len(devices))
   160  	events := []accounts.WalletEvent{}
   161  
   162  	for _, device := range devices {
   163  		url := accounts.URL{Scheme: hub.scheme, Path: device.Path}
   164  
   165  		// Drop wallets in front of the next device or those that failed for some reason
   166  		for len(hub.wallets) > 0 {
   167  			// Abort if we're past the current device and found an operational one
   168  			_, failure := hub.wallets[0].Status()
   169  			if hub.wallets[0].URL().Cmp(url) >= 0 || failure == nil {
   170  				break
   171  			}
   172  			// Drop the stale and failed devices
   173  			events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Kind: accounts.WalletDropped})
   174  			hub.wallets = hub.wallets[1:]
   175  		}
   176  		// If there are no more wallets or the device is before the next, wrap new wallet
   177  		if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
   178  			logger := log.New("url", url)
   179  			wallet := &wallet{hub: hub, driver: hub.makeDriver(logger), url: &url, info: device, log: logger}
   180  
   181  			events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
   182  			wallets = append(wallets, wallet)
   183  			continue
   184  		}
   185  		// If the device is the same as the first wallet, keep it
   186  		if hub.wallets[0].URL().Cmp(url) == 0 {
   187  			wallets = append(wallets, hub.wallets[0])
   188  			hub.wallets = hub.wallets[1:]
   189  			continue
   190  		}
   191  	}
   192  	// Drop any leftover wallets and set the new batch
   193  	for _, wallet := range hub.wallets {
   194  		events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
   195  	}
   196  	hub.refreshed = time.Now()
   197  	hub.wallets = wallets
   198  	hub.stateLock.Unlock()
   199  
   200  	// Fire all wallet events and return
   201  	for _, event := range events {
   202  		hub.updateFeed.SendCtx(ctx, event)
   203  	}
   204  }
   205  
   206  // Subscribe implements accounts.Backend, creating an async subscription to
   207  // receive notifications on the addition or removal of USB wallets.
   208  func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
   209  	// We need the mutex to reliably start/stop the update loop
   210  	hub.stateLock.Lock()
   211  	defer hub.stateLock.Unlock()
   212  
   213  	// Subscribe the caller and track the subscriber count
   214  	sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
   215  
   216  	// Subscribers require an active notification loop, start it
   217  	if !hub.updating {
   218  		hub.updating = true
   219  		go hub.updater()
   220  	}
   221  	return sub
   222  }
   223  
   224  // updater is responsible for maintaining an up-to-date list of wallets managed
   225  // by the USB hub, and for firing wallet addition/removal events.
   226  func (hub *Hub) updater() {
   227  	for {
   228  		// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
   229  		// <-hub.changes
   230  		time.Sleep(refreshCycle)
   231  
   232  		// Run the wallet refresher
   233  		hub.refreshWallets()
   234  
   235  		// If all our subscribers left, stop the updater
   236  		hub.stateLock.Lock()
   237  		if hub.updateScope.Count() == 0 {
   238  			hub.updating = false
   239  			hub.stateLock.Unlock()
   240  			return
   241  		}
   242  		hub.stateLock.Unlock()
   243  	}
   244  }