aboutsummaryrefslogtreecommitdiff
path: root/internal/api/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/server.go')
-rw-r--r--internal/api/server.go287
1 files changed, 287 insertions, 0 deletions
diff --git a/internal/api/server.go b/internal/api/server.go
new file mode 100644
index 0000000..2159029
--- /dev/null
+++ b/internal/api/server.go
@@ -0,0 +1,287 @@
+package api
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/Fuwn/plutia/internal/checkpoint"
+ "github.com/Fuwn/plutia/internal/config"
+ "github.com/Fuwn/plutia/internal/ingest"
+ "github.com/Fuwn/plutia/internal/merkle"
+ "github.com/Fuwn/plutia/internal/storage"
+ "github.com/Fuwn/plutia/internal/types"
+ "github.com/Fuwn/plutia/pkg/proof"
+)
+
+type Server struct {
+ cfg config.Config
+ store storage.Store
+ ingestor *ingest.Service
+ checkpoints *checkpoint.Manager
+}
+
+func NewServer(cfg config.Config, store storage.Store, ingestor *ingest.Service, checkpoints *checkpoint.Manager) *Server {
+ return &Server{cfg: cfg, store: store, ingestor: ingestor, checkpoints: checkpoints}
+}
+
+func (s *Server) Handler() http.Handler {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/health", s.handleHealth)
+ mux.HandleFunc("/metrics", s.handleMetrics)
+ mux.HandleFunc("/status", s.handleStatus)
+ mux.HandleFunc("/checkpoints/latest", s.handleLatestCheckpoint)
+ mux.HandleFunc("/checkpoints/", s.handleCheckpointBySequence)
+ mux.HandleFunc("/did/", s.handleDID)
+ return mux
+}
+
+func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
+ writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
+}
+
+func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
+ st := s.ingestor.Stats()
+ seq, _ := s.store.GetGlobalSeq()
+ w.Header().Set("Content-Type", "text/plain; version=0.0.4")
+ _, _ = fmt.Fprintf(w, "plutia_ingested_ops %d\n", st.IngestedOps)
+ _, _ = fmt.Fprintf(w, "plutia_ingest_errors %d\n", st.Errors)
+ _, _ = fmt.Fprintf(w, "plutia_last_seq %d\n", seq)
+}
+
+func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
+ seq, err := s.store.GetGlobalSeq()
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, err)
+ return
+ }
+ cp, ok, err := s.store.GetLatestCheckpoint()
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, err)
+ return
+ }
+ payload := map[string]any{
+ "mode": s.cfg.Mode,
+ "verify_policy": s.cfg.VerifyPolicy,
+ "global_seq": seq,
+ "stats": s.ingestor.Stats(),
+ "corrupted": s.ingestor.IsCorrupted(),
+ }
+ if err := s.ingestor.CorruptionError(); err != nil {
+ payload["corruption_error"] = err.Error()
+ }
+ if ok {
+ payload["latest_checkpoint"] = cp
+ }
+ writeJSON(w, http.StatusOK, payload)
+}
+
+func (s *Server) handleLatestCheckpoint(w http.ResponseWriter, r *http.Request) {
+ cp, ok, err := s.store.GetLatestCheckpoint()
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, err)
+ return
+ }
+ if !ok {
+ writeErr(w, http.StatusNotFound, fmt.Errorf("no checkpoint"))
+ return
+ }
+ writeJSON(w, http.StatusOK, cp)
+}
+
+func (s *Server) handleCheckpointBySequence(w http.ResponseWriter, r *http.Request) {
+ path := strings.TrimPrefix(r.URL.Path, "/checkpoints/")
+ if path == "" {
+ writeErr(w, http.StatusNotFound, fmt.Errorf("missing checkpoint sequence"))
+ return
+ }
+ seq, err := strconv.ParseUint(path, 10, 64)
+ if err != nil {
+ writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid checkpoint sequence"))
+ return
+ }
+ cp, ok, err := s.store.GetCheckpoint(seq)
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, err)
+ return
+ }
+ if !ok {
+ writeErr(w, http.StatusNotFound, fmt.Errorf("checkpoint not found"))
+ return
+ }
+ writeJSON(w, http.StatusOK, cp)
+}
+
+func (s *Server) handleDID(w http.ResponseWriter, r *http.Request) {
+ path := strings.TrimPrefix(r.URL.Path, "/did/")
+ if path == "" {
+ writeErr(w, http.StatusBadRequest, fmt.Errorf("missing did"))
+ return
+ }
+ if strings.HasSuffix(path, "/proof") {
+ did := strings.TrimSuffix(path, "/proof")
+ s.handleDIDProof(w, r, did)
+ return
+ }
+ s.handleDIDResolve(w, r, path)
+}
+
+func (s *Server) handleDIDResolve(w http.ResponseWriter, r *http.Request, did string) {
+ state, ok, err := s.store.GetState(did)
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, err)
+ return
+ }
+ if !ok {
+ writeErr(w, http.StatusNotFound, fmt.Errorf("did not found"))
+ return
+ }
+ cp, cpOK, err := s.store.GetLatestCheckpoint()
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, err)
+ return
+ }
+ resp := map[string]any{
+ "did": did,
+ "did_document": json.RawMessage(state.DIDDocument),
+ "chain_tip_hash": state.ChainTipHash,
+ }
+ if cpOK {
+ resp["checkpoint_reference"] = map[string]any{
+ "sequence": cp.Sequence,
+ "checkpoint_hash": cp.CheckpointHash,
+ }
+ }
+ writeJSON(w, http.StatusOK, resp)
+}
+
+func (s *Server) handleDIDProof(w http.ResponseWriter, r *http.Request, did string) {
+ if err := s.ingestor.CorruptionError(); err != nil {
+ writeErr(w, http.StatusServiceUnavailable, err)
+ return
+ }
+
+ cp, verifyCheckpointUnchanged, err := s.selectCheckpointForProof(r)
+ if err != nil {
+ writeErr(w, http.StatusBadRequest, err)
+ return
+ }
+
+ tipHash, seqs, err := s.ingestor.RecomputeTipAtOrBefore(r.Context(), did, cp.Sequence)
+ if err != nil {
+ writeErr(w, http.StatusNotFound, err)
+ return
+ }
+ siblings, leafHash, found, err := s.checkpoints.BuildDIDProofAtCheckpoint(did, tipHash, cp.Sequence)
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, err)
+ return
+ }
+ if !found {
+ writeErr(w, http.StatusNotFound, fmt.Errorf("did not present in checkpoint state"))
+ return
+ }
+
+ leafBytes, err := hex.DecodeString(leafHash)
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, fmt.Errorf("invalid leaf hash: %w", err))
+ return
+ }
+ root, err := hex.DecodeString(cp.DIDMerkleRoot)
+ if err != nil {
+ writeErr(w, http.StatusInternalServerError, fmt.Errorf("invalid checkpoint root"))
+ return
+ }
+ if !merkle.VerifyProof(leafBytes, siblings, root) {
+ writeErr(w, http.StatusInternalServerError, fmt.Errorf("inclusion proof failed consistency check"))
+ return
+ }
+
+ if err := verifyCheckpointUnchanged(); err != nil {
+ writeErr(w, http.StatusConflict, err)
+ return
+ }
+
+ response := proof.DIDInclusionProof{
+ DID: did,
+ ChainTipHash: tipHash,
+ LeafHash: leafHash,
+ MerkleRoot: cp.DIDMerkleRoot,
+ Siblings: siblings,
+ CheckpointSeq: cp.Sequence,
+ CheckpointHash: cp.CheckpointHash,
+ CheckpointSig: cp.Signature,
+ CheckpointKeyID: cp.KeyID,
+ }
+ writeJSON(w, http.StatusOK, map[string]any{
+ "did": did,
+ "checkpoint_sequence": cp.Sequence,
+ "checkpoint_hash": cp.CheckpointHash,
+ "checkpoint_signature": cp.Signature,
+ "merkle_root": cp.DIDMerkleRoot,
+ "chain_tip_reference": tipHash,
+ "inclusion_proof": response,
+ "chain_operation_indices": seqs,
+ })
+}
+
+func (s *Server) selectCheckpointForProof(r *http.Request) (types.CheckpointV1, func() error, error) {
+ checkpointParam := strings.TrimSpace(r.URL.Query().Get("checkpoint"))
+ if checkpointParam == "" {
+ cp, ok, err := s.store.GetLatestCheckpoint()
+ if err != nil {
+ return types.CheckpointV1{}, nil, err
+ }
+ if !ok {
+ return types.CheckpointV1{}, nil, fmt.Errorf("no checkpoint available")
+ }
+ return cp, func() error {
+ now, ok, err := s.store.GetLatestCheckpoint()
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return fmt.Errorf("latest checkpoint disappeared during request")
+ }
+ if now.CheckpointHash != cp.CheckpointHash {
+ return fmt.Errorf("checkpoint advanced during proof generation")
+ }
+ return nil
+ }, nil
+ }
+
+ seq, err := strconv.ParseUint(checkpointParam, 10, 64)
+ if err != nil {
+ return types.CheckpointV1{}, nil, fmt.Errorf("invalid checkpoint query parameter")
+ }
+ cp, ok, err := s.store.GetCheckpoint(seq)
+ if err != nil {
+ return types.CheckpointV1{}, nil, err
+ }
+ if !ok {
+ return types.CheckpointV1{}, nil, fmt.Errorf("checkpoint %d unavailable", seq)
+ }
+ return cp, func() error {
+ again, ok, err := s.store.GetCheckpoint(seq)
+ if err != nil {
+ return err
+ }
+ if !ok || again.CheckpointHash != cp.CheckpointHash {
+ return fmt.Errorf("checkpoint %d changed during proof generation", seq)
+ }
+ return nil
+ }, nil
+}
+
+func writeJSON(w http.ResponseWriter, code int, v any) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(code)
+ _ = json.NewEncoder(w).Encode(v)
+}
+
+func writeErr(w http.ResponseWriter, code int, err error) {
+ writeJSON(w, code, map[string]any{"error": err.Error()})
+}