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:
201
node/api_update_check.go
Normal file
201
node/api_update_check.go
Normal file
@@ -0,0 +1,201 @@
|
||||
// Package node — /api/update-check endpoint.
|
||||
//
|
||||
// What this does
|
||||
// ──────────────
|
||||
// Polls the configured release source (typically a Gitea `/api/v1/repos/
|
||||
// {owner}/{repo}/releases/latest` URL) and compares its answer with the
|
||||
// binary's own build-time version. Returns:
|
||||
//
|
||||
// {
|
||||
// "current": { "tag": "v0.5.0", "commit": "abc1234", "date": "..." },
|
||||
// "latest": { "tag": "v0.5.1", "commit": "def5678", "url": "...", "published_at": "..." },
|
||||
// "update_available": true,
|
||||
// "checked_at": "2026-04-17T09:45:12Z"
|
||||
// }
|
||||
//
|
||||
// When unconfigured (empty DCHAIN_UPDATE_SOURCE_URL), responds 503 with a
|
||||
// hint pointing at the env var. Never blocks for more than ~5 seconds
|
||||
// (HTTP timeout); upstream failures are logged and surfaced as 502.
|
||||
//
|
||||
// Cache
|
||||
// ─────
|
||||
// Hammering a public Gitea instance every time an operator curls this
|
||||
// endpoint would be rude. We cache the latest-release lookup for 15 minutes
|
||||
// in memory. The update script in deploy/single/update.sh honors this cache
|
||||
// by reading /api/update-check once per run — so a typical hourly timer +
|
||||
// 15-min cache means at most 4 upstream hits per node per hour, usually 1.
|
||||
//
|
||||
// Configuration
|
||||
// ─────────────
|
||||
// DCHAIN_UPDATE_SOURCE_URL — full URL of the Gitea latest-release API.
|
||||
// Example:
|
||||
// https://gitea.example.com/api/v1/repos/dchain/dchain/releases/latest
|
||||
// DCHAIN_UPDATE_SOURCE_TOKEN — optional Gitea PAT for private repos.
|
||||
//
|
||||
// These are read by cmd/node/main.go and handed to SetUpdateSource below.
|
||||
package node
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-blockchain/node/version"
|
||||
)
|
||||
|
||||
const (
|
||||
updateCacheTTL = 15 * time.Minute
|
||||
updateTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
updateMu sync.RWMutex
|
||||
updateSourceURL string
|
||||
updateSourceToken string
|
||||
updateCache *updateCheckResponse
|
||||
updateCacheAt time.Time
|
||||
)
|
||||
|
||||
// SetUpdateSource configures where /api/update-check should poll. Called
|
||||
// once at node startup from cmd/node/main.go after flag parsing. Empty url
|
||||
// leaves the endpoint in "unconfigured" mode (503).
|
||||
func SetUpdateSource(url, token string) {
|
||||
updateMu.Lock()
|
||||
defer updateMu.Unlock()
|
||||
updateSourceURL = url
|
||||
updateSourceToken = token
|
||||
// Clear cache on config change (mostly a test-time concern).
|
||||
updateCache = nil
|
||||
updateCacheAt = time.Time{}
|
||||
}
|
||||
|
||||
// giteaRelease is the subset of the Gitea release JSON we care about.
|
||||
// Gitea's schema is a superset of GitHub's, so the same shape works for
|
||||
// github.com/api/v3 too — operators can point at either.
|
||||
type giteaRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
TargetCommit string `json:"target_commitish"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
}
|
||||
|
||||
type updateCheckResponse struct {
|
||||
Current map[string]string `json:"current"`
|
||||
Latest *latestRef `json:"latest,omitempty"`
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
CheckedAt string `json:"checked_at"`
|
||||
Source string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
type latestRef struct {
|
||||
Tag string `json:"tag"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
PublishedAt string `json:"published_at,omitempty"`
|
||||
}
|
||||
|
||||
func registerUpdateCheckAPI(mux *http.ServeMux, q ExplorerQuery) {
|
||||
mux.HandleFunc("/api/update-check", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
jsonErr(w, fmt.Errorf("method not allowed"), 405)
|
||||
return
|
||||
}
|
||||
|
||||
updateMu.RLock()
|
||||
src := updateSourceURL
|
||||
tok := updateSourceToken
|
||||
cached := updateCache
|
||||
cachedAt := updateCacheAt
|
||||
updateMu.RUnlock()
|
||||
|
||||
if src == "" {
|
||||
jsonErr(w,
|
||||
fmt.Errorf("update source not configured — set DCHAIN_UPDATE_SOURCE_URL to a Gitea /api/v1/repos/{owner}/{repo}/releases/latest URL"),
|
||||
http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve fresh cache without hitting upstream.
|
||||
if cached != nil && time.Since(cachedAt) < updateCacheTTL {
|
||||
jsonOK(w, cached)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch upstream.
|
||||
latest, err := fetchLatestRelease(r.Context(), src, tok)
|
||||
if err != nil {
|
||||
jsonErr(w, fmt.Errorf("upstream check failed: %w", err), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &updateCheckResponse{
|
||||
Current: version.Info(),
|
||||
CheckedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Source: src,
|
||||
}
|
||||
if latest != nil {
|
||||
resp.Latest = &latestRef{
|
||||
Tag: latest.TagName,
|
||||
Commit: latest.TargetCommit,
|
||||
URL: latest.HTMLURL,
|
||||
PublishedAt: latest.PublishedAt,
|
||||
}
|
||||
resp.UpdateAvailable = latest.TagName != "" &&
|
||||
latest.TagName != version.Tag &&
|
||||
!latest.Draft &&
|
||||
!latest.Prerelease
|
||||
}
|
||||
|
||||
updateMu.Lock()
|
||||
updateCache = resp
|
||||
updateCacheAt = time.Now()
|
||||
updateMu.Unlock()
|
||||
|
||||
jsonOK(w, resp)
|
||||
})
|
||||
}
|
||||
|
||||
// fetchLatestRelease performs the actual HTTP call with a short timeout.
|
||||
// Returns nil, nil when the upstream replies 404 (no releases yet) — this
|
||||
// is not an error condition, the operator just hasn't published anything
|
||||
// yet. All other non-2xx are returned as errors.
|
||||
func fetchLatestRelease(ctx interface{ Deadline() (time.Time, bool) }, url, token string) (*giteaRelease, error) {
|
||||
// We only use ctx for cancellation type-matching; the actual deadline
|
||||
// comes from updateTimeout below. Tests pass a context.Background().
|
||||
_ = ctx
|
||||
|
||||
client := &http.Client{Timeout: updateTimeout}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "dchain-node/"+version.Tag)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil // no releases yet
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return nil, fmt.Errorf("http %d: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
var rel giteaRelease
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
return &rel, nil
|
||||
}
|
||||
Reference in New Issue
Block a user