// Package node — node-onboarding API routes. // // These endpoints let a brand-new node (or a client) discover enough state // about an existing DChain network to bootstrap itself, without requiring // the operator to hand-copy validator keys, contract IDs, or peer multiaddrs. // // Endpoints: // // GET /api/peers → live libp2p peers of this node // GET /api/network-info → genesis hash + chain id + validators + peers + well-known contracts // // Design rationale: // - /api/peers returns libp2p multiaddrs that include /p2p/. A joiner // can pass any of these to its own `--peers` flag and immediately dial // into the DHT/gossipsub mesh. // - /api/network-info is a ONE-SHOT bootstrap payload. Instead of curling // six different endpoints, an operator points their new node at a seed // node's HTTP and pulls everything they need. Fields are optional where // not applicable so partial responses work even on trimmed-down nodes // (e.g. non-validator observers). package node import ( "encoding/json" "fmt" "net/http" ) func registerOnboardingAPI(mux *http.ServeMux, q ExplorerQuery) { mux.HandleFunc("/api/peers", apiPeers(q)) mux.HandleFunc("/api/network-info", apiNetworkInfo(q)) } // apiPeers — GET /api/peers // // Returns this node's current view of connected libp2p peers. Empty list is // a valid response (node is isolated). 503 if the node was built without p2p // wiring (rare — mostly tests). func apiPeers(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } if q.ConnectedPeers == nil { jsonErr(w, fmt.Errorf("p2p not configured on this node"), 503) return } peers := q.ConnectedPeers() if peers == nil { peers = []ConnectedPeerRef{} } jsonOK(w, map[string]any{ "count": len(peers), "peers": peers, }) } } // apiNetworkInfo — GET /api/network-info // // One-shot bootstrap payload for new joiners. Returns: // - chain_id — stable network identifier (from ChainID()) // - genesis_hash — hex hash of block 0; joiners MUST verify a local replay matches this // - genesis_validator — pubkey of the node that created block 0 // - tip_height — current committed height (lock-free read) // - validators — active validator set (pubkey hex) // - peers — live libp2p peers (for --peers bootstrap list) // - contracts — well-known contracts by ABI name (same as /api/well-known-contracts) // - stats — a snapshot of NetStats for a quick sanity check // // Any field may be omitted if its query func is nil, so the endpoint // degrades gracefully on slimmed-down builds. func apiNetworkInfo(q ExplorerQuery) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } out := map[string]any{} // --- chain_id --- if q.ChainID != nil { out["chain_id"] = q.ChainID() } // --- genesis block --- if q.GetBlock != nil { if g, err := q.GetBlock(0); err == nil && g != nil { out["genesis_hash"] = g.HashHex() out["genesis_validator"] = g.Validator out["genesis_time"] = g.Timestamp.UTC().Format("2006-01-02T15:04:05Z") } } // --- current tip + aggregate stats --- if q.NetStats != nil { if s, err := q.NetStats(); err == nil { out["tip_height"] = s.TotalBlocks out["stats"] = s } } // --- active validators --- if q.ValidatorSet != nil { if vs, err := q.ValidatorSet(); err == nil { if vs == nil { vs = []string{} } out["validators"] = vs } } // --- live peers --- if q.ConnectedPeers != nil { peers := q.ConnectedPeers() if peers == nil { peers = []ConnectedPeerRef{} } out["peers"] = peers } // --- well-known contracts (reuse registerWellKnownAPI's logic) --- if q.GetContracts != nil { out["contracts"] = collectWellKnownContracts(q) } jsonOK(w, out) } } // collectWellKnownContracts is the same reduction used by /api/well-known-contracts // but inlined here so /api/network-info is a single HTTP round-trip for joiners. func collectWellKnownContracts(q ExplorerQuery) map[string]WellKnownContract { out := map[string]WellKnownContract{} all, err := q.GetContracts() if err != nil { return out } for _, rec := range all { if rec.ABIJson == "" { continue } var abi abiHeader if err := json.Unmarshal([]byte(rec.ABIJson), &abi); err != nil { continue } if abi.Contract == "" { continue } existing, ok := out[abi.Contract] if !ok || rec.DeployedAt < existing.DeployedAt { out[abi.Contract] = WellKnownContract{ ContractID: rec.ContractID, Name: abi.Contract, Version: abi.Version, DeployedAt: rec.DeployedAt, } } } return out }