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
This commit is contained in:
162
node/api_onboarding.go
Normal file
162
node/api_onboarding.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user