diff options
Diffstat (limited to 'internal/api/server.go')
| -rw-r--r-- | internal/api/server.go | 287 |
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()}) +} |