Files
dchain/node/api_onboarding.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

163 lines
4.9 KiB
Go

// 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/<id>. 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
}