diff --git a/backend/mpc-system/config.example.yaml b/backend/mpc-system/config.example.yaml new file mode 100644 index 00000000..a4cc2491 --- /dev/null +++ b/backend/mpc-system/config.example.yaml @@ -0,0 +1,69 @@ +# MPC System Configuration Example +# Copy this file to config.yaml and modify as needed + +# Server configuration +server: + grpc_port: 50051 + http_port: 8080 + environment: development # development, staging, production + timeout: 30s + tls_enabled: false + tls_cert_file: "" + tls_key_file: "" + +# Database configuration (PostgreSQL) +database: + host: localhost + port: 5432 + user: mpc_user + password: mpc_secret_password + dbname: mpc_system + sslmode: disable # disable, require, verify-ca, verify-full + max_open_conns: 25 + max_idle_conns: 5 + conn_max_life: 5m + +# Redis configuration +redis: + host: localhost + port: 6379 + password: "" + db: 0 + +# RabbitMQ configuration +rabbitmq: + host: localhost + port: 5672 + user: mpc_user + password: mpc_rabbit_password + vhost: / + +# Consul configuration (optional, for service discovery) +consul: + host: localhost + port: 8500 + service_id: "" + tags: [] + +# JWT configuration +jwt: + secret_key: "change-this-to-a-secure-random-string-in-production" + issuer: mpc-system + token_expiry: 15m + refresh_expiry: 24h + +# MPC configuration +mpc: + default_threshold_n: 3 + default_threshold_t: 2 + session_timeout: 10m + message_timeout: 30s + keygen_timeout: 10m + signing_timeout: 5m + max_parties: 10 + +# Logger configuration +logger: + level: info # debug, info, warn, error + encoding: json # json, console + output_path: stdout diff --git a/backend/mpc-system/services/message-router/cmd/server/main.go b/backend/mpc-system/services/message-router/cmd/server/main.go index a56e9239..8fa71b3e 100644 --- a/backend/mpc-system/services/message-router/cmd/server/main.go +++ b/backend/mpc-system/services/message-router/cmd/server/main.go @@ -20,6 +20,7 @@ import ( "github.com/rwadurian/mpc-system/pkg/config" "github.com/rwadurian/mpc-system/pkg/logger" + grpcadapter "github.com/rwadurian/mpc-system/services/message-router/adapters/input/grpc" "github.com/rwadurian/mpc-system/services/message-router/adapters/output/postgres" "github.com/rwadurian/mpc-system/services/message-router/adapters/output/rabbitmq" "github.com/rwadurian/mpc-system/services/message-router/application/use_cases" @@ -165,6 +166,16 @@ func startGRPCServer( grpcServer := grpc.NewServer() + // Create and register the message router gRPC handler + messageRouterServer := grpcadapter.NewMessageRouterServer( + routeMessageUC, + getPendingMessagesUC, + messageBroker, + ) + // Note: In production with proto-generated code, you would register like: + // pb.RegisterMessageRouterServer(grpcServer, messageRouterServer) + _ = messageRouterServer // Handler is ready for proto registration + // Enable reflection for debugging reflection.Register(grpcServer) diff --git a/backend/mpc-system/services/server-party/cmd/server/main.go b/backend/mpc-system/services/server-party/cmd/server/main.go new file mode 100644 index 00000000..e417d183 --- /dev/null +++ b/backend/mpc-system/services/server-party/cmd/server/main.go @@ -0,0 +1,228 @@ +package main + +import ( + "context" + "database/sql" + "encoding/hex" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + _ "github.com/lib/pq" + + "github.com/rwadurian/mpc-system/pkg/config" + "github.com/rwadurian/mpc-system/pkg/crypto" + "github.com/rwadurian/mpc-system/pkg/logger" + "github.com/rwadurian/mpc-system/services/server-party/adapters/output/postgres" + "github.com/rwadurian/mpc-system/services/server-party/application/use_cases" + "go.uber.org/zap" +) + +func main() { + // Parse flags + configPath := flag.String("config", "", "Path to config file") + flag.Parse() + + // Load configuration + cfg, err := config.Load(*configPath) + if err != nil { + fmt.Printf("Failed to load config: %v\n", err) + os.Exit(1) + } + + // Initialize logger + if err := logger.Init(&logger.Config{ + Level: cfg.Logger.Level, + Encoding: cfg.Logger.Encoding, + }); err != nil { + fmt.Printf("Failed to initialize logger: %v\n", err) + os.Exit(1) + } + defer logger.Sync() + + logger.Info("Starting Server Party Service", + zap.String("environment", cfg.Server.Environment), + zap.Int("http_port", cfg.Server.HTTPPort)) + + // Initialize database connection + db, err := initDatabase(cfg.Database) + if err != nil { + logger.Fatal("Failed to connect to database", zap.Error(err)) + } + defer db.Close() + + // Initialize crypto service with master key from environment + masterKeyHex := os.Getenv("MPC_CRYPTO_MASTER_KEY") + if masterKeyHex == "" { + masterKeyHex = "0123456789abcdef0123456789abcdef" // Default 32 bytes for development + } + masterKey, err := hex.DecodeString(masterKeyHex) + if err != nil { + logger.Fatal("Invalid master key format", zap.Error(err)) + } + cryptoService, err := crypto.NewCryptoService(masterKey) + if err != nil { + logger.Fatal("Failed to create crypto service", zap.Error(err)) + } + + // Initialize repositories + keyShareRepo := postgres.NewKeySharePostgresRepo(db) + + // Initialize use cases + // Note: SessionCoordinatorClient and MessageRouterClient would be + // implemented as gRPC clients in production + participateKeygenUC := use_cases.NewParticipateKeygenUseCase( + keyShareRepo, + nil, // sessionClient - would be gRPC client + nil, // messageRouter - would be gRPC client + cryptoService, + ) + participateSigningUC := use_cases.NewParticipateSigningUseCase( + keyShareRepo, + nil, // sessionClient + nil, // messageRouter + cryptoService, + ) + + // Create shutdown context + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start HTTP server + errChan := make(chan error, 1) + go func() { + if err := startHTTPServer(cfg, participateKeygenUC, participateSigningUC); err != nil { + errChan <- fmt.Errorf("HTTP server error: %w", err) + } + }() + + // Wait for shutdown signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + select { + case sig := <-sigChan: + logger.Info("Received shutdown signal", zap.String("signal", sig.String())) + case err := <-errChan: + logger.Error("Server error", zap.Error(err)) + } + + // Graceful shutdown + logger.Info("Shutting down...") + cancel() + + time.Sleep(5 * time.Second) + logger.Info("Shutdown complete") + + _ = ctx +} + +func initDatabase(cfg config.DatabaseConfig) (*sql.DB, error) { + db, err := sql.Open("postgres", cfg.DSN()) + if err != nil { + return nil, err + } + + db.SetMaxOpenConns(cfg.MaxOpenConns) + db.SetMaxIdleConns(cfg.MaxIdleConns) + db.SetConnMaxLifetime(cfg.ConnMaxLife) + + if err := db.Ping(); err != nil { + return nil, err + } + + logger.Info("Connected to PostgreSQL") + return db, nil +} + +func startHTTPServer( + cfg *config.Config, + participateKeygenUC *use_cases.ParticipateKeygenUseCase, + participateSigningUC *use_cases.ParticipateSigningUseCase, +) error { + if cfg.Server.Environment == "production" { + gin.SetMode(gin.ReleaseMode) + } + + router := gin.New() + router.Use(gin.Recovery()) + router.Use(gin.Logger()) + + // Health check + router.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "service": "server-party", + }) + }) + + // API routes + api := router.Group("/api/v1") + { + // Keygen participation endpoint + api.POST("/keygen/participate", func(c *gin.Context) { + var req struct { + SessionID string `json:"session_id" binding:"required"` + PartyID string `json:"party_id" binding:"required"` + JoinToken string `json:"join_token" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Note: In production, this would trigger async keygen participation + c.JSON(http.StatusAccepted, gin.H{ + "message": "keygen participation initiated", + "session_id": req.SessionID, + "party_id": req.PartyID, + }) + }) + + // Signing participation endpoint + api.POST("/sign/participate", func(c *gin.Context) { + var req struct { + SessionID string `json:"session_id" binding:"required"` + PartyID string `json:"party_id" binding:"required"` + JoinToken string `json:"join_token" binding:"required"` + MessageHash string `json:"message_hash" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Note: In production, this would trigger async signing participation + c.JSON(http.StatusAccepted, gin.H{ + "message": "signing participation initiated", + "session_id": req.SessionID, + "party_id": req.PartyID, + }) + }) + + // Get key shares for a party + api.GET("/shares/:party_id", func(c *gin.Context) { + partyID := c.Param("party_id") + + // Note: In production, would fetch from repository + c.JSON(http.StatusOK, gin.H{ + "party_id": partyID, + "shares": []interface{}{}, + }) + }) + } + + // Placeholder for use cases to avoid unused variable warnings + _ = participateKeygenUC + _ = participateSigningUC + + logger.Info("Starting HTTP server", zap.Int("port", cfg.Server.HTTPPort)) + return router.Run(fmt.Sprintf(":%d", cfg.Server.HTTPPort)) +}