niko_trust/cmd/server/main.go
Niko Marmeladkov 20cc52c3a5 feat: network layer — PoW, checkpoint chain, gossip, light node, WS, delegation, rotation, BFT
- BLAKE3 keyed proof-of-work on object storage and auth challenges,
  with frozen vectors cross-checked against an independent Python
  reference implementing the single-block hash it needs.
- Sparse Merkle trie over object IDs: order-independent roots,
  inclusion and absence proofs (internal/smt).
- Signed checkpoint chain per relay: transport key amendment to INV-1,
  /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe
  epoch continuity (internal/checkpoint).
- Head gossip with TOFU pinning and equivocation detection; light node
  (cmd/lightnode) that stores no history: quorum of pinned relays,
  every served object proven against the agreed root, LRU disk cache.
- WebSocket streaming on relay and light node (coder/websocket):
  scoped channels mirroring REST, raw envelopes verified client-side;
  light node marks streamed objects unproven until checkpoint coverage.
- Protocol v1 additions: DelegationClaim tag 0x07 with deterministic
  chain resolution in verify.Graph, KeyRotationRequest/Confirm tags
  0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge;
  spec sections, frozen vectors appended byte-identically, Python
  reference extended.
- Optional permissioned BFT finality over gossip (internal/bft):
  prevote/precommit with quorum certificates verifiable offline.
- Quick wins: Policy.TrustedIssuers, per-type stored metrics,
  batch fetch, lexicographic lists with stable cursor pagination.
- Security review of the network layer (docs/SECURITY-REVIEW.md) with
  findings F-01..F-09; hub send/close race and unstable pagination
  fixed under review.

12 packages green, vet/gofmt clean, protocol fuzzing stable.
2026-08-25 20:38:40 +03:00

88 lines
2.9 KiB
Go

// Command server runs the trust relay over HTTP using the JSON transport.
//
// It holds no signing key: it stores and serves signed TCE objects and brokers
// authentication, but never forges (INV-1) and never decides authorization
// (INV-5).
package main
import (
"context"
"flag"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"git.n1ko.dev/Niko/niko_trust/internal/server"
)
func main() {
addr := flag.String("addr", ":8080", "listen address")
audience := flag.String("audience", "trust.n1ko.dev", "server audience bound into auth assertions")
data := flag.String("data", "", "directory to persist objects (empty = in-memory)")
powPutBits := flag.Int("pow-put-bits", 22, "proof-of-work difficulty for storing objects, leading zero bits (0 = off)")
powAuthBits := flag.Int("pow-auth-bits", 18, "proof-of-work difficulty for auth challenge issuance, leading zero bits (0 = off)")
ckptInterval := flag.Duration("ckpt-interval", time.Minute, "maximum time between signed checkpoints when the object set changed")
ckptEvery := flag.Int("ckpt-every", 128, "sign a checkpoint after this many new objects (0 = interval only)")
bftValidators := flag.String("bft-validators", "", "comma-separated validator public keys (hex); enables BFT finality")
bftURLs := flag.String("bft-urls", "", "comma-separated validator base URLs, aligned with keys")
bftTimeout := flag.Duration("bft-round-timeout", 2*time.Second, "BFT round timeout")
flag.Parse()
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
srv := server.New(*audience, *data,
server.WithPow(*powPutBits, *powAuthBits),
server.WithCheckpoints(*data, server.CheckpointConfig{Interval: *ckptInterval, EveryN: *ckptEvery}),
)
if *bftValidators != "" && *bftURLs != "" {
srv.SetBFT(server.BFTConfig{
ValidatorKeys: splitCSV(*bftValidators),
ValidatorURLs: splitCSV(*bftURLs),
RoundTimeout: *bftTimeout,
})
}
ctx, stop := context.WithCancel(context.Background())
srv.StartCheckpoints(ctx)
srv.StartBFT(ctx)
defer stop()
h := &http.Server{
Addr: *addr,
Handler: srv.Handler(),
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
logger.Info("trust relay listening", "addr", *addr, "audience", *audience)
if err := h.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("listen", "err", err)
os.Exit(1)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
stop()
logger.Info("shutting down")
ctx2, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := h.Shutdown(ctx2); err != nil {
logger.Error("shutdown", "err", err)
}
logger.Info("stopped")
}
func splitCSV(s string) []string {
var out []string
for _, part := range strings.Split(s, ",") {
if part != "" {
out = append(out, part)
}
}
return out
}