feat(mpc-system): add signing parties configuration and delegate signing support

- Add signing-config API endpoints (POST/PUT/DELETE/GET) for configuring
  which parties should participate in signing operations
- Add SigningParties field to Account entity with database migration
- Modify CreateSigningSession to use configured parties if set,
  otherwise use all active parties (backward compatible)
- Add delegate party signing support: user provides encrypted share
  at sign time for delegate party to use
- Update protobuf definitions for DelegateUserShare in session events
- Add ShareTypeDelegate to support hybrid custody model

API endpoints:
- POST /accounts/:id/signing-config - Set signing parties (first time)
- PUT /accounts/:id/signing-config - Update signing parties
- DELETE /accounts/:id/signing-config - Clear config (use all parties)
- GET /accounts/:id/signing-config - Get current configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-05 22:47:55 -08:00
parent 55f5ec49f2
commit aa74e2b2e2
33 changed files with 4331 additions and 783 deletions

View File

@ -0,0 +1,59 @@
# =============================================================================
# MPC-System Production Party Configuration
# =============================================================================
# Copy to .env.party and configure for your party's environment
#
# Usage:
# cp .env.party.example .env.party
# # Edit .env.party with your values
# ./deploy.sh party up
#
# Each party machine needs its own .env.party with unique PARTY_ID
# =============================================================================
# =============================================================================
# Party Identity (REQUIRED - must be unique across all parties)
# =============================================================================
PARTY_ID=server-party-1
# Options: persistent (default), delegate, temporary
PARTY_ROLE=persistent
# =============================================================================
# Central Service (REQUIRED - public address)
# =============================================================================
# Message Router gRPC endpoint (the ONLY connection parties need)
# Session operations are proxied through Message Router to Session Coordinator
MESSAGE_ROUTER_ADDR=grpc.mpc.example.com:50051
# =============================================================================
# Local Database (for storing encrypted key shares)
# =============================================================================
POSTGRES_USER=mpc_user
POSTGRES_PASSWORD=your_secure_local_postgres_password
# =============================================================================
# Security Keys (REQUIRED)
# =============================================================================
# Master key for encrypting key shares (64 hex characters = 256-bit)
# IMPORTANT: Use the same key across all parties in the same MPC group
# Generate with: openssl rand -hex 32
CRYPTO_MASTER_KEY=your_64_character_hex_master_key_here
# =============================================================================
# Optional: Notification Channels (for offline mode)
# =============================================================================
# If any of these are set, party operates in offline mode (24h async)
# If none are set, party operates in real-time mode (Message Router push)
NOTIFICATION_EMAIL=
NOTIFICATION_PHONE=
NOTIFICATION_PUSH_TOKEN=
# =============================================================================
# Optional: Local HTTP Port (for health checks only)
# =============================================================================
# This party doesn't need to expose any ports - it connects outbound
# HTTP port is optional for local health monitoring
PARTY_HTTP_PORT=8080
# Environment
ENVIRONMENT=production

View File

@ -0,0 +1,57 @@
# =============================================================================
# MPC-System Production Central Configuration
# =============================================================================
# Copy to .env.prod and configure for your environment
#
# Usage:
# cp .env.prod.example .env.prod
# # Edit .env.prod with your values
# ./deploy.sh prod up
# =============================================================================
# Environment
ENVIRONMENT=production
# =============================================================================
# Database Configuration
# =============================================================================
POSTGRES_USER=mpc_user
POSTGRES_PASSWORD=your_secure_postgres_password_here
# =============================================================================
# Security Keys (IMPORTANT: Generate unique values!)
# =============================================================================
# Generate with: openssl rand -base64 32
JWT_SECRET_KEY=your_jwt_secret_key_here_min_32_chars
# API Key for backend integration (shared with mpc-service)
# Generate with: openssl rand -base64 32
MPC_API_KEY=your_api_key_here
# Master key for encrypting key shares (64 hex characters = 256-bit)
# Generate with: openssl rand -hex 32
CRYPTO_MASTER_KEY=your_64_character_hex_master_key_here
# =============================================================================
# Public Ports (must be accessible from server-parties)
# =============================================================================
# Message Router gRPC - parties connect here
MESSAGE_ROUTER_GRPC_PORT=50051
MESSAGE_ROUTER_HTTP_PORT=8082
# Session Coordinator gRPC - parties connect here
SESSION_COORDINATOR_GRPC_PORT=50052
SESSION_COORDINATOR_HTTP_PORT=8081
# Account Service HTTP - backend API
ACCOUNT_SERVICE_PORT=4000
# Server Party API (optional)
SERVER_PARTY_API_PORT=8083
# =============================================================================
# IP Whitelist (optional)
# =============================================================================
# Comma-separated list of IPs allowed to access Account Service API
# Leave empty to allow all (protected by API_KEY)
ALLOWED_IPS=

View File

@ -31,6 +31,8 @@ type CreateSessionRequest struct {
MessageHash []byte `protobuf:"bytes,5,opt,name=message_hash,json=messageHash,proto3" json:"message_hash,omitempty"` // Required for sign sessions
ExpiresInSeconds int64 `protobuf:"varint,6,opt,name=expires_in_seconds,json=expiresInSeconds,proto3" json:"expires_in_seconds,omitempty"` // Session expiration time
PartyComposition *PartyComposition `protobuf:"bytes,7,opt,name=party_composition,json=partyComposition,proto3" json:"party_composition,omitempty"` // Optional: party composition requirements for auto-selection
// For sign sessions with delegate party: user must provide their encrypted share
DelegateUserShare *DelegateUserShare `protobuf:"bytes,8,opt,name=delegate_user_share,json=delegateUserShare,proto3" json:"delegate_user_share,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -114,6 +116,74 @@ func (x *CreateSessionRequest) GetPartyComposition() *PartyComposition {
return nil
}
func (x *CreateSessionRequest) GetDelegateUserShare() *DelegateUserShare {
if x != nil {
return x.DelegateUserShare
}
return nil
}
// DelegateUserShare contains user's share for delegate party to use in signing
type DelegateUserShare struct {
state protoimpl.MessageState `protogen:"open.v1"`
DelegatePartyId string `protobuf:"bytes,1,opt,name=delegate_party_id,json=delegatePartyId,proto3" json:"delegate_party_id,omitempty"` // The delegate party that will use this share
EncryptedShare []byte `protobuf:"bytes,2,opt,name=encrypted_share,json=encryptedShare,proto3" json:"encrypted_share,omitempty"` // User's encrypted share (same as received from keygen)
PartyIndex int32 `protobuf:"varint,3,opt,name=party_index,json=partyIndex,proto3" json:"party_index,omitempty"` // Party index for this share
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DelegateUserShare) Reset() {
*x = DelegateUserShare{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DelegateUserShare) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DelegateUserShare) ProtoMessage() {}
func (x *DelegateUserShare) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DelegateUserShare.ProtoReflect.Descriptor instead.
func (*DelegateUserShare) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{1}
}
func (x *DelegateUserShare) GetDelegatePartyId() string {
if x != nil {
return x.DelegatePartyId
}
return ""
}
func (x *DelegateUserShare) GetEncryptedShare() []byte {
if x != nil {
return x.EncryptedShare
}
return nil
}
func (x *DelegateUserShare) GetPartyIndex() int32 {
if x != nil {
return x.PartyIndex
}
return 0
}
// PartyComposition specifies requirements for automatic party selection
type PartyComposition struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -126,7 +196,7 @@ type PartyComposition struct {
func (x *PartyComposition) Reset() {
*x = PartyComposition{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[1]
mi := &file_api_proto_session_coordinator_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -138,7 +208,7 @@ func (x *PartyComposition) String() string {
func (*PartyComposition) ProtoMessage() {}
func (x *PartyComposition) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[1]
mi := &file_api_proto_session_coordinator_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -151,7 +221,7 @@ func (x *PartyComposition) ProtoReflect() protoreflect.Message {
// Deprecated: Use PartyComposition.ProtoReflect.Descriptor instead.
func (*PartyComposition) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{1}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{2}
}
func (x *PartyComposition) GetPersistentCount() int32 {
@ -186,7 +256,7 @@ type ParticipantInfo struct {
func (x *ParticipantInfo) Reset() {
*x = ParticipantInfo{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[2]
mi := &file_api_proto_session_coordinator_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -198,7 +268,7 @@ func (x *ParticipantInfo) String() string {
func (*ParticipantInfo) ProtoMessage() {}
func (x *ParticipantInfo) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[2]
mi := &file_api_proto_session_coordinator_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -211,7 +281,7 @@ func (x *ParticipantInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use ParticipantInfo.ProtoReflect.Descriptor instead.
func (*ParticipantInfo) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{2}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{3}
}
func (x *ParticipantInfo) GetPartyId() string {
@ -241,7 +311,7 @@ type DeviceInfo struct {
func (x *DeviceInfo) Reset() {
*x = DeviceInfo{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[3]
mi := &file_api_proto_session_coordinator_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -253,7 +323,7 @@ func (x *DeviceInfo) String() string {
func (*DeviceInfo) ProtoMessage() {}
func (x *DeviceInfo) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[3]
mi := &file_api_proto_session_coordinator_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -266,7 +336,7 @@ func (x *DeviceInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeviceInfo.ProtoReflect.Descriptor instead.
func (*DeviceInfo) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{3}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{4}
}
func (x *DeviceInfo) GetDeviceType() string {
@ -311,7 +381,7 @@ type CreateSessionResponse struct {
func (x *CreateSessionResponse) Reset() {
*x = CreateSessionResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[4]
mi := &file_api_proto_session_coordinator_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -323,7 +393,7 @@ func (x *CreateSessionResponse) String() string {
func (*CreateSessionResponse) ProtoMessage() {}
func (x *CreateSessionResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[4]
mi := &file_api_proto_session_coordinator_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -336,7 +406,7 @@ func (x *CreateSessionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateSessionResponse.ProtoReflect.Descriptor instead.
func (*CreateSessionResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{4}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{5}
}
func (x *CreateSessionResponse) GetSessionId() string {
@ -387,7 +457,7 @@ type JoinSessionRequest struct {
func (x *JoinSessionRequest) Reset() {
*x = JoinSessionRequest{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[5]
mi := &file_api_proto_session_coordinator_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -399,7 +469,7 @@ func (x *JoinSessionRequest) String() string {
func (*JoinSessionRequest) ProtoMessage() {}
func (x *JoinSessionRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[5]
mi := &file_api_proto_session_coordinator_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -412,7 +482,7 @@ func (x *JoinSessionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use JoinSessionRequest.ProtoReflect.Descriptor instead.
func (*JoinSessionRequest) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{5}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{6}
}
func (x *JoinSessionRequest) GetSessionId() string {
@ -455,7 +525,7 @@ type JoinSessionResponse struct {
func (x *JoinSessionResponse) Reset() {
*x = JoinSessionResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[6]
mi := &file_api_proto_session_coordinator_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -467,7 +537,7 @@ func (x *JoinSessionResponse) String() string {
func (*JoinSessionResponse) ProtoMessage() {}
func (x *JoinSessionResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[6]
mi := &file_api_proto_session_coordinator_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -480,7 +550,7 @@ func (x *JoinSessionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use JoinSessionResponse.ProtoReflect.Descriptor instead.
func (*JoinSessionResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{6}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{7}
}
func (x *JoinSessionResponse) GetSuccess() bool {
@ -519,7 +589,7 @@ type SessionInfo struct {
func (x *SessionInfo) Reset() {
*x = SessionInfo{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[7]
mi := &file_api_proto_session_coordinator_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -531,7 +601,7 @@ func (x *SessionInfo) String() string {
func (*SessionInfo) ProtoMessage() {}
func (x *SessionInfo) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[7]
mi := &file_api_proto_session_coordinator_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -544,7 +614,7 @@ func (x *SessionInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use SessionInfo.ProtoReflect.Descriptor instead.
func (*SessionInfo) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{7}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{8}
}
func (x *SessionInfo) GetSessionId() string {
@ -601,7 +671,7 @@ type PartyInfo struct {
func (x *PartyInfo) Reset() {
*x = PartyInfo{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[8]
mi := &file_api_proto_session_coordinator_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -613,7 +683,7 @@ func (x *PartyInfo) String() string {
func (*PartyInfo) ProtoMessage() {}
func (x *PartyInfo) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[8]
mi := &file_api_proto_session_coordinator_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -626,7 +696,7 @@ func (x *PartyInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use PartyInfo.ProtoReflect.Descriptor instead.
func (*PartyInfo) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{8}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{9}
}
func (x *PartyInfo) GetPartyId() string {
@ -660,7 +730,7 @@ type GetSessionStatusRequest struct {
func (x *GetSessionStatusRequest) Reset() {
*x = GetSessionStatusRequest{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[9]
mi := &file_api_proto_session_coordinator_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -672,7 +742,7 @@ func (x *GetSessionStatusRequest) String() string {
func (*GetSessionStatusRequest) ProtoMessage() {}
func (x *GetSessionStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[9]
mi := &file_api_proto_session_coordinator_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -685,7 +755,7 @@ func (x *GetSessionStatusRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetSessionStatusRequest.ProtoReflect.Descriptor instead.
func (*GetSessionStatusRequest) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{9}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{10}
}
func (x *GetSessionStatusRequest) GetSessionId() string {
@ -701,15 +771,22 @@ type GetSessionStatusResponse struct {
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
CompletedParties int32 `protobuf:"varint,2,opt,name=completed_parties,json=completedParties,proto3" json:"completed_parties,omitempty"`
TotalParties int32 `protobuf:"varint,3,opt,name=total_parties,json=totalParties,proto3" json:"total_parties,omitempty"`
PublicKey []byte `protobuf:"bytes,4,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` // For completed keygen
Signature []byte `protobuf:"bytes,5,opt,name=signature,proto3" json:"signature,omitempty"` // For completed sign
SessionType string `protobuf:"bytes,4,opt,name=session_type,json=sessionType,proto3" json:"session_type,omitempty"` // "keygen" or "sign"
PublicKey []byte `protobuf:"bytes,5,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` // For completed keygen
Signature []byte `protobuf:"bytes,6,opt,name=signature,proto3" json:"signature,omitempty"` // For completed sign
// has_delegate indicates whether this keygen session has a delegate party
// Only meaningful for keygen sessions. Always false for sign sessions.
HasDelegate bool `protobuf:"varint,7,opt,name=has_delegate,json=hasDelegate,proto3" json:"has_delegate,omitempty"`
// Delegate share info (returned when keygen session completed and delegate party submitted share)
// Only populated if session_type="keygen" AND has_delegate=true AND session is completed
DelegateShare *DelegateShareInfo `protobuf:"bytes,8,opt,name=delegate_share,json=delegateShare,proto3" json:"delegate_share,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetSessionStatusResponse) Reset() {
*x = GetSessionStatusResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[10]
mi := &file_api_proto_session_coordinator_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -721,7 +798,7 @@ func (x *GetSessionStatusResponse) String() string {
func (*GetSessionStatusResponse) ProtoMessage() {}
func (x *GetSessionStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[10]
mi := &file_api_proto_session_coordinator_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -734,7 +811,7 @@ func (x *GetSessionStatusResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetSessionStatusResponse.ProtoReflect.Descriptor instead.
func (*GetSessionStatusResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{10}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{11}
}
func (x *GetSessionStatusResponse) GetStatus() string {
@ -758,6 +835,13 @@ func (x *GetSessionStatusResponse) GetTotalParties() int32 {
return 0
}
func (x *GetSessionStatusResponse) GetSessionType() string {
if x != nil {
return x.SessionType
}
return ""
}
func (x *GetSessionStatusResponse) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
@ -772,6 +856,81 @@ func (x *GetSessionStatusResponse) GetSignature() []byte {
return nil
}
func (x *GetSessionStatusResponse) GetHasDelegate() bool {
if x != nil {
return x.HasDelegate
}
return false
}
func (x *GetSessionStatusResponse) GetDelegateShare() *DelegateShareInfo {
if x != nil {
return x.DelegateShare
}
return nil
}
// DelegateShareInfo contains the delegate party's share for user
type DelegateShareInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
EncryptedShare []byte `protobuf:"bytes,1,opt,name=encrypted_share,json=encryptedShare,proto3" json:"encrypted_share,omitempty"` // Encrypted share for user
PartyIndex int32 `protobuf:"varint,2,opt,name=party_index,json=partyIndex,proto3" json:"party_index,omitempty"` // Party's index in the session
PartyId string `protobuf:"bytes,3,opt,name=party_id,json=partyId,proto3" json:"party_id,omitempty"` // Delegate party ID
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DelegateShareInfo) Reset() {
*x = DelegateShareInfo{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DelegateShareInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DelegateShareInfo) ProtoMessage() {}
func (x *DelegateShareInfo) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DelegateShareInfo.ProtoReflect.Descriptor instead.
func (*DelegateShareInfo) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{12}
}
func (x *DelegateShareInfo) GetEncryptedShare() []byte {
if x != nil {
return x.EncryptedShare
}
return nil
}
func (x *DelegateShareInfo) GetPartyIndex() int32 {
if x != nil {
return x.PartyIndex
}
return 0
}
func (x *DelegateShareInfo) GetPartyId() string {
if x != nil {
return x.PartyId
}
return ""
}
// ReportCompletionRequest reports that a participant has completed
type ReportCompletionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -785,7 +944,7 @@ type ReportCompletionRequest struct {
func (x *ReportCompletionRequest) Reset() {
*x = ReportCompletionRequest{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[11]
mi := &file_api_proto_session_coordinator_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -797,7 +956,7 @@ func (x *ReportCompletionRequest) String() string {
func (*ReportCompletionRequest) ProtoMessage() {}
func (x *ReportCompletionRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[11]
mi := &file_api_proto_session_coordinator_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -810,7 +969,7 @@ func (x *ReportCompletionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReportCompletionRequest.ProtoReflect.Descriptor instead.
func (*ReportCompletionRequest) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{11}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{13}
}
func (x *ReportCompletionRequest) GetSessionId() string {
@ -852,7 +1011,7 @@ type ReportCompletionResponse struct {
func (x *ReportCompletionResponse) Reset() {
*x = ReportCompletionResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[12]
mi := &file_api_proto_session_coordinator_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -864,7 +1023,7 @@ func (x *ReportCompletionResponse) String() string {
func (*ReportCompletionResponse) ProtoMessage() {}
func (x *ReportCompletionResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[12]
mi := &file_api_proto_session_coordinator_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -877,7 +1036,7 @@ func (x *ReportCompletionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ReportCompletionResponse.ProtoReflect.Descriptor instead.
func (*ReportCompletionResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{12}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{14}
}
func (x *ReportCompletionResponse) GetSuccess() bool {
@ -904,7 +1063,7 @@ type CloseSessionRequest struct {
func (x *CloseSessionRequest) Reset() {
*x = CloseSessionRequest{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[13]
mi := &file_api_proto_session_coordinator_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -916,7 +1075,7 @@ func (x *CloseSessionRequest) String() string {
func (*CloseSessionRequest) ProtoMessage() {}
func (x *CloseSessionRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[13]
mi := &file_api_proto_session_coordinator_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -929,7 +1088,7 @@ func (x *CloseSessionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseSessionRequest.ProtoReflect.Descriptor instead.
func (*CloseSessionRequest) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{13}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{15}
}
func (x *CloseSessionRequest) GetSessionId() string {
@ -949,7 +1108,7 @@ type CloseSessionResponse struct {
func (x *CloseSessionResponse) Reset() {
*x = CloseSessionResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[14]
mi := &file_api_proto_session_coordinator_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -961,7 +1120,7 @@ func (x *CloseSessionResponse) String() string {
func (*CloseSessionResponse) ProtoMessage() {}
func (x *CloseSessionResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[14]
mi := &file_api_proto_session_coordinator_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -974,7 +1133,7 @@ func (x *CloseSessionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseSessionResponse.ProtoReflect.Descriptor instead.
func (*CloseSessionResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{14}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{16}
}
func (x *CloseSessionResponse) GetSuccess() bool {
@ -995,7 +1154,7 @@ type MarkPartyReadyRequest struct {
func (x *MarkPartyReadyRequest) Reset() {
*x = MarkPartyReadyRequest{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[15]
mi := &file_api_proto_session_coordinator_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1007,7 +1166,7 @@ func (x *MarkPartyReadyRequest) String() string {
func (*MarkPartyReadyRequest) ProtoMessage() {}
func (x *MarkPartyReadyRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[15]
mi := &file_api_proto_session_coordinator_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1020,7 +1179,7 @@ func (x *MarkPartyReadyRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use MarkPartyReadyRequest.ProtoReflect.Descriptor instead.
func (*MarkPartyReadyRequest) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{15}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{17}
}
func (x *MarkPartyReadyRequest) GetSessionId() string {
@ -1050,7 +1209,7 @@ type MarkPartyReadyResponse struct {
func (x *MarkPartyReadyResponse) Reset() {
*x = MarkPartyReadyResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[16]
mi := &file_api_proto_session_coordinator_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1062,7 +1221,7 @@ func (x *MarkPartyReadyResponse) String() string {
func (*MarkPartyReadyResponse) ProtoMessage() {}
func (x *MarkPartyReadyResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[16]
mi := &file_api_proto_session_coordinator_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1075,7 +1234,7 @@ func (x *MarkPartyReadyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use MarkPartyReadyResponse.ProtoReflect.Descriptor instead.
func (*MarkPartyReadyResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{16}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{18}
}
func (x *MarkPartyReadyResponse) GetSuccess() bool {
@ -1116,7 +1275,7 @@ type StartSessionRequest struct {
func (x *StartSessionRequest) Reset() {
*x = StartSessionRequest{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[17]
mi := &file_api_proto_session_coordinator_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1128,7 +1287,7 @@ func (x *StartSessionRequest) String() string {
func (*StartSessionRequest) ProtoMessage() {}
func (x *StartSessionRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[17]
mi := &file_api_proto_session_coordinator_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1141,7 +1300,7 @@ func (x *StartSessionRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartSessionRequest.ProtoReflect.Descriptor instead.
func (*StartSessionRequest) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{17}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{19}
}
func (x *StartSessionRequest) GetSessionId() string {
@ -1162,7 +1321,7 @@ type StartSessionResponse struct {
func (x *StartSessionResponse) Reset() {
*x = StartSessionResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[18]
mi := &file_api_proto_session_coordinator_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1174,7 +1333,7 @@ func (x *StartSessionResponse) String() string {
func (*StartSessionResponse) ProtoMessage() {}
func (x *StartSessionResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[18]
mi := &file_api_proto_session_coordinator_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1187,7 +1346,7 @@ func (x *StartSessionResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use StartSessionResponse.ProtoReflect.Descriptor instead.
func (*StartSessionResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{18}
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{20}
}
func (x *StartSessionResponse) GetSuccess() bool {
@ -1204,11 +1363,133 @@ func (x *StartSessionResponse) GetStatus() string {
return ""
}
// SubmitDelegateShareRequest submits user's share from delegate party
type SubmitDelegateShareRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
PartyId string `protobuf:"bytes,2,opt,name=party_id,json=partyId,proto3" json:"party_id,omitempty"`
EncryptedShare []byte `protobuf:"bytes,3,opt,name=encrypted_share,json=encryptedShare,proto3" json:"encrypted_share,omitempty"` // Encrypted share for user
PublicKey []byte `protobuf:"bytes,4,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` // Public key from keygen
PartyIndex int32 `protobuf:"varint,5,opt,name=party_index,json=partyIndex,proto3" json:"party_index,omitempty"` // Party's index in the session
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SubmitDelegateShareRequest) Reset() {
*x = SubmitDelegateShareRequest{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SubmitDelegateShareRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubmitDelegateShareRequest) ProtoMessage() {}
func (x *SubmitDelegateShareRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SubmitDelegateShareRequest.ProtoReflect.Descriptor instead.
func (*SubmitDelegateShareRequest) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{21}
}
func (x *SubmitDelegateShareRequest) GetSessionId() string {
if x != nil {
return x.SessionId
}
return ""
}
func (x *SubmitDelegateShareRequest) GetPartyId() string {
if x != nil {
return x.PartyId
}
return ""
}
func (x *SubmitDelegateShareRequest) GetEncryptedShare() []byte {
if x != nil {
return x.EncryptedShare
}
return nil
}
func (x *SubmitDelegateShareRequest) GetPublicKey() []byte {
if x != nil {
return x.PublicKey
}
return nil
}
func (x *SubmitDelegateShareRequest) GetPartyIndex() int32 {
if x != nil {
return x.PartyIndex
}
return 0
}
// SubmitDelegateShareResponse contains result of share submission
type SubmitDelegateShareResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SubmitDelegateShareResponse) Reset() {
*x = SubmitDelegateShareResponse{}
mi := &file_api_proto_session_coordinator_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SubmitDelegateShareResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubmitDelegateShareResponse) ProtoMessage() {}
func (x *SubmitDelegateShareResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_proto_session_coordinator_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SubmitDelegateShareResponse.ProtoReflect.Descriptor instead.
func (*SubmitDelegateShareResponse) Descriptor() ([]byte, []int) {
return file_api_proto_session_coordinator_proto_rawDescGZIP(), []int{22}
}
func (x *SubmitDelegateShareResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
var File_api_proto_session_coordinator_proto protoreflect.FileDescriptor
const file_api_proto_session_coordinator_proto_rawDesc = "" +
"\n" +
"#api/proto/session_coordinator.proto\x12\x12mpc.coordinator.v1\"\xe8\x02\n" +
"#api/proto/session_coordinator.proto\x12\x12mpc.coordinator.v1\"\xbf\x03\n" +
"\x14CreateSessionRequest\x12!\n" +
"\fsession_type\x18\x01 \x01(\tR\vsessionType\x12\x1f\n" +
"\vthreshold_n\x18\x02 \x01(\x05R\n" +
@ -1218,7 +1499,13 @@ const file_api_proto_session_coordinator_proto_rawDesc = "" +
"\fparticipants\x18\x04 \x03(\v2#.mpc.coordinator.v1.ParticipantInfoR\fparticipants\x12!\n" +
"\fmessage_hash\x18\x05 \x01(\fR\vmessageHash\x12,\n" +
"\x12expires_in_seconds\x18\x06 \x01(\x03R\x10expiresInSeconds\x12Q\n" +
"\x11party_composition\x18\a \x01(\v2$.mpc.coordinator.v1.PartyCompositionR\x10partyComposition\"\x8d\x01\n" +
"\x11party_composition\x18\a \x01(\v2$.mpc.coordinator.v1.PartyCompositionR\x10partyComposition\x12U\n" +
"\x13delegate_user_share\x18\b \x01(\v2%.mpc.coordinator.v1.DelegateUserShareR\x11delegateUserShare\"\x89\x01\n" +
"\x11DelegateUserShare\x12*\n" +
"\x11delegate_party_id\x18\x01 \x01(\tR\x0fdelegatePartyId\x12'\n" +
"\x0fencrypted_share\x18\x02 \x01(\fR\x0eencryptedShare\x12\x1f\n" +
"\vparty_index\x18\x03 \x01(\x05R\n" +
"partyIndex\"\x8d\x01\n" +
"\x10PartyComposition\x12)\n" +
"\x10persistent_count\x18\x01 \x01(\x05R\x0fpersistentCount\x12%\n" +
"\x0edelegate_count\x18\x02 \x01(\x05R\rdelegateCount\x12'\n" +
@ -1277,14 +1564,22 @@ const file_api_proto_session_coordinator_proto_rawDesc = "" +
"deviceInfo\"8\n" +
"\x17GetSessionStatusRequest\x12\x1d\n" +
"\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\"\xc1\x01\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\"\xd5\x02\n" +
"\x18GetSessionStatusResponse\x12\x16\n" +
"\x06status\x18\x01 \x01(\tR\x06status\x12+\n" +
"\x11completed_parties\x18\x02 \x01(\x05R\x10completedParties\x12#\n" +
"\rtotal_parties\x18\x03 \x01(\x05R\ftotalParties\x12\x1d\n" +
"\rtotal_parties\x18\x03 \x01(\x05R\ftotalParties\x12!\n" +
"\fsession_type\x18\x04 \x01(\tR\vsessionType\x12\x1d\n" +
"\n" +
"public_key\x18\x04 \x01(\fR\tpublicKey\x12\x1c\n" +
"\tsignature\x18\x05 \x01(\fR\tsignature\"\x90\x01\n" +
"public_key\x18\x05 \x01(\fR\tpublicKey\x12\x1c\n" +
"\tsignature\x18\x06 \x01(\fR\tsignature\x12!\n" +
"\fhas_delegate\x18\a \x01(\bR\vhasDelegate\x12L\n" +
"\x0edelegate_share\x18\b \x01(\v2%.mpc.coordinator.v1.DelegateShareInfoR\rdelegateShare\"x\n" +
"\x11DelegateShareInfo\x12'\n" +
"\x0fencrypted_share\x18\x01 \x01(\fR\x0eencryptedShare\x12\x1f\n" +
"\vparty_index\x18\x02 \x01(\x05R\n" +
"partyIndex\x12\x19\n" +
"\bparty_id\x18\x03 \x01(\tR\apartyId\"\x90\x01\n" +
"\x17ReportCompletionRequest\x12\x1d\n" +
"\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\x12\x19\n" +
@ -1315,7 +1610,18 @@ const file_api_proto_session_coordinator_proto_rawDesc = "" +
"session_id\x18\x01 \x01(\tR\tsessionId\"H\n" +
"\x14StartSessionResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x16\n" +
"\x06status\x18\x02 \x01(\tR\x06status2\xe7\x05\n" +
"\x06status\x18\x02 \x01(\tR\x06status\"\xbf\x01\n" +
"\x1aSubmitDelegateShareRequest\x12\x1d\n" +
"\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\x12\x19\n" +
"\bparty_id\x18\x02 \x01(\tR\apartyId\x12'\n" +
"\x0fencrypted_share\x18\x03 \x01(\fR\x0eencryptedShare\x12\x1d\n" +
"\n" +
"public_key\x18\x04 \x01(\fR\tpublicKey\x12\x1f\n" +
"\vparty_index\x18\x05 \x01(\x05R\n" +
"partyIndex\"7\n" +
"\x1bSubmitDelegateShareResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess2\xdf\x06\n" +
"\x12SessionCoordinator\x12d\n" +
"\rCreateSession\x12(.mpc.coordinator.v1.CreateSessionRequest\x1a).mpc.coordinator.v1.CreateSessionResponse\x12^\n" +
"\vJoinSession\x12&.mpc.coordinator.v1.JoinSessionRequest\x1a'.mpc.coordinator.v1.JoinSessionResponse\x12m\n" +
@ -1323,7 +1629,8 @@ const file_api_proto_session_coordinator_proto_rawDesc = "" +
"\x0eMarkPartyReady\x12).mpc.coordinator.v1.MarkPartyReadyRequest\x1a*.mpc.coordinator.v1.MarkPartyReadyResponse\x12a\n" +
"\fStartSession\x12'.mpc.coordinator.v1.StartSessionRequest\x1a(.mpc.coordinator.v1.StartSessionResponse\x12m\n" +
"\x10ReportCompletion\x12+.mpc.coordinator.v1.ReportCompletionRequest\x1a,.mpc.coordinator.v1.ReportCompletionResponse\x12a\n" +
"\fCloseSession\x12'.mpc.coordinator.v1.CloseSessionRequest\x1a(.mpc.coordinator.v1.CloseSessionResponseBEZCgithub.com/rwadurian/mpc-system/api/grpc/coordinator/v1;coordinatorb\x06proto3"
"\fCloseSession\x12'.mpc.coordinator.v1.CloseSessionRequest\x1a(.mpc.coordinator.v1.CloseSessionResponse\x12v\n" +
"\x13SubmitDelegateShare\x12..mpc.coordinator.v1.SubmitDelegateShareRequest\x1a/.mpc.coordinator.v1.SubmitDelegateShareResponseBEZCgithub.com/rwadurian/mpc-system/api/grpc/coordinator/v1;coordinatorb\x06proto3"
var (
file_api_proto_session_coordinator_proto_rawDescOnce sync.Once
@ -1337,57 +1644,65 @@ func file_api_proto_session_coordinator_proto_rawDescGZIP() []byte {
return file_api_proto_session_coordinator_proto_rawDescData
}
var file_api_proto_session_coordinator_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
var file_api_proto_session_coordinator_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_api_proto_session_coordinator_proto_goTypes = []any{
(*CreateSessionRequest)(nil), // 0: mpc.coordinator.v1.CreateSessionRequest
(*PartyComposition)(nil), // 1: mpc.coordinator.v1.PartyComposition
(*ParticipantInfo)(nil), // 2: mpc.coordinator.v1.ParticipantInfo
(*DeviceInfo)(nil), // 3: mpc.coordinator.v1.DeviceInfo
(*CreateSessionResponse)(nil), // 4: mpc.coordinator.v1.CreateSessionResponse
(*JoinSessionRequest)(nil), // 5: mpc.coordinator.v1.JoinSessionRequest
(*JoinSessionResponse)(nil), // 6: mpc.coordinator.v1.JoinSessionResponse
(*SessionInfo)(nil), // 7: mpc.coordinator.v1.SessionInfo
(*PartyInfo)(nil), // 8: mpc.coordinator.v1.PartyInfo
(*GetSessionStatusRequest)(nil), // 9: mpc.coordinator.v1.GetSessionStatusRequest
(*GetSessionStatusResponse)(nil), // 10: mpc.coordinator.v1.GetSessionStatusResponse
(*ReportCompletionRequest)(nil), // 11: mpc.coordinator.v1.ReportCompletionRequest
(*ReportCompletionResponse)(nil), // 12: mpc.coordinator.v1.ReportCompletionResponse
(*CloseSessionRequest)(nil), // 13: mpc.coordinator.v1.CloseSessionRequest
(*CloseSessionResponse)(nil), // 14: mpc.coordinator.v1.CloseSessionResponse
(*MarkPartyReadyRequest)(nil), // 15: mpc.coordinator.v1.MarkPartyReadyRequest
(*MarkPartyReadyResponse)(nil), // 16: mpc.coordinator.v1.MarkPartyReadyResponse
(*StartSessionRequest)(nil), // 17: mpc.coordinator.v1.StartSessionRequest
(*StartSessionResponse)(nil), // 18: mpc.coordinator.v1.StartSessionResponse
nil, // 19: mpc.coordinator.v1.CreateSessionResponse.JoinTokensEntry
(*DelegateUserShare)(nil), // 1: mpc.coordinator.v1.DelegateUserShare
(*PartyComposition)(nil), // 2: mpc.coordinator.v1.PartyComposition
(*ParticipantInfo)(nil), // 3: mpc.coordinator.v1.ParticipantInfo
(*DeviceInfo)(nil), // 4: mpc.coordinator.v1.DeviceInfo
(*CreateSessionResponse)(nil), // 5: mpc.coordinator.v1.CreateSessionResponse
(*JoinSessionRequest)(nil), // 6: mpc.coordinator.v1.JoinSessionRequest
(*JoinSessionResponse)(nil), // 7: mpc.coordinator.v1.JoinSessionResponse
(*SessionInfo)(nil), // 8: mpc.coordinator.v1.SessionInfo
(*PartyInfo)(nil), // 9: mpc.coordinator.v1.PartyInfo
(*GetSessionStatusRequest)(nil), // 10: mpc.coordinator.v1.GetSessionStatusRequest
(*GetSessionStatusResponse)(nil), // 11: mpc.coordinator.v1.GetSessionStatusResponse
(*DelegateShareInfo)(nil), // 12: mpc.coordinator.v1.DelegateShareInfo
(*ReportCompletionRequest)(nil), // 13: mpc.coordinator.v1.ReportCompletionRequest
(*ReportCompletionResponse)(nil), // 14: mpc.coordinator.v1.ReportCompletionResponse
(*CloseSessionRequest)(nil), // 15: mpc.coordinator.v1.CloseSessionRequest
(*CloseSessionResponse)(nil), // 16: mpc.coordinator.v1.CloseSessionResponse
(*MarkPartyReadyRequest)(nil), // 17: mpc.coordinator.v1.MarkPartyReadyRequest
(*MarkPartyReadyResponse)(nil), // 18: mpc.coordinator.v1.MarkPartyReadyResponse
(*StartSessionRequest)(nil), // 19: mpc.coordinator.v1.StartSessionRequest
(*StartSessionResponse)(nil), // 20: mpc.coordinator.v1.StartSessionResponse
(*SubmitDelegateShareRequest)(nil), // 21: mpc.coordinator.v1.SubmitDelegateShareRequest
(*SubmitDelegateShareResponse)(nil), // 22: mpc.coordinator.v1.SubmitDelegateShareResponse
nil, // 23: mpc.coordinator.v1.CreateSessionResponse.JoinTokensEntry
}
var file_api_proto_session_coordinator_proto_depIdxs = []int32{
2, // 0: mpc.coordinator.v1.CreateSessionRequest.participants:type_name -> mpc.coordinator.v1.ParticipantInfo
1, // 1: mpc.coordinator.v1.CreateSessionRequest.party_composition:type_name -> mpc.coordinator.v1.PartyComposition
3, // 2: mpc.coordinator.v1.ParticipantInfo.device_info:type_name -> mpc.coordinator.v1.DeviceInfo
19, // 3: mpc.coordinator.v1.CreateSessionResponse.join_tokens:type_name -> mpc.coordinator.v1.CreateSessionResponse.JoinTokensEntry
3, // 4: mpc.coordinator.v1.JoinSessionRequest.device_info:type_name -> mpc.coordinator.v1.DeviceInfo
7, // 5: mpc.coordinator.v1.JoinSessionResponse.session_info:type_name -> mpc.coordinator.v1.SessionInfo
8, // 6: mpc.coordinator.v1.JoinSessionResponse.other_parties:type_name -> mpc.coordinator.v1.PartyInfo
3, // 7: mpc.coordinator.v1.PartyInfo.device_info:type_name -> mpc.coordinator.v1.DeviceInfo
0, // 8: mpc.coordinator.v1.SessionCoordinator.CreateSession:input_type -> mpc.coordinator.v1.CreateSessionRequest
5, // 9: mpc.coordinator.v1.SessionCoordinator.JoinSession:input_type -> mpc.coordinator.v1.JoinSessionRequest
9, // 10: mpc.coordinator.v1.SessionCoordinator.GetSessionStatus:input_type -> mpc.coordinator.v1.GetSessionStatusRequest
15, // 11: mpc.coordinator.v1.SessionCoordinator.MarkPartyReady:input_type -> mpc.coordinator.v1.MarkPartyReadyRequest
17, // 12: mpc.coordinator.v1.SessionCoordinator.StartSession:input_type -> mpc.coordinator.v1.StartSessionRequest
11, // 13: mpc.coordinator.v1.SessionCoordinator.ReportCompletion:input_type -> mpc.coordinator.v1.ReportCompletionRequest
13, // 14: mpc.coordinator.v1.SessionCoordinator.CloseSession:input_type -> mpc.coordinator.v1.CloseSessionRequest
4, // 15: mpc.coordinator.v1.SessionCoordinator.CreateSession:output_type -> mpc.coordinator.v1.CreateSessionResponse
6, // 16: mpc.coordinator.v1.SessionCoordinator.JoinSession:output_type -> mpc.coordinator.v1.JoinSessionResponse
10, // 17: mpc.coordinator.v1.SessionCoordinator.GetSessionStatus:output_type -> mpc.coordinator.v1.GetSessionStatusResponse
16, // 18: mpc.coordinator.v1.SessionCoordinator.MarkPartyReady:output_type -> mpc.coordinator.v1.MarkPartyReadyResponse
18, // 19: mpc.coordinator.v1.SessionCoordinator.StartSession:output_type -> mpc.coordinator.v1.StartSessionResponse
12, // 20: mpc.coordinator.v1.SessionCoordinator.ReportCompletion:output_type -> mpc.coordinator.v1.ReportCompletionResponse
14, // 21: mpc.coordinator.v1.SessionCoordinator.CloseSession:output_type -> mpc.coordinator.v1.CloseSessionResponse
15, // [15:22] is the sub-list for method output_type
8, // [8:15] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
3, // 0: mpc.coordinator.v1.CreateSessionRequest.participants:type_name -> mpc.coordinator.v1.ParticipantInfo
2, // 1: mpc.coordinator.v1.CreateSessionRequest.party_composition:type_name -> mpc.coordinator.v1.PartyComposition
1, // 2: mpc.coordinator.v1.CreateSessionRequest.delegate_user_share:type_name -> mpc.coordinator.v1.DelegateUserShare
4, // 3: mpc.coordinator.v1.ParticipantInfo.device_info:type_name -> mpc.coordinator.v1.DeviceInfo
23, // 4: mpc.coordinator.v1.CreateSessionResponse.join_tokens:type_name -> mpc.coordinator.v1.CreateSessionResponse.JoinTokensEntry
4, // 5: mpc.coordinator.v1.JoinSessionRequest.device_info:type_name -> mpc.coordinator.v1.DeviceInfo
8, // 6: mpc.coordinator.v1.JoinSessionResponse.session_info:type_name -> mpc.coordinator.v1.SessionInfo
9, // 7: mpc.coordinator.v1.JoinSessionResponse.other_parties:type_name -> mpc.coordinator.v1.PartyInfo
4, // 8: mpc.coordinator.v1.PartyInfo.device_info:type_name -> mpc.coordinator.v1.DeviceInfo
12, // 9: mpc.coordinator.v1.GetSessionStatusResponse.delegate_share:type_name -> mpc.coordinator.v1.DelegateShareInfo
0, // 10: mpc.coordinator.v1.SessionCoordinator.CreateSession:input_type -> mpc.coordinator.v1.CreateSessionRequest
6, // 11: mpc.coordinator.v1.SessionCoordinator.JoinSession:input_type -> mpc.coordinator.v1.JoinSessionRequest
10, // 12: mpc.coordinator.v1.SessionCoordinator.GetSessionStatus:input_type -> mpc.coordinator.v1.GetSessionStatusRequest
17, // 13: mpc.coordinator.v1.SessionCoordinator.MarkPartyReady:input_type -> mpc.coordinator.v1.MarkPartyReadyRequest
19, // 14: mpc.coordinator.v1.SessionCoordinator.StartSession:input_type -> mpc.coordinator.v1.StartSessionRequest
13, // 15: mpc.coordinator.v1.SessionCoordinator.ReportCompletion:input_type -> mpc.coordinator.v1.ReportCompletionRequest
15, // 16: mpc.coordinator.v1.SessionCoordinator.CloseSession:input_type -> mpc.coordinator.v1.CloseSessionRequest
21, // 17: mpc.coordinator.v1.SessionCoordinator.SubmitDelegateShare:input_type -> mpc.coordinator.v1.SubmitDelegateShareRequest
5, // 18: mpc.coordinator.v1.SessionCoordinator.CreateSession:output_type -> mpc.coordinator.v1.CreateSessionResponse
7, // 19: mpc.coordinator.v1.SessionCoordinator.JoinSession:output_type -> mpc.coordinator.v1.JoinSessionResponse
11, // 20: mpc.coordinator.v1.SessionCoordinator.GetSessionStatus:output_type -> mpc.coordinator.v1.GetSessionStatusResponse
18, // 21: mpc.coordinator.v1.SessionCoordinator.MarkPartyReady:output_type -> mpc.coordinator.v1.MarkPartyReadyResponse
20, // 22: mpc.coordinator.v1.SessionCoordinator.StartSession:output_type -> mpc.coordinator.v1.StartSessionResponse
14, // 23: mpc.coordinator.v1.SessionCoordinator.ReportCompletion:output_type -> mpc.coordinator.v1.ReportCompletionResponse
16, // 24: mpc.coordinator.v1.SessionCoordinator.CloseSession:output_type -> mpc.coordinator.v1.CloseSessionResponse
22, // 25: mpc.coordinator.v1.SessionCoordinator.SubmitDelegateShare:output_type -> mpc.coordinator.v1.SubmitDelegateShareResponse
18, // [18:26] is the sub-list for method output_type
10, // [10:18] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
}
func init() { file_api_proto_session_coordinator_proto_init() }
@ -1401,7 +1716,7 @@ func file_api_proto_session_coordinator_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_proto_session_coordinator_proto_rawDesc), len(file_api_proto_session_coordinator_proto_rawDesc)),
NumEnums: 0,
NumMessages: 20,
NumMessages: 24,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -26,6 +26,7 @@ const (
SessionCoordinator_StartSession_FullMethodName = "/mpc.coordinator.v1.SessionCoordinator/StartSession"
SessionCoordinator_ReportCompletion_FullMethodName = "/mpc.coordinator.v1.SessionCoordinator/ReportCompletion"
SessionCoordinator_CloseSession_FullMethodName = "/mpc.coordinator.v1.SessionCoordinator/CloseSession"
SessionCoordinator_SubmitDelegateShare_FullMethodName = "/mpc.coordinator.v1.SessionCoordinator/SubmitDelegateShare"
)
// SessionCoordinatorClient is the client API for SessionCoordinator service.
@ -42,6 +43,8 @@ type SessionCoordinatorClient interface {
StartSession(ctx context.Context, in *StartSessionRequest, opts ...grpc.CallOption) (*StartSessionResponse, error)
ReportCompletion(ctx context.Context, in *ReportCompletionRequest, opts ...grpc.CallOption) (*ReportCompletionResponse, error)
CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionResponse, error)
// Delegate party share submission (delegate party submits user's share after keygen)
SubmitDelegateShare(ctx context.Context, in *SubmitDelegateShareRequest, opts ...grpc.CallOption) (*SubmitDelegateShareResponse, error)
}
type sessionCoordinatorClient struct {
@ -122,6 +125,16 @@ func (c *sessionCoordinatorClient) CloseSession(ctx context.Context, in *CloseSe
return out, nil
}
func (c *sessionCoordinatorClient) SubmitDelegateShare(ctx context.Context, in *SubmitDelegateShareRequest, opts ...grpc.CallOption) (*SubmitDelegateShareResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SubmitDelegateShareResponse)
err := c.cc.Invoke(ctx, SessionCoordinator_SubmitDelegateShare_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// SessionCoordinatorServer is the server API for SessionCoordinator service.
// All implementations must embed UnimplementedSessionCoordinatorServer
// for forward compatibility.
@ -136,6 +149,8 @@ type SessionCoordinatorServer interface {
StartSession(context.Context, *StartSessionRequest) (*StartSessionResponse, error)
ReportCompletion(context.Context, *ReportCompletionRequest) (*ReportCompletionResponse, error)
CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionResponse, error)
// Delegate party share submission (delegate party submits user's share after keygen)
SubmitDelegateShare(context.Context, *SubmitDelegateShareRequest) (*SubmitDelegateShareResponse, error)
mustEmbedUnimplementedSessionCoordinatorServer()
}
@ -167,6 +182,9 @@ func (UnimplementedSessionCoordinatorServer) ReportCompletion(context.Context, *
func (UnimplementedSessionCoordinatorServer) CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CloseSession not implemented")
}
func (UnimplementedSessionCoordinatorServer) SubmitDelegateShare(context.Context, *SubmitDelegateShareRequest) (*SubmitDelegateShareResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SubmitDelegateShare not implemented")
}
func (UnimplementedSessionCoordinatorServer) mustEmbedUnimplementedSessionCoordinatorServer() {}
func (UnimplementedSessionCoordinatorServer) testEmbeddedByValue() {}
@ -314,6 +332,24 @@ func _SessionCoordinator_CloseSession_Handler(srv interface{}, ctx context.Conte
return interceptor(ctx, in, info, handler)
}
func _SessionCoordinator_SubmitDelegateShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SubmitDelegateShareRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SessionCoordinatorServer).SubmitDelegateShare(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SessionCoordinator_SubmitDelegateShare_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SessionCoordinatorServer).SubmitDelegateShare(ctx, req.(*SubmitDelegateShareRequest))
}
return interceptor(ctx, in, info, handler)
}
// SessionCoordinator_ServiceDesc is the grpc.ServiceDesc for SessionCoordinator service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -349,6 +385,10 @@ var SessionCoordinator_ServiceDesc = grpc.ServiceDesc{
MethodName: "CloseSession",
Handler: _SessionCoordinator_CloseSession_Handler,
},
{
MethodName: "SubmitDelegateShare",
Handler: _SessionCoordinator_SubmitDelegateShare_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/proto/session_coordinator.proto",

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,11 @@ const (
MessageRouter_SubscribeSessionEvents_FullMethodName = "/mpc.router.v1.MessageRouter/SubscribeSessionEvents"
MessageRouter_PublishSessionEvent_FullMethodName = "/mpc.router.v1.MessageRouter/PublishSessionEvent"
MessageRouter_GetRegisteredParties_FullMethodName = "/mpc.router.v1.MessageRouter/GetRegisteredParties"
MessageRouter_JoinSession_FullMethodName = "/mpc.router.v1.MessageRouter/JoinSession"
MessageRouter_MarkPartyReady_FullMethodName = "/mpc.router.v1.MessageRouter/MarkPartyReady"
MessageRouter_ReportCompletion_FullMethodName = "/mpc.router.v1.MessageRouter/ReportCompletion"
MessageRouter_GetSessionStatus_FullMethodName = "/mpc.router.v1.MessageRouter/GetSessionStatus"
MessageRouter_SubmitDelegateShare_FullMethodName = "/mpc.router.v1.MessageRouter/SubmitDelegateShare"
)
// MessageRouterClient is the client API for MessageRouter service.
@ -36,6 +41,8 @@ const (
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// MessageRouter service handles MPC message routing
// This is the ONLY service that server-parties need to connect to.
// All session operations are proxied through Message Router to Session Coordinator.
type MessageRouterClient interface {
// RouteMessage routes a message from one party to others
RouteMessage(ctx context.Context, in *RouteMessageRequest, opts ...grpc.CallOption) (*RouteMessageResponse, error)
@ -58,6 +65,16 @@ type MessageRouterClient interface {
PublishSessionEvent(ctx context.Context, in *PublishSessionEventRequest, opts ...grpc.CallOption) (*PublishSessionEventResponse, error)
// GetRegisteredParties returns all registered parties (for Session Coordinator party discovery)
GetRegisteredParties(ctx context.Context, in *GetRegisteredPartiesRequest, opts ...grpc.CallOption) (*GetRegisteredPartiesResponse, error)
// JoinSession joins a session (proxied to Session Coordinator)
JoinSession(ctx context.Context, in *JoinSessionRequest, opts ...grpc.CallOption) (*JoinSessionResponse, error)
// MarkPartyReady marks a party as ready (proxied to Session Coordinator)
MarkPartyReady(ctx context.Context, in *MarkPartyReadyRequest, opts ...grpc.CallOption) (*MarkPartyReadyResponse, error)
// ReportCompletion reports completion (proxied to Session Coordinator)
ReportCompletion(ctx context.Context, in *ReportCompletionRequest, opts ...grpc.CallOption) (*ReportCompletionResponse, error)
// GetSessionStatus gets session status (proxied to Session Coordinator)
GetSessionStatus(ctx context.Context, in *GetSessionStatusRequest, opts ...grpc.CallOption) (*GetSessionStatusResponse, error)
// SubmitDelegateShare submits user's share from delegate party (proxied to Session Coordinator)
SubmitDelegateShare(ctx context.Context, in *SubmitDelegateShareRequest, opts ...grpc.CallOption) (*SubmitDelegateShareResponse, error)
}
type messageRouterClient struct {
@ -186,11 +203,63 @@ func (c *messageRouterClient) GetRegisteredParties(ctx context.Context, in *GetR
return out, nil
}
func (c *messageRouterClient) JoinSession(ctx context.Context, in *JoinSessionRequest, opts ...grpc.CallOption) (*JoinSessionResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(JoinSessionResponse)
err := c.cc.Invoke(ctx, MessageRouter_JoinSession_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *messageRouterClient) MarkPartyReady(ctx context.Context, in *MarkPartyReadyRequest, opts ...grpc.CallOption) (*MarkPartyReadyResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MarkPartyReadyResponse)
err := c.cc.Invoke(ctx, MessageRouter_MarkPartyReady_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *messageRouterClient) ReportCompletion(ctx context.Context, in *ReportCompletionRequest, opts ...grpc.CallOption) (*ReportCompletionResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ReportCompletionResponse)
err := c.cc.Invoke(ctx, MessageRouter_ReportCompletion_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *messageRouterClient) GetSessionStatus(ctx context.Context, in *GetSessionStatusRequest, opts ...grpc.CallOption) (*GetSessionStatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetSessionStatusResponse)
err := c.cc.Invoke(ctx, MessageRouter_GetSessionStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *messageRouterClient) SubmitDelegateShare(ctx context.Context, in *SubmitDelegateShareRequest, opts ...grpc.CallOption) (*SubmitDelegateShareResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SubmitDelegateShareResponse)
err := c.cc.Invoke(ctx, MessageRouter_SubmitDelegateShare_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// MessageRouterServer is the server API for MessageRouter service.
// All implementations must embed UnimplementedMessageRouterServer
// for forward compatibility.
//
// MessageRouter service handles MPC message routing
// This is the ONLY service that server-parties need to connect to.
// All session operations are proxied through Message Router to Session Coordinator.
type MessageRouterServer interface {
// RouteMessage routes a message from one party to others
RouteMessage(context.Context, *RouteMessageRequest) (*RouteMessageResponse, error)
@ -213,6 +282,16 @@ type MessageRouterServer interface {
PublishSessionEvent(context.Context, *PublishSessionEventRequest) (*PublishSessionEventResponse, error)
// GetRegisteredParties returns all registered parties (for Session Coordinator party discovery)
GetRegisteredParties(context.Context, *GetRegisteredPartiesRequest) (*GetRegisteredPartiesResponse, error)
// JoinSession joins a session (proxied to Session Coordinator)
JoinSession(context.Context, *JoinSessionRequest) (*JoinSessionResponse, error)
// MarkPartyReady marks a party as ready (proxied to Session Coordinator)
MarkPartyReady(context.Context, *MarkPartyReadyRequest) (*MarkPartyReadyResponse, error)
// ReportCompletion reports completion (proxied to Session Coordinator)
ReportCompletion(context.Context, *ReportCompletionRequest) (*ReportCompletionResponse, error)
// GetSessionStatus gets session status (proxied to Session Coordinator)
GetSessionStatus(context.Context, *GetSessionStatusRequest) (*GetSessionStatusResponse, error)
// SubmitDelegateShare submits user's share from delegate party (proxied to Session Coordinator)
SubmitDelegateShare(context.Context, *SubmitDelegateShareRequest) (*SubmitDelegateShareResponse, error)
mustEmbedUnimplementedMessageRouterServer()
}
@ -253,6 +332,21 @@ func (UnimplementedMessageRouterServer) PublishSessionEvent(context.Context, *Pu
func (UnimplementedMessageRouterServer) GetRegisteredParties(context.Context, *GetRegisteredPartiesRequest) (*GetRegisteredPartiesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRegisteredParties not implemented")
}
func (UnimplementedMessageRouterServer) JoinSession(context.Context, *JoinSessionRequest) (*JoinSessionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method JoinSession not implemented")
}
func (UnimplementedMessageRouterServer) MarkPartyReady(context.Context, *MarkPartyReadyRequest) (*MarkPartyReadyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MarkPartyReady not implemented")
}
func (UnimplementedMessageRouterServer) ReportCompletion(context.Context, *ReportCompletionRequest) (*ReportCompletionResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ReportCompletion not implemented")
}
func (UnimplementedMessageRouterServer) GetSessionStatus(context.Context, *GetSessionStatusRequest) (*GetSessionStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetSessionStatus not implemented")
}
func (UnimplementedMessageRouterServer) SubmitDelegateShare(context.Context, *SubmitDelegateShareRequest) (*SubmitDelegateShareResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SubmitDelegateShare not implemented")
}
func (UnimplementedMessageRouterServer) mustEmbedUnimplementedMessageRouterServer() {}
func (UnimplementedMessageRouterServer) testEmbeddedByValue() {}
@ -440,6 +534,96 @@ func _MessageRouter_GetRegisteredParties_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler)
}
func _MessageRouter_JoinSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(JoinSessionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageRouterServer).JoinSession(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageRouter_JoinSession_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageRouterServer).JoinSession(ctx, req.(*JoinSessionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MessageRouter_MarkPartyReady_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MarkPartyReadyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageRouterServer).MarkPartyReady(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageRouter_MarkPartyReady_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageRouterServer).MarkPartyReady(ctx, req.(*MarkPartyReadyRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MessageRouter_ReportCompletion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReportCompletionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageRouterServer).ReportCompletion(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageRouter_ReportCompletion_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageRouterServer).ReportCompletion(ctx, req.(*ReportCompletionRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MessageRouter_GetSessionStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetSessionStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageRouterServer).GetSessionStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageRouter_GetSessionStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageRouterServer).GetSessionStatus(ctx, req.(*GetSessionStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MessageRouter_SubmitDelegateShare_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SubmitDelegateShareRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MessageRouterServer).SubmitDelegateShare(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MessageRouter_SubmitDelegateShare_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MessageRouterServer).SubmitDelegateShare(ctx, req.(*SubmitDelegateShareRequest))
}
return interceptor(ctx, in, info, handler)
}
// MessageRouter_ServiceDesc is the grpc.ServiceDesc for MessageRouter service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -479,6 +663,26 @@ var MessageRouter_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetRegisteredParties",
Handler: _MessageRouter_GetRegisteredParties_Handler,
},
{
MethodName: "JoinSession",
Handler: _MessageRouter_JoinSession_Handler,
},
{
MethodName: "MarkPartyReady",
Handler: _MessageRouter_MarkPartyReady_Handler,
},
{
MethodName: "ReportCompletion",
Handler: _MessageRouter_ReportCompletion_Handler,
},
{
MethodName: "GetSessionStatus",
Handler: _MessageRouter_GetSessionStatus_Handler,
},
{
MethodName: "SubmitDelegateShare",
Handler: _MessageRouter_SubmitDelegateShare_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@ -5,7 +5,13 @@ package mpc.router.v1;
option go_package = "github.com/rwadurian/mpc-system/api/grpc/router/v1;router";
// MessageRouter service handles MPC message routing
// This is the ONLY service that server-parties need to connect to.
// All session operations are proxied through Message Router to Session Coordinator.
service MessageRouter {
// ============================================
// Message Routing
// ============================================
// RouteMessage routes a message from one party to others
rpc RouteMessage(RouteMessageRequest) returns (RouteMessageResponse);
@ -22,20 +28,52 @@ service MessageRouter {
// GetMessageStatus gets the delivery status of a message
rpc GetMessageStatus(GetMessageStatusRequest) returns (GetMessageStatusResponse);
// ============================================
// Party Registration & Heartbeat
// ============================================
// RegisterParty registers a party with the message router (party actively connects)
rpc RegisterParty(RegisterPartyRequest) returns (RegisterPartyResponse);
// Heartbeat sends a heartbeat to keep the party alive
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse);
// ============================================
// Session Events (Push from Coordinator)
// ============================================
// SubscribeSessionEvents subscribes to session lifecycle events (session start, etc.)
rpc SubscribeSessionEvents(SubscribeSessionEventsRequest) returns (stream SessionEvent);
// PublishSessionEvent publishes a session event (called by Session Coordinator)
rpc PublishSessionEvent(PublishSessionEventRequest) returns (PublishSessionEventResponse);
// ============================================
// Party Discovery (for Session Coordinator)
// ============================================
// GetRegisteredParties returns all registered parties (for Session Coordinator party discovery)
rpc GetRegisteredParties(GetRegisteredPartiesRequest) returns (GetRegisteredPartiesResponse);
// ============================================
// Session Operations (Proxied to Coordinator)
// These allow server-parties to only connect to Message Router
// ============================================
// JoinSession joins a session (proxied to Session Coordinator)
rpc JoinSession(JoinSessionRequest) returns (JoinSessionResponse);
// MarkPartyReady marks a party as ready (proxied to Session Coordinator)
rpc MarkPartyReady(MarkPartyReadyRequest) returns (MarkPartyReadyResponse);
// ReportCompletion reports completion (proxied to Session Coordinator)
rpc ReportCompletion(ReportCompletionRequest) returns (ReportCompletionResponse);
// GetSessionStatus gets session status (proxied to Session Coordinator)
rpc GetSessionStatus(GetSessionStatusRequest) returns (GetSessionStatusResponse);
// SubmitDelegateShare submits user's share from delegate party (proxied to Session Coordinator)
rpc SubmitDelegateShare(SubmitDelegateShareRequest) returns (SubmitDelegateShareResponse);
}
// RouteMessageRequest routes an MPC message
@ -126,6 +164,15 @@ message SessionEvent {
bytes message_hash = 8; // For sign sessions
int64 created_at = 9; // Unix timestamp milliseconds
int64 expires_at = 10; // Unix timestamp milliseconds
// For sign sessions with delegate party: user's share for delegate to use
DelegateUserShare delegate_user_share = 11;
}
// DelegateUserShare contains user's share for delegate party to use in signing
message DelegateUserShare {
string delegate_party_id = 1; // The delegate party that will use this share
bytes encrypted_share = 2; // User's encrypted share
int32 party_index = 3; // Party index for this share
}
// PublishSessionEventRequest publishes a session event
@ -211,3 +258,102 @@ message HeartbeatResponse {
int64 server_timestamp = 2; // Server timestamp for clock sync
int32 pending_messages = 3; // Number of pending messages for this party
}
// ============================================
// Session Operations (Proxied to Coordinator)
// ============================================
// DeviceInfo contains device information
message DeviceInfo {
string device_type = 1; // server, mobile, web
string device_id = 2;
string platform = 3;
string app_version = 4;
}
// PartyInfo contains party information
message PartyInfo {
string party_id = 1;
int32 party_index = 2;
DeviceInfo device_info = 3;
}
// SessionInfo contains session information
message SessionInfo {
string session_id = 1;
string session_type = 2; // keygen, sign
int32 threshold_n = 3;
int32 threshold_t = 4;
bytes message_hash = 5; // For sign sessions
string status = 6;
}
// JoinSessionRequest joins a session
message JoinSessionRequest {
string session_id = 1;
string party_id = 2;
string join_token = 3;
DeviceInfo device_info = 4;
}
// JoinSessionResponse returns session info after joining
message JoinSessionResponse {
bool success = 1;
SessionInfo session_info = 2;
repeated PartyInfo other_parties = 3; // All participants in the session
int32 party_index = 4; // This party's index
}
// MarkPartyReadyRequest marks a party as ready
message MarkPartyReadyRequest {
string session_id = 1;
string party_id = 2;
}
// MarkPartyReadyResponse confirms ready status
message MarkPartyReadyResponse {
bool success = 1;
bool all_ready = 2; // True if all parties are ready
}
// ReportCompletionRequest reports protocol completion
message ReportCompletionRequest {
string session_id = 1;
string party_id = 2;
bytes public_key = 3; // For keygen: resulting public key
bytes signature = 4; // For signing: resulting signature
}
// ReportCompletionResponse confirms completion report
message ReportCompletionResponse {
bool success = 1;
bool all_completed = 2; // True if all parties completed
}
// GetSessionStatusRequest requests session status
message GetSessionStatusRequest {
string session_id = 1;
}
// GetSessionStatusResponse returns session status
message GetSessionStatusResponse {
string session_id = 1;
string status = 2; // created, in_progress, completed, failed, expired
int32 threshold_n = 3;
int32 threshold_t = 4;
repeated PartyInfo participants = 5;
}
// SubmitDelegateShareRequest submits user's share from delegate party
message SubmitDelegateShareRequest {
string session_id = 1;
string party_id = 2;
bytes encrypted_share = 3; // Encrypted share for user
bytes public_key = 4; // Public key from keygen
int32 party_index = 5; // Party's index in the session
}
// SubmitDelegateShareResponse contains result of share submission
message SubmitDelegateShareResponse {
bool success = 1;
}

View File

@ -14,6 +14,9 @@ service SessionCoordinator {
rpc StartSession(StartSessionRequest) returns (StartSessionResponse);
rpc ReportCompletion(ReportCompletionRequest) returns (ReportCompletionResponse);
rpc CloseSession(CloseSessionRequest) returns (CloseSessionResponse);
// Delegate party share submission (delegate party submits user's share after keygen)
rpc SubmitDelegateShare(SubmitDelegateShareRequest) returns (SubmitDelegateShareResponse);
}
// CreateSessionRequest creates a new MPC session
@ -25,6 +28,15 @@ message CreateSessionRequest {
bytes message_hash = 5; // Required for sign sessions
int64 expires_in_seconds = 6; // Session expiration time
PartyComposition party_composition = 7; // Optional: party composition requirements for auto-selection
// For sign sessions with delegate party: user must provide their encrypted share
DelegateUserShare delegate_user_share = 8;
}
// DelegateUserShare contains user's share for delegate party to use in signing
message DelegateUserShare {
string delegate_party_id = 1; // The delegate party that will use this share
bytes encrypted_share = 2; // User's encrypted share (same as received from keygen)
int32 party_index = 3; // Party index for this share
}
// PartyComposition specifies requirements for automatic party selection
@ -99,8 +111,22 @@ message GetSessionStatusResponse {
string status = 1;
int32 completed_parties = 2;
int32 total_parties = 3;
bytes public_key = 4; // For completed keygen
bytes signature = 5; // For completed sign
string session_type = 4; // "keygen" or "sign"
bytes public_key = 5; // For completed keygen
bytes signature = 6; // For completed sign
// has_delegate indicates whether this keygen session has a delegate party
// Only meaningful for keygen sessions. Always false for sign sessions.
bool has_delegate = 7;
// Delegate share info (returned when keygen session completed and delegate party submitted share)
// Only populated if session_type="keygen" AND has_delegate=true AND session is completed
DelegateShareInfo delegate_share = 8;
}
// DelegateShareInfo contains the delegate party's share for user
message DelegateShareInfo {
bytes encrypted_share = 1; // Encrypted share for user
int32 party_index = 2; // Party's index in the session
string party_id = 3; // Delegate party ID
}
// ReportCompletionRequest reports that a participant has completed
@ -151,3 +177,17 @@ message StartSessionResponse {
bool success = 1;
string status = 2; // New session status
}
// SubmitDelegateShareRequest submits user's share from delegate party
message SubmitDelegateShareRequest {
string session_id = 1;
string party_id = 2;
bytes encrypted_share = 3; // Encrypted share for user
bytes public_key = 4; // Public key from keygen
int32 party_index = 5; // Party's index in the session
}
// SubmitDelegateShareResponse contains result of share submission
message SubmitDelegateShareResponse {
bool success = 1;
}

View File

@ -4,11 +4,23 @@
# =============================================================================
# This script manages the MPC System Docker services
#
# External Ports:
# Deployment Modes:
# 1. Development (default): All services on one machine (docker-compose.yml)
# 2. Production Central: Central services only (docker-compose.prod.yml)
# 3. Production Party: Standalone party (docker-compose.party.yml)
#
# External Ports (Development):
# 4000 - Account Service HTTP API
# 8081 - Session Coordinator API
# 8082 - Message Router WebSocket
# 8083 - Server Party API (user share generation)
# 8082 - Message Router HTTP
# 8083 - Server Party API
#
# External Ports (Production Central):
# 50051 - Message Router gRPC (for party connections)
# 50052 - Session Coordinator gRPC (for party connections)
# 4000 - Account Service HTTP API
# 8081 - Session Coordinator HTTP API
# 8082 - Message Router HTTP API
# =============================================================================
set -e
@ -18,34 +30,70 @@ RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_header() { echo -e "${CYAN}=== $1 ===${NC}"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Load environment
if [ -f ".env" ]; then
log_info "Loading environment from .env file"
# Determine which environment file to load
load_env() {
local env_file="$1"
if [ -f "$env_file" ]; then
log_info "Loading environment from $env_file"
set -a
source .env
source "$env_file"
set +a
elif [ ! -f ".env" ] && [ -f ".env.example" ]; then
return 0
fi
return 1
}
# Load environment based on mode
load_environment() {
local mode="$1"
case "$mode" in
prod)
load_env ".env.prod" || load_env ".env" || {
log_error "No .env.prod or .env file found"
exit 1
}
;;
party)
load_env ".env.party" || {
log_error "No .env.party file found. Create from .env.party.example"
exit 1
}
;;
*)
load_env ".env" || {
if [ -f ".env.example" ]; then
log_warn ".env file not found. Creating from .env.example"
log_warn "Please edit .env and configure for your environment!"
cp .env.example .env
log_error "Please configure .env file and run again"
exit 1
fi
exit 1
}
;;
esac
}
# Core services list
CORE_SERVICES="postgres redis rabbitmq"
MPC_SERVICES="session-coordinator message-router server-party-1 server-party-2 server-party-3 server-party-api account-service"
ALL_SERVICES="$CORE_SERVICES $MPC_SERVICES"
# Service lists
CORE_SERVICES="postgres"
DEV_MPC_SERVICES="session-coordinator message-router server-party-1 server-party-2 server-party-3 server-party-api account-service"
PROD_CENTRAL_SERVICES="postgres message-router session-coordinator account-service server-party-api"
# ============================================
# Development Mode Commands (docker-compose.yml)
# ============================================
dev_commands() {
load_environment "dev"
case "$1" in
build)
@ -61,7 +109,7 @@ case "$1" in
;;
up|start)
log_info "Starting MPC System..."
log_info "Starting MPC System (Development)..."
docker compose up -d
log_success "MPC System started"
echo ""
@ -105,10 +153,8 @@ case "$1" in
health)
log_info "Checking MPC System health..."
# Check infrastructure
echo ""
echo "=== Infrastructure ==="
log_header "Infrastructure"
for svc in $CORE_SERVICES; do
if docker compose ps "$svc" --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
log_success "$svc is healthy"
@ -117,10 +163,9 @@ case "$1" in
fi
done
# Check MPC services
echo ""
echo "=== MPC Services ==="
for svc in $MPC_SERVICES; do
log_header "MPC Services"
for svc in $DEV_MPC_SERVICES; do
if docker compose ps "$svc" --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
log_success "$svc is healthy"
else
@ -128,9 +173,8 @@ case "$1" in
fi
done
# Check external API
echo ""
echo "=== External API ==="
log_header "External API"
if curl -sf "http://localhost:4000/health" > /dev/null 2>&1; then
log_success "Account Service API (port 4000) is accessible"
else
@ -138,50 +182,6 @@ case "$1" in
fi
;;
infra)
case "$2" in
up)
log_info "Starting infrastructure services..."
docker compose up -d $CORE_SERVICES
log_success "Infrastructure started"
;;
down)
log_info "Stopping infrastructure services..."
docker compose stop $CORE_SERVICES
log_success "Infrastructure stopped"
;;
*)
echo "Usage: $0 infra {up|down}"
exit 1
;;
esac
;;
mpc)
case "$2" in
up)
log_info "Starting MPC services..."
docker compose up -d $MPC_SERVICES
log_success "MPC services started"
;;
down)
log_info "Stopping MPC services..."
docker compose stop $MPC_SERVICES
log_success "MPC services stopped"
;;
restart)
log_info "Restarting MPC services..."
docker compose stop $MPC_SERVICES
docker compose up -d $MPC_SERVICES
log_success "MPC services restarted"
;;
*)
echo "Usage: $0 mpc {up|down|restart}"
exit 1
;;
esac
;;
clean)
log_warn "This will remove all containers and volumes!"
read -p "Are you sure? (y/N) " -n 1 -r
@ -213,31 +213,305 @@ case "$1" in
;;
*)
echo "MPC System Deployment Script"
echo ""
echo "Usage: $0 <command> [options]"
echo ""
echo "Commands:"
echo " build - Build all Docker images"
echo " build-no-cache - Build images without cache"
echo " up|start - Start all services"
echo " down|stop - Stop all services"
echo " restart - Restart all services"
echo " logs [service] - Follow logs (all or specific service)"
echo " logs-tail [svc] - Show last 100 log lines"
echo " status|ps - Show services status"
echo " health - Check all services health"
echo ""
echo " infra up|down - Start/stop infrastructure only"
echo " mpc up|down|restart - Start/stop/restart MPC services only"
echo ""
echo " shell [service] - Open shell in container"
echo " test-api - Test Account Service API"
echo " clean - Remove all containers and volumes"
echo ""
echo "Services:"
echo " Infrastructure: $CORE_SERVICES"
echo " MPC Services: $MPC_SERVICES"
exit 1
return 1
;;
esac
}
# ============================================
# Production Central Commands (docker-compose.prod.yml)
# ============================================
prod_commands() {
load_environment "prod"
case "$1" in
build)
log_info "Building Production Central services..."
docker compose -f docker-compose.prod.yml build
log_success "Production services built"
;;
up|start)
log_info "Starting Production Central services..."
docker compose -f docker-compose.prod.yml up -d
log_success "Production Central services started"
echo ""
log_header "Services Status"
docker compose -f docker-compose.prod.yml ps
echo ""
log_header "Public Endpoints"
echo " Message Router gRPC: ${MESSAGE_ROUTER_GRPC_PORT:-50051}"
echo " Session Coordinator gRPC: ${SESSION_COORDINATOR_GRPC_PORT:-50052}"
echo " Account Service HTTP: ${ACCOUNT_SERVICE_PORT:-4000}"
;;
down|stop)
log_info "Stopping Production Central services..."
docker compose -f docker-compose.prod.yml down
log_success "Production Central services stopped"
;;
restart)
log_info "Restarting Production Central services..."
docker compose -f docker-compose.prod.yml down
docker compose -f docker-compose.prod.yml up -d
log_success "Production Central services restarted"
;;
logs)
if [ -n "$2" ]; then
docker compose -f docker-compose.prod.yml logs -f "$2"
else
docker compose -f docker-compose.prod.yml logs -f
fi
;;
status|ps)
log_info "Production Central status:"
docker compose -f docker-compose.prod.yml ps
;;
health)
log_info "Checking Production Central health..."
echo ""
log_header "Central Services"
for svc in $PROD_CENTRAL_SERVICES; do
if docker compose -f docker-compose.prod.yml ps "$svc" --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
log_success "$svc is healthy"
else
log_warn "$svc is not healthy"
fi
done
echo ""
log_header "Public Endpoints"
if curl -sf "http://localhost:${MESSAGE_ROUTER_HTTP_PORT:-8082}/health" > /dev/null 2>&1; then
log_success "Message Router (port ${MESSAGE_ROUTER_HTTP_PORT:-8082}) is accessible"
else
log_error "Message Router is not accessible"
fi
if curl -sf "http://localhost:${SESSION_COORDINATOR_HTTP_PORT:-8081}/health" > /dev/null 2>&1; then
log_success "Session Coordinator (port ${SESSION_COORDINATOR_HTTP_PORT:-8081}) is accessible"
else
log_error "Session Coordinator is not accessible"
fi
if curl -sf "http://localhost:${ACCOUNT_SERVICE_PORT:-4000}/health" > /dev/null 2>&1; then
log_success "Account Service (port ${ACCOUNT_SERVICE_PORT:-4000}) is accessible"
else
log_error "Account Service is not accessible"
fi
;;
clean)
log_warn "This will remove all Production Central containers and volumes!"
read -p "Are you sure? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
docker compose -f docker-compose.prod.yml down -v
log_success "Production Central cleaned"
else
log_info "Cancelled"
fi
;;
*)
return 1
;;
esac
}
# ============================================
# Production Party Commands (docker-compose.party.yml)
# ============================================
party_commands() {
load_environment "party"
# Validate required environment variables
if [ -z "$PARTY_ID" ]; then
log_error "PARTY_ID must be set (e.g., server-party-1)"
exit 1
fi
if [ -z "$MESSAGE_ROUTER_ADDR" ]; then
log_error "MESSAGE_ROUTER_ADDR must be set (e.g., grpc.mpc.example.com:50051)"
exit 1
fi
case "$1" in
build)
log_info "Building Party ($PARTY_ID)..."
docker compose -f docker-compose.party.yml build
log_success "Party built"
;;
up|start)
log_info "Starting Party: $PARTY_ID"
log_info "Connecting to Message Router: $MESSAGE_ROUTER_ADDR"
docker compose -f docker-compose.party.yml up -d
log_success "Party $PARTY_ID started"
echo ""
docker compose -f docker-compose.party.yml ps
;;
down|stop)
log_info "Stopping Party: $PARTY_ID..."
docker compose -f docker-compose.party.yml down
log_success "Party stopped"
;;
restart)
log_info "Restarting Party: $PARTY_ID..."
docker compose -f docker-compose.party.yml down
docker compose -f docker-compose.party.yml up -d
log_success "Party restarted"
;;
logs)
docker compose -f docker-compose.party.yml logs -f server-party
;;
status|ps)
log_info "Party $PARTY_ID status:"
docker compose -f docker-compose.party.yml ps
;;
health)
log_info "Checking Party $PARTY_ID health..."
echo ""
log_header "Local Services"
if docker compose -f docker-compose.party.yml ps postgres --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
log_success "Local PostgreSQL is healthy"
else
log_warn "Local PostgreSQL is not healthy"
fi
if docker compose -f docker-compose.party.yml ps server-party --format json 2>/dev/null | grep -q '"Health":"healthy"'; then
log_success "Server Party is healthy"
else
log_warn "Server Party is not healthy"
fi
echo ""
log_header "Central Service Connectivity"
# Extract host and port from MESSAGE_ROUTER_ADDR
MR_HOST=$(echo "$MESSAGE_ROUTER_ADDR" | cut -d: -f1)
MR_PORT=$(echo "$MESSAGE_ROUTER_ADDR" | cut -d: -f2)
if timeout 5 bash -c "echo >/dev/tcp/$MR_HOST/$MR_PORT" 2>/dev/null; then
log_success "Message Router ($MESSAGE_ROUTER_ADDR) is reachable"
else
log_error "Message Router ($MESSAGE_ROUTER_ADDR) is NOT reachable"
fi
;;
clean)
log_warn "This will remove Party $PARTY_ID containers and LOCAL KEY STORAGE!"
log_warn "Your encrypted key shares will be DELETED!"
read -p "Are you absolutely sure? (yes/N) " confirm
echo
if [ "$confirm" = "yes" ]; then
docker compose -f docker-compose.party.yml down -v
log_success "Party $PARTY_ID cleaned"
else
log_info "Cancelled"
fi
;;
*)
return 1
;;
esac
}
# ============================================
# Main Command Router
# ============================================
show_help() {
echo "MPC System Deployment Script"
echo ""
echo "Usage: $0 <mode> <command> [options]"
echo ""
echo "Deployment Modes:"
echo " (default) Development mode - all services on one machine"
echo " prod Production Central - Message Router, Session Coordinator, Account"
echo " party Production Party - standalone server-party (distributed)"
echo ""
echo "Development Commands (default mode):"
echo " $0 build Build all Docker images"
echo " $0 up|start Start all services"
echo " $0 down|stop Stop all services"
echo " $0 restart Restart all services"
echo " $0 logs [service] Follow logs"
echo " $0 status|ps Show services status"
echo " $0 health Check all services health"
echo " $0 clean Remove containers and volumes"
echo ""
echo "Production Central Commands:"
echo " $0 prod build Build central services"
echo " $0 prod up Start central services"
echo " $0 prod down Stop central services"
echo " $0 prod logs Follow central logs"
echo " $0 prod health Check central health"
echo ""
echo "Production Party Commands (run on each party machine):"
echo " $0 party build Build party service"
echo " $0 party up Start party (connects to central)"
echo " $0 party down Stop party"
echo " $0 party logs Follow party logs"
echo " $0 party health Check party health and connectivity"
echo ""
echo "Environment Files:"
echo " .env Development configuration"
echo " .env.prod Production Central configuration"
echo " .env.party Production Party configuration"
echo ""
echo "Examples:"
echo " # Development (all on one machine)"
echo " $0 up"
echo ""
echo " # Production Central (on central server)"
echo " $0 prod up"
echo ""
echo " # Production Party (on each party machine)"
echo " PARTY_ID=server-party-1 $0 party up"
echo " PARTY_ID=server-party-2 $0 party up"
echo " PARTY_ID=server-party-3 $0 party up"
}
# Route commands based on first argument
case "$1" in
prod)
shift
prod_commands "$@" || {
echo "Usage: $0 prod {build|up|down|restart|logs|status|health|clean}"
exit 1
}
;;
party)
shift
party_commands "$@" || {
echo "Usage: $0 party {build|up|down|restart|logs|status|health|clean}"
echo ""
echo "Required environment variables:"
echo " PARTY_ID Unique party identifier"
echo " MESSAGE_ROUTER_ADDR Central Message Router address (only connection needed)"
echo " CRYPTO_MASTER_KEY Encryption key for key shares"
exit 1
}
;;
help|--help|-h)
show_help
;;
"")
show_help
exit 1
;;
*)
# Default to development mode
dev_commands "$@" || {
show_help
exit 1
}
;;
esac

View File

@ -0,0 +1,127 @@
# =============================================================================
# MPC-System Production Deployment - Standalone Server Party
# =============================================================================
# Purpose: Deploy a single server-party that connects to central Message Router
# This configuration is used for distributed deployment where parties run on
# different physical machines, possibly behind NAT.
#
# Usage:
# # On each party machine:
# PARTY_ID=server-party-1 ./deploy.sh party up
# PARTY_ID=server-party-2 ./deploy.sh party up
# PARTY_ID=server-party-3 ./deploy.sh party up
#
# Required Environment Variables:
# PARTY_ID - Unique party identifier (e.g., server-party-1)
# MESSAGE_ROUTER_ADDR - Public address of Message Router (e.g., grpc.mpc.example.com:50051)
# CRYPTO_MASTER_KEY - 64-character hex key for share encryption
#
# Architecture:
# This Party (NAT OK) --[outbound gRPC]--> Message Router (Public Internet)
#
# Note: Parties ONLY connect to Message Router. Session operations are
# proxied through Message Router to Session Coordinator internally.
#
# NAT Traversal:
# - Party initiates single outbound connection (no inbound ports needed)
# - gRPC keepalive maintains connection through NAT
# - Heartbeat every 30 seconds keeps connection alive
# =============================================================================
services:
# ============================================
# PostgreSQL for Party's Local Key Storage
# ============================================
postgres:
image: postgres:15-alpine
container_name: mpc-party-postgres-${PARTY_ID:-party}
environment:
POSTGRES_DB: mpc_party
POSTGRES_USER: ${POSTGRES_USER:-mpc_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
volumes:
- party-postgres-data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mpc_user} -d mpc_party"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- party-network
restart: unless-stopped
# ============================================
# Server Party - Connects to Central Services
# ============================================
server-party:
build:
context: .
dockerfile: services/server-party/Dockerfile
container_name: mpc-${PARTY_ID:-server-party}
# No ports exposed - party connects outbound to Message Router
# HTTP port is optional for local health checks
ports:
- "${PARTY_HTTP_PORT:-8080}:8080" # Optional: local health check only
environment:
# Party Identity
PARTY_ID: ${PARTY_ID:?PARTY_ID must be set (e.g., server-party-1)}
PARTY_ROLE: ${PARTY_ROLE:-persistent}
# Server Configuration
MPC_SERVER_GRPC_PORT: 50051
MPC_SERVER_HTTP_PORT: 8080
MPC_SERVER_ENVIRONMENT: ${ENVIRONMENT:-production}
# Local Database for Key Storage
MPC_DATABASE_HOST: postgres
MPC_DATABASE_PORT: 5432
MPC_DATABASE_USER: ${POSTGRES_USER:-mpc_user}
MPC_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
MPC_DATABASE_DBNAME: mpc_party
MPC_DATABASE_SSLMODE: disable
# Central Service (PUBLIC address - accessible from this party's location)
# Parties ONLY connect to Message Router (session ops proxied internally)
MESSAGE_ROUTER_ADDR: ${MESSAGE_ROUTER_ADDR:?MESSAGE_ROUTER_ADDR must be set (e.g., grpc.mpc.example.com:50051)}
# Encryption Key for Key Shares
MPC_CRYPTO_MASTER_KEY: ${CRYPTO_MASTER_KEY:?CRYPTO_MASTER_KEY must be set (64 hex characters)}
# Optional: Notification channels for offline mode
NOTIFICATION_EMAIL: ${NOTIFICATION_EMAIL:-}
NOTIFICATION_PHONE: ${NOTIFICATION_PHONE:-}
NOTIFICATION_PUSH_TOKEN: ${NOTIFICATION_PUSH_TOKEN:-}
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- party-network
restart: unless-stopped
# Important: Allow container to resolve external DNS
dns:
- 8.8.8.8
- 8.8.4.4
# ============================================
# Networks
# ============================================
networks:
party-network:
driver: bridge
# ============================================
# Volumes - Party's Local Key Storage
# IMPORTANT: Back up this volume! It contains encrypted key shares.
# ============================================
volumes:
party-postgres-data:
driver: local
name: mpc-party-${PARTY_ID:-party}-postgres-data

View File

@ -0,0 +1,206 @@
# =============================================================================
# MPC-System Production Deployment - Central Services
# =============================================================================
# Purpose: Deploy central infrastructure (Message Router, Session Coordinator, Account)
# Server Parties are deployed separately on different machines/locations
#
# Usage:
# ./deploy.sh prod up # Start central services
# ./deploy.sh prod down # Stop central services
#
# External Ports (must be accessible from server-parties):
# 50051 - Message Router gRPC (for party connections)
# 50052 - Session Coordinator gRPC (for party connections)
# 4000 - Account Service HTTP API (for backend integration)
# 8081 - Session Coordinator HTTP API (for backend integration)
# 8082 - Message Router HTTP API (health checks)
#
# Architecture:
# Server Parties (NAT-friendly) --> Message Router (Public) --> Session Coordinator
# --> PostgreSQL (Internal)
# =============================================================================
services:
# ============================================
# Infrastructure Services (Internal Only)
# ============================================
postgres:
image: postgres:15-alpine
container_name: mpc-postgres
environment:
POSTGRES_DB: mpc_system
POSTGRES_USER: ${POSTGRES_USER:-mpc_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mpc_user} -d mpc_system"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- mpc-internal
restart: unless-stopped
# ============================================
# Message Router - Public gRPC Endpoint
# Server Parties connect here from anywhere
# ============================================
message-router:
build:
context: .
dockerfile: services/message-router/Dockerfile
container_name: mpc-message-router
ports:
- "${MESSAGE_ROUTER_GRPC_PORT:-50051}:50051" # gRPC for party connections (PUBLIC)
- "${MESSAGE_ROUTER_HTTP_PORT:-8082}:8080" # HTTP for health checks
environment:
MPC_SERVER_GRPC_PORT: 50051
MPC_SERVER_HTTP_PORT: 8080
MPC_SERVER_ENVIRONMENT: ${ENVIRONMENT:-production}
MPC_DATABASE_HOST: postgres
MPC_DATABASE_PORT: 5432
MPC_DATABASE_USER: ${POSTGRES_USER:-mpc_user}
MPC_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
MPC_DATABASE_DBNAME: mpc_system
MPC_DATABASE_SSLMODE: disable
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- mpc-internal
restart: unless-stopped
# ============================================
# Session Coordinator - Public gRPC Endpoint
# ============================================
session-coordinator:
build:
context: .
dockerfile: services/session-coordinator/Dockerfile
container_name: mpc-session-coordinator
ports:
- "${SESSION_COORDINATOR_GRPC_PORT:-50052}:50051" # gRPC for party connections (PUBLIC)
- "${SESSION_COORDINATOR_HTTP_PORT:-8081}:8080" # HTTP API
environment:
MPC_SERVER_GRPC_PORT: 50051
MPC_SERVER_HTTP_PORT: 8080
MPC_SERVER_ENVIRONMENT: ${ENVIRONMENT:-production}
MPC_DATABASE_HOST: postgres
MPC_DATABASE_PORT: 5432
MPC_DATABASE_USER: ${POSTGRES_USER:-mpc_user}
MPC_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
MPC_DATABASE_DBNAME: mpc_system
MPC_DATABASE_SSLMODE: disable
MPC_JWT_SECRET_KEY: ${JWT_SECRET_KEY}
MPC_JWT_ISSUER: mpc-system
MESSAGE_ROUTER_ADDR: message-router:50051
depends_on:
postgres:
condition: service_healthy
message-router:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- mpc-internal
restart: unless-stopped
# ============================================
# Account Service - External API Entry Point
# ============================================
account-service:
build:
context: .
dockerfile: services/account/Dockerfile
container_name: mpc-account-service
ports:
- "${ACCOUNT_SERVICE_PORT:-4000}:8080" # HTTP API for external access
environment:
MPC_SERVER_GRPC_PORT: 50051
MPC_SERVER_HTTP_PORT: 8080
MPC_SERVER_ENVIRONMENT: ${ENVIRONMENT:-production}
MPC_DATABASE_HOST: postgres
MPC_DATABASE_PORT: 5432
MPC_DATABASE_USER: ${POSTGRES_USER:-mpc_user}
MPC_DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
MPC_DATABASE_DBNAME: mpc_system
MPC_DATABASE_SSLMODE: disable
MPC_COORDINATOR_URL: session-coordinator:50051
MPC_JWT_SECRET_KEY: ${JWT_SECRET_KEY}
MPC_API_KEY: ${MPC_API_KEY}
ALLOWED_IPS: ${ALLOWED_IPS:-}
depends_on:
postgres:
condition: service_healthy
session-coordinator:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- mpc-internal
restart: unless-stopped
# ============================================
# Server Party API - User Share Generation
# (Optional: only needed if generating user shares)
# ============================================
server-party-api:
build:
context: .
dockerfile: services/server-party-api/Dockerfile
container_name: mpc-server-party-api
ports:
- "${SERVER_PARTY_API_PORT:-8083}:8080"
environment:
MPC_SERVER_HTTP_PORT: 8080
MPC_SERVER_ENVIRONMENT: ${ENVIRONMENT:-production}
SESSION_COORDINATOR_ADDR: session-coordinator:50051
MESSAGE_ROUTER_ADDR: message-router:50051
MPC_CRYPTO_MASTER_KEY: ${CRYPTO_MASTER_KEY}
MPC_API_KEY: ${MPC_API_KEY}
depends_on:
session-coordinator:
condition: service_healthy
message-router:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- mpc-internal
restart: unless-stopped
# ============================================
# Networks
# ============================================
networks:
mpc-internal:
driver: bridge
# ============================================
# Volumes
# ============================================
volumes:
postgres-data:
driver: local

View File

@ -0,0 +1,6 @@
-- Rollback: Remove signing_parties column from accounts table
DROP INDEX IF EXISTS idx_accounts_signing_parties;
ALTER TABLE accounts
DROP COLUMN IF EXISTS signing_parties;

View File

@ -0,0 +1,11 @@
-- Add signing_parties column to accounts table
-- This column stores configured party IDs for signing operations
-- If NULL or empty, all active parties will be used for signing
ALTER TABLE accounts
ADD COLUMN signing_parties TEXT[];
-- Add index for querying accounts with configured signing parties
CREATE INDEX idx_accounts_signing_parties ON accounts USING GIN(signing_parties) WHERE signing_parties IS NOT NULL;
COMMENT ON COLUMN accounts.signing_parties IS 'Configured party IDs for signing operations. NULL means use all active parties.';

View File

@ -9,9 +9,10 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/rwadurian/mpc-system/pkg/logger"
"github.com/rwadurian/mpc-system/services/account/adapters/output/grpc"
grpcclient "github.com/rwadurian/mpc-system/services/account/adapters/output/grpc"
"github.com/rwadurian/mpc-system/services/account/application/ports"
"github.com/rwadurian/mpc-system/services/account/application/use_cases"
"github.com/rwadurian/mpc-system/services/account/domain/entities"
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
"go.uber.org/zap"
)
@ -31,7 +32,7 @@ type AccountHTTPHandler struct {
completeRecoveryUC *use_cases.CompleteRecoveryUseCase
getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase
cancelRecoveryUC *use_cases.CancelRecoveryUseCase
sessionCoordinatorClient *grpc.SessionCoordinatorClient
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient
}
// NewAccountHTTPHandler creates a new AccountHTTPHandler
@ -49,7 +50,7 @@ func NewAccountHTTPHandler(
completeRecoveryUC *use_cases.CompleteRecoveryUseCase,
getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase,
cancelRecoveryUC *use_cases.CancelRecoveryUseCase,
sessionCoordinatorClient *grpc.SessionCoordinatorClient,
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient,
) *AccountHTTPHandler {
return &AccountHTTPHandler{
createAccountUC: createAccountUC,
@ -80,6 +81,11 @@ func (h *AccountHTTPHandler) RegisterRoutes(router *gin.RouterGroup) {
accounts.PUT("/:id", h.UpdateAccount)
accounts.GET("/:id/shares", h.GetAccountShares)
accounts.DELETE("/:id/shares/:shareId", h.DeactivateShare)
// Signing parties configuration
accounts.POST("/:id/signing-config", h.SetSigningParties)
accounts.PUT("/:id/signing-config", h.UpdateSigningParties)
accounts.DELETE("/:id/signing-config", h.ClearSigningParties)
accounts.GET("/:id/signing-config", h.GetSigningParties)
}
auth := router.Group("/auth")
@ -615,7 +621,7 @@ func (h *AccountHTTPHandler) CreateKeygenSession(c *gin.Context) {
type CreateSigningSessionRequest struct {
AccountID string `json:"account_id" binding:"required"` // Account to sign for
MessageHash string `json:"message_hash" binding:"required"` // SHA-256 hash to sign (hex encoded)
UserShare string `json:"user_share"` // Optional: user's encrypted share (hex) if delegate party is used
UserShare string `json:"user_share"` // Required if account has delegate share: user's encrypted share (hex)
}
// CreateSigningSession handles creating a new signing session
@ -655,20 +661,80 @@ func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) {
return
}
// Get the party IDs from account shares
var partyIDs []string
// Build a map of active shares for validation
activeSharesMap := make(map[string]*entities.AccountShare)
var allActivePartyIDs []string
var delegateShare *entities.AccountShare
for _, share := range accountOutput.Shares {
if share.IsActive {
partyIDs = append(partyIDs, share.PartyID)
activeSharesMap[share.PartyID] = share
allActivePartyIDs = append(allActivePartyIDs, share.PartyID)
// Check if this is a delegate share
if share.ShareType == value_objects.ShareTypeDelegate {
delegateShare = share
}
}
}
// Validate we have enough active shares
// Determine which parties to use for signing
var partyIDs []string
if accountOutput.Account.HasSigningPartiesConfig() {
// Use configured signing parties
configuredParties := accountOutput.Account.GetSigningParties()
// Validate all configured parties are still active
for _, partyID := range configuredParties {
if _, exists := activeSharesMap[partyID]; !exists {
c.JSON(http.StatusBadRequest, gin.H{
"error": "configured signing party is no longer active",
"party_id": partyID,
"hint": "update signing-config with active parties",
})
return
}
}
partyIDs = configuredParties
logger.Info("Using configured signing parties",
zap.String("account_id", req.AccountID),
zap.Strings("configured_parties", partyIDs))
} else {
// Use all active parties (original behavior)
partyIDs = allActivePartyIDs
logger.Info("Using all active parties for signing",
zap.String("account_id", req.AccountID),
zap.Strings("active_parties", partyIDs))
}
// Check if any of the selected parties is a delegate
// (delegate share might not be in the configured signing parties)
var selectedDelegateShare *entities.AccountShare
for _, partyID := range partyIDs {
if share := activeSharesMap[partyID]; share != nil && share.ShareType == value_objects.ShareTypeDelegate {
selectedDelegateShare = share
break
}
}
// If selected parties include delegate share, user_share is required
if selectedDelegateShare != nil && req.UserShare == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "user_share is required for accounts with delegate party",
"delegate_party_id": selectedDelegateShare.PartyID,
})
return
}
// Use the selected delegate for signing (not necessarily the account's delegate)
delegateShare = selectedDelegateShare
// Validate we have enough parties
if len(partyIDs) < accountOutput.Account.ThresholdT {
c.JSON(http.StatusBadRequest, gin.H{
"error": "insufficient active shares for signing",
"error": "insufficient parties for signing",
"required": accountOutput.Account.ThresholdT,
"active": len(partyIDs),
"selected": len(partyIDs),
})
return
}
@ -677,10 +743,30 @@ func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Prepare delegate user share if needed
var delegateUserShare *grpcclient.DelegateUserShareInput
if delegateShare != nil {
userShareBytes, err := hex.DecodeString(req.UserShare)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_share format (expected hex)"})
return
}
delegateUserShare = &grpcclient.DelegateUserShareInput{
DelegatePartyID: delegateShare.PartyID,
EncryptedShare: userShareBytes,
PartyIndex: int32(delegateShare.PartyIndex),
}
logger.Info("Calling CreateSigningSession with delegate user share",
zap.String("account_id", req.AccountID),
zap.String("delegate_party_id", delegateShare.PartyID),
zap.Int("threshold_t", accountOutput.Account.ThresholdT),
zap.Int("available_parties", len(partyIDs)))
} else {
logger.Info("Calling CreateSigningSession via gRPC (auto party selection)",
zap.String("account_id", req.AccountID),
zap.Int("threshold_t", accountOutput.Account.ThresholdT),
zap.Int("available_parties", len(partyIDs)))
}
resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto(
ctx,
@ -688,6 +774,7 @@ func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) {
partyIDs,
messageHash,
600, // 10 minutes expiry
delegateUserShare,
)
if err != nil {
@ -700,7 +787,7 @@ func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) {
zap.String("session_id", resp.SessionID),
zap.Int("num_parties", len(resp.SelectedParties)))
c.JSON(http.StatusCreated, gin.H{
response := gin.H{
"session_id": resp.SessionID,
"session_type": "sign",
"account_id": req.AccountID,
@ -708,7 +795,17 @@ func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) {
"threshold_t": accountOutput.Account.ThresholdT,
"selected_parties": resp.SelectedParties,
"status": "created",
})
}
// Include has_delegate for sign sessions too
if delegateShare != nil {
response["has_delegate"] = true
response["delegate_party_id"] = delegateShare.PartyID
} else {
response["has_delegate"] = false
}
c.JSON(http.StatusCreated, response)
}
// GetSessionStatus handles getting session status
@ -734,17 +831,38 @@ func (h *AccountHTTPHandler) GetSessionStatus(c *gin.Context) {
response := gin.H{
"session_id": sessionID,
"status": resp.Status,
"session_type": resp.SessionType, // "keygen" or "sign"
"completed_parties": resp.CompletedParties,
"total_parties": resp.TotalParties,
}
// Keygen-specific fields
if resp.SessionType == "keygen" {
// has_delegate only meaningful for keygen sessions
response["has_delegate"] = resp.HasDelegate
if len(resp.PublicKey) > 0 {
response["public_key"] = hex.EncodeToString(resp.PublicKey)
}
// Include delegate share if keygen session has delegate party
// has_delegate=true + delegate_share=null means share was already retrieved (one-time)
// has_delegate=false means no delegate party (pure persistent keygen)
if resp.HasDelegate && resp.DelegateShare != nil {
response["delegate_share"] = gin.H{
"encrypted_share": hex.EncodeToString(resp.DelegateShare.EncryptedShare),
"party_index": resp.DelegateShare.PartyIndex,
"party_id": resp.DelegateShare.PartyID,
}
}
}
// Sign-specific fields
if resp.SessionType == "sign" {
if len(resp.Signature) > 0 {
response["signature"] = hex.EncodeToString(resp.Signature)
}
}
c.JSON(http.StatusOK, response)
}
@ -857,3 +975,256 @@ func (h *AccountHTTPHandler) CreateAccountFromKeygen(c *gin.Context) {
"public_key": hex.EncodeToString(output.Account.PublicKey),
})
}
// ============================================
// Signing Parties Configuration Endpoints
// ============================================
// SigningPartiesRequest represents the request for setting/updating signing parties
type SigningPartiesRequest struct {
PartyIDs []string `json:"party_ids" binding:"required,min=1"`
}
// SetSigningParties handles setting signing parties for the first time
// POST /accounts/:id/signing-config
func (h *AccountHTTPHandler) SetSigningParties(c *gin.Context) {
idStr := c.Param("id")
accountID, err := value_objects.AccountIDFromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
return
}
var req SigningPartiesRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get account
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
AccountID: &accountID,
})
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
return
}
// Check if signing parties already configured
if accountOutput.Account.HasSigningPartiesConfig() {
c.JSON(http.StatusConflict, gin.H{
"error": "signing parties already configured, use PUT to update",
"current_signing_parties": accountOutput.Account.GetSigningParties(),
})
return
}
// Validate that all party IDs are valid shares for this account
validPartyIDs := make(map[string]bool)
for _, share := range accountOutput.Shares {
if share.IsActive {
validPartyIDs[share.PartyID] = true
}
}
for _, partyID := range req.PartyIDs {
if !validPartyIDs[partyID] {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid party ID - not an active share for this account",
"party_id": partyID,
})
return
}
}
// Set signing parties
if err := accountOutput.Account.SetSigningParties(req.PartyIDs); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update account in database
_, err = h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{
AccountID: accountID,
SigningParties: req.PartyIDs,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
logger.Info("Signing parties configured",
zap.String("account_id", idStr),
zap.Strings("signing_parties", req.PartyIDs))
c.JSON(http.StatusCreated, gin.H{
"message": "signing parties configured successfully",
"signing_parties": req.PartyIDs,
"threshold_t": accountOutput.Account.ThresholdT,
})
}
// UpdateSigningParties handles updating existing signing parties configuration
// PUT /accounts/:id/signing-config
func (h *AccountHTTPHandler) UpdateSigningParties(c *gin.Context) {
idStr := c.Param("id")
accountID, err := value_objects.AccountIDFromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
return
}
var req SigningPartiesRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get account
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
AccountID: &accountID,
})
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
return
}
// Check if signing parties are configured (must exist to update)
if !accountOutput.Account.HasSigningPartiesConfig() {
c.JSON(http.StatusNotFound, gin.H{
"error": "signing parties not configured, use POST to set",
})
return
}
// Validate that all party IDs are valid shares for this account
validPartyIDs := make(map[string]bool)
for _, share := range accountOutput.Shares {
if share.IsActive {
validPartyIDs[share.PartyID] = true
}
}
for _, partyID := range req.PartyIDs {
if !validPartyIDs[partyID] {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid party ID - not an active share for this account",
"party_id": partyID,
})
return
}
}
// Set signing parties (validates count matches threshold)
if err := accountOutput.Account.SetSigningParties(req.PartyIDs); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update account in database
_, err = h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{
AccountID: accountID,
SigningParties: req.PartyIDs,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
logger.Info("Signing parties updated",
zap.String("account_id", idStr),
zap.Strings("signing_parties", req.PartyIDs))
c.JSON(http.StatusOK, gin.H{
"message": "signing parties updated successfully",
"signing_parties": req.PartyIDs,
"threshold_t": accountOutput.Account.ThresholdT,
})
}
// ClearSigningParties handles clearing signing parties configuration
// DELETE /accounts/:id/signing-config
func (h *AccountHTTPHandler) ClearSigningParties(c *gin.Context) {
idStr := c.Param("id")
accountID, err := value_objects.AccountIDFromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
return
}
// Get account
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
AccountID: &accountID,
})
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
return
}
// Check if signing parties are configured
if !accountOutput.Account.HasSigningPartiesConfig() {
c.JSON(http.StatusNotFound, gin.H{
"error": "signing parties not configured",
})
return
}
// Clear signing parties - pass empty slice
_, err = h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{
AccountID: accountID,
ClearSigningParties: true,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
logger.Info("Signing parties cleared",
zap.String("account_id", idStr))
c.JSON(http.StatusOK, gin.H{
"message": "signing parties cleared - all active parties will be used for signing",
})
}
// GetSigningParties handles getting current signing parties configuration
// GET /accounts/:id/signing-config
func (h *AccountHTTPHandler) GetSigningParties(c *gin.Context) {
idStr := c.Param("id")
accountID, err := value_objects.AccountIDFromString(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
return
}
// Get account
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
AccountID: &accountID,
})
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "account not found"})
return
}
if accountOutput.Account.HasSigningPartiesConfig() {
c.JSON(http.StatusOK, gin.H{
"configured": true,
"signing_parties": accountOutput.Account.GetSigningParties(),
"threshold_t": accountOutput.Account.ThresholdT,
})
} else {
// Get all active parties
var activeParties []string
for _, share := range accountOutput.Shares {
if share.IsActive {
activeParties = append(activeParties, share.PartyID)
}
}
c.JSON(http.StatusOK, gin.H{
"configured": false,
"message": "no signing parties configured - all active parties will be used",
"active_parties": activeParties,
"threshold_t": accountOutput.Account.ThresholdT,
})
}
}

View File

@ -121,14 +121,23 @@ func (c *SessionCoordinatorClient) CreateKeygenSessionAuto(
}, nil
}
// DelegateUserShareInput contains user's share for delegate party in signing
type DelegateUserShareInput struct {
DelegatePartyID string
EncryptedShare []byte
PartyIndex int32
}
// CreateSigningSessionAuto creates a new signing session with automatic party selection
// Coordinator will select parties from the provided party IDs (from account shares)
// delegateUserShare is required if any of the parties is a delegate party
func (c *SessionCoordinatorClient) CreateSigningSessionAuto(
ctx context.Context,
thresholdT int32,
partyIDs []string,
messageHash []byte,
expiresInSeconds int64,
delegateUserShare *DelegateUserShareInput,
) (*CreateSessionAutoResponse, error) {
// Convert party IDs to participant info (minimal info, coordinator will fill in details)
pbParticipants := make([]*coordinatorpb.ParticipantInfo, len(partyIDs))
@ -147,9 +156,22 @@ func (c *SessionCoordinatorClient) CreateSigningSessionAuto(
ExpiresInSeconds: expiresInSeconds,
}
// Add delegate user share if provided
if delegateUserShare != nil {
req.DelegateUserShare = &coordinatorpb.DelegateUserShare{
DelegatePartyId: delegateUserShare.DelegatePartyID,
EncryptedShare: delegateUserShare.EncryptedShare,
PartyIndex: delegateUserShare.PartyIndex,
}
logger.Info("Sending CreateSigningSession gRPC request with delegate user share",
zap.Int32("threshold_t", thresholdT),
zap.Int("num_parties", len(partyIDs)),
zap.String("delegate_party_id", delegateUserShare.DelegatePartyID))
} else {
logger.Info("Sending CreateSigningSession gRPC request",
zap.Int32("threshold_t", thresholdT),
zap.Int("num_parties", len(partyIDs)))
}
resp, err := c.client.CreateSession(ctx, req)
if err != nil {
@ -170,6 +192,7 @@ func (c *SessionCoordinatorClient) CreateSigningSessionAuto(
return &CreateSessionAutoResponse{
SessionID: resp.SessionId,
SelectedParties: selectedParties,
DelegateParty: resp.DelegatePartyId,
JoinTokens: resp.JoinTokens,
ExpiresAt: resp.ExpiresAt,
}, nil
@ -189,13 +212,26 @@ func (c *SessionCoordinatorClient) GetSessionStatus(
return nil, fmt.Errorf("failed to get session status: %w", err)
}
return &SessionStatusResponse{
result := &SessionStatusResponse{
Status: resp.Status,
CompletedParties: resp.CompletedParties,
TotalParties: resp.TotalParties,
SessionType: resp.SessionType,
PublicKey: resp.PublicKey,
Signature: resp.Signature,
}, nil
HasDelegate: resp.HasDelegate,
}
// Include delegate share if present
if resp.DelegateShare != nil {
result.DelegateShare = &DelegateShareInfo{
EncryptedShare: resp.DelegateShare.EncryptedShare,
PartyIndex: resp.DelegateShare.PartyIndex,
PartyID: resp.DelegateShare.PartyId,
}
}
return result, nil
}
// Close closes the gRPC connection
@ -236,6 +272,20 @@ type SessionStatusResponse struct {
Status string
CompletedParties int32
TotalParties int32
SessionType string // "keygen" or "sign"
PublicKey []byte
Signature []byte
// HasDelegate indicates whether this keygen session has a delegate party
// Only meaningful for keygen sessions. Always false for sign sessions.
HasDelegate bool
// DelegateShare is non-nil when session_type="keygen" AND has_delegate=true AND session is completed
// nil with has_delegate=true means share was already retrieved (one-time retrieval)
DelegateShare *DelegateShareInfo
}
// DelegateShareInfo contains the delegate party's share for user
type DelegateShareInfo struct {
EncryptedShare []byte
PartyIndex int32
PartyID string
}

View File

@ -6,6 +6,7 @@ import (
"errors"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/rwadurian/mpc-system/services/account/domain/entities"
"github.com/rwadurian/mpc-system/services/account/domain/repositories"
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
@ -25,8 +26,8 @@ func NewAccountPostgresRepo(db *sql.DB) repositories.AccountRepository {
func (r *AccountPostgresRepo) Create(ctx context.Context, account *entities.Account) error {
query := `
INSERT INTO accounts (id, username, email, phone, public_key, keygen_session_id,
threshold_n, threshold_t, status, created_at, updated_at, last_login_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
`
_, err := r.db.ExecContext(ctx, query,
@ -42,6 +43,7 @@ func (r *AccountPostgresRepo) Create(ctx context.Context, account *entities.Acco
account.CreatedAt,
account.UpdatedAt,
account.LastLoginAt,
pq.Array(account.SigningParties),
)
return err
@ -51,7 +53,7 @@ func (r *AccountPostgresRepo) Create(ctx context.Context, account *entities.Acco
func (r *AccountPostgresRepo) GetByID(ctx context.Context, id value_objects.AccountID) (*entities.Account, error) {
query := `
SELECT id, username, email, phone, public_key, keygen_session_id,
threshold_n, threshold_t, status, created_at, updated_at, last_login_at
threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties
FROM accounts
WHERE id = $1
`
@ -63,7 +65,7 @@ func (r *AccountPostgresRepo) GetByID(ctx context.Context, id value_objects.Acco
func (r *AccountPostgresRepo) GetByUsername(ctx context.Context, username string) (*entities.Account, error) {
query := `
SELECT id, username, email, phone, public_key, keygen_session_id,
threshold_n, threshold_t, status, created_at, updated_at, last_login_at
threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties
FROM accounts
WHERE username = $1
`
@ -75,7 +77,7 @@ func (r *AccountPostgresRepo) GetByUsername(ctx context.Context, username string
func (r *AccountPostgresRepo) GetByEmail(ctx context.Context, email string) (*entities.Account, error) {
query := `
SELECT id, username, email, phone, public_key, keygen_session_id,
threshold_n, threshold_t, status, created_at, updated_at, last_login_at
threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties
FROM accounts
WHERE email = $1
`
@ -87,7 +89,7 @@ func (r *AccountPostgresRepo) GetByEmail(ctx context.Context, email string) (*en
func (r *AccountPostgresRepo) GetByPublicKey(ctx context.Context, publicKey []byte) (*entities.Account, error) {
query := `
SELECT id, username, email, phone, public_key, keygen_session_id,
threshold_n, threshold_t, status, created_at, updated_at, last_login_at
threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties
FROM accounts
WHERE public_key = $1
`
@ -100,7 +102,8 @@ func (r *AccountPostgresRepo) Update(ctx context.Context, account *entities.Acco
query := `
UPDATE accounts
SET username = $2, email = $3, phone = $4, public_key = $5, keygen_session_id = $6,
threshold_n = $7, threshold_t = $8, status = $9, updated_at = $10, last_login_at = $11
threshold_n = $7, threshold_t = $8, status = $9, updated_at = $10, last_login_at = $11,
signing_parties = $12
WHERE id = $1
`
@ -116,6 +119,7 @@ func (r *AccountPostgresRepo) Update(ctx context.Context, account *entities.Acco
account.Status.String(),
account.UpdatedAt,
account.LastLoginAt,
pq.Array(account.SigningParties),
)
if err != nil {
@ -177,7 +181,7 @@ func (r *AccountPostgresRepo) ExistsByEmail(ctx context.Context, email string) (
func (r *AccountPostgresRepo) List(ctx context.Context, offset, limit int) ([]*entities.Account, error) {
query := `
SELECT id, username, email, phone, public_key, keygen_session_id,
threshold_n, threshold_t, status, created_at, updated_at, last_login_at
threshold_n, threshold_t, status, created_at, updated_at, last_login_at, signing_parties
FROM accounts
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
@ -222,6 +226,7 @@ func (r *AccountPostgresRepo) scanAccount(row *sql.Row) (*entities.Account, erro
thresholdN int
thresholdT int
status string
signingParties pq.StringArray
account entities.Account
)
@ -238,6 +243,7 @@ func (r *AccountPostgresRepo) scanAccount(row *sql.Row) (*entities.Account, erro
&account.CreatedAt,
&account.UpdatedAt,
&account.LastLoginAt,
&signingParties,
)
if err != nil {
@ -260,6 +266,7 @@ func (r *AccountPostgresRepo) scanAccount(row *sql.Row) (*entities.Account, erro
account.ThresholdN = thresholdN
account.ThresholdT = thresholdT
account.Status = value_objects.AccountStatus(status)
account.SigningParties = signingParties
return &account, nil
}
@ -276,6 +283,7 @@ func (r *AccountPostgresRepo) scanAccountFromRows(rows *sql.Rows) (*entities.Acc
thresholdN int
thresholdT int
status string
signingParties pq.StringArray
account entities.Account
)
@ -292,6 +300,7 @@ func (r *AccountPostgresRepo) scanAccountFromRows(rows *sql.Rows) (*entities.Acc
&account.CreatedAt,
&account.UpdatedAt,
&account.LastLoginAt,
&signingParties,
)
if err != nil {
@ -311,6 +320,7 @@ func (r *AccountPostgresRepo) scanAccountFromRows(rows *sql.Rows) (*entities.Acc
account.ThresholdN = thresholdN
account.ThresholdT = thresholdT
account.Status = value_objects.AccountStatus(status)
account.SigningParties = signingParties
return &account, nil
}

View File

@ -116,6 +116,8 @@ type CompleteRecoveryPort interface {
type UpdateAccountInput struct {
AccountID value_objects.AccountID
Phone *string
SigningParties []string // Set/update signing parties (nil means no change)
ClearSigningParties bool // If true, clear signing parties configuration
}
// UpdateAccountOutput represents output from updating an account

View File

@ -165,6 +165,15 @@ func (uc *UpdateAccountUseCase) Execute(ctx context.Context, input ports.UpdateA
account.SetPhone(*input.Phone)
}
// Handle signing parties update
if input.ClearSigningParties {
account.ClearSigningParties()
} else if len(input.SigningParties) > 0 {
if err := account.SetSigningParties(input.SigningParties); err != nil {
return nil, err
}
}
if err := uc.accountRepo.Update(ctx, account); err != nil {
return nil, err
}

View File

@ -21,6 +21,10 @@ type Account struct {
CreatedAt time.Time
UpdatedAt time.Time
LastLoginAt *time.Time
// SigningParties is a list of party IDs designated for signing operations.
// If nil or empty, all active parties will be used for signing.
// Must contain exactly ThresholdT parties when set.
SigningParties []string
}
// NewAccount creates a new Account
@ -135,6 +139,52 @@ func (a *Account) Validate() error {
return nil
}
// SetSigningParties sets the designated signing parties for this account
// partyIDs must contain exactly ThresholdT parties
func (a *Account) SetSigningParties(partyIDs []string) error {
if len(partyIDs) != a.ThresholdT {
return ErrInvalidSigningPartiesCount
}
// Check for duplicates
seen := make(map[string]bool)
for _, id := range partyIDs {
if id == "" {
return ErrInvalidPartyID
}
if seen[id] {
return ErrDuplicatePartyID
}
seen[id] = true
}
a.SigningParties = partyIDs
a.UpdatedAt = time.Now().UTC()
return nil
}
// ClearSigningParties removes the signing parties configuration
// After clearing, all active parties will be used for signing
func (a *Account) ClearSigningParties() {
a.SigningParties = nil
a.UpdatedAt = time.Now().UTC()
}
// HasSigningPartiesConfig returns true if signing parties are configured
func (a *Account) HasSigningPartiesConfig() bool {
return len(a.SigningParties) > 0
}
// GetSigningParties returns the configured signing parties
// Returns nil if not configured (meaning all active parties should be used)
func (a *Account) GetSigningParties() []string {
if len(a.SigningParties) == 0 {
return nil
}
// Return a copy to prevent modification
result := make([]string, len(a.SigningParties))
copy(result, a.SigningParties)
return result
}
// Account errors
var (
ErrInvalidUsername = &AccountError{Code: "INVALID_USERNAME", Message: "username is required"}
@ -147,6 +197,9 @@ var (
ErrAccountNotFound = &AccountError{Code: "ACCOUNT_NOT_FOUND", Message: "account not found"}
ErrDuplicateUsername = &AccountError{Code: "DUPLICATE_USERNAME", Message: "username already exists"}
ErrDuplicateEmail = &AccountError{Code: "DUPLICATE_EMAIL", Message: "email already exists"}
ErrInvalidSigningPartiesCount = &AccountError{Code: "INVALID_SIGNING_PARTIES_COUNT", Message: "signing parties count must equal threshold T"}
ErrInvalidPartyID = &AccountError{Code: "INVALID_PARTY_ID", Message: "party ID cannot be empty"}
ErrDuplicatePartyID = &AccountError{Code: "DUPLICATE_PARTY_ID", Message: "duplicate party ID in signing parties"}
)
// AccountError represents an account domain error

View File

@ -77,6 +77,16 @@ func (s *AccountShare) IsRecoveryShare() bool {
return s.ShareType == value_objects.ShareTypeRecovery
}
// IsDelegateShare checks if this is a delegate share
func (s *AccountShare) IsDelegateShare() bool {
return s.ShareType == value_objects.ShareTypeDelegate
}
// RequiresUserShare checks if this share type requires user to provide share for signing
func (s *AccountShare) RequiresUserShare() bool {
return s.ShareType.RequiresUserShare()
}
// Validate validates the account share
func (s *AccountShare) Validate() error {
if s.AccountID.IsZero() {

View File

@ -39,9 +39,10 @@ func (s AccountStatus) CanInitiateRecovery() bool {
type ShareType string
const (
ShareTypeUserDevice ShareType = "user_device"
ShareTypeServer ShareType = "server"
ShareTypeRecovery ShareType = "recovery"
ShareTypeUserDevice ShareType = "user_device" // Share stored on user's device
ShareTypeServer ShareType = "server" // Share stored by persistent server party (in DB)
ShareTypeRecovery ShareType = "recovery" // Share stored for recovery
ShareTypeDelegate ShareType = "delegate" // Share returned to user via delegate party (not stored in party DB)
)
// String returns the string representation
@ -52,13 +53,18 @@ func (st ShareType) String() string {
// IsValid checks if the share type is valid
func (st ShareType) IsValid() bool {
switch st {
case ShareTypeUserDevice, ShareTypeServer, ShareTypeRecovery:
case ShareTypeUserDevice, ShareTypeServer, ShareTypeRecovery, ShareTypeDelegate:
return true
default:
return false
}
}
// RequiresUserShare checks if this share type requires user to provide share for signing
func (st ShareType) RequiresUserShare() bool {
return st == ShareTypeDelegate || st == ShareTypeUserDevice
}
// RecoveryType represents the type of account recovery
type RecoveryType string

View File

@ -4,6 +4,7 @@ import (
"context"
"time"
coordinator "github.com/rwadurian/mpc-system/api/grpc/coordinator/v1"
pb "github.com/rwadurian/mpc-system/api/grpc/router/v1"
"github.com/rwadurian/mpc-system/pkg/logger"
"github.com/rwadurian/mpc-system/services/message-router/application/use_cases"
@ -11,6 +12,7 @@ import (
"github.com/rwadurian/mpc-system/services/message-router/domain/entities"
"github.com/rwadurian/mpc-system/services/message-router/domain/repositories"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@ -30,6 +32,7 @@ type MessageRouterServer struct {
partyRegistry *domain.PartyRegistry
eventBroadcaster *domain.SessionEventBroadcaster
messageRepo repositories.MessageRepository
coordinatorConn *grpc.ClientConn // Connection to Session Coordinator for proxying
}
// NewMessageRouterServer creates a new gRPC server
@ -51,6 +54,13 @@ func NewMessageRouterServer(
}
}
// SetCoordinatorConnection sets the Session Coordinator gRPC connection for proxying
// This allows server-parties to only connect to Message Router
func (s *MessageRouterServer) SetCoordinatorConnection(conn *grpc.ClientConn) {
s.coordinatorConn = conn
logger.Info("Session Coordinator connection configured for proxying")
}
// RouteMessage routes an MPC message
func (s *MessageRouterServer) RouteMessage(
ctx context.Context,
@ -452,3 +462,218 @@ func toGRPCError(err error) error {
return status.Error(codes.Internal, err.Error())
}
}
// ============================================
// Session Operations (Proxied to Coordinator)
// These methods allow server-parties to only connect to Message Router
// ============================================
// JoinSession proxies the join session request to Session Coordinator
func (s *MessageRouterServer) JoinSession(
ctx context.Context,
req *pb.JoinSessionRequest,
) (*pb.JoinSessionResponse, error) {
if s.coordinatorConn == nil {
return nil, status.Error(codes.Unavailable, "session coordinator not configured")
}
// Convert to coordinator request
coordReq := &coordinator.JoinSessionRequest{
SessionId: req.SessionId,
PartyId: req.PartyId,
JoinToken: req.JoinToken,
}
if req.DeviceInfo != nil {
coordReq.DeviceInfo = &coordinator.DeviceInfo{
DeviceType: req.DeviceInfo.DeviceType,
DeviceId: req.DeviceInfo.DeviceId,
Platform: req.DeviceInfo.Platform,
AppVersion: req.DeviceInfo.AppVersion,
}
}
// Call Session Coordinator
coordResp := &coordinator.JoinSessionResponse{}
err := s.coordinatorConn.Invoke(ctx, "/mpc.coordinator.v1.SessionCoordinator/JoinSession", coordReq, coordResp)
if err != nil {
logger.Error("Failed to proxy JoinSession to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Error(err))
return nil, err
}
// Convert response
resp := &pb.JoinSessionResponse{
Success: coordResp.Success,
}
if coordResp.SessionInfo != nil {
resp.SessionInfo = &pb.SessionInfo{
SessionId: req.SessionId,
SessionType: coordResp.SessionInfo.SessionType,
ThresholdN: coordResp.SessionInfo.ThresholdN,
ThresholdT: coordResp.SessionInfo.ThresholdT,
MessageHash: coordResp.SessionInfo.MessageHash,
}
}
if len(coordResp.OtherParties) > 0 {
resp.OtherParties = make([]*pb.PartyInfo, len(coordResp.OtherParties))
for i, p := range coordResp.OtherParties {
resp.OtherParties[i] = &pb.PartyInfo{
PartyId: p.PartyId,
PartyIndex: p.PartyIndex,
}
// Find this party's index
if p.PartyId == req.PartyId {
resp.PartyIndex = p.PartyIndex
}
}
}
logger.Info("Proxied JoinSession to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Bool("success", resp.Success))
return resp, nil
}
// MarkPartyReady proxies the mark ready request to Session Coordinator
func (s *MessageRouterServer) MarkPartyReady(
ctx context.Context,
req *pb.MarkPartyReadyRequest,
) (*pb.MarkPartyReadyResponse, error) {
if s.coordinatorConn == nil {
return nil, status.Error(codes.Unavailable, "session coordinator not configured")
}
coordReq := &coordinator.MarkPartyReadyRequest{
SessionId: req.SessionId,
PartyId: req.PartyId,
}
coordResp := &coordinator.MarkPartyReadyResponse{}
err := s.coordinatorConn.Invoke(ctx, "/mpc.coordinator.v1.SessionCoordinator/MarkPartyReady", coordReq, coordResp)
if err != nil {
logger.Error("Failed to proxy MarkPartyReady to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Error(err))
return nil, err
}
logger.Debug("Proxied MarkPartyReady to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Bool("all_ready", coordResp.AllReady))
return &pb.MarkPartyReadyResponse{
Success: true,
AllReady: coordResp.AllReady,
}, nil
}
// ReportCompletion proxies the completion report to Session Coordinator
func (s *MessageRouterServer) ReportCompletion(
ctx context.Context,
req *pb.ReportCompletionRequest,
) (*pb.ReportCompletionResponse, error) {
if s.coordinatorConn == nil {
return nil, status.Error(codes.Unavailable, "session coordinator not configured")
}
coordReq := &coordinator.ReportCompletionRequest{
SessionId: req.SessionId,
PartyId: req.PartyId,
PublicKey: req.PublicKey,
}
coordResp := &coordinator.ReportCompletionResponse{}
err := s.coordinatorConn.Invoke(ctx, "/mpc.coordinator.v1.SessionCoordinator/ReportCompletion", coordReq, coordResp)
if err != nil {
logger.Error("Failed to proxy ReportCompletion to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Error(err))
return nil, err
}
logger.Info("Proxied ReportCompletion to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Bool("all_completed", coordResp.AllCompleted))
return &pb.ReportCompletionResponse{
Success: true,
AllCompleted: coordResp.AllCompleted,
}, nil
}
// GetSessionStatus proxies the status request to Session Coordinator
func (s *MessageRouterServer) GetSessionStatus(
ctx context.Context,
req *pb.GetSessionStatusRequest,
) (*pb.GetSessionStatusResponse, error) {
if s.coordinatorConn == nil {
return nil, status.Error(codes.Unavailable, "session coordinator not configured")
}
coordReq := &coordinator.GetSessionStatusRequest{
SessionId: req.SessionId,
}
coordResp := &coordinator.GetSessionStatusResponse{}
err := s.coordinatorConn.Invoke(ctx, "/mpc.coordinator.v1.SessionCoordinator/GetSessionStatus", coordReq, coordResp)
if err != nil {
logger.Error("Failed to proxy GetSessionStatus to coordinator",
zap.String("session_id", req.SessionId),
zap.Error(err))
return nil, err
}
return &pb.GetSessionStatusResponse{
SessionId: req.SessionId,
Status: coordResp.Status,
ThresholdN: coordResp.TotalParties, // Use TotalParties as N
ThresholdT: coordResp.CompletedParties, // Return completed count in ThresholdT for info
}, nil
}
// SubmitDelegateShare proxies the delegate share submission to Session Coordinator
func (s *MessageRouterServer) SubmitDelegateShare(
ctx context.Context,
req *pb.SubmitDelegateShareRequest,
) (*pb.SubmitDelegateShareResponse, error) {
if s.coordinatorConn == nil {
return nil, status.Error(codes.Unavailable, "session coordinator not configured")
}
coordReq := &coordinator.SubmitDelegateShareRequest{
SessionId: req.SessionId,
PartyId: req.PartyId,
EncryptedShare: req.EncryptedShare,
PublicKey: req.PublicKey,
PartyIndex: req.PartyIndex,
}
coordResp := &coordinator.SubmitDelegateShareResponse{}
err := s.coordinatorConn.Invoke(ctx, "/mpc.coordinator.v1.SessionCoordinator/SubmitDelegateShare", coordReq, coordResp)
if err != nil {
logger.Error("Failed to proxy SubmitDelegateShare to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Error(err))
return nil, err
}
logger.Info("Proxied SubmitDelegateShare to coordinator",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Bool("success", coordResp.Success))
return &pb.SubmitDelegateShareResponse{
Success: coordResp.Success,
}, nil
}

View File

@ -15,6 +15,7 @@ import (
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
pb "github.com/rwadurian/mpc-system/api/grpc/router/v1"
@ -77,6 +78,26 @@ func main() {
routeMessageUC := use_cases.NewRouteMessageUseCase(messageRepo, messageBroker)
getPendingMessagesUC := use_cases.NewGetPendingMessagesUseCase(messageRepo)
// Connect to Session Coordinator for proxying session operations
// This allows server-parties to only connect to Message Router
coordinatorAddr := os.Getenv("SESSION_COORDINATOR_ADDR")
if coordinatorAddr == "" {
coordinatorAddr = "session-coordinator:50051" // Default in docker-compose
}
var coordinatorConn *grpc.ClientConn
coordinatorConn, err = grpc.NewClient(coordinatorAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
logger.Warn("Failed to connect to Session Coordinator for proxying (session operations will be unavailable)",
zap.String("address", coordinatorAddr),
zap.Error(err))
} else {
defer coordinatorConn.Close()
logger.Info("Connected to Session Coordinator for proxying session operations",
zap.String("address", coordinatorAddr))
}
// Start message cleanup background job
go runMessageCleanup(messageRepo)
@ -92,7 +113,7 @@ func main() {
// Start gRPC server
go func() {
if err := startGRPCServer(cfg, routeMessageUC, getPendingMessagesUC, messageBroker, partyRegistry, eventBroadcaster, messageRepo); err != nil {
if err := startGRPCServer(cfg, routeMessageUC, getPendingMessagesUC, messageBroker, partyRegistry, eventBroadcaster, messageRepo, coordinatorConn); err != nil {
errChan <- fmt.Errorf("gRPC server error: %w", err)
}
}()
@ -189,6 +210,7 @@ func startGRPCServer(
partyRegistry *domain.PartyRegistry,
eventBroadcaster *domain.SessionEventBroadcaster,
messageRepo *postgres.MessagePostgresRepo,
coordinatorConn *grpc.ClientConn,
) error {
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Server.GRPCPort))
if err != nil {
@ -206,6 +228,13 @@ func startGRPCServer(
eventBroadcaster,
messageRepo,
)
// Set coordinator connection for proxying session operations
// This allows server-parties to only connect to Message Router
if coordinatorConn != nil {
messageRouterServer.SetCoordinatorConnection(coordinatorConn)
}
pb.RegisterMessageRouterServer(grpcServer, messageRouterServer)
// Enable reflection for debugging

View File

@ -14,18 +14,15 @@ import (
"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"
"github.com/rwadurian/mpc-system/services/server-party/infrastructure/cache"
"go.uber.org/zap"
)
// Global share cache for delegate parties
var globalShareCache *cache.ShareCache
func main() {
// Parse flags
configPath := flag.String("config", "", "Path to config file")
@ -48,14 +45,10 @@ func main() {
}
defer logger.Sync()
logger.Info("Starting Server Party API Service (Delegate Mode)",
logger.Info("Starting Delegate Party Service",
zap.String("environment", cfg.Server.Environment),
zap.Int("http_port", cfg.Server.HTTPPort))
// Initialize share cache for delegate parties (15 minute TTL)
globalShareCache = cache.NewShareCache(15 * time.Minute)
logger.Info("Share cache initialized", zap.Duration("ttl", 15*time.Minute))
// Initialize crypto service with master key from environment
masterKeyHex := os.Getenv("MPC_CRYPTO_MASTER_KEY")
if masterKeyHex == "" {
@ -70,29 +63,14 @@ func main() {
logger.Fatal("Failed to create crypto service", zap.Error(err))
}
// Get API key for authentication
apiKey := os.Getenv("MPC_API_KEY")
if apiKey == "" {
logger.Warn("MPC_API_KEY not set, API will be unprotected")
}
// Get gRPC service addresses from environment
coordinatorAddr := os.Getenv("SESSION_COORDINATOR_ADDR")
if coordinatorAddr == "" {
coordinatorAddr = "session-coordinator:50051"
}
// 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 gRPC clients
sessionClient, err := grpcclient.NewSessionCoordinatorClient(coordinatorAddr)
if err != nil {
logger.Fatal("Failed to connect to session coordinator", zap.Error(err))
}
defer sessionClient.Close()
// 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))
@ -106,44 +84,74 @@ func main() {
// Get party ID from environment (or use default)
partyID := os.Getenv("PARTY_ID")
if partyID == "" {
partyID = "server-party-api"
partyID = "delegate-party"
}
// Force PARTY_ROLE to delegate for this service
os.Setenv("PARTY_ROLE", "delegate")
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", "delegate"))
zap.String("role", partyRole))
if err := messageRouter.RegisterParty(ctx, partyID, "delegate", "1.0.0"); err != nil {
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", "delegate"))
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)
// The use cases check PARTY_ROLE env var to determine behavior
// MessageRouter handles both messaging AND session operations (proxied to coordinator)
participateKeygenUC := use_cases.NewParticipateKeygenUseCase(
nil, // No database storage for delegate
sessionClient,
messageRouter,
messageRouter,
cryptoService,
)
participateSigningUC := use_cases.NewParticipateSigningUseCase(
nil, // No database storage for delegate
sessionClient,
messageRouter,
messageRouter,
cryptoService,
)
// Start HTTP server
// 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, participateKeygenUC, participateSigningUC, cryptoService, apiKey); err != nil {
if err := startHTTPServer(cfg); err != nil {
errChan <- fmt.Errorf("HTTP server error: %w", err)
}
}()
@ -165,258 +173,162 @@ func main() {
time.Sleep(5 * time.Second)
logger.Info("Shutdown complete")
_ = ctx
}
func startHTTPServer(
cfg *config.Config,
participateKeygenUC *use_cases.ParticipateKeygenUseCase,
participateSigningUC *use_cases.ParticipateSigningUseCase,
cryptoService *crypto.CryptoService,
apiKey string,
) error {
// startHTTPServer starts HTTP server for health checks only
func startHTTPServer(cfg *config.Config) error {
if cfg.Server.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery())
router.Use(gin.Logger())
r := gin.New()
r.Use(gin.Recovery())
// Health check
router.GET("/health", func(c *gin.Context) {
// Health check only
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "server-party-api",
"service": "delegate-party",
"role": "delegate",
})
})
// API routes with optional authentication
api := router.Group("/api/v1")
if apiKey != "" {
api.Use(apiKeyAuth(apiKey))
logger.Info("Starting HTTP server (health check only)", zap.Int("port", cfg.Server.HTTPPort))
return r.Run(fmt.Sprintf(":%d", cfg.Server.HTTPPort))
}
{
// Keygen participation - same as server-party but returns share
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"`
// 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 err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
if !isSelected {
logger.Debug("Party not selected for this session",
zap.String("session_id", event.SessionId),
zap.String("party_id", partyID))
return
}
sessionID, err := uuid.Parse(req.SessionID)
// 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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session_id format"})
logger.Error("Invalid session ID", zap.Error(err))
return
}
logger.Info("Starting keygen participation (delegate)",
zap.String("session_id", req.SessionID),
zap.String("party_id", req.PartyID))
// Automatically participate based on session type
go func() {
ctx := context.Background()
// Execute keygen synchronously for delegate party
ctx, cancel := context.WithTimeout(c.Request.Context(), 10*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: req.PartyID,
JoinToken: req.JoinToken,
PartyID: partyID,
JoinToken: joinToken,
}
output, err := participateKeygenUC.Execute(ctx, input)
result, err := participateKeygenUC.Execute(ctx, input)
if err != nil {
logger.Error("Keygen participation failed",
zap.String("session_id", req.SessionID),
zap.String("party_id", req.PartyID),
zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "keygen failed",
"details": err.Error(),
"session_id": req.SessionID,
"party_id": req.PartyID,
})
zap.Error(err),
zap.String("session_id", event.SessionId))
return
}
logger.Info("Keygen participation completed (delegate)",
zap.String("session_id", req.SessionID),
zap.String("party_id", req.PartyID),
zap.Bool("success", output.Success))
zap.String("session_id", event.SessionId),
zap.String("public_key", hex.EncodeToString(result.PublicKey)))
// For delegate party, ShareForUser contains the encrypted share
if len(output.ShareForUser) == 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "share not generated for delegate party",
})
return
}
// 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)))
// Store in cache for retrieval (optional, for async pattern)
globalShareCache.Store(sessionID, req.PartyID, output.ShareForUser, output.PublicKey)
// Return share directly
c.JSON(http.StatusOK, gin.H{
"success": true,
"session_id": req.SessionID,
"party_id": req.PartyID,
"party_index": output.KeyShare.PartyIndex,
"share_data": hex.EncodeToString(output.ShareForUser),
"public_key": hex.EncodeToString(output.PublicKey),
})
})
// Signing with user-provided share
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"`
ShareData string `json:"share_data" binding:"required"` // User's encrypted share
MessageHash string `json:"message_hash"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
sessionID, err := uuid.Parse(req.SessionID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session_id format"})
return
}
shareData, err := hex.DecodeString(req.ShareData)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid share_data format (expected hex)"})
return
}
var messageHash []byte
if req.MessageHash != "" {
messageHash, err = hex.DecodeString(req.MessageHash)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message_hash format (expected hex)"})
return
if err := messageRouter.SubmitDelegateShare(
ctx,
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))
}
}
logger.Info("Starting signing participation (delegate)",
zap.String("session_id", req.SessionID),
zap.String("party_id", req.PartyID))
// Execute signing synchronously
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Minute)
defer cancel()
} 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: req.PartyID,
JoinToken: req.JoinToken,
MessageHash: messageHash,
UserShareData: shareData, // Pass user's share
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
}
output, err := participateSigningUC.Execute(ctx, input)
result, err := participateSigningUC.Execute(ctx, input)
if err != nil {
logger.Error("Signing participation failed",
zap.String("session_id", req.SessionID),
zap.String("party_id", req.PartyID),
zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "signing failed",
"details": err.Error(),
"session_id": req.SessionID,
"party_id": req.PartyID,
})
zap.Error(err),
zap.String("session_id", event.SessionId))
return
}
logger.Info("Signing participation completed (delegate)",
zap.String("session_id", req.SessionID),
zap.String("party_id", req.PartyID),
zap.Bool("success", output.Success))
// Return signature
var rHex, sHex string
if output.R != nil {
rHex = hex.EncodeToString(output.R.Bytes())
}
if output.S != nil {
sHex = hex.EncodeToString(output.S.Bytes())
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"session_id": req.SessionID,
"party_id": req.PartyID,
"signature": hex.EncodeToString(output.Signature),
"r": rHex,
"s": sHex,
})
})
// Get user share from cache (for async keygen pattern)
api.GET("/sessions/:session_id/user-share", func(c *gin.Context) {
sessionIDStr := c.Param("session_id")
sessionID, err := uuid.Parse(sessionIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid session_id format",
})
return
}
// Retrieve and delete share from cache (one-time retrieval)
entry, exists := globalShareCache.GetAndDelete(sessionID)
if !exists {
c.JSON(http.StatusNotFound, gin.H{
"error": "Share not found or already retrieved",
"note": "Shares can only be retrieved once and expire after 15 minutes",
})
return
}
logger.Info("User share retrieved successfully",
zap.String("session_id", sessionIDStr),
zap.String("party_id", entry.PartyID))
c.JSON(http.StatusOK, gin.H{
"session_id": sessionIDStr,
"party_id": entry.PartyID,
"share": hex.EncodeToString(entry.Share),
"public_key": hex.EncodeToString(entry.PublicKey),
"note": "This share has been deleted from memory and cannot be retrieved again",
})
})
}
logger.Info("Starting HTTP server", zap.Int("port", cfg.Server.HTTPPort))
return router.Run(fmt.Sprintf(":%d", cfg.Server.HTTPPort))
}
func apiKeyAuth(expectedKey string) gin.HandlerFunc {
return func(c *gin.Context) {
apiKey := c.GetHeader("X-API-Key")
if apiKey == "" {
apiKey = c.Query("api_key")
}
if apiKey != expectedKey {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or missing API key"})
c.Abort()
return
}
c.Next()
zap.String("session_id", event.SessionId),
zap.String("signature", hex.EncodeToString(result.Signature)))
}
}
}()
}
}

View File

@ -599,3 +599,194 @@ func (c *MessageRouterClient) StartHeartbeat(
return cancel
}
// ============================================
// Session Operations (Proxied through Message Router)
// These methods allow server-parties to only connect to Message Router
// ============================================
// JoinSession joins an MPC session via Message Router
// Includes automatic retry with exponential backoff for transient failures
func (c *MessageRouterClient) JoinSession(
ctx context.Context,
sessionID uuid.UUID,
partyID, joinToken string,
) (*use_cases.SessionInfo, error) {
req := &router.JoinSessionRequest{
SessionId: sessionID.String(),
PartyId: partyID,
JoinToken: joinToken,
DeviceInfo: &router.DeviceInfo{
DeviceType: "server",
DeviceId: partyID,
Platform: "linux",
AppVersion: "1.0.0",
},
}
return retry.Do(ctx, c.retryCfg, "JoinSession", func() (*use_cases.SessionInfo, error) {
resp := &router.JoinSessionResponse{}
err := c.getConn().Invoke(ctx, "/mpc.router.v1.MessageRouter/JoinSession", req, resp)
if err != nil {
return nil, err
}
if !resp.Success {
logger.Error("Join session failed", zap.String("session_id", sessionID.String()))
return nil, use_cases.ErrInvalidSession
}
// Convert response to SessionInfo
participants := make([]use_cases.ParticipantInfo, len(resp.OtherParties))
for i, p := range resp.OtherParties {
logger.Debug("Received party_index from Message Router",
zap.String("party_id", p.PartyId),
zap.Int32("party_index", p.PartyIndex))
participants[i] = use_cases.ParticipantInfo{
PartyID: p.PartyId,
PartyIndex: int(p.PartyIndex),
}
}
sessionInfo := &use_cases.SessionInfo{
SessionID: sessionID,
Participants: participants,
}
if resp.SessionInfo != nil {
sessionInfo.SessionType = resp.SessionInfo.SessionType
sessionInfo.ThresholdN = int(resp.SessionInfo.ThresholdN)
sessionInfo.ThresholdT = int(resp.SessionInfo.ThresholdT)
sessionInfo.MessageHash = resp.SessionInfo.MessageHash
}
logger.Info("Joined session via Message Router",
zap.String("session_id", sessionID.String()),
zap.String("party_id", partyID),
zap.String("session_type", sessionInfo.SessionType))
return sessionInfo, nil
})
}
// ReportCompletion reports protocol completion via Message Router
// Includes automatic retry with exponential backoff for transient failures
func (c *MessageRouterClient) ReportCompletion(
ctx context.Context,
sessionID uuid.UUID,
partyID string,
resultData []byte,
) error {
req := &router.ReportCompletionRequest{
SessionId: sessionID.String(),
PartyId: partyID,
PublicKey: resultData, // For keygen: public key; for signing: signature
}
return retry.DoVoid(ctx, c.retryCfg, "ReportCompletion", func() error {
resp := &router.ReportCompletionResponse{}
err := c.getConn().Invoke(ctx, "/mpc.router.v1.MessageRouter/ReportCompletion", req, resp)
if err != nil {
return err
}
logger.Info("Reported completion via Message Router",
zap.String("session_id", sessionID.String()),
zap.String("party_id", partyID),
zap.Bool("all_completed", resp.AllCompleted))
return nil
})
}
// MarkPartyReady marks the party as ready to start the protocol via Message Router
// Includes automatic retry with exponential backoff for transient failures
func (c *MessageRouterClient) MarkPartyReady(
ctx context.Context,
sessionID uuid.UUID,
partyID string,
) (bool, error) {
req := &router.MarkPartyReadyRequest{
SessionId: sessionID.String(),
PartyId: partyID,
}
return retry.Do(ctx, c.retryCfg, "MarkPartyReady", func() (bool, error) {
resp := &router.MarkPartyReadyResponse{}
err := c.getConn().Invoke(ctx, "/mpc.router.v1.MessageRouter/MarkPartyReady", req, resp)
if err != nil {
return false, err
}
logger.Info("Marked party ready via Message Router",
zap.String("session_id", sessionID.String()),
zap.String("party_id", partyID),
zap.Bool("all_ready", resp.AllReady))
return resp.AllReady, nil
})
}
// GetSessionStatus gets the current session status via Message Router
// Includes automatic retry with exponential backoff for transient failures
func (c *MessageRouterClient) GetSessionStatus(
ctx context.Context,
sessionID uuid.UUID,
) (string, error) {
req := &router.GetSessionStatusRequest{
SessionId: sessionID.String(),
}
return retry.Do(ctx, c.retryCfg, "GetSessionStatus", func() (string, error) {
resp := &router.GetSessionStatusResponse{}
err := c.getConn().Invoke(ctx, "/mpc.router.v1.MessageRouter/GetSessionStatus", req, resp)
if err != nil {
return "", err
}
return resp.Status, nil
})
}
// SubmitDelegateShare submits user's share from delegate party to Session Coordinator
// This is called after keygen by delegate parties to return the share for user retrieval
// Includes automatic retry with exponential backoff for transient failures
func (c *MessageRouterClient) SubmitDelegateShare(
ctx context.Context,
sessionID string,
partyID string,
encryptedShare []byte,
publicKey []byte,
partyIndex int32,
) error {
req := &router.SubmitDelegateShareRequest{
SessionId: sessionID,
PartyId: partyID,
EncryptedShare: encryptedShare,
PublicKey: publicKey,
PartyIndex: partyIndex,
}
return retry.DoVoid(ctx, c.retryCfg, "SubmitDelegateShare", func() error {
resp := &router.SubmitDelegateShareResponse{}
err := c.getConn().Invoke(ctx, "/mpc.router.v1.MessageRouter/SubmitDelegateShare", req, resp)
if err != nil {
return err
}
if !resp.Success {
logger.Error("Submit delegate share failed",
zap.String("session_id", sessionID),
zap.String("party_id", partyID))
return use_cases.ErrKeygenFailed
}
logger.Info("Delegate share submitted successfully via Message Router",
zap.String("session_id", sessionID),
zap.String("party_id", partyID),
zap.Int("share_size", len(encryptedShare)))
return nil
})
}

View File

@ -81,23 +81,15 @@ func main() {
logger.Fatal("Failed to create crypto service", zap.Error(err))
}
// Get gRPC service addresses from environment
coordinatorAddr := os.Getenv("SESSION_COORDINATOR_ADDR")
if coordinatorAddr == "" {
coordinatorAddr = "localhost:9091"
}
// Get Message Router address from environment
// Server-parties ONLY connect to Message Router (not Session Coordinator)
// Message Router proxies session operations to Session Coordinator
routerAddr := os.Getenv("MESSAGE_ROUTER_ADDR")
if routerAddr == "" {
routerAddr = "localhost:9092"
}
// Initialize gRPC clients
sessionClient, err := grpcclient.NewSessionCoordinatorClient(coordinatorAddr)
if err != nil {
logger.Fatal("Failed to connect to session coordinator", zap.Error(err))
}
defer sessionClient.Close()
// 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))
@ -107,16 +99,17 @@ func main() {
// Initialize repositories
keyShareRepo := postgres.NewKeySharePostgresRepo(db)
// Initialize use cases with real gRPC clients
// Initialize use cases with Message Router client
// Message Router handles both messaging AND session operations (proxied to coordinator)
participateKeygenUC := use_cases.NewParticipateKeygenUseCase(
keyShareRepo,
sessionClient,
messageRouter, // MessageRouterClient implements SessionCoordinatorClient interface
messageRouter,
cryptoService,
)
participateSigningUC := use_cases.NewParticipateSigningUseCase(
keyShareRepo,
sessionClient,
messageRouter, // MessageRouterClient implements SessionCoordinatorClient interface
messageRouter,
cryptoService,
)
@ -192,7 +185,6 @@ func main() {
partyID,
participateKeygenUC,
participateSigningUC,
sessionClient,
)
if err := messageRouter.SubscribeSessionEvents(ctx, partyID, eventHandler); err != nil {
@ -535,7 +527,6 @@ func createSessionEventHandler(
partyID string,
participateKeygenUC *use_cases.ParticipateKeygenUseCase,
participateSigningUC *use_cases.ParticipateSigningUseCase,
sessionClient *grpcclient.SessionCoordinatorClient,
) func(*router.SessionEvent) {
return func(event *router.SessionEvent) {
// Check if this party is selected for the session
@ -617,6 +608,15 @@ func createSessionEventHandler(
MessageHash: event.MessageHash,
}
// If this party is the delegate party and user share is provided, use it
if event.DelegateUserShare != nil && event.DelegateUserShare.DelegatePartyId == partyID {
input.UserShareData = event.DelegateUserShare.EncryptedShare
logger.Info("Using user-provided share for delegate party signing",
zap.String("session_id", event.SessionId),
zap.String("party_id", partyID),
zap.Int32("party_index", event.DelegateUserShare.PartyIndex))
}
result, err := participateSigningUC.Execute(ctx, input)
if err != nil {
logger.Error("Signing participation failed",

View File

@ -2,6 +2,7 @@ package grpc
import (
"context"
"sync"
"time"
"github.com/google/uuid"
@ -17,6 +18,79 @@ import (
"google.golang.org/grpc/status"
)
// DelegateShareEntry represents a delegate party's share for user retrieval
type DelegateShareEntry struct {
PartyID string
EncryptedShare []byte
PublicKey []byte
PartyIndex int32
CreatedAt time.Time
}
// DelegateShareCache stores delegate shares temporarily for user retrieval
type DelegateShareCache struct {
mu sync.RWMutex
shares map[string]*DelegateShareEntry // sessionID -> share
ttl time.Duration
}
// NewDelegateShareCache creates a new cache with TTL
func NewDelegateShareCache(ttl time.Duration) *DelegateShareCache {
cache := &DelegateShareCache{
shares: make(map[string]*DelegateShareEntry),
ttl: ttl,
}
// Start cleanup goroutine
go cache.cleanupLoop()
return cache
}
// Store stores a delegate share
func (c *DelegateShareCache) Store(sessionID string, entry *DelegateShareEntry) {
c.mu.Lock()
defer c.mu.Unlock()
entry.CreatedAt = time.Now()
c.shares[sessionID] = entry
logger.Info("Delegate share stored",
zap.String("session_id", sessionID),
zap.String("party_id", entry.PartyID))
}
// Get retrieves and deletes a delegate share (one-time retrieval)
func (c *DelegateShareCache) Get(sessionID string) (*DelegateShareEntry, bool) {
c.mu.Lock()
defer c.mu.Unlock()
entry, exists := c.shares[sessionID]
if exists {
delete(c.shares, sessionID)
logger.Info("Delegate share retrieved and deleted",
zap.String("session_id", sessionID),
zap.String("party_id", entry.PartyID))
}
return entry, exists
}
// cleanupLoop removes expired entries
func (c *DelegateShareCache) cleanupLoop() {
ticker := time.NewTicker(c.ttl / 2)
defer ticker.Stop()
for range ticker.C {
c.mu.Lock()
now := time.Now()
for sessionID, entry := range c.shares {
if now.Sub(entry.CreatedAt) > c.ttl {
delete(c.shares, sessionID)
logger.Debug("Expired delegate share removed",
zap.String("session_id", sessionID))
}
}
c.mu.Unlock()
}
}
// Global delegate share cache (15 minute TTL)
var delegateShareCache = NewDelegateShareCache(15 * time.Minute)
// SessionCoordinatorServer implements the gRPC SessionCoordinator service
type SessionCoordinatorServer struct {
pb.UnimplementedSessionCoordinatorServer
@ -86,6 +160,15 @@ func (s *SessionCoordinatorServer) CreateSession(
ExpiresIn: time.Duration(req.ExpiresInSeconds) * time.Second,
}
// Add delegate user share if provided (for sign sessions with delegate party)
if req.DelegateUserShare != nil {
inputData.DelegateUserShare = &input.DelegateUserShare{
DelegatePartyID: req.DelegateUserShare.DelegatePartyId,
EncryptedShare: req.DelegateUserShare.EncryptedShare,
PartyIndex: req.DelegateUserShare.PartyIndex,
}
}
// Execute use case
output, err := s.createSessionUC.Execute(ctx, inputData)
if err != nil {
@ -194,13 +277,34 @@ func (s *SessionCoordinatorServer) GetSessionStatus(
}
}
return &pb.GetSessionStatusResponse{
resp := &pb.GetSessionStatusResponse{
Status: output.Status,
CompletedParties: int32(completedParties),
TotalParties: int32(len(output.Participants)),
SessionType: output.SessionType,
PublicKey: output.PublicKey,
Signature: output.Signature,
}, nil
HasDelegate: output.HasDelegate, // Only meaningful for keygen sessions
}
// Try to get delegate share from cache
// Only include if session has delegate and is completed
if output.HasDelegate && output.Status == "completed" {
entry, found := delegateShareCache.Get(req.SessionId)
if found {
resp.DelegateShare = &pb.DelegateShareInfo{
EncryptedShare: entry.EncryptedShare,
PartyIndex: entry.PartyIndex,
PartyId: entry.PartyID,
}
logger.Info("Delegate share included in GetSessionStatus response",
zap.String("session_id", req.SessionId),
zap.String("party_id", entry.PartyID))
}
// If has_delegate=true but delegate_share=nil, share was already retrieved
}
return resp, nil
}
// ReportCompletion reports that a participant has completed
@ -323,6 +427,40 @@ func (s *SessionCoordinatorServer) StartSession(
}, nil
}
// SubmitDelegateShare stores a delegate party's share for user retrieval
func (s *SessionCoordinatorServer) SubmitDelegateShare(
ctx context.Context,
req *pb.SubmitDelegateShareRequest,
) (*pb.SubmitDelegateShareResponse, error) {
if req.SessionId == "" {
return nil, status.Error(codes.InvalidArgument, "session_id is required")
}
if req.PartyId == "" {
return nil, status.Error(codes.InvalidArgument, "party_id is required")
}
if len(req.EncryptedShare) == 0 {
return nil, status.Error(codes.InvalidArgument, "encrypted_share is required")
}
// Store in cache
entry := &DelegateShareEntry{
PartyID: req.PartyId,
EncryptedShare: req.EncryptedShare,
PublicKey: req.PublicKey,
PartyIndex: req.PartyIndex,
}
delegateShareCache.Store(req.SessionId, entry)
logger.Info("Delegate share submitted",
zap.String("session_id", req.SessionId),
zap.String("party_id", req.PartyId),
zap.Int("share_size", len(req.EncryptedShare)))
return &pb.SubmitDelegateShareResponse{
Success: true,
}, nil
}
// toGRPCError converts domain errors to gRPC errors
func toGRPCError(err error) error {
switch err {

View File

@ -128,7 +128,7 @@ func (h *SessionHTTPHandler) CreateSession(c *gin.Context) {
expiresIn := time.Duration(req.ExpiresIn) * time.Second
if expiresIn == 0 {
expiresIn = 10 * time.Minute // Default
expiresIn = 24 * time.Hour // Default: 24-hour session validity
}
inputData := input.CreateSessionInput{

View File

@ -8,6 +8,7 @@ import (
"github.com/rwadurian/mpc-system/pkg/grpcutil"
"github.com/rwadurian/mpc-system/pkg/logger"
"github.com/rwadurian/mpc-system/pkg/retry"
"github.com/rwadurian/mpc-system/services/session-coordinator/application/use_cases"
"go.uber.org/zap"
)
@ -120,6 +121,7 @@ func (c *MessageRouterClient) PublishSessionCreated(
messageHash []byte,
createdAt int64,
expiresAt int64,
delegateUserShare *use_cases.DelegateUserShareInfo,
) error {
event := &router.SessionEvent{
EventId: uuid.New().String(),
@ -134,5 +136,14 @@ func (c *MessageRouterClient) PublishSessionCreated(
ExpiresAt: expiresAt,
}
// Add delegate user share if provided (for sign sessions with delegate)
if delegateUserShare != nil {
event.DelegateUserShare = &router.DelegateUserShare{
DelegatePartyId: delegateUserShare.DelegatePartyID,
EncryptedShare: delegateUserShare.EncryptedShare,
PartyIndex: delegateUserShare.PartyIndex,
}
}
return c.PublishSessionEvent(ctx, event)
}

View File

@ -36,6 +36,13 @@ type PartyComposition struct {
CustomFilters []output.PartySelectionFilter // Custom party selection filters
}
// DelegateUserShare contains user's share for delegate party to use in signing
type DelegateUserShare struct {
DelegatePartyID string // The delegate party that will use this share
EncryptedShare []byte // User's encrypted share
PartyIndex int32 // Party index for this share
}
// CreateSessionInput contains input for creating a session
type CreateSessionInput struct {
InitiatorID string
@ -46,6 +53,7 @@ type CreateSessionInput struct {
PartyComposition *PartyComposition // Optional: specify party composition by role
MessageHash []byte // For sign sessions
ExpiresIn time.Duration
DelegateUserShare *DelegateUserShare // For sign sessions with delegate party
}
// ParticipantInfo contains information about a participant
@ -98,11 +106,14 @@ type PartyInfo struct {
type SessionStatusOutput struct {
SessionID uuid.UUID
Status string
SessionType string // "keygen" or "sign"
ThresholdT int
ThresholdN int
Participants []ParticipantStatus
PublicKey []byte // For completed keygen
Signature []byte // For completed sign
HasDelegate bool // True if keygen session has a delegate party (always false for sign)
DelegatePartyID string // The delegate party ID (if any, only for keygen)
}
// ParticipantStatus contains participant status information

View File

@ -16,6 +16,13 @@ import (
"go.uber.org/zap"
)
// DelegateUserShareInfo contains user's share for delegate party
type DelegateUserShareInfo struct {
DelegatePartyID string
EncryptedShare []byte
PartyIndex int32
}
// MessageRouterClient interface for publishing session events
type MessageRouterClient interface {
PublishSessionCreated(
@ -28,6 +35,7 @@ type MessageRouterClient interface {
messageHash []byte,
createdAt int64,
expiresAt int64,
delegateUserShare *DelegateUserShareInfo,
) error
}
@ -286,6 +294,17 @@ func (uc *CreateSessionUseCase) Execute(
// Only publish if parties were selected from pool
if len(session.GetPartyIDs()) > 0 && uc.messageRouterClient != nil {
selectedParties := session.GetPartyIDs()
// Convert delegate user share if provided (for sign sessions with delegate)
var delegateUserShare *DelegateUserShareInfo
if req.DelegateUserShare != nil {
delegateUserShare = &DelegateUserShareInfo{
DelegatePartyID: req.DelegateUserShare.DelegatePartyID,
EncryptedShare: req.DelegateUserShare.EncryptedShare,
PartyIndex: req.DelegateUserShare.PartyIndex,
}
}
err := uc.messageRouterClient.PublishSessionCreated(
ctx,
session.ID.UUID(),
@ -296,6 +315,7 @@ func (uc *CreateSessionUseCase) Execute(
session.MessageHash,
session.CreatedAt.UnixMilli(),
session.ExpiresAt.UnixMilli(),
delegateUserShare,
)
if err != nil {
// Log error but don't fail the operation

View File

@ -46,12 +46,17 @@ func (uc *GetSessionStatusUseCase) Execute(
}
// 3. Build response
// has_delegate is only meaningful for keygen sessions
hasDelegate := session.DelegatePartyID != "" && string(session.SessionType) == "keygen"
return &input.SessionStatusOutput{
SessionID: session.ID.UUID(),
Status: session.Status.String(),
SessionType: string(session.SessionType),
ThresholdT: session.Threshold.T(),
ThresholdN: session.Threshold.N(),
Participants: participants,
PublicKey: session.PublicKey,
HasDelegate: hasDelegate,
DelegatePartyID: session.DelegatePartyID,
}, nil
}

View File

@ -84,15 +84,11 @@ func (s *SessionCoordinatorService) ShouldExpireSession(session *entities.MPCSes
}
// CalculateSessionTimeout calculates the timeout for a session type
// Sessions are valid for 24 hours to allow users sufficient time to complete MPC operations
// Note: This is separate from reliability timeouts (party heartbeat/activity) which remain at 2 minutes
func (s *SessionCoordinatorService) CalculateSessionTimeout(sessionType entities.SessionType) time.Duration {
switch sessionType {
case entities.SessionTypeKeygen:
return 10 * time.Minute
case entities.SessionTypeSign:
return 5 * time.Minute
default:
return 10 * time.Minute
}
// All session types have a 24-hour validity period
return 24 * time.Hour
}
// ValidateMessageRouting validates if a message can be routed