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  }