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 }