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:
vsecoder
2026-04-17 14:16:44 +03:00
commit 7e7393e4f8
196 changed files with 55947 additions and 0 deletions

270
node/api_routes.go Normal file
View File

@@ -0,0 +1,270 @@
// Package node - chain explorer HTTP API and minimal web UI.
package node
import (
"net/http"
"go-blockchain/blockchain"
)
// ConnectedPeerRef is an entry returned by /api/peers — one currently-connected
// libp2p peer. Mirrors p2p.ConnectedConnectedPeerRef but kept in the node package
// so api routes don't pull the p2p package directly.
type ConnectedPeerRef struct {
ID string `json:"id"`
Addrs []string `json:"addrs"`
// Version is the peer's last-seen version announce (from gossipsub topic
// dchain/version/v1). Empty when the peer hasn't announced yet — either
// it's running an older binary without gossip, or it hasn't reached its
// first publish tick (up to 60s after connect).
Version *PeerVersionRef `json:"version,omitempty"`
}
// PeerVersionRef mirrors p2p.PeerVersion in a package-local type so
// api_routes doesn't import p2p directly.
type PeerVersionRef struct {
Tag string `json:"tag"`
Commit string `json:"commit"`
ProtocolVersion int `json:"protocol_version"`
Timestamp int64 `json:"timestamp"`
ReceivedAt string `json:"received_at,omitempty"`
}
// NativeContractInfo is the shape ExplorerQuery.NativeContracts returns.
// Passed from main.go (which has the blockchain package imported) to the
// well-known endpoint below so it can merge native contracts with WASM ones.
type NativeContractInfo struct {
ContractID string
ABIJson string
}
// ExplorerQuery holds all functions the explorer API needs to read chain state
// and accept new transactions.
type ExplorerQuery struct {
GetBlock func(index uint64) (*blockchain.Block, error)
GetTx func(txID string) (*blockchain.TxRecord, error)
AddressToPubKey func(addr string) (string, error)
Balance func(pubKey string) (uint64, error)
Reputation func(pubKey string) (blockchain.RepStats, error)
WalletBinding func(pubKey string) (string, error)
TxsByAddress func(pubKey string, limit, offset int) ([]*blockchain.TxRecord, error)
RecentBlocks func(limit int) ([]*blockchain.Block, error)
RecentTxs func(limit int) ([]*blockchain.TxRecord, error)
NetStats func() (blockchain.NetStats, error)
RegisteredRelays func() ([]blockchain.RegisteredRelayInfo, error)
IdentityInfo func(pubKeyOrAddr string) (*blockchain.IdentityInfo, error)
ValidatorSet func() ([]string, error)
SubmitTx func(tx *blockchain.Transaction) error
// ConnectedPeers (optional) returns the local libp2p view of currently
// connected peers. Used by /api/peers and /api/network-info so new nodes
// can bootstrap from any existing peer's view of the network. May be nil
// if the node binary is built without p2p (tests).
ConnectedPeers func() []ConnectedPeerRef
// ChainID (optional) returns a stable identifier for this chain so a
// joiner can sanity-check it's syncing from the right network.
ChainID func() string
// NativeContracts (optional) returns the list of built-in Go contracts
// registered on this node. These appear in /api/well-known-contracts
// alongside WASM contracts, so the client doesn't need to distinguish.
NativeContracts func() []NativeContractInfo
GetContract func(contractID string) (*blockchain.ContractRecord, error)
GetContracts func() ([]blockchain.ContractRecord, error)
GetContractState func(contractID, key string) ([]byte, error)
GetContractLogs func(contractID string, limit int) ([]blockchain.ContractLogEntry, error)
Stake func(pubKey string) (uint64, error)
GetToken func(tokenID string) (*blockchain.TokenRecord, error)
GetTokens func() ([]blockchain.TokenRecord, error)
TokenBalance func(tokenID, pubKey string) (uint64, error)
GetNFT func(nftID string) (*blockchain.NFTRecord, error)
GetNFTs func() ([]blockchain.NFTRecord, error)
NFTsByOwner func(ownerPub string) ([]blockchain.NFTRecord, error)
// Channel group-messaging lookups (R1). GetChannel returns metadata;
// GetChannelMembers returns the Ed25519 pubkey of every current member.
// Both may be nil on nodes that don't expose channel state (tests).
GetChannel func(channelID string) (*blockchain.CreateChannelPayload, error)
GetChannelMembers func(channelID string) ([]string, error)
// Events is the SSE hub for the live event stream. Optional — if nil the
// /api/events endpoint returns 501 Not Implemented.
Events *SSEHub
// WS is the websocket hub for low-latency push to mobile/desktop clients.
// Optional — if nil the /api/ws endpoint returns 501.
WS *WSHub
}
// ExplorerRouteFlags toggles the optional HTML frontend surfaces. API
// endpoints (/api/*) always register — these flags only affect static pages.
type ExplorerRouteFlags struct {
// DisableUI suppresses the embedded block explorer at `/`, `/address`,
// `/tx`, `/node`, `/relays`, `/validators`, `/contract`, `/tokens`,
// `/token`, and their `/assets/explorer/*.js|css` dependencies. Useful
// for JSON-API-only deployments (headless nodes, mobile-backend nodes).
DisableUI bool
// DisableSwagger suppresses `/swagger` and `/swagger/openapi.json`.
// Useful for hardened private deployments where even API documentation
// shouldn't be exposed. Does NOT affect the JSON API itself.
DisableSwagger bool
}
// RegisterExplorerRoutes adds all explorer API, chain API and docs routes to mux.
// The variadic flags parameter is optional — passing none (or an empty struct)
// registers the full surface (UI + Swagger + JSON API) for backwards compatibility.
func RegisterExplorerRoutes(mux *http.ServeMux, q ExplorerQuery, flags ...ExplorerRouteFlags) {
var f ExplorerRouteFlags
if len(flags) > 0 {
f = flags[0]
}
if !f.DisableUI {
registerExplorerPages(mux)
}
registerExplorerAPI(mux, q)
registerChainAPI(mux, q)
registerContractAPI(mux, q)
registerWellKnownAPI(mux, q)
registerWellKnownVersionAPI(mux, q)
registerUpdateCheckAPI(mux, q)
registerOnboardingAPI(mux, q)
registerTokenAPI(mux, q)
registerChannelAPI(mux, q)
if !f.DisableSwagger {
registerSwaggerRoutes(mux)
}
}
// RegisterRelayRoutes adds relay mailbox HTTP endpoints to mux.
// Call this after RegisterExplorerRoutes with the relay config.
func RegisterRelayRoutes(mux *http.ServeMux, rc RelayConfig) {
if rc.Mailbox == nil {
return
}
registerRelayRoutes(mux, rc)
}
func registerExplorerPages(mux *http.ServeMux) {
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
serveExplorerIndex(w, r)
})
mux.HandleFunc("/address", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/address" {
http.NotFound(w, r)
return
}
serveExplorerAddressPage(w, r)
})
mux.HandleFunc("/tx", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tx" {
http.NotFound(w, r)
return
}
serveExplorerTxPage(w, r)
})
mux.HandleFunc("/node", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/node" {
http.NotFound(w, r)
return
}
serveExplorerNodePage(w, r)
})
mux.HandleFunc("/relays", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/relays" {
http.NotFound(w, r)
return
}
serveExplorerRelaysPage(w, r)
})
mux.HandleFunc("/validators", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/validators" {
http.NotFound(w, r)
return
}
serveExplorerValidatorsPage(w, r)
})
mux.HandleFunc("/assets/explorer/style.css", serveExplorerCSS)
mux.HandleFunc("/assets/explorer/common.js", serveExplorerCommonJS)
mux.HandleFunc("/assets/explorer/app.js", serveExplorerJS)
mux.HandleFunc("/assets/explorer/address.js", serveExplorerAddressJS)
mux.HandleFunc("/assets/explorer/tx.js", serveExplorerTxJS)
mux.HandleFunc("/assets/explorer/node.js", serveExplorerNodeJS)
mux.HandleFunc("/assets/explorer/relays.js", serveExplorerRelaysJS)
mux.HandleFunc("/assets/explorer/validators.js", serveExplorerValidatorsJS)
mux.HandleFunc("/contract", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/contract" {
http.NotFound(w, r)
return
}
serveExplorerContractPage(w, r)
})
mux.HandleFunc("/assets/explorer/contract.js", serveExplorerContractJS)
mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/tokens" {
http.NotFound(w, r)
return
}
serveExplorerTokensPage(w, r)
})
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/token" {
http.NotFound(w, r)
return
}
serveExplorerTokenPage(w, r)
})
mux.HandleFunc("/assets/explorer/tokens.js", serveExplorerTokensJS)
mux.HandleFunc("/assets/explorer/token.js", serveExplorerTokenJS)
}
func registerExplorerAPI(mux *http.ServeMux, q ExplorerQuery) {
mux.HandleFunc("/api/netstats", apiNetStats(q))
mux.HandleFunc("/api/blocks", apiRecentBlocks(q))
mux.HandleFunc("/api/txs/recent", apiRecentTxs(q)) // GET /api/txs/recent?limit=20
mux.HandleFunc("/api/block/", apiBlock(q)) // GET /api/block/{index}
mux.HandleFunc("/api/tx/", apiTxByID(q)) // GET /api/tx/{txid}
mux.HandleFunc("/api/address/", apiAddress(q)) // GET /api/address/{addr}
mux.HandleFunc("/api/node/", apiNode(q)) // GET /api/node/{pubkey|DC...}
mux.HandleFunc("/api/relays", apiRelays(q)) // GET /api/relays
mux.HandleFunc("/api/identity/", apiIdentity(q)) // GET /api/identity/{pubkey|addr}
mux.HandleFunc("/api/validators", apiValidators(q))// GET /api/validators
mux.HandleFunc("/api/tx", withWriteTokenGuard(withSubmitTxGuards(apiSubmitTx(q)))) // POST /api/tx (body size + per-IP rate limit + optional token gate)
// Live event stream (SSE) — GET /api/events
mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
if q.Events == nil {
http.Error(w, "event stream not available", http.StatusNotImplemented)
return
}
q.Events.ServeHTTP(w, r)
})
// WebSocket gateway — GET /api/ws (upgrades to ws://). Low-latency push
// for clients that would otherwise poll balance/inbox/contacts.
mux.HandleFunc("/api/ws", func(w http.ResponseWriter, r *http.Request) {
if q.WS == nil {
http.Error(w, "websocket not available", http.StatusNotImplemented)
return
}
q.WS.ServeHTTP(w, r)
})
// Prometheus exposition endpoint — scraped by a Prometheus server. Has
// no auth; operators running this in public should put the node behind
// a reverse proxy that restricts /metrics to trusted scrapers only.
mux.HandleFunc("/metrics", metricsHandler)
}
func registerTokenAPI(mux *http.ServeMux, q ExplorerQuery) {
mux.HandleFunc("/api/tokens", apiTokens(q)) // GET /api/tokens
mux.HandleFunc("/api/tokens/", apiTokenByID(q)) // GET /api/tokens/{id} and /api/tokens/{id}/balance/{pubkey}
mux.HandleFunc("/api/nfts", apiNFTs(q)) // GET /api/nfts
mux.HandleFunc("/api/nfts/", apiNFTByID(q)) // GET /api/nfts/{id} and /api/nfts/owner/{pubkey}
}
func registerChainAPI(mux *http.ServeMux, q ExplorerQuery) {
// Similar to blockchain APIs, but only with features supported by this chain.
mux.HandleFunc("/v2/chain/accounts/", apiV2ChainAccountTransactions(q)) // GET /v2/chain/accounts/{account_id}/transactions
mux.HandleFunc("/v2/chain/transactions/", apiV2ChainTxByID(q)) // GET /v2/chain/transactions/{tx_id}
mux.HandleFunc("/v2/chain/transactions/draft", apiV2ChainDraftTx()) // POST /v2/chain/transactions/draft
mux.HandleFunc("/v2/chain/transactions", withWriteTokenGuard(withSubmitTxGuards(apiV2ChainSendTx(q)))) // POST /v2/chain/transactions (body size + rate limit + optional token gate)
}