github.com/theQRL/go-zond@v0.1.1/zondstats/zondstats.go (about) 1 // Copyright 2016 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 zondstats implements the network stats reporting service. 18 package zondstats 19 20 import ( 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "math/big" 26 "net/http" 27 "runtime" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/gorilla/websocket" 34 "github.com/theQRL/go-zond" 35 "github.com/theQRL/go-zond/common" 36 "github.com/theQRL/go-zond/common/mclock" 37 "github.com/theQRL/go-zond/consensus" 38 "github.com/theQRL/go-zond/core" 39 "github.com/theQRL/go-zond/core/types" 40 "github.com/theQRL/go-zond/event" 41 "github.com/theQRL/go-zond/les" 42 "github.com/theQRL/go-zond/log" 43 "github.com/theQRL/go-zond/miner" 44 "github.com/theQRL/go-zond/node" 45 "github.com/theQRL/go-zond/p2p" 46 "github.com/theQRL/go-zond/rpc" 47 ethproto "github.com/theQRL/go-zond/zond/protocols/zond" 48 ) 49 50 const ( 51 // historyUpdateRange is the number of blocks a node should report upon login or 52 // history request. 53 historyUpdateRange = 50 54 55 // txChanSize is the size of channel listening to NewTxsEvent. 56 // The number is referenced from the size of tx pool. 57 txChanSize = 4096 58 // chainHeadChanSize is the size of channel listening to ChainHeadEvent. 59 chainHeadChanSize = 10 60 61 messageSizeLimit = 15 * 1024 * 1024 62 ) 63 64 // backend encompasses the bare-minimum functionality needed for zondstats reporting 65 type backend interface { 66 SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription 67 SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription 68 CurrentHeader() *types.Header 69 HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) 70 GetTd(ctx context.Context, hash common.Hash) *big.Int 71 Stats() (pending int, queued int) 72 SyncProgress() zond.SyncProgress 73 } 74 75 // fullNodeBackend encompasses the functionality necessary for a full node 76 // reporting to zondstats 77 type fullNodeBackend interface { 78 backend 79 Miner() *miner.Miner 80 BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) 81 CurrentBlock() *types.Block 82 SuggestGasTipCap(ctx context.Context) (*big.Int, error) 83 } 84 85 // Service implements an Ethereum netstats reporting daemon that pushes local 86 // chain statistics up to a monitoring server. 87 type Service struct { 88 server *p2p.Server // Peer-to-peer server to retrieve networking infos 89 backend backend 90 engine consensus.Engine // Consensus engine to retrieve variadic block fields 91 92 node string // Name of the node to display on the monitoring page 93 pass string // Password to authorize access to the monitoring page 94 host string // Remote address of the monitoring service 95 96 pongCh chan struct{} // Pong notifications are fed into this channel 97 histCh chan []uint64 // History request block numbers are fed into this channel 98 99 headSub event.Subscription 100 txSub event.Subscription 101 } 102 103 // connWrapper is a wrapper to prevent concurrent-write or concurrent-read on the 104 // websocket. 105 // 106 // From Gorilla websocket docs: 107 // 108 // Connections support one concurrent reader and one concurrent writer. Applications are 109 // responsible for ensuring that 110 // - no more than one goroutine calls the write methods 111 // NextWriter, SetWriteDeadline, WriteMessage, WriteJSON, EnableWriteCompression, 112 // SetCompressionLevel concurrently; and 113 // - that no more than one goroutine calls the 114 // read methods NextReader, SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, 115 // SetPingHandler concurrently. 116 // 117 // The Close and WriteControl methods can be called concurrently with all other methods. 118 type connWrapper struct { 119 conn *websocket.Conn 120 121 rlock sync.Mutex 122 wlock sync.Mutex 123 } 124 125 func newConnectionWrapper(conn *websocket.Conn) *connWrapper { 126 conn.SetReadLimit(messageSizeLimit) 127 return &connWrapper{conn: conn} 128 } 129 130 // WriteJSON wraps corresponding method on the websocket but is safe for concurrent calling 131 func (w *connWrapper) WriteJSON(v interface{}) error { 132 w.wlock.Lock() 133 defer w.wlock.Unlock() 134 135 return w.conn.WriteJSON(v) 136 } 137 138 // ReadJSON wraps corresponding method on the websocket but is safe for concurrent calling 139 func (w *connWrapper) ReadJSON(v interface{}) error { 140 w.rlock.Lock() 141 defer w.rlock.Unlock() 142 143 return w.conn.ReadJSON(v) 144 } 145 146 // Close wraps corresponding method on the websocket but is safe for concurrent calling 147 func (w *connWrapper) Close() error { 148 // The Close and WriteControl methods can be called concurrently with all other methods, 149 // so the mutex is not used here 150 return w.conn.Close() 151 } 152 153 // parseEthstatsURL parses the netstats connection url. 154 // URL argument should be of the form <nodename:secret@host:port> 155 // If non-erroring, the returned slice contains 3 elements: [nodename, pass, host] 156 func parseEthstatsURL(url string) (parts []string, err error) { 157 err = fmt.Errorf("invalid netstats url: \"%s\", should be nodename:secret@host:port", url) 158 159 hostIndex := strings.LastIndex(url, "@") 160 if hostIndex == -1 || hostIndex == len(url)-1 { 161 return nil, err 162 } 163 preHost, host := url[:hostIndex], url[hostIndex+1:] 164 165 passIndex := strings.LastIndex(preHost, ":") 166 if passIndex == -1 { 167 return []string{preHost, "", host}, nil 168 } 169 nodename, pass := preHost[:passIndex], "" 170 if passIndex != len(preHost)-1 { 171 pass = preHost[passIndex+1:] 172 } 173 174 return []string{nodename, pass, host}, nil 175 } 176 177 // New returns a monitoring service ready for stats reporting. 178 func New(node *node.Node, backend backend, engine consensus.Engine, url string) error { 179 parts, err := parseEthstatsURL(url) 180 if err != nil { 181 return err 182 } 183 ethstats := &Service{ 184 backend: backend, 185 engine: engine, 186 server: node.Server(), 187 node: parts[0], 188 pass: parts[1], 189 host: parts[2], 190 pongCh: make(chan struct{}), 191 histCh: make(chan []uint64, 1), 192 } 193 194 node.RegisterLifecycle(ethstats) 195 return nil 196 } 197 198 // Start implements node.Lifecycle, starting up the monitoring and reporting daemon. 199 func (s *Service) Start() error { 200 // Subscribe to chain events to execute updates on 201 chainHeadCh := make(chan core.ChainHeadEvent, chainHeadChanSize) 202 s.headSub = s.backend.SubscribeChainHeadEvent(chainHeadCh) 203 txEventCh := make(chan core.NewTxsEvent, txChanSize) 204 s.txSub = s.backend.SubscribeNewTxsEvent(txEventCh) 205 go s.loop(chainHeadCh, txEventCh) 206 207 log.Info("Stats daemon started") 208 return nil 209 } 210 211 // Stop implements node.Lifecycle, terminating the monitoring and reporting daemon. 212 func (s *Service) Stop() error { 213 s.headSub.Unsubscribe() 214 s.txSub.Unsubscribe() 215 log.Info("Stats daemon stopped") 216 return nil 217 } 218 219 // loop keeps trying to connect to the netstats server, reporting chain events 220 // until termination. 221 func (s *Service) loop(chainHeadCh chan core.ChainHeadEvent, txEventCh chan core.NewTxsEvent) { 222 // Start a goroutine that exhausts the subscriptions to avoid events piling up 223 var ( 224 quitCh = make(chan struct{}) 225 headCh = make(chan *types.Block, 1) 226 txCh = make(chan struct{}, 1) 227 ) 228 go func() { 229 var lastTx mclock.AbsTime 230 231 HandleLoop: 232 for { 233 select { 234 // Notify of chain head events, but drop if too frequent 235 case head := <-chainHeadCh: 236 select { 237 case headCh <- head.Block: 238 default: 239 } 240 241 // Notify of new transaction events, but drop if too frequent 242 case <-txEventCh: 243 if time.Duration(mclock.Now()-lastTx) < time.Second { 244 continue 245 } 246 lastTx = mclock.Now() 247 248 select { 249 case txCh <- struct{}{}: 250 default: 251 } 252 253 // node stopped 254 case <-s.txSub.Err(): 255 break HandleLoop 256 case <-s.headSub.Err(): 257 break HandleLoop 258 } 259 } 260 close(quitCh) 261 }() 262 263 // Resolve the URL, defaulting to TLS, but falling back to none too 264 path := fmt.Sprintf("%s/api", s.host) 265 urls := []string{path} 266 267 // url.Parse and url.IsAbs is unsuitable (https://github.com/golang/go/issues/19779) 268 if !strings.Contains(path, "://") { 269 urls = []string{"wss://" + path, "ws://" + path} 270 } 271 272 errTimer := time.NewTimer(0) 273 defer errTimer.Stop() 274 // Loop reporting until termination 275 for { 276 select { 277 case <-quitCh: 278 return 279 case <-errTimer.C: 280 // Establish a websocket connection to the server on any supported URL 281 var ( 282 conn *connWrapper 283 err error 284 ) 285 dialer := websocket.Dialer{HandshakeTimeout: 5 * time.Second} 286 header := make(http.Header) 287 header.Set("origin", "http://localhost") 288 for _, url := range urls { 289 c, _, e := dialer.Dial(url, header) 290 err = e 291 if err == nil { 292 conn = newConnectionWrapper(c) 293 break 294 } 295 } 296 if err != nil { 297 log.Warn("Stats server unreachable", "err", err) 298 errTimer.Reset(10 * time.Second) 299 continue 300 } 301 // Authenticate the client with the server 302 if err = s.login(conn); err != nil { 303 log.Warn("Stats login failed", "err", err) 304 conn.Close() 305 errTimer.Reset(10 * time.Second) 306 continue 307 } 308 go s.readLoop(conn) 309 310 // Send the initial stats so our node looks decent from the get go 311 if err = s.report(conn); err != nil { 312 log.Warn("Initial stats report failed", "err", err) 313 conn.Close() 314 errTimer.Reset(0) 315 continue 316 } 317 // Keep sending status updates until the connection breaks 318 fullReport := time.NewTicker(15 * time.Second) 319 320 for err == nil { 321 select { 322 case <-quitCh: 323 fullReport.Stop() 324 // Make sure the connection is closed 325 conn.Close() 326 return 327 328 case <-fullReport.C: 329 if err = s.report(conn); err != nil { 330 log.Warn("Full stats report failed", "err", err) 331 } 332 case list := <-s.histCh: 333 if err = s.reportHistory(conn, list); err != nil { 334 log.Warn("Requested history report failed", "err", err) 335 } 336 case head := <-headCh: 337 if err = s.reportBlock(conn, head); err != nil { 338 log.Warn("Block stats report failed", "err", err) 339 } 340 if err = s.reportPending(conn); err != nil { 341 log.Warn("Post-block transaction stats report failed", "err", err) 342 } 343 case <-txCh: 344 if err = s.reportPending(conn); err != nil { 345 log.Warn("Transaction stats report failed", "err", err) 346 } 347 } 348 } 349 fullReport.Stop() 350 351 // Close the current connection and establish a new one 352 conn.Close() 353 errTimer.Reset(0) 354 } 355 } 356 } 357 358 // readLoop loops as long as the connection is alive and retrieves data packets 359 // from the network socket. If any of them match an active request, it forwards 360 // it, if they themselves are requests it initiates a reply, and lastly it drops 361 // unknown packets. 362 func (s *Service) readLoop(conn *connWrapper) { 363 // If the read loop exits, close the connection 364 defer conn.Close() 365 366 for { 367 // Retrieve the next generic network packet and bail out on error 368 var blob json.RawMessage 369 if err := conn.ReadJSON(&blob); err != nil { 370 log.Warn("Failed to retrieve stats server message", "err", err) 371 return 372 } 373 // If the network packet is a system ping, respond to it directly 374 var ping string 375 if err := json.Unmarshal(blob, &ping); err == nil && strings.HasPrefix(ping, "primus::ping::") { 376 if err := conn.WriteJSON(strings.ReplaceAll(ping, "ping", "pong")); err != nil { 377 log.Warn("Failed to respond to system ping message", "err", err) 378 return 379 } 380 continue 381 } 382 // Not a system ping, try to decode an actual state message 383 var msg map[string][]interface{} 384 if err := json.Unmarshal(blob, &msg); err != nil { 385 log.Warn("Failed to decode stats server message", "err", err) 386 return 387 } 388 log.Trace("Received message from stats server", "msg", msg) 389 if len(msg["emit"]) == 0 { 390 log.Warn("Stats server sent non-broadcast", "msg", msg) 391 return 392 } 393 command, ok := msg["emit"][0].(string) 394 if !ok { 395 log.Warn("Invalid stats server message type", "type", msg["emit"][0]) 396 return 397 } 398 // If the message is a ping reply, deliver (someone must be listening!) 399 if len(msg["emit"]) == 2 && command == "node-pong" { 400 select { 401 case s.pongCh <- struct{}{}: 402 // Pong delivered, continue listening 403 continue 404 default: 405 // Ping routine dead, abort 406 log.Warn("Stats server pinger seems to have died") 407 return 408 } 409 } 410 // If the message is a history request, forward to the event processor 411 if len(msg["emit"]) == 2 && command == "history" { 412 // Make sure the request is valid and doesn't crash us 413 request, ok := msg["emit"][1].(map[string]interface{}) 414 if !ok { 415 log.Warn("Invalid stats history request", "msg", msg["emit"][1]) 416 select { 417 case s.histCh <- nil: // Treat it as an no indexes request 418 default: 419 } 420 continue 421 } 422 list, ok := request["list"].([]interface{}) 423 if !ok { 424 log.Warn("Invalid stats history block list", "list", request["list"]) 425 return 426 } 427 // Convert the block number list to an integer list 428 numbers := make([]uint64, len(list)) 429 for i, num := range list { 430 n, ok := num.(float64) 431 if !ok { 432 log.Warn("Invalid stats history block number", "number", num) 433 return 434 } 435 numbers[i] = uint64(n) 436 } 437 select { 438 case s.histCh <- numbers: 439 continue 440 default: 441 } 442 } 443 // Report anything else and continue 444 log.Info("Unknown stats message", "msg", msg) 445 } 446 } 447 448 // nodeInfo is the collection of meta information about a node that is displayed 449 // on the monitoring page. 450 type nodeInfo struct { 451 Name string `json:"name"` 452 Node string `json:"node"` 453 Port int `json:"port"` 454 Network string `json:"net"` 455 Protocol string `json:"protocol"` 456 API string `json:"api"` 457 Os string `json:"os"` 458 OsVer string `json:"os_v"` 459 Client string `json:"client"` 460 History bool `json:"canUpdateHistory"` 461 } 462 463 // authMsg is the authentication infos needed to login to a monitoring server. 464 type authMsg struct { 465 ID string `json:"id"` 466 Info nodeInfo `json:"info"` 467 Secret string `json:"secret"` 468 } 469 470 // login tries to authorize the client at the remote server. 471 func (s *Service) login(conn *connWrapper) error { 472 // Construct and send the login authentication 473 infos := s.server.NodeInfo() 474 475 var protocols []string 476 for _, proto := range s.server.Protocols { 477 protocols = append(protocols, fmt.Sprintf("%s/%d", proto.Name, proto.Version)) 478 } 479 var network string 480 if info := infos.Protocols["eth"]; info != nil { 481 network = fmt.Sprintf("%d", info.(*ethproto.NodeInfo).Network) 482 } else { 483 network = fmt.Sprintf("%d", infos.Protocols["les"].(*les.NodeInfo).Network) 484 } 485 auth := &authMsg{ 486 ID: s.node, 487 Info: nodeInfo{ 488 Name: s.node, 489 Node: infos.Name, 490 Port: infos.Ports.Listener, 491 Network: network, 492 Protocol: strings.Join(protocols, ", "), 493 API: "No", 494 Os: runtime.GOOS, 495 OsVer: runtime.GOARCH, 496 Client: "0.1.1", 497 History: true, 498 }, 499 Secret: s.pass, 500 } 501 login := map[string][]interface{}{ 502 "emit": {"hello", auth}, 503 } 504 if err := conn.WriteJSON(login); err != nil { 505 return err 506 } 507 // Retrieve the remote ack or connection termination 508 var ack map[string][]string 509 if err := conn.ReadJSON(&ack); err != nil || len(ack["emit"]) != 1 || ack["emit"][0] != "ready" { 510 return errors.New("unauthorized") 511 } 512 return nil 513 } 514 515 // report collects all possible data to report and send it to the stats server. 516 // This should only be used on reconnects or rarely to avoid overloading the 517 // server. Use the individual methods for reporting subscribed events. 518 func (s *Service) report(conn *connWrapper) error { 519 if err := s.reportLatency(conn); err != nil { 520 return err 521 } 522 if err := s.reportBlock(conn, nil); err != nil { 523 return err 524 } 525 if err := s.reportPending(conn); err != nil { 526 return err 527 } 528 if err := s.reportStats(conn); err != nil { 529 return err 530 } 531 return nil 532 } 533 534 // reportLatency sends a ping request to the server, measures the RTT time and 535 // finally sends a latency update. 536 func (s *Service) reportLatency(conn *connWrapper) error { 537 // Send the current time to the zondstats server 538 start := time.Now() 539 540 ping := map[string][]interface{}{ 541 "emit": {"node-ping", map[string]string{ 542 "id": s.node, 543 "clientTime": start.String(), 544 }}, 545 } 546 if err := conn.WriteJSON(ping); err != nil { 547 return err 548 } 549 // Wait for the pong request to arrive back 550 select { 551 case <-s.pongCh: 552 // Pong delivered, report the latency 553 case <-time.After(5 * time.Second): 554 // Ping timeout, abort 555 return errors.New("ping timed out") 556 } 557 latency := strconv.Itoa(int((time.Since(start) / time.Duration(2)).Nanoseconds() / 1000000)) 558 559 // Send back the measured latency 560 log.Trace("Sending measured latency to zondstats", "latency", latency) 561 562 stats := map[string][]interface{}{ 563 "emit": {"latency", map[string]string{ 564 "id": s.node, 565 "latency": latency, 566 }}, 567 } 568 return conn.WriteJSON(stats) 569 } 570 571 // blockStats is the information to report about individual blocks. 572 type blockStats struct { 573 Number *big.Int `json:"number"` 574 Hash common.Hash `json:"hash"` 575 ParentHash common.Hash `json:"parentHash"` 576 Timestamp *big.Int `json:"timestamp"` 577 Miner common.Address `json:"miner"` 578 GasUsed uint64 `json:"gasUsed"` 579 GasLimit uint64 `json:"gasLimit"` 580 Diff string `json:"difficulty"` 581 TotalDiff string `json:"totalDifficulty"` 582 Txs []txStats `json:"transactions"` 583 TxHash common.Hash `json:"transactionsRoot"` 584 Root common.Hash `json:"stateRoot"` 585 Uncles uncleStats `json:"uncles"` 586 } 587 588 // txStats is the information to report about individual transactions. 589 type txStats struct { 590 Hash common.Hash `json:"hash"` 591 } 592 593 // uncleStats is a custom wrapper around an uncle array to force serializing 594 // empty arrays instead of returning null for them. 595 type uncleStats []*types.Header 596 597 func (s uncleStats) MarshalJSON() ([]byte, error) { 598 if uncles := ([]*types.Header)(s); len(uncles) > 0 { 599 return json.Marshal(uncles) 600 } 601 return []byte("[]"), nil 602 } 603 604 // reportBlock retrieves the current chain head and reports it to the stats server. 605 func (s *Service) reportBlock(conn *connWrapper, block *types.Block) error { 606 // Gather the block details from the header or block chain 607 details := s.assembleBlockStats(block) 608 609 // Assemble the block report and send it to the server 610 log.Trace("Sending new block to zondstats", "number", details.Number, "hash", details.Hash) 611 612 stats := map[string]interface{}{ 613 "id": s.node, 614 "block": details, 615 } 616 report := map[string][]interface{}{ 617 "emit": {"block", stats}, 618 } 619 return conn.WriteJSON(report) 620 } 621 622 // assembleBlockStats retrieves any required metadata to report a single block 623 // and assembles the block stats. If block is nil, the current head is processed. 624 func (s *Service) assembleBlockStats(block *types.Block) *blockStats { 625 // Gather the block infos from the local blockchain 626 var ( 627 header *types.Header 628 td *big.Int 629 txs []txStats 630 uncles []*types.Header 631 ) 632 633 // check if backend is a full node 634 fullBackend, ok := s.backend.(fullNodeBackend) 635 if ok { 636 if block == nil { 637 block = fullBackend.CurrentBlock() 638 } 639 header = block.Header() 640 td = fullBackend.GetTd(context.Background(), header.Hash()) 641 642 txs = make([]txStats, len(block.Transactions())) 643 for i, tx := range block.Transactions() { 644 txs[i].Hash = tx.Hash() 645 } 646 uncles = block.Uncles() 647 } else { 648 // Light nodes would need on-demand lookups for transactions/uncles, skip 649 if block != nil { 650 header = block.Header() 651 } else { 652 header = s.backend.CurrentHeader() 653 } 654 td = s.backend.GetTd(context.Background(), header.Hash()) 655 txs = []txStats{} 656 } 657 658 // Assemble and return the block stats 659 author, _ := s.engine.Author(header) 660 661 return &blockStats{ 662 Number: header.Number, 663 Hash: header.Hash(), 664 ParentHash: header.ParentHash, 665 Timestamp: new(big.Int).SetUint64(header.Time), 666 Miner: author, 667 GasUsed: header.GasUsed, 668 GasLimit: header.GasLimit, 669 Diff: header.Difficulty.String(), 670 TotalDiff: td.String(), 671 Txs: txs, 672 TxHash: header.TxHash, 673 Root: header.Root, 674 Uncles: uncles, 675 } 676 } 677 678 // reportHistory retrieves the most recent batch of blocks and reports it to the 679 // stats server. 680 func (s *Service) reportHistory(conn *connWrapper, list []uint64) error { 681 // Figure out the indexes that need reporting 682 indexes := make([]uint64, 0, historyUpdateRange) 683 if len(list) > 0 { 684 // Specific indexes requested, send them back in particular 685 indexes = append(indexes, list...) 686 } else { 687 // No indexes requested, send back the top ones 688 head := s.backend.CurrentHeader().Number.Int64() 689 start := head - historyUpdateRange + 1 690 if start < 0 { 691 start = 0 692 } 693 for i := uint64(start); i <= uint64(head); i++ { 694 indexes = append(indexes, i) 695 } 696 } 697 // Gather the batch of blocks to report 698 history := make([]*blockStats, len(indexes)) 699 for i, number := range indexes { 700 fullBackend, ok := s.backend.(fullNodeBackend) 701 // Retrieve the next block if it's known to us 702 var block *types.Block 703 if ok { 704 block, _ = fullBackend.BlockByNumber(context.Background(), rpc.BlockNumber(number)) // TODO ignore error here ? 705 } else { 706 if header, _ := s.backend.HeaderByNumber(context.Background(), rpc.BlockNumber(number)); header != nil { 707 block = types.NewBlockWithHeader(header) 708 } 709 } 710 // If we do have the block, add to the history and continue 711 if block != nil { 712 history[len(history)-1-i] = s.assembleBlockStats(block) 713 continue 714 } 715 // Ran out of blocks, cut the report short and send 716 history = history[len(history)-i:] 717 break 718 } 719 // Assemble the history report and send it to the server 720 if len(history) > 0 { 721 log.Trace("Sending historical blocks to zondstats", "first", history[0].Number, "last", history[len(history)-1].Number) 722 } else { 723 log.Trace("No history to send to stats server") 724 } 725 stats := map[string]interface{}{ 726 "id": s.node, 727 "history": history, 728 } 729 report := map[string][]interface{}{ 730 "emit": {"history", stats}, 731 } 732 return conn.WriteJSON(report) 733 } 734 735 // pendStats is the information to report about pending transactions. 736 type pendStats struct { 737 Pending int `json:"pending"` 738 } 739 740 // reportPending retrieves the current number of pending transactions and reports 741 // it to the stats server. 742 func (s *Service) reportPending(conn *connWrapper) error { 743 // Retrieve the pending count from the local blockchain 744 pending, _ := s.backend.Stats() 745 // Assemble the transaction stats and send it to the server 746 log.Trace("Sending pending transactions to zondstats", "count", pending) 747 748 stats := map[string]interface{}{ 749 "id": s.node, 750 "stats": &pendStats{ 751 Pending: pending, 752 }, 753 } 754 report := map[string][]interface{}{ 755 "emit": {"pending", stats}, 756 } 757 return conn.WriteJSON(report) 758 } 759 760 // nodeStats is the information to report about the local node. 761 type nodeStats struct { 762 Active bool `json:"active"` 763 Syncing bool `json:"syncing"` 764 Mining bool `json:"mining"` 765 Hashrate int `json:"hashrate"` 766 Peers int `json:"peers"` 767 GasPrice int `json:"gasPrice"` 768 Uptime int `json:"uptime"` 769 } 770 771 // reportStats retrieves various stats about the node at the networking and 772 // mining layer and reports it to the stats server. 773 func (s *Service) reportStats(conn *connWrapper) error { 774 // Gather the syncing and mining infos from the local miner instance 775 var ( 776 mining bool 777 hashrate int 778 syncing bool 779 gasprice int 780 ) 781 // check if backend is a full node 782 fullBackend, ok := s.backend.(fullNodeBackend) 783 if ok { 784 mining = fullBackend.Miner().Mining() 785 hashrate = int(fullBackend.Miner().Hashrate()) 786 787 sync := fullBackend.SyncProgress() 788 syncing = fullBackend.CurrentHeader().Number.Uint64() >= sync.HighestBlock 789 790 price, _ := fullBackend.SuggestGasTipCap(context.Background()) 791 gasprice = int(price.Uint64()) 792 if basefee := fullBackend.CurrentHeader().BaseFee; basefee != nil { 793 gasprice += int(basefee.Uint64()) 794 } 795 } else { 796 sync := s.backend.SyncProgress() 797 syncing = s.backend.CurrentHeader().Number.Uint64() >= sync.HighestBlock 798 } 799 // Assemble the node stats and send it to the server 800 log.Trace("Sending node details to zondstats") 801 802 stats := map[string]interface{}{ 803 "id": s.node, 804 "stats": &nodeStats{ 805 Active: true, 806 Mining: mining, 807 Hashrate: hashrate, 808 Peers: s.server.PeerCount(), 809 GasPrice: gasprice, 810 Syncing: syncing, 811 Uptime: 100, 812 }, 813 } 814 report := map[string][]interface{}{ 815 "emit": {"stats", stats}, 816 } 817 return conn.WriteJSON(report) 818 }