decred.org/dcrdex@v1.0.5/tatanka/tatanka.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 tatanka 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 "sync/atomic" 16 "time" 17 18 "decred.org/dcrdex/dex" 19 "decred.org/dcrdex/dex/fiatrates" 20 "decred.org/dcrdex/dex/msgjson" 21 "decred.org/dcrdex/server/comms" 22 "decred.org/dcrdex/tatanka/chain" 23 "decred.org/dcrdex/tatanka/db" 24 "decred.org/dcrdex/tatanka/mj" 25 "decred.org/dcrdex/tatanka/tanka" 26 "decred.org/dcrdex/tatanka/tcp" 27 "github.com/decred/dcrd/dcrec/secp256k1/v4" 28 "golang.org/x/text/cases" 29 "golang.org/x/text/language" 30 ) 31 32 const ( 33 version = 0 34 35 // tatankaUniqueID is the unique ID used to register a Tatanka as a fiat 36 // rate listener. 37 tatankaUniqueID = "Tatanka" 38 ) 39 40 // remoteTatanka is a remote tatanka node. A remote tatanka node can either 41 // be outgoing (whitelist loop) or incoming via handleInboundTatankaConnect. 42 type remoteTatanka struct { 43 *peer 44 cfg atomic.Value // mj.TatankaConfig 45 } 46 47 type Topic struct { 48 subjects map[tanka.Subject]map[tanka.PeerID]struct{} 49 subscribers map[tanka.PeerID]struct{} 50 } 51 52 func (topic *Topic) unsubUser(peerID tanka.PeerID) { 53 if _, found := topic.subscribers[peerID]; !found { 54 return 55 } 56 for subID, subs := range topic.subjects { 57 delete(subs, peerID) 58 if len(subs) == 0 { 59 delete(topic.subjects, subID) 60 } 61 } 62 delete(topic.subscribers, peerID) 63 } 64 65 // BootNode represents a configured boot node. Tatanka is whitelist only, and 66 // node operators are responsible for keeping their whitelist up to date. 67 type BootNode struct { 68 // Protocol is one of ("ws", "wss"), though other tatanka comms protocols 69 // may be implemented later. Or we may end up using e.g. go-libp2p. 70 Protocol string 71 PeerID dex.Bytes 72 // Config can take different forms depending on the comms protocol, but is 73 // probably a tcp.RemoteNodeConfig. 74 Config json.RawMessage 75 } 76 77 // parsedBootNode is the unexported version of BootNode, but with a PeerID 78 // instead of []byte. 79 type parsedBootNode struct { 80 peerID tanka.PeerID 81 cfg json.RawMessage 82 protocol string 83 } 84 85 // Tatanka is a server node on Tatanka Mesh. Tatanka implements two APIs, one 86 // for fellow tatanka nodes, and one for clients. The primary roles of a 87 // tatanka node are 88 // 1. Maintain reputation information about client nodes. 89 // 2. Distribute broadcasts and relay tankagrams. 90 // 3. Provide some basic oracle services. 91 type Tatanka struct { 92 net dex.Network 93 log dex.Logger 94 tcpSrv *tcp.Server 95 dataDir string 96 ctx context.Context 97 wg *sync.WaitGroup 98 whitelist map[tanka.PeerID]*parsedBootNode 99 db *db.DB 100 nets atomic.Value // []uint32 101 handlers map[string]func(tanka.Sender, *msgjson.Message) *msgjson.Error 102 routes []string 103 // bondTier atomic.Uint64 104 105 priv *secp256k1.PrivateKey 106 id tanka.PeerID 107 108 chainMtx sync.RWMutex 109 chains map[uint32]chain.Chain 110 111 relayMtx sync.Mutex 112 recentRelays map[[32]byte]time.Time 113 114 clientMtx sync.RWMutex 115 clients map[tanka.PeerID]*client 116 topics map[tanka.Topic]*Topic 117 118 clientJobs chan *clientJob 119 remoteClients map[tanka.PeerID]map[tanka.PeerID]struct{} 120 121 tatankasMtx sync.RWMutex 122 tatankas map[tanka.PeerID]*remoteTatanka 123 124 fiatRateOracle *fiatrates.Oracle 125 fiatRateChan chan map[string]*fiatrates.FiatRateInfo 126 } 127 128 // Config is the configuration of the Tatanka. 129 type Config struct { 130 Net dex.Network 131 DataDir string 132 Logger dex.Logger 133 RPC comms.RPCConfig 134 ConfigPath string 135 136 // TODO: Change to whitelist 137 WhiteList []BootNode 138 139 FiatOracleConfig fiatrates.Config 140 } 141 142 func New(cfg *Config) (*Tatanka, error) { 143 chainCfg, err := loadConfig(cfg.ConfigPath) 144 if err != nil { 145 return nil, fmt.Errorf("error loading config %w", err) 146 } 147 148 chains := make(map[uint32]chain.Chain) 149 nets := make([]uint32, 0, len(chainCfg.Chains)) 150 for _, c := range chainCfg.Chains { 151 chainID, found := dex.BipSymbolID(c.Symbol) 152 if !found { 153 return nil, fmt.Errorf("no chain ID found for symbol %s", c.Symbol) 154 } 155 chains[chainID], err = chain.New(chainID, c.Config, cfg.Logger.SubLogger(c.Symbol), cfg.Net) 156 if err != nil { 157 return nil, fmt.Errorf("error creating chain backend: %w", err) 158 } 159 nets = append(nets, chainID) 160 } 161 162 db, err := db.New(filepath.Join(cfg.DataDir, "db"), cfg.Logger.SubLogger("DB")) 163 if err != nil { 164 return nil, fmt.Errorf("db.New error: %w", err) 165 } 166 167 keyPath := filepath.Join(cfg.DataDir, "priv.key") 168 keyB, err := os.ReadFile(keyPath) 169 if err != nil { 170 if !os.IsNotExist(err) { 171 return nil, fmt.Errorf("error reading key file") 172 } 173 cfg.Logger.Infof("No key file found. Generating new identity.") 174 priv, err := secp256k1.GeneratePrivateKey() 175 if err != nil { 176 return nil, fmt.Errorf("GeneratePrivateKey error: %w", err) 177 } 178 keyB = priv.Serialize() 179 if err = os.WriteFile(keyPath, keyB, 0600); err != nil { 180 return nil, fmt.Errorf("error writing newly-generated key to %q: %v", keyPath, err) 181 } 182 } 183 priv := secp256k1.PrivKeyFromBytes(keyB) 184 var peerID tanka.PeerID 185 copy(peerID[:], priv.PubKey().SerializeCompressed()) 186 187 whitelist := make(map[tanka.PeerID]*parsedBootNode, len(cfg.WhiteList)) 188 for _, n := range cfg.WhiteList { 189 if len(n.PeerID) != tanka.PeerIDLength { 190 return nil, fmt.Errorf("invalid peer ID length %d for %s boot node with configuration %q", len(n.PeerID), n.Protocol, n.Config) 191 } 192 var peerID tanka.PeerID 193 copy(peerID[:], n.PeerID) 194 whitelist[peerID] = &parsedBootNode{ 195 peerID: peerID, 196 cfg: n.Config, 197 protocol: n.Protocol, 198 } 199 } 200 201 t := &Tatanka{ 202 net: cfg.Net, 203 dataDir: cfg.DataDir, 204 log: cfg.Logger, 205 whitelist: whitelist, 206 db: db, 207 priv: priv, 208 id: peerID, 209 chains: chains, 210 tatankas: make(map[tanka.PeerID]*remoteTatanka), 211 clients: make(map[tanka.PeerID]*client), 212 remoteClients: make(map[tanka.PeerID]map[tanka.PeerID]struct{}), 213 topics: make(map[tanka.Topic]*Topic), 214 recentRelays: make(map[[32]byte]time.Time), 215 clientJobs: make(chan *clientJob, 128), 216 } 217 218 if !cfg.FiatOracleConfig.AllFiatSourceDisabled() { 219 var tickers string 220 upperCaser := cases.Upper(language.AmericanEnglish) 221 for _, c := range chainCfg.Chains { 222 tickers += upperCaser.String(c.Symbol) + "," 223 } 224 tickers = strings.Trim(tickers, ",") 225 226 t.fiatRateOracle, err = fiatrates.NewFiatOracle(cfg.FiatOracleConfig, tickers, t.log) 227 if err != nil { 228 return nil, fmt.Errorf("error initializing fiat oracle: %w", err) 229 } 230 231 // Register tatanka as a listener 232 t.fiatRateChan = make(chan map[string]*fiatrates.FiatRateInfo) 233 t.fiatRateOracle.AddFiatRateListener(tatankaUniqueID, t.fiatRateChan) 234 } 235 236 t.nets.Store(nets) 237 t.prepareHandlers() 238 t.tcpSrv, err = tcp.NewServer(&cfg.RPC, &tcpCore{t}, cfg.Logger.SubLogger("TCP")) 239 if err != nil { 240 return nil, fmt.Errorf("error starting TPC server:: %v", err) 241 } 242 243 return t, nil 244 } 245 246 func (t *Tatanka) prepareHandlers() { 247 t.handlers = map[string]func(tanka.Sender, *msgjson.Message) *msgjson.Error{ 248 // tatanka messages 249 mj.RouteTatankaConnect: t.handleInboundTatankaConnect, 250 mj.RouteTatankaConfig: t.handleTatankaMessage, 251 mj.RouteRelayBroadcast: t.handleTatankaMessage, 252 mj.RouteNewClient: t.handleTatankaMessage, 253 mj.RouteClientDisconnect: t.handleTatankaMessage, 254 mj.RouteRelayTankagram: t.handleTatankaMessage, 255 mj.RoutePathInquiry: t.handleTatankaMessage, 256 // client messages 257 mj.RouteConnect: t.handleClientConnect, 258 mj.RoutePostBond: t.handlePostBond, 259 mj.RouteSubscribe: t.handleClientMessage, 260 mj.RouteUnsubscribe: t.handleClientMessage, 261 mj.RouteBroadcast: t.handleClientMessage, 262 mj.RouteTankagram: t.handleClientMessage, 263 } 264 for route := range t.handlers { 265 t.routes = append(t.routes, route) 266 } 267 } 268 269 func (t *Tatanka) assets() []uint32 { 270 return t.nets.Load().([]uint32) 271 } 272 273 func (t *Tatanka) fiatOracleEnabled() bool { 274 return t.fiatRateOracle != nil 275 } 276 277 func (t *Tatanka) tatankaNodes() []*remoteTatanka { 278 t.tatankasMtx.RLock() 279 defer t.tatankasMtx.RUnlock() 280 nodes := make([]*remoteTatanka, 0, len(t.tatankas)) 281 for _, n := range t.tatankas { 282 nodes = append(nodes, n) 283 } 284 return nodes 285 } 286 287 func (t *Tatanka) tatankaNode(peerID tanka.PeerID) *remoteTatanka { 288 t.tatankasMtx.RLock() 289 defer t.tatankasMtx.RUnlock() 290 return t.tatankas[peerID] 291 } 292 293 func (t *Tatanka) clientNode(peerID tanka.PeerID) *client { 294 t.clientMtx.RLock() 295 defer t.clientMtx.RUnlock() 296 return t.clients[peerID] 297 } 298 299 func (t *Tatanka) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) { 300 t.ctx = ctx 301 var wg sync.WaitGroup 302 t.wg = &wg 303 304 t.log.Infof("Starting Tatanka node with peer ID %s", t.id) 305 306 // Start WebSocket server 307 cm := dex.NewConnectionMaster(t.tcpSrv) 308 if err := cm.ConnectOnce(ctx); err != nil { 309 return nil, fmt.Errorf("error connecting TCP server: %v", err) 310 } 311 312 wg.Add(1) 313 go func() { 314 cm.Wait() 315 wg.Done() 316 }() 317 318 // Start a ticker to clean up the recent relays map. 319 wg.Add(1) 320 go func() { 321 defer wg.Done() 322 tick := time.NewTicker(tanka.EpochLength * 4) // 1 minute 323 for { 324 select { 325 case <-tick.C: 326 t.relayMtx.Lock() 327 for bid, stamp := range t.recentRelays { 328 if time.Since(stamp) > time.Minute { 329 delete(t.recentRelays, bid) 330 } 331 } 332 t.relayMtx.Unlock() 333 case <-ctx.Done(): 334 return 335 } 336 } 337 }() 338 339 wg.Add(1) 340 go func() { 341 defer wg.Done() 342 t.runRemoteClientsLoop(ctx) 343 }() 344 345 var success bool 346 defer func() { 347 if !success { 348 cm.Disconnect() 349 cm.Wait() 350 } 351 }() 352 353 t.chainMtx.RLock() 354 for assetID, c := range t.chains { 355 feeRater, is := c.(chain.FeeRater) 356 if !is { 357 continue 358 } 359 wg.Add(1) 360 go func(assetID uint32, feeRater chain.FeeRater) { 361 defer wg.Done() 362 t.monitorChainFees(ctx, assetID, feeRater) 363 }(assetID, feeRater) 364 } 365 t.chainMtx.RUnlock() 366 367 wg.Add(1) 368 go func() { 369 defer wg.Done() 370 t.runWhitelistLoop(ctx) 371 }() 372 373 if t.fiatOracleEnabled() { 374 wg.Add(2) 375 go func() { 376 defer wg.Done() 377 t.fiatRateOracle.Run(t.ctx) 378 }() 379 380 go func() { 381 defer wg.Done() 382 t.broadcastRates() 383 }() 384 } 385 386 success = true 387 return &wg, nil 388 } 389 390 // runWhitelistLoop attempts to connect to the whitelist, and then periodically 391 // tries again. 392 func (t *Tatanka) runWhitelistLoop(ctx context.Context) { 393 connectWhitelist := func() { 394 for proto, n := range t.whitelist { 395 t.tatankasMtx.RLock() 396 _, exists := t.tatankas[n.peerID] 397 t.tatankasMtx.RUnlock() 398 if exists { 399 continue 400 } 401 402 p, rrs, err := t.loadPeer(n.peerID) 403 if err != nil { 404 t.log.Errorf("error getting peer info for boot node at %q (proto %q): %v", string(n.cfg), proto, err) 405 continue 406 } 407 408 bondTier := p.BondTier() 409 // TODO: Check Tatanka Node reputation too 410 // if calcTier(rep, bondTier) <= 0 { 411 // t.log.Errorf("not attempting to contact banned boot node at %q (proto %q)", string(n.cfg), proto) 412 // } 413 414 handleDisconnect := func() { 415 // TODO: schedule a reconnect? 416 t.tatankasMtx.Lock() 417 delete(t.tatankas, p.ID) 418 t.tatankasMtx.Unlock() 419 } 420 421 handleMessage := func(cl tanka.Sender, msg *msgjson.Message) { 422 t.handleTatankaMessage(cl, msg) 423 } 424 425 var cl tanka.Sender 426 switch n.protocol { 427 case "ws", "wss": 428 cl, err = t.tcpSrv.ConnectBootNode(ctx, n.cfg, handleMessage, handleDisconnect) 429 default: 430 t.log.Errorf("unknown boot node network protocol: %s", proto) 431 continue 432 } 433 if err != nil { 434 t.log.Errorf("error connecting boot node with proto = %s, config = %s", proto, string(n.cfg)) 435 continue 436 } 437 438 t.log.Infof("Connected to boot node with peer ID %s, config %s", n.peerID, string(n.cfg)) 439 440 cl.SetPeerID(p.ID) 441 pp := &peer{Peer: p, Sender: cl, rrs: rrs} 442 tt := &remoteTatanka{peer: pp} 443 t.tatankasMtx.Lock() 444 t.tatankas[p.ID] = tt 445 t.tatankasMtx.Unlock() 446 447 cfgMsg := mj.MustRequest(mj.RouteTatankaConnect, t.generateConfig(bondTier)) 448 if err := t.request(cl, cfgMsg, func(msg *msgjson.Message) { 449 // Nothing to do. The only non-error result is payload = true. 450 }); err != nil { 451 t.log.Errorf("Error sending connect message: %w", err) 452 cl.Disconnect() 453 } 454 } 455 } 456 457 for { 458 connectWhitelist() 459 460 select { 461 case <-time.After(time.Minute * 5): 462 case <-ctx.Done(): 463 return 464 } 465 } 466 } 467 468 // monitorChainFees monitors chains for new fee rates, and will distribute them 469 // as part of the not-yet-implemented oracle services the mesh provides. 470 func (t *Tatanka) monitorChainFees(ctx context.Context, assetID uint32, c chain.FeeRater) { 471 feeC := c.FeeChannel() 472 for { 473 select { 474 case feeRate := <-feeC: 475 // TODO: Distribute the fee rate to other Tatanka nodes, then to 476 // clients. Should fee rates be averaged across tatankas somehow? 477 fmt.Printf("new fee rate from %s: %d\n", dex.BipIDSymbol(assetID), feeRate) 478 case <-ctx.Done(): 479 return 480 } 481 } 482 } 483 484 // sendResult sends the response to a request and logs errors. 485 func (t *Tatanka) sendResult(cl tanka.Sender, msgID uint64, result interface{}) { 486 resp := mj.MustResponse(msgID, result, nil) 487 if err := t.send(cl, resp); err != nil { 488 peerID := cl.PeerID() 489 t.log.Errorf("error sending result to %q: %v", dex.Bytes(peerID[:]), err) 490 } 491 } 492 493 // batchSend must be called with the clientMtx >= RLocked. 494 func (t *Tatanka) batchSend(peers map[tanka.PeerID]struct{}, msg *msgjson.Message) { 495 mj.SignMessage(t.priv, msg) 496 msgB, err := json.Marshal(msg) 497 if err != nil { 498 t.log.Errorf("error marshaling batch send message: %v", err) 499 return 500 } 501 disconnects := make(map[tanka.PeerID]struct{}) 502 t.clientMtx.RLock() 503 for peerID := range peers { 504 if c, found := t.clients[peerID]; found { 505 if err := c.SendRaw(msgB); err != nil { 506 t.log.Tracef("Disconnecting client %s after SendRaw error: %v", peerID, err) 507 disconnects[peerID] = struct{}{} 508 } 509 } else { 510 t.log.Error("found a subscriber ID without a client") 511 } 512 } 513 t.clientMtx.RUnlock() 514 if len(disconnects) > 0 { 515 for peerID := range disconnects { 516 t.clientDisconnected(peerID) 517 } 518 } 519 } 520 521 // send signs and sends the message, returning any errors. 522 func (t *Tatanka) send(s tanka.Sender, msg *msgjson.Message) error { 523 mj.SignMessage(t.priv, msg) 524 err := s.Send(msg) 525 if err != nil { 526 t.clientDisconnected(s.PeerID()) 527 } 528 return err 529 } 530 531 // request signs and sends the request, returning any errors. 532 func (t *Tatanka) request(s tanka.Sender, msg *msgjson.Message, respHandler func(*msgjson.Message)) error { 533 mj.SignMessage(t.priv, msg) 534 err := s.Request(msg, respHandler) 535 if err != nil { 536 t.clientDisconnected(s.PeerID()) 537 } 538 return err 539 } 540 541 // loadPeer loads and resolves peer reputation data from the database. 542 func (t *Tatanka) loadPeer(peerID tanka.PeerID) (*tanka.Peer, map[tanka.PeerID]*mj.RemoteReputation, error) { 543 p, err := t.db.GetPeer(peerID) 544 if err == nil { 545 return p, nil, nil 546 } 547 548 if !errors.Is(err, db.ErrNotFound) { 549 return nil, nil, err 550 } 551 pubKey, err := secp256k1.ParsePubKey(peerID[:]) 552 if err != nil { 553 return nil, nil, fmt.Errorf("ParsePubKey error: %w", err) 554 } 555 rep, rrs, err := t.resolveReputation(peerID) 556 if err != nil { 557 return nil, nil, fmt.Errorf("error fetching reputation: %w", err) 558 } 559 560 return &tanka.Peer{ 561 ID: peerID, 562 PubKey: pubKey, 563 Reputation: rep, 564 }, rrs, nil 565 } 566 567 // resolveReputation constructs a user reputation, doing a "soft sync" with 568 // the mesh if our data is scant. 569 func (t *Tatanka) resolveReputation(peerID tanka.PeerID) (*tanka.Reputation, map[tanka.PeerID]*mj.RemoteReputation, error) { 570 rep, err := t.db.Reputation(peerID) 571 if err != nil { 572 return nil, nil, fmt.Errorf("error fetching reputation: %w", err) 573 } 574 // If we have a fully-established reputation, we don't care what our peers 575 // think of this guy. 576 if len(rep.Points) == tanka.MaxReputationEntries { 577 return rep, nil, nil 578 } 579 580 if true { 581 fmt.Println("!!!! Skipping reputation resolution") 582 return rep, nil, nil 583 } 584 585 // We don't have enough info. We'll reach out to others to see what we can 586 // figure out. 587 tankas := t.tatankaNodes() 588 n := len(tankas) 589 type res struct { 590 rr *mj.RemoteReputation 591 id tanka.PeerID 592 } 593 resC := make(chan *res) 594 595 report := func(rr *res) { 596 select { 597 case resC <- rr: 598 case <-time.After(time.Second): 599 t.log.Errorf("blocking remote reputation result channel") 600 } 601 } 602 603 requestReputation := func(tt *remoteTatanka) { 604 req := mj.MustRequest(mj.RouteGetReputation, nil) 605 t.request(tt, req, func(respMsg *msgjson.Message) { 606 var rr mj.RemoteReputation 607 if err := respMsg.UnmarshalResult(&rr); err == nil { 608 report(&res{ 609 id: tt.ID, 610 rr: &rr, 611 }) 612 } else { 613 t.log.Errorf("error requesting remote reputation from %q: %v", tt.ID, err) 614 report(nil) 615 } 616 }) 617 } 618 for _, tt := range tankas { 619 requestReputation(tt) 620 } 621 622 received := make(map[tanka.PeerID]*mj.RemoteReputation, n) 623 timedOut := time.After(time.Second * 10) 624 625 out: 626 for { 627 select { 628 case res := <-resC: 629 received[res.id] = res.rr 630 if len(received) == n { 631 break out 632 } 633 case <-timedOut: 634 t.log.Errorf("timed out waiting for remote reputations. %d received out of %d requested", len(received), len(tankas)) 635 break out 636 } 637 } 638 return rep, received, nil 639 } 640 641 func (t *Tatanka) generateConfig(bondTier uint64) *mj.TatankaConfig { 642 return &mj.TatankaConfig{ 643 ID: t.id, 644 Version: version, 645 Chains: t.assets(), 646 BondTier: bondTier, 647 } 648 } 649 650 func calcTier(r *tanka.Reputation, bondTier uint64) int64 { 651 return int64(bondTier) + int64(r.Score)/tanka.TierIncrement 652 } 653 654 // ChainConfig is how the chain configuration is specified in the Tatanka 655 // configuration file. 656 type ChainConfig struct { 657 Symbol string `json:"symbol"` 658 Config json.RawMessage `json:"config"` 659 } 660 661 // ConfigFile represents the JSON Tatanka configuration file. 662 type ConfigFile struct { 663 Chains []ChainConfig `json:"chains"` 664 } 665 666 func loadConfig(configPath string) (*ConfigFile, error) { 667 var cfg ConfigFile 668 b, err := os.ReadFile(configPath) 669 if err != nil { 670 return nil, fmt.Errorf("OpenFile error: %w", err) 671 } 672 return &cfg, json.Unmarshal(b, &cfg) 673 } 674 675 // tcpCore implements tcp.TankaCore. 676 type tcpCore struct { 677 *Tatanka 678 } 679 680 func (t *tcpCore) Routes() []string { 681 return t.routes 682 } 683 684 func (t *tcpCore) HandleMessage(cl tanka.Sender, msg *msgjson.Message) *msgjson.Error { 685 if t.log.Level() == dex.LevelTrace { 686 t.log.Tracef("Tatanka node handling message. route = %s, payload = %s", msg.Route, mj.Truncate(msg.Payload)) 687 } 688 689 handle, found := t.handlers[msg.Route] 690 if !found { 691 return msgjson.NewError(mj.ErrBadRequest, "route %q not known", msg.Route) 692 } 693 return handle(cl, msg) 694 } 695 696 // clientDisconnected handle a client disconnect, removing the client from the 697 // clients map and unsubscribing from all topics. 698 func (t *Tatanka) clientDisconnected(peerID tanka.PeerID) { 699 unsubs := make(map[tanka.Topic]*Topic) 700 701 t.clientMtx.Lock() 702 delete(t.clients, peerID) 703 for n, topic := range t.topics { 704 if _, found := topic.subscribers[peerID]; found { 705 unsubs[n] = topic 706 delete(topic.subscribers, peerID) 707 for _, subs := range topic.subjects { 708 delete(subs, peerID) 709 } 710 } 711 } 712 t.clientMtx.Unlock() 713 714 if len(unsubs) == 0 { 715 return 716 } 717 718 stamp := time.Now() 719 for n, topic := range unsubs { 720 note := mj.MustNotification(mj.RouteBroadcast, &mj.Broadcast{ 721 Topic: n, 722 PeerID: peerID, 723 MessageType: mj.MessageTypeUnsubTopic, 724 Stamp: stamp, 725 }) 726 t.batchSend(topic.subscribers, note) 727 } 728 729 note := mj.MustNotification(mj.RouteClientDisconnect, &mj.Disconnect{ID: peerID}) 730 mj.SignMessage(t.priv, note) 731 for _, tt := range t.tatankaNodes() { 732 tt.Send(note) 733 } 734 } 735 736 // broadcastRates sends market rates to all fiat rate subscribers once new rates 737 // are received from the fiat oracle. 738 func (t *Tatanka) broadcastRates() { 739 for { 740 select { 741 case <-t.ctx.Done(): 742 return 743 case rates, ok := <-t.fiatRateChan: 744 if !ok { 745 t.log.Debug("Tatanka stopped listening for fiat rates.") 746 return 747 } 748 749 t.clientMtx.RLock() 750 topic := t.topics[mj.TopicFiatRate] 751 t.clientMtx.RUnlock() 752 753 if topic != nil && len(topic.subscribers) > 0 { 754 t.batchSend(topic.subscribers, mj.MustNotification(mj.RouteRates, &mj.RateMessage{ 755 Topic: mj.TopicFiatRate, 756 Rates: rates, 757 })) 758 } 759 } 760 } 761 }