Files
dchain/node/stats.go
vsecoder 7e7393e4f8 chore: initial commit for v0.0.1
DChain single-node blockchain + React Native messenger client.

Core:
- PBFT consensus with multi-sig validator admission + equivocation slashing
- BadgerDB + schema migration scaffold (CurrentSchemaVersion=0)
- libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1)
- Native Go contracts (username_registry) alongside WASM (wazero)
- WebSocket gateway with topic-based fanout + Ed25519-nonce auth
- Relay mailbox with NaCl envelope encryption (X25519 + Ed25519)
- Prometheus /metrics, per-IP rate limit, body-size cap

Deployment:
- Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus
- 3-node dev compose (docker-compose.yml) with mocked internet topology
- 3-validator prod compose (deploy/prod/) for federation
- Auto-update from Gitea via /api/update-check + systemd timer
- Build-time version injection (ldflags → node --version)
- UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER)

Client (client-app/):
- Expo / React Native / NativeWind
- E2E NaCl encryption, typing indicator, contact requests
- Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch

Documentation:
- README.md, CHANGELOG.md, CONTEXT.md
- deploy/single/README.md with 6 operator scenarios
- deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design
- docs/contracts/*.md per contract
2026-04-17 14:16:44 +03:00

317 lines
9.1 KiB
Go

// Package node provides runtime statistics tracking for a validator node.
// Stats are accumulated with atomic counters (lock-free hot path) and
// exposed as a JSON HTTP endpoint on /stats and per-peer on /stats/peers.
package node
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/libp2p/go-libp2p/core/peer"
"go-blockchain/blockchain"
"go-blockchain/economy"
"go-blockchain/wallet"
)
// Tracker accumulates statistics for the running node.
// All counter fields are accessed atomically — safe from any goroutine.
type Tracker struct {
startTime time.Time
// Consensus counters
BlocksProposed atomic.Int64
BlocksCommitted atomic.Int64
ViewChanges atomic.Int64
VotesCast atomic.Int64 // PREPARE + COMMIT votes sent
// Network counters
ConsensusMsgsSent atomic.Int64
ConsensusMsgsRecv atomic.Int64
BlocksGossipSent atomic.Int64
BlocksGossipRecv atomic.Int64
TxsGossipSent atomic.Int64
TxsGossipRecv atomic.Int64
BlocksSynced atomic.Int64 // downloaded via sync protocol
// Per-peer routing stats
peersMu sync.RWMutex
peers map[peer.ID]*PeerStats
}
// PeerStats tracks per-peer message counters.
type PeerStats struct {
PeerID peer.ID `json:"peer_id"`
ConnectedAt time.Time `json:"connected_at"`
MsgsSent atomic.Int64
MsgsRecv atomic.Int64
BlocksSent atomic.Int64
BlocksRecv atomic.Int64
SyncRequests atomic.Int64 // times we synced blocks from this peer
}
// NewTracker creates a new Tracker with start time set to now.
func NewTracker() *Tracker {
return &Tracker{
startTime: time.Now(),
peers: make(map[peer.ID]*PeerStats),
}
}
// PeerConnected registers a new peer connection.
func (t *Tracker) PeerConnected(id peer.ID) {
t.peersMu.Lock()
defer t.peersMu.Unlock()
if _, ok := t.peers[id]; !ok {
t.peers[id] = &PeerStats{PeerID: id, ConnectedAt: time.Now()}
}
}
// PeerDisconnected removes a peer (keeps the slot so history is visible).
func (t *Tracker) PeerDisconnected(id peer.ID) {
// intentionally kept — routing history is useful even after disconnect
}
// peer returns (or lazily creates) the PeerStats for id.
func (t *Tracker) peer(id peer.ID) *PeerStats {
t.peersMu.RLock()
ps, ok := t.peers[id]
t.peersMu.RUnlock()
if ok {
return ps
}
t.peersMu.Lock()
defer t.peersMu.Unlock()
if ps, ok = t.peers[id]; ok {
return ps
}
ps = &PeerStats{PeerID: id, ConnectedAt: time.Now()}
t.peers[id] = ps
return ps
}
// RecordConsensusSent increments consensus message sent counters for a peer.
func (t *Tracker) RecordConsensusSent(to peer.ID) {
t.ConsensusMsgsSent.Add(1)
t.peer(to).MsgsSent.Add(1)
}
// RecordConsensusRecv increments consensus message received counters.
func (t *Tracker) RecordConsensusRecv(from peer.ID) {
t.ConsensusMsgsRecv.Add(1)
t.peer(from).MsgsRecv.Add(1)
}
// RecordBlockGossipSent records a block broadcast to a peer.
func (t *Tracker) RecordBlockGossipSent() {
t.BlocksGossipSent.Add(1)
}
// RecordBlockGossipRecv records receiving a block via gossip.
func (t *Tracker) RecordBlockGossipRecv(from peer.ID) {
t.BlocksGossipRecv.Add(1)
t.peer(from).BlocksRecv.Add(1)
}
// RecordSyncFrom records blocks downloaded from a peer via the sync protocol.
func (t *Tracker) RecordSyncFrom(from peer.ID, count int) {
t.BlocksSynced.Add(int64(count))
t.peer(from).SyncRequests.Add(1)
}
// UptimeSeconds returns seconds since the tracker was created.
func (t *Tracker) UptimeSeconds() int64 {
return int64(time.Since(t.startTime).Seconds())
}
// --- HTTP API ---
// StatsResponse is the full JSON payload returned by /stats.
type StatsResponse struct {
Node NodeInfo `json:"node"`
Chain ChainInfo `json:"chain"`
Consensus ConsensusInfo `json:"consensus"`
Network NetworkInfo `json:"network"`
Economy EconomyInfo `json:"economy"`
Reputation RepInfo `json:"reputation"`
Peers []PeerInfo `json:"peers"`
}
type NodeInfo struct {
PubKey string `json:"pub_key"`
Address string `json:"address"`
PeerID string `json:"peer_id"`
UptimeSecs int64 `json:"uptime_secs"`
WalletBinding string `json:"wallet_binding,omitempty"` // DC address if bound
}
type ChainInfo struct {
Height uint64 `json:"height"`
TipHash string `json:"tip_hash"`
TipTime string `json:"tip_time,omitempty"`
}
type ConsensusInfo struct {
BlocksProposed int64 `json:"blocks_proposed"`
BlocksCommitted int64 `json:"blocks_committed"`
ViewChanges int64 `json:"view_changes"`
VotesCast int64 `json:"votes_cast"`
}
type NetworkInfo struct {
PeersConnected int `json:"peers_connected"`
ConsensusMsgsSent int64 `json:"consensus_msgs_sent"`
ConsensusMsgsRecv int64 `json:"consensus_msgs_recv"`
BlocksGossipSent int64 `json:"blocks_gossip_sent"`
BlocksGossipRecv int64 `json:"blocks_gossip_recv"`
TxsGossipSent int64 `json:"txs_gossip_sent"`
TxsGossipRecv int64 `json:"txs_gossip_recv"`
BlocksSynced int64 `json:"blocks_synced"`
}
type EconomyInfo struct {
BalanceMicroT uint64 `json:"balance_ut"`
BalanceDisplay string `json:"balance"`
WalletBalanceMicroT uint64 `json:"wallet_balance_ut,omitempty"`
WalletBalance string `json:"wallet_balance,omitempty"`
}
type RepInfo struct {
Score int64 `json:"score"`
BlocksProduced uint64 `json:"blocks_produced"`
RelayProofs uint64 `json:"relay_proofs"`
SlashCount uint64 `json:"slash_count"`
Heartbeats uint64 `json:"heartbeats"`
Rank string `json:"rank"`
}
type PeerInfo struct {
PeerID string `json:"peer_id"`
ConnectedAt string `json:"connected_at"`
MsgsSent int64 `json:"msgs_sent"`
MsgsRecv int64 `json:"msgs_recv"`
BlocksSent int64 `json:"blocks_sent"`
BlocksRecv int64 `json:"blocks_recv"`
SyncRequests int64 `json:"sync_requests"`
}
// QueryFunc is called by the HTTP handler to fetch live chain/wallet state.
type QueryFunc struct {
PubKey func() string
PeerID func() string
PeersCount func() int
ChainTip func() *blockchain.Block
Balance func(pubKey string) uint64
WalletBinding func(pubKey string) string // returns wallet DC address or ""
Reputation func(pubKey string) blockchain.RepStats
}
// ServeHTTP returns a mux with /stats and /health, plus any extra routes registered via fns.
func (t *Tracker) ServeHTTP(q QueryFunc, fns ...func(*http.ServeMux)) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) {
pubKey := q.PubKey()
tip := q.ChainTip()
var height uint64
var tipHash, tipTime string
if tip != nil {
height = tip.Index
tipHash = tip.HashHex()
tipTime = tip.Timestamp.Format(time.RFC3339)
}
bal := q.Balance(pubKey)
rep := q.Reputation(pubKey)
walletBinding := q.WalletBinding(pubKey)
t.peersMu.RLock()
var peerInfos []PeerInfo
for _, ps := range t.peers {
peerInfos = append(peerInfos, PeerInfo{
PeerID: ps.PeerID.String(),
ConnectedAt: ps.ConnectedAt.Format(time.RFC3339),
MsgsSent: ps.MsgsSent.Load(),
MsgsRecv: ps.MsgsRecv.Load(),
BlocksSent: ps.BlocksSent.Load(),
BlocksRecv: ps.BlocksRecv.Load(),
SyncRequests: ps.SyncRequests.Load(),
})
}
t.peersMu.RUnlock()
resp := StatsResponse{
Node: NodeInfo{
PubKey: pubKey,
Address: wallet.PubKeyToAddress(pubKey),
PeerID: q.PeerID(),
UptimeSecs: t.UptimeSeconds(),
WalletBinding: walletBinding,
},
Chain: ChainInfo{
Height: height,
TipHash: tipHash,
TipTime: tipTime,
},
Consensus: ConsensusInfo{
BlocksProposed: t.BlocksProposed.Load(),
BlocksCommitted: t.BlocksCommitted.Load(),
ViewChanges: t.ViewChanges.Load(),
VotesCast: t.VotesCast.Load(),
},
Network: NetworkInfo{
PeersConnected: q.PeersCount(),
ConsensusMsgsSent: t.ConsensusMsgsSent.Load(),
ConsensusMsgsRecv: t.ConsensusMsgsRecv.Load(),
BlocksGossipSent: t.BlocksGossipSent.Load(),
BlocksGossipRecv: t.BlocksGossipRecv.Load(),
TxsGossipSent: t.TxsGossipSent.Load(),
TxsGossipRecv: t.TxsGossipRecv.Load(),
BlocksSynced: t.BlocksSynced.Load(),
},
Economy: EconomyInfo{
BalanceMicroT: bal,
BalanceDisplay: economy.FormatTokens(bal),
},
Reputation: RepInfo{
Score: rep.Score,
BlocksProduced: rep.BlocksProduced,
RelayProofs: rep.RelayProofs,
SlashCount: rep.SlashCount,
Heartbeats: rep.Heartbeats,
Rank: rep.Rank(),
},
Peers: peerInfos,
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(resp); err != nil {
http.Error(w, `{"error":"failed to encode response"}`, http.StatusInternalServerError)
}
})
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"ok","uptime_secs":%d}`, t.UptimeSeconds())
})
for _, fn := range fns {
fn(mux)
}
return mux
}
// ListenAndServe starts the HTTP stats server on addr (e.g. ":8080").
func (t *Tracker) ListenAndServe(addr string, q QueryFunc, fns ...func(*http.ServeMux)) error {
handler := t.ServeHTTP(q, fns...)
return http.ListenAndServe(addr, handler)
}