decred.org/dcrdex@v1.0.3/client/asset/btc/spv_peer_manager.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package btc 5 6 import ( 7 "encoding/json" 8 "errors" 9 "fmt" 10 "net" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 16 "decred.org/dcrdex/client/asset" 17 "decred.org/dcrdex/dex" 18 ) 19 20 type peerSource uint16 21 22 const ( 23 added peerSource = iota 24 defaultPeer 25 ) 26 27 func (s peerSource) toAssetPeerSource() asset.PeerSource { 28 switch s { 29 case added: 30 return asset.UserAdded 31 case defaultPeer: 32 return asset.WalletDefault 33 } 34 return asset.Discovered 35 } 36 37 type walletPeer struct { 38 source peerSource 39 resolvedName string 40 } 41 42 // PeerManagerChainService are the functions needed for an SPVPeerManager 43 // to communicate with a chain service. 44 type PeerManagerChainService interface { 45 AddPeer(addr string) error 46 RemovePeer(addr string) error 47 Peers() []SPVPeer 48 } 49 50 // SPVPeerManager implements peer management functionality for all bitcoin 51 // clone SPV wallets. 52 type SPVPeerManager struct { 53 cs PeerManagerChainService 54 55 peersMtx sync.RWMutex 56 peers map[string]*walletPeer 57 58 savedPeersFilePath string 59 60 defaultPeers []string 61 62 defaultPort string 63 64 log dex.Logger 65 } 66 67 // NewSPVPeerManager creates a new SPVPeerManager. 68 func NewSPVPeerManager(cs PeerManagerChainService, defaultPeers []string, dir string, log dex.Logger, defaultPort string) *SPVPeerManager { 69 return &SPVPeerManager{ 70 cs: cs, 71 defaultPeers: defaultPeers, 72 peers: make(map[string]*walletPeer), 73 savedPeersFilePath: filepath.Join(dir, "dexc-peers.json"), // peers.json is used by neutrino 74 log: log, 75 defaultPort: defaultPort, 76 } 77 } 78 79 func (s *SPVPeerManager) connectedPeers() map[string]struct{} { 80 peers := s.cs.Peers() 81 connectedPeers := make(map[string]struct{}, len(peers)) 82 for _, peer := range peers { 83 connectedPeers[peer.Addr()] = struct{}{} 84 } 85 return connectedPeers 86 } 87 88 // Peers returns the list of peers that the wallet is connected to. It also 89 // returns the peers that the user added that the wallet may not currently 90 // be connected to. 91 func (s *SPVPeerManager) Peers() ([]*asset.WalletPeer, error) { 92 s.peersMtx.RLock() 93 defer s.peersMtx.RUnlock() 94 95 connectedPeers := s.connectedPeers() 96 97 walletPeers := make([]*asset.WalletPeer, 0, len(connectedPeers)) 98 99 for originalAddr, peer := range s.peers { 100 _, connected := connectedPeers[peer.resolvedName] 101 delete(connectedPeers, peer.resolvedName) 102 walletPeers = append(walletPeers, &asset.WalletPeer{ 103 Addr: originalAddr, 104 Connected: connected, 105 Source: peer.source.toAssetPeerSource(), 106 }) 107 } 108 109 for peer := range connectedPeers { 110 walletPeers = append(walletPeers, &asset.WalletPeer{ 111 Addr: peer, 112 Connected: true, 113 Source: asset.Discovered, 114 }) 115 } 116 117 return walletPeers, nil 118 } 119 120 // resolveAddress resolves an address to ip:port. This is needed because neutrino 121 // internally resolves the address, and when neutrino is called to return its list 122 // of peers, it will return the resolved addresses. Therefore, we call neutrino 123 // with the resolved address, then we keep track of the mapping of address to 124 // resolved address in order to be able to display the address the user provided 125 // back to the user. 126 func (s *SPVPeerManager) resolveAddress(addr string) (string, error) { 127 host, strPort, err := net.SplitHostPort(addr) 128 if err != nil { 129 switch err.(type) { 130 case *net.AddrError: 131 host = addr 132 strPort = s.defaultPort 133 default: 134 return "", err 135 } 136 } 137 138 // Tor addresses cannot be resolved to an IP, so just return onionAddr 139 // instead. 140 if strings.HasSuffix(host, ".onion") { 141 return addr, nil 142 } 143 144 ips, err := net.LookupIP(host) 145 if err != nil { 146 return "", err 147 } 148 if len(ips) == 0 { 149 return "", fmt.Errorf("no addresses found for %s", host) 150 } 151 152 var ip string 153 if host == "localhost" && len(ips) > 1 { 154 ip = "127.0.0.1" 155 } else { 156 ip = ips[0].String() 157 } 158 159 return net.JoinHostPort(ip, strPort), nil 160 } 161 162 // peerWithResolvedAddress checks to see if there is a peer with a resolved 163 // address in s.peers, and if so, returns the address that was user to add 164 // the peer. 165 func (s *SPVPeerManager) peerWithResolvedAddr(resolvedAddr string) (string, bool) { 166 for originalAddr, peer := range s.peers { 167 if peer.resolvedName == resolvedAddr { 168 return originalAddr, true 169 } 170 } 171 return "", false 172 } 173 174 // loadSavedPeersFromFile returns the contents of dexc-peers.json. 175 func (s *SPVPeerManager) loadSavedPeersFromFile() (map[string]peerSource, error) { 176 content, err := os.ReadFile(s.savedPeersFilePath) 177 if errors.Is(err, os.ErrNotExist) { 178 return make(map[string]peerSource), nil 179 } 180 if err != nil { 181 return nil, err 182 } 183 184 peers := make(map[string]peerSource) 185 err = json.Unmarshal(content, &peers) 186 if err != nil { 187 return nil, err 188 } 189 190 return peers, nil 191 } 192 193 // writeSavedPeersToFile replaces the contents of dexc-peers.json. 194 func (s *SPVPeerManager) writeSavedPeersToFile(peers map[string]peerSource) error { 195 content, err := json.Marshal(peers) 196 if err != nil { 197 return err 198 } 199 return os.WriteFile(s.savedPeersFilePath, content, 0644) 200 } 201 202 func (s *SPVPeerManager) addPeer(addr string, source peerSource, initialLoad bool) error { 203 s.peersMtx.Lock() 204 defer s.peersMtx.Unlock() 205 206 resolvedAddr, err := s.resolveAddress(addr) 207 if err != nil { 208 if initialLoad { 209 // If this is the initial load, we still want to add peers that are 210 // not able to be connected to the peers map, in order to display them 211 // to the user. If a user previously added a peer that originally connected 212 // but now the address cannot be resolved to an IP, it should be displayed 213 // that the wallet was unable to connect to that peer. 214 s.peers[addr] = &walletPeer{source: source} 215 } 216 return fmt.Errorf("failed to resolve address: %v", err) 217 } 218 219 if duplicatePeer, found := s.peerWithResolvedAddr(resolvedAddr); found { 220 return fmt.Errorf("%s and %s resolve to the same node", duplicatePeer, addr) 221 } 222 223 s.peers[addr] = &walletPeer{source: source, resolvedName: resolvedAddr} 224 225 if !initialLoad { 226 savedPeers, err := s.loadSavedPeersFromFile() 227 if err != nil { 228 s.log.Errorf("failed to load saved peers from file") 229 } else { 230 savedPeers[addr] = source 231 err = s.writeSavedPeersToFile(savedPeers) 232 if err != nil { 233 s.log.Errorf("failed to add peer to saved peers file: %v") 234 } 235 } 236 } 237 238 connectedPeers := s.connectedPeers() 239 _, connected := connectedPeers[resolvedAddr] 240 if !connected { 241 return s.cs.AddPeer(resolvedAddr) 242 } 243 244 return nil 245 } 246 247 // AddPeer connects to a new peer and stores it in the db. 248 func (s *SPVPeerManager) AddPeer(addr string) error { 249 return s.addPeer(addr, added, false) 250 } 251 252 // RemovePeer disconnects from a peer added by the user and removes it from 253 // the db. 254 func (s *SPVPeerManager) RemovePeer(addr string) error { 255 s.peersMtx.Lock() 256 defer s.peersMtx.Unlock() 257 258 peer, found := s.peers[addr] 259 if !found { 260 return fmt.Errorf("peer not found: %v", addr) 261 } 262 263 savedPeers, err := s.loadSavedPeersFromFile() 264 if err != nil { 265 return err 266 } 267 delete(savedPeers, addr) 268 err = s.writeSavedPeersToFile(savedPeers) 269 if err != nil { 270 s.log.Errorf("failed to delete peer from saved peers file: %v") 271 } else { 272 delete(s.peers, addr) 273 } 274 275 connectedPeers := s.connectedPeers() 276 _, connected := connectedPeers[peer.resolvedName] 277 if connected { 278 return s.cs.RemovePeer(peer.resolvedName) 279 } 280 281 return nil 282 } 283 284 // ConnectToInitialWalletPeers connects to the default peers and the peers 285 // that were added by the user and persisted in the db. 286 func (s *SPVPeerManager) ConnectToInitialWalletPeers() { 287 for _, peer := range s.defaultPeers { 288 err := s.addPeer(peer, defaultPeer, true) 289 if err != nil { 290 s.log.Errorf("failed to add default peer %s: %v", peer, err) 291 } 292 } 293 294 savedPeers, err := s.loadSavedPeersFromFile() 295 if err != nil { 296 s.log.Errorf("failed to load saved peers from file: v", err) 297 return 298 } 299 300 for addr := range savedPeers { 301 err := s.addPeer(addr, added, true) 302 if err != nil { 303 s.log.Errorf("failed to add peer %s: %v", addr, err) 304 } 305 } 306 }