fix(mpc): session_started事件携带完整participants列表

问题:server-party-co-managed 使用 JoinSession 缓存的 participants,
但如果它是第一个加入的,缓存的列表只有自己,导致 keygen 失败。

修复:
- proto: SessionEvent 添加 repeated PartyInfo participants 字段
- session-coordinator: PublishSessionStarted 时包含完整 participants
- server-party-co-managed: 优先使用事件中的 participants

这确保所有 party 在收到 session_started 时都能获得完整的参与者列表。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-27 07:26:25 -08:00
parent aeb70a6579
commit d04f0a08e0
5 changed files with 130 additions and 56 deletions

View File

@ -680,8 +680,11 @@ type SessionEvent struct {
ExpiresAt int64 `protobuf:"varint,10,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Unix timestamp milliseconds ExpiresAt int64 `protobuf:"varint,10,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Unix timestamp milliseconds
// For sign sessions with delegate party: user's share for delegate to use // For sign sessions with delegate party: user's share for delegate to use
DelegateUserShare *DelegateUserShare `protobuf:"bytes,11,opt,name=delegate_user_share,json=delegateUserShare,proto3" json:"delegate_user_share,omitempty"` DelegateUserShare *DelegateUserShare `protobuf:"bytes,11,opt,name=delegate_user_share,json=delegateUserShare,proto3" json:"delegate_user_share,omitempty"`
unknownFields protoimpl.UnknownFields // For session_started event: complete list of participants with their indices
sizeCache protoimpl.SizeCache // CRITICAL: Use this for TSS protocol instead of JoinSession response
Participants []*PartyInfo `protobuf:"bytes,12,rep,name=participants,proto3" json:"participants,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *SessionEvent) Reset() { func (x *SessionEvent) Reset() {
@ -791,6 +794,13 @@ func (x *SessionEvent) GetDelegateUserShare() *DelegateUserShare {
return nil return nil
} }
func (x *SessionEvent) GetParticipants() []*PartyInfo {
if x != nil {
return x.Participants
}
return nil
}
// DelegateUserShare contains user's share for delegate party to use in signing // DelegateUserShare contains user's share for delegate party to use in signing
type DelegateUserShare struct { type DelegateUserShare struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -2479,7 +2489,7 @@ const file_api_proto_message_router_proto_rawDesc = "" +
"\x1dSubscribeSessionEventsRequest\x12\x19\n" + "\x1dSubscribeSessionEventsRequest\x12\x19\n" +
"\bparty_id\x18\x01 \x01(\tR\apartyId\x12\x1f\n" + "\bparty_id\x18\x01 \x01(\tR\apartyId\x12\x1f\n" +
"\vevent_types\x18\x02 \x03(\tR\n" + "\vevent_types\x18\x02 \x03(\tR\n" +
"eventTypes\"\x94\x04\n" + "eventTypes\"\xd2\x04\n" +
"\fSessionEvent\x12\x19\n" + "\fSessionEvent\x12\x19\n" +
"\bevent_id\x18\x01 \x01(\tR\aeventId\x12\x1d\n" + "\bevent_id\x18\x01 \x01(\tR\aeventId\x12\x1d\n" +
"\n" + "\n" +
@ -2499,7 +2509,8 @@ const file_api_proto_message_router_proto_rawDesc = "" +
"\n" + "\n" +
"expires_at\x18\n" + "expires_at\x18\n" +
" \x01(\x03R\texpiresAt\x12P\n" + " \x01(\x03R\texpiresAt\x12P\n" +
"\x13delegate_user_share\x18\v \x01(\v2 .mpc.router.v1.DelegateUserShareR\x11delegateUserShare\x1a=\n" + "\x13delegate_user_share\x18\v \x01(\v2 .mpc.router.v1.DelegateUserShareR\x11delegateUserShare\x12<\n" +
"\fparticipants\x18\f \x03(\v2\x18.mpc.router.v1.PartyInfoR\fparticipants\x1a=\n" +
"\x0fJoinTokensEntry\x12\x10\n" + "\x0fJoinTokensEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x89\x01\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x89\x01\n" +
@ -2723,50 +2734,51 @@ var file_api_proto_message_router_proto_depIdxs = []int32{
6, // 1: mpc.router.v1.RegisterPartyRequest.notification:type_name -> mpc.router.v1.NotificationChannel 6, // 1: mpc.router.v1.RegisterPartyRequest.notification:type_name -> mpc.router.v1.NotificationChannel
37, // 2: mpc.router.v1.SessionEvent.join_tokens:type_name -> mpc.router.v1.SessionEvent.JoinTokensEntry 37, // 2: mpc.router.v1.SessionEvent.join_tokens:type_name -> mpc.router.v1.SessionEvent.JoinTokensEntry
11, // 3: mpc.router.v1.SessionEvent.delegate_user_share:type_name -> mpc.router.v1.DelegateUserShare 11, // 3: mpc.router.v1.SessionEvent.delegate_user_share:type_name -> mpc.router.v1.DelegateUserShare
10, // 4: mpc.router.v1.PublishSessionEventRequest.event:type_name -> mpc.router.v1.SessionEvent 25, // 4: mpc.router.v1.SessionEvent.participants:type_name -> mpc.router.v1.PartyInfo
6, // 5: mpc.router.v1.RegisteredParty.notification:type_name -> mpc.router.v1.NotificationChannel 10, // 5: mpc.router.v1.PublishSessionEventRequest.event:type_name -> mpc.router.v1.SessionEvent
15, // 6: mpc.router.v1.GetRegisteredPartiesResponse.parties:type_name -> mpc.router.v1.RegisteredParty 6, // 6: mpc.router.v1.RegisteredParty.notification:type_name -> mpc.router.v1.NotificationChannel
20, // 7: mpc.router.v1.GetMessageStatusResponse.deliveries:type_name -> mpc.router.v1.MessageDeliveryStatus 15, // 7: mpc.router.v1.GetRegisteredPartiesResponse.parties:type_name -> mpc.router.v1.RegisteredParty
24, // 8: mpc.router.v1.PartyInfo.device_info:type_name -> mpc.router.v1.DeviceInfo 20, // 8: mpc.router.v1.GetMessageStatusResponse.deliveries:type_name -> mpc.router.v1.MessageDeliveryStatus
24, // 9: mpc.router.v1.JoinSessionRequest.device_info:type_name -> mpc.router.v1.DeviceInfo 24, // 9: mpc.router.v1.PartyInfo.device_info:type_name -> mpc.router.v1.DeviceInfo
26, // 10: mpc.router.v1.JoinSessionResponse.session_info:type_name -> mpc.router.v1.SessionInfo 24, // 10: mpc.router.v1.JoinSessionRequest.device_info:type_name -> mpc.router.v1.DeviceInfo
25, // 11: mpc.router.v1.JoinSessionResponse.other_parties:type_name -> mpc.router.v1.PartyInfo 26, // 11: mpc.router.v1.JoinSessionResponse.session_info:type_name -> mpc.router.v1.SessionInfo
25, // 12: mpc.router.v1.GetSessionStatusResponse.participants:type_name -> mpc.router.v1.PartyInfo 25, // 12: mpc.router.v1.JoinSessionResponse.other_parties:type_name -> mpc.router.v1.PartyInfo
0, // 13: mpc.router.v1.MessageRouter.RouteMessage:input_type -> mpc.router.v1.RouteMessageRequest 25, // 13: mpc.router.v1.GetSessionStatusResponse.participants:type_name -> mpc.router.v1.PartyInfo
2, // 14: mpc.router.v1.MessageRouter.SubscribeMessages:input_type -> mpc.router.v1.SubscribeMessagesRequest 0, // 14: mpc.router.v1.MessageRouter.RouteMessage:input_type -> mpc.router.v1.RouteMessageRequest
4, // 15: mpc.router.v1.MessageRouter.GetPendingMessages:input_type -> mpc.router.v1.GetPendingMessagesRequest 2, // 15: mpc.router.v1.MessageRouter.SubscribeMessages:input_type -> mpc.router.v1.SubscribeMessagesRequest
17, // 16: mpc.router.v1.MessageRouter.AcknowledgeMessage:input_type -> mpc.router.v1.AcknowledgeMessageRequest 4, // 16: mpc.router.v1.MessageRouter.GetPendingMessages:input_type -> mpc.router.v1.GetPendingMessagesRequest
19, // 17: mpc.router.v1.MessageRouter.GetMessageStatus:input_type -> mpc.router.v1.GetMessageStatusRequest 17, // 17: mpc.router.v1.MessageRouter.AcknowledgeMessage:input_type -> mpc.router.v1.AcknowledgeMessageRequest
7, // 18: mpc.router.v1.MessageRouter.RegisterParty:input_type -> mpc.router.v1.RegisterPartyRequest 19, // 18: mpc.router.v1.MessageRouter.GetMessageStatus:input_type -> mpc.router.v1.GetMessageStatusRequest
22, // 19: mpc.router.v1.MessageRouter.Heartbeat:input_type -> mpc.router.v1.HeartbeatRequest 7, // 19: mpc.router.v1.MessageRouter.RegisterParty:input_type -> mpc.router.v1.RegisterPartyRequest
9, // 20: mpc.router.v1.MessageRouter.SubscribeSessionEvents:input_type -> mpc.router.v1.SubscribeSessionEventsRequest 22, // 20: mpc.router.v1.MessageRouter.Heartbeat:input_type -> mpc.router.v1.HeartbeatRequest
12, // 21: mpc.router.v1.MessageRouter.PublishSessionEvent:input_type -> mpc.router.v1.PublishSessionEventRequest 9, // 21: mpc.router.v1.MessageRouter.SubscribeSessionEvents:input_type -> mpc.router.v1.SubscribeSessionEventsRequest
14, // 22: mpc.router.v1.MessageRouter.GetRegisteredParties:input_type -> mpc.router.v1.GetRegisteredPartiesRequest 12, // 22: mpc.router.v1.MessageRouter.PublishSessionEvent:input_type -> mpc.router.v1.PublishSessionEventRequest
27, // 23: mpc.router.v1.MessageRouter.JoinSession:input_type -> mpc.router.v1.JoinSessionRequest 14, // 23: mpc.router.v1.MessageRouter.GetRegisteredParties:input_type -> mpc.router.v1.GetRegisteredPartiesRequest
29, // 24: mpc.router.v1.MessageRouter.MarkPartyReady:input_type -> mpc.router.v1.MarkPartyReadyRequest 27, // 24: mpc.router.v1.MessageRouter.JoinSession:input_type -> mpc.router.v1.JoinSessionRequest
31, // 25: mpc.router.v1.MessageRouter.ReportCompletion:input_type -> mpc.router.v1.ReportCompletionRequest 29, // 25: mpc.router.v1.MessageRouter.MarkPartyReady:input_type -> mpc.router.v1.MarkPartyReadyRequest
33, // 26: mpc.router.v1.MessageRouter.GetSessionStatus:input_type -> mpc.router.v1.GetSessionStatusRequest 31, // 26: mpc.router.v1.MessageRouter.ReportCompletion:input_type -> mpc.router.v1.ReportCompletionRequest
35, // 27: mpc.router.v1.MessageRouter.SubmitDelegateShare:input_type -> mpc.router.v1.SubmitDelegateShareRequest 33, // 27: mpc.router.v1.MessageRouter.GetSessionStatus:input_type -> mpc.router.v1.GetSessionStatusRequest
1, // 28: mpc.router.v1.MessageRouter.RouteMessage:output_type -> mpc.router.v1.RouteMessageResponse 35, // 28: mpc.router.v1.MessageRouter.SubmitDelegateShare:input_type -> mpc.router.v1.SubmitDelegateShareRequest
3, // 29: mpc.router.v1.MessageRouter.SubscribeMessages:output_type -> mpc.router.v1.MPCMessage 1, // 29: mpc.router.v1.MessageRouter.RouteMessage:output_type -> mpc.router.v1.RouteMessageResponse
5, // 30: mpc.router.v1.MessageRouter.GetPendingMessages:output_type -> mpc.router.v1.GetPendingMessagesResponse 3, // 30: mpc.router.v1.MessageRouter.SubscribeMessages:output_type -> mpc.router.v1.MPCMessage
18, // 31: mpc.router.v1.MessageRouter.AcknowledgeMessage:output_type -> mpc.router.v1.AcknowledgeMessageResponse 5, // 31: mpc.router.v1.MessageRouter.GetPendingMessages:output_type -> mpc.router.v1.GetPendingMessagesResponse
21, // 32: mpc.router.v1.MessageRouter.GetMessageStatus:output_type -> mpc.router.v1.GetMessageStatusResponse 18, // 32: mpc.router.v1.MessageRouter.AcknowledgeMessage:output_type -> mpc.router.v1.AcknowledgeMessageResponse
8, // 33: mpc.router.v1.MessageRouter.RegisterParty:output_type -> mpc.router.v1.RegisterPartyResponse 21, // 33: mpc.router.v1.MessageRouter.GetMessageStatus:output_type -> mpc.router.v1.GetMessageStatusResponse
23, // 34: mpc.router.v1.MessageRouter.Heartbeat:output_type -> mpc.router.v1.HeartbeatResponse 8, // 34: mpc.router.v1.MessageRouter.RegisterParty:output_type -> mpc.router.v1.RegisterPartyResponse
10, // 35: mpc.router.v1.MessageRouter.SubscribeSessionEvents:output_type -> mpc.router.v1.SessionEvent 23, // 35: mpc.router.v1.MessageRouter.Heartbeat:output_type -> mpc.router.v1.HeartbeatResponse
13, // 36: mpc.router.v1.MessageRouter.PublishSessionEvent:output_type -> mpc.router.v1.PublishSessionEventResponse 10, // 36: mpc.router.v1.MessageRouter.SubscribeSessionEvents:output_type -> mpc.router.v1.SessionEvent
16, // 37: mpc.router.v1.MessageRouter.GetRegisteredParties:output_type -> mpc.router.v1.GetRegisteredPartiesResponse 13, // 37: mpc.router.v1.MessageRouter.PublishSessionEvent:output_type -> mpc.router.v1.PublishSessionEventResponse
28, // 38: mpc.router.v1.MessageRouter.JoinSession:output_type -> mpc.router.v1.JoinSessionResponse 16, // 38: mpc.router.v1.MessageRouter.GetRegisteredParties:output_type -> mpc.router.v1.GetRegisteredPartiesResponse
30, // 39: mpc.router.v1.MessageRouter.MarkPartyReady:output_type -> mpc.router.v1.MarkPartyReadyResponse 28, // 39: mpc.router.v1.MessageRouter.JoinSession:output_type -> mpc.router.v1.JoinSessionResponse
32, // 40: mpc.router.v1.MessageRouter.ReportCompletion:output_type -> mpc.router.v1.ReportCompletionResponse 30, // 40: mpc.router.v1.MessageRouter.MarkPartyReady:output_type -> mpc.router.v1.MarkPartyReadyResponse
34, // 41: mpc.router.v1.MessageRouter.GetSessionStatus:output_type -> mpc.router.v1.GetSessionStatusResponse 32, // 41: mpc.router.v1.MessageRouter.ReportCompletion:output_type -> mpc.router.v1.ReportCompletionResponse
36, // 42: mpc.router.v1.MessageRouter.SubmitDelegateShare:output_type -> mpc.router.v1.SubmitDelegateShareResponse 34, // 42: mpc.router.v1.MessageRouter.GetSessionStatus:output_type -> mpc.router.v1.GetSessionStatusResponse
28, // [28:43] is the sub-list for method output_type 36, // 43: mpc.router.v1.MessageRouter.SubmitDelegateShare:output_type -> mpc.router.v1.SubmitDelegateShareResponse
13, // [13:28] is the sub-list for method input_type 29, // [29:44] is the sub-list for method output_type
13, // [13:13] is the sub-list for extension type_name 14, // [14:29] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension extendee 14, // [14:14] is the sub-list for extension type_name
0, // [0:13] is the sub-list for field type_name 14, // [14:14] is the sub-list for extension extendee
0, // [0:14] is the sub-list for field type_name
} }
func init() { file_api_proto_message_router_proto_init() } func init() { file_api_proto_message_router_proto_init() }

View File

@ -166,6 +166,9 @@ message SessionEvent {
int64 expires_at = 10; // Unix timestamp milliseconds int64 expires_at = 10; // Unix timestamp milliseconds
// For sign sessions with delegate party: user's share for delegate to use // For sign sessions with delegate party: user's share for delegate to use
DelegateUserShare delegate_user_share = 11; DelegateUserShare delegate_user_share = 11;
// For session_started event: complete list of participants with their indices
// CRITICAL: Use this for TSS protocol instead of JoinSession response
repeated PartyInfo participants = 12;
} }
// DelegateUserShare contains user's share for delegate party to use in signing // DelegateUserShare contains user's share for delegate party to use in signing

View File

@ -432,17 +432,43 @@ func createCoManagedSessionEventHandler(
return return
} }
// CRITICAL FIX: Use participants from session_started event, NOT from JoinSession cache
// The JoinSession response only contains parties that had joined at that moment,
// but session_started event contains the COMPLETE list of all participants
var participants []use_cases.ParticipantInfo
if len(event.Participants) > 0 {
// Use participants from event (preferred - complete list)
participants = make([]use_cases.ParticipantInfo, len(event.Participants))
for i, p := range event.Participants {
participants[i] = use_cases.ParticipantInfo{
PartyID: p.PartyId,
PartyIndex: int(p.PartyIndex),
}
}
logger.Info("Using participants from session_started event",
zap.String("session_id", event.SessionId),
zap.Int("participant_count", len(participants)))
} else {
// Fallback to cached participants (for backward compatibility)
participants = pendingSession.Participants
logger.Warn("No participants in session_started event, using cached participants",
zap.String("session_id", event.SessionId),
zap.Int("participant_count", len(participants)))
}
// Determine session type based on message_hash // Determine session type based on message_hash
isSignSession := len(pendingSession.MessageHash) > 0 isSignSession := len(pendingSession.MessageHash) > 0
if isSignSession { if isSignSession {
logger.Info("Session started event received, beginning TSS signing protocol", logger.Info("Session started event received, beginning TSS signing protocol",
zap.String("session_id", event.SessionId), zap.String("session_id", event.SessionId),
zap.String("party_id", partyID)) zap.String("party_id", partyID),
zap.Int("participant_count", len(participants)))
} else { } else {
logger.Info("Session started event received, beginning TSS keygen protocol", logger.Info("Session started event received, beginning TSS keygen protocol",
zap.String("session_id", event.SessionId), zap.String("session_id", event.SessionId),
zap.String("party_id", partyID)) zap.String("party_id", partyID),
zap.Int("participant_count", len(participants)))
} }
// Execute TSS protocol in goroutine // Execute TSS protocol in goroutine
@ -452,11 +478,6 @@ func createCoManagedSessionEventHandler(
participateCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) participateCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel() defer cancel()
// CRITICAL: Use participants from pendingSession (which came from JoinSession response)
// These contain the correct PartyIndex values from the database, NOT loop indices
// The JoinSession response already includes all participants with their assigned indices
participants := pendingSession.Participants
if isSignSession { if isSignSession {
// Execute signing protocol // Execute signing protocol
logger.Info("Auto-participating in co_managed_sign session", logger.Info("Auto-participating in co_managed_sign session",

View File

@ -149,6 +149,8 @@ func (c *MessageRouterClient) PublishSessionCreated(
} }
// PublishSessionStarted publishes a session_started event when all parties have joined // PublishSessionStarted publishes a session_started event when all parties have joined
// CRITICAL: participants contains the complete list of all parties with their indices
// Receivers should use this list for TSS protocol instead of JoinSession response
func (c *MessageRouterClient) PublishSessionStarted( func (c *MessageRouterClient) PublishSessionStarted(
ctx context.Context, ctx context.Context,
sessionID string, sessionID string,
@ -157,7 +159,17 @@ func (c *MessageRouterClient) PublishSessionStarted(
selectedParties []string, selectedParties []string,
joinTokens map[string]string, joinTokens map[string]string,
startedAt int64, startedAt int64,
participants []use_cases.SessionParticipantInfo,
) error { ) error {
// Convert participants to proto format
protoParticipants := make([]*router.PartyInfo, len(participants))
for i, p := range participants {
protoParticipants[i] = &router.PartyInfo{
PartyId: p.PartyID,
PartyIndex: p.PartyIndex,
}
}
event := &router.SessionEvent{ event := &router.SessionEvent{
EventId: uuid.New().String(), EventId: uuid.New().String(),
EventType: "session_started", EventType: "session_started",
@ -167,8 +179,13 @@ func (c *MessageRouterClient) PublishSessionStarted(
SelectedParties: selectedParties, SelectedParties: selectedParties,
JoinTokens: joinTokens, JoinTokens: joinTokens,
CreatedAt: startedAt, CreatedAt: startedAt,
Participants: protoParticipants,
} }
logger.Info("Publishing session_started event with participants",
zap.String("session_id", sessionID),
zap.Int("participant_count", len(participants)))
return c.PublishSessionEvent(ctx, event) return c.PublishSessionEvent(ctx, event)
} }

View File

@ -21,8 +21,16 @@ import (
// Maximum retries for optimistic lock conflicts during join session // Maximum retries for optimistic lock conflicts during join session
const joinSessionMaxRetries = 3 const joinSessionMaxRetries = 3
// SessionParticipantInfo contains party ID and index for session_started event
type SessionParticipantInfo struct {
PartyID string
PartyIndex int32
}
// JoinSessionMessageRouterClient defines the interface for publishing session events via gRPC // JoinSessionMessageRouterClient defines the interface for publishing session events via gRPC
type JoinSessionMessageRouterClient interface { type JoinSessionMessageRouterClient interface {
// PublishSessionStarted publishes session_started event with complete participants list
// CRITICAL: participants contains all parties with their indices for TSS protocol
PublishSessionStarted( PublishSessionStarted(
ctx context.Context, ctx context.Context,
sessionID string, sessionID string,
@ -31,6 +39,7 @@ type JoinSessionMessageRouterClient interface {
selectedParties []string, selectedParties []string,
joinTokens map[string]string, joinTokens map[string]string,
startedAt int64, startedAt int64,
participants []SessionParticipantInfo,
) error ) error
// PublishParticipantJoined broadcasts a participant_joined event to all parties in the session // PublishParticipantJoined broadcasts a participant_joined event to all parties in the session
@ -248,6 +257,16 @@ func (uc *JoinSessionUseCase) executeWithRetry(
// Build join tokens map (empty for session_started, parties already have tokens) // Build join tokens map (empty for session_started, parties already have tokens)
joinTokens := make(map[string]string) joinTokens := make(map[string]string)
// CRITICAL: Build complete participants list with party indices
// This ensures all parties have the same participant list for TSS protocol
participants := make([]SessionParticipantInfo, len(session.Participants))
for i, p := range session.Participants {
participants[i] = SessionParticipantInfo{
PartyID: p.PartyID.String(),
PartyIndex: int32(p.PartyIndex),
}
}
if err := uc.messageRouterClient.PublishSessionStarted( if err := uc.messageRouterClient.PublishSessionStarted(
ctx, ctx,
session.ID.String(), session.ID.String(),
@ -256,6 +275,7 @@ func (uc *JoinSessionUseCase) executeWithRetry(
selectedParties, selectedParties,
joinTokens, joinTokens,
startedAt, startedAt,
participants,
); err != nil { ); err != nil {
logger.Error("failed to publish session started event to message router", logger.Error("failed to publish session started event to message router",
zap.String("session_id", session.ID.String()), zap.String("session_id", session.ID.String()),
@ -263,7 +283,8 @@ func (uc *JoinSessionUseCase) executeWithRetry(
} else { } else {
logger.Info("published session started event to message router", logger.Info("published session started event to message router",
zap.String("session_id", session.ID.String()), zap.String("session_id", session.ID.String()),
zap.Int("party_count", len(selectedParties))) zap.Int("party_count", len(selectedParties)),
zap.Int("participant_count", len(participants)))
} }
} }
} }