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
163 lines
4.9 KiB
Go
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
|
|
}
|