package main import ( "context" "encoding/hex" "flag" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" router "github.com/rwadurian/mpc-system/api/grpc/router/v1" "github.com/rwadurian/mpc-system/pkg/config" "github.com/rwadurian/mpc-system/pkg/crypto" "github.com/rwadurian/mpc-system/pkg/logger" grpcclient "github.com/rwadurian/mpc-system/services/server-party/adapters/output/grpc" "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 Delegate Party Service", zap.String("environment", cfg.Server.Environment), zap.Int("http_port", cfg.Server.HTTPPort)) // Initialize crypto service with master key from environment masterKeyHex := os.Getenv("MPC_CRYPTO_MASTER_KEY") if masterKeyHex == "" { masterKeyHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } 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)) } // Get Message Router address from environment // Delegate party (like all parties) ONLY connects to Message Router routerAddr := os.Getenv("MESSAGE_ROUTER_ADDR") if routerAddr == "" { routerAddr = "message-router:50051" } // Initialize Message Router client (the only gRPC connection needed) messageRouter, err := grpcclient.NewMessageRouterClient(routerAddr) if err != nil { logger.Fatal("Failed to connect to message router", zap.Error(err)) } defer messageRouter.Close() // Create shutdown context ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Get party ID from environment (or use default) partyID := os.Getenv("PARTY_ID") if partyID == "" { partyID = "delegate-party" } // Force PARTY_ROLE to delegate for this service partyRole := "delegate" // Register this party as a delegate party with Message Router logger.Info("Registering party with Message Router", zap.String("party_id", partyID), zap.String("role", partyRole)) if err := messageRouter.RegisterParty(ctx, partyID, partyRole, "1.0.0"); err != nil { logger.Fatal("Failed to register party", zap.Error(err)) } logger.Info("Party registered successfully", zap.String("party_id", partyID), zap.String("role", partyRole)) // Start heartbeat to keep party registered heartbeatCancel := messageRouter.StartHeartbeat(ctx, partyID, 30*time.Second, func(pendingCount int32) { if pendingCount > 0 { logger.Info("Pending messages detected via heartbeat", zap.String("party_id", partyID), zap.Int32("pending_count", pendingCount)) } }) defer heartbeatCancel() logger.Info("Heartbeat started", zap.String("party_id", partyID), zap.Duration("interval", 30*time.Second)) // Initialize use cases with nil keyShareRepo (delegate doesn't use DB) // MessageRouter handles both messaging AND session operations (proxied to coordinator) participateKeygenUC := use_cases.NewParticipateKeygenUseCase( nil, // No database storage for delegate messageRouter, messageRouter, cryptoService, ) participateSigningUC := use_cases.NewParticipateSigningUseCase( nil, // No database storage for delegate messageRouter, messageRouter, cryptoService, ) // Subscribe to session events and handle them automatically (SAME AS SERVER-PARTY) logger.Info("Subscribing to session events", zap.String("party_id", partyID)) eventHandler := createSessionEventHandler( ctx, partyID, participateKeygenUC, participateSigningUC, messageRouter, ) if err := messageRouter.SubscribeSessionEvents(ctx, partyID, eventHandler); err != nil { logger.Fatal("Failed to subscribe to session events", zap.Error(err)) } logger.Info("Delegate party initialized successfully (party-driven architecture)", zap.String("party_id", partyID), zap.String("role", partyRole)) // Start HTTP server (health check only) errChan := make(chan error, 1) go func() { if err := startHTTPServer(cfg); 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") } // startHTTPServer starts HTTP server for health checks only func startHTTPServer(cfg *config.Config) error { if cfg.Server.Environment == "production" { gin.SetMode(gin.ReleaseMode) } r := gin.New() r.Use(gin.Recovery()) // Health check only r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "healthy", "service": "delegate-party", "role": "delegate", }) }) logger.Info("Starting HTTP server (health check only)", zap.Int("port", cfg.Server.HTTPPort)) return r.Run(fmt.Sprintf(":%d", cfg.Server.HTTPPort)) } // createSessionEventHandler creates a handler for session events (party-driven architecture) // Delegate party automatically responds to session creation events by joining keygen or signing sessions // After keygen, it submits the user's share to Session Coordinator (instead of saving to DB) func createSessionEventHandler( ctx context.Context, partyID string, participateKeygenUC *use_cases.ParticipateKeygenUseCase, participateSigningUC *use_cases.ParticipateSigningUseCase, messageRouter *grpcclient.MessageRouterClient, ) func(*router.SessionEvent) { return func(event *router.SessionEvent) { // Check if this party is selected for the session isSelected := false for _, selectedParty := range event.SelectedParties { if selectedParty == partyID { isSelected = true break } } if !isSelected { logger.Debug("Party not selected for this session", zap.String("session_id", event.SessionId), zap.String("party_id", partyID)) return } // Get join token for this party joinToken, exists := event.JoinTokens[partyID] if !exists { logger.Error("No join token found for party", zap.String("session_id", event.SessionId), zap.String("party_id", partyID)) return } logger.Info("Delegate party selected for session, auto-participating", zap.String("session_id", event.SessionId), zap.String("party_id", partyID), zap.String("event_type", event.EventType)) // Parse session ID sessionID, err := uuid.Parse(event.SessionId) if err != nil { logger.Error("Invalid session ID", zap.Error(err)) return } // Automatically participate based on session type go func() { // Use parent context to allow proper cancellation participateCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() // Determine session type from event if event.EventType == "session_created" { // Check if it's keygen or sign based on message_hash if len(event.MessageHash) == 0 { // Keygen session logger.Info("Auto-participating in keygen session (delegate)", zap.String("session_id", event.SessionId), zap.String("party_id", partyID)) input := use_cases.ParticipateKeygenInput{ SessionID: sessionID, PartyID: partyID, JoinToken: joinToken, } result, err := participateKeygenUC.Execute(participateCtx, input) if err != nil { logger.Error("Keygen participation failed", zap.Error(err), zap.String("session_id", event.SessionId)) return } logger.Info("Keygen participation completed (delegate)", zap.String("session_id", event.SessionId), zap.String("public_key", hex.EncodeToString(result.PublicKey))) // Delegate party: Submit share to Session Coordinator (via Message Router) // This is the key difference from persistent party which saves to DB if len(result.ShareForUser) > 0 { logger.Info("Submitting delegate share to Session Coordinator", zap.String("session_id", event.SessionId), zap.String("party_id", partyID), zap.Int("share_size", len(result.ShareForUser))) if err := messageRouter.SubmitDelegateShare( participateCtx, event.SessionId, partyID, result.ShareForUser, result.PublicKey, int32(result.KeyShare.PartyIndex), ); err != nil { logger.Error("Failed to submit delegate share", zap.Error(err), zap.String("session_id", event.SessionId)) } else { logger.Info("Delegate share submitted successfully", zap.String("session_id", event.SessionId), zap.String("party_id", partyID)) } } } else { // Sign session logger.Info("Auto-participating in sign session (delegate)", zap.String("session_id", event.SessionId), zap.String("party_id", partyID)) input := use_cases.ParticipateSigningInput{ SessionID: sessionID, PartyID: partyID, JoinToken: joinToken, MessageHash: event.MessageHash, // Note: For signing, user must provide their share via Account Service // This will be passed through the session event or retrieved separately } result, err := participateSigningUC.Execute(participateCtx, input) if err != nil { logger.Error("Signing participation failed", zap.Error(err), zap.String("session_id", event.SessionId)) return } logger.Info("Signing participation completed (delegate)", zap.String("session_id", event.SessionId), zap.String("signature", hex.EncodeToString(result.Signature))) } } }() } }