mirror of
https://github.com/luxfi/threshold.git
synced 2026-10-30 15:15:53 +00:00
2088 lines
61 KiB
Go
2088 lines
61 KiB
Go
// Package protocol provides the ultimate optimized protocol handler with Lux integration
|
|
package protocol
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/metric"
|
|
"github.com/luxfi/threshold/internal/round"
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
)
|
|
|
|
// StartFunc creates the first round of a protocol
|
|
type StartFunc func(sessionID []byte) (round.Session, error)
|
|
|
|
// MultiHandler is an alias for Handler for temporary compatibility
|
|
type MultiHandler = Handler
|
|
|
|
// NewMultiHandler creates a handler with default config (temporary compatibility)
|
|
func NewMultiHandler(create StartFunc, sessionID []byte) (*Handler, error) {
|
|
// Create a test logger for compatibility
|
|
logger := log.NewTestLogger(log.InfoLevel)
|
|
config := DefaultConfig()
|
|
return NewHandler(context.Background(), logger, nil, create, sessionID, config)
|
|
}
|
|
|
|
// Handler is the ONLY handler for threshold protocols - optimized for perfection
|
|
type Handler struct {
|
|
// Core state with lock-free atomic operations
|
|
currentRound atomic.Value // stores round.Session
|
|
rounds sync.Map // round.Number -> round.Session
|
|
result atomic.Value // stores interface{}
|
|
err atomic.Value // stores *Error
|
|
stopped atomic.Bool // tracks if handler is stopped
|
|
|
|
// Sharded message storage for zero contention
|
|
messages *MessageStore
|
|
broadcast *MessageStore
|
|
|
|
// Processed message tracking - prevents race conditions
|
|
processedBroadcasts sync.Map // "round:from" -> bool
|
|
processedMessages sync.Map // "round:from" -> bool
|
|
|
|
// Verification in progress tracking - prevents concurrent verification of same message
|
|
verifyingBroadcasts sync.Map // "round:from" -> bool
|
|
verifyingMessages sync.Map // "round:from" -> bool
|
|
|
|
// Retry tracking - prevents goroutine accumulation from multiple retries
|
|
pendingRetries sync.Map // round.Number -> bool
|
|
|
|
// High-performance channels
|
|
out chan *Message
|
|
incoming chan *Message
|
|
priority chan *Message // High-priority messages
|
|
|
|
// Output channel protection.
|
|
//
|
|
// outMu serializes "check outClosed then send to h.out" against "set outClosed
|
|
// then close(h.out)". Senders hold RLock for the duration of the check+send
|
|
// critical section; Stop / protocol-complete paths take the write lock around
|
|
// the close. This is the only way to make the check-then-send race-free —
|
|
// atomic.Bool alone can flip between the load and the chan op.
|
|
outMu sync.RWMutex
|
|
outClosed atomic.Bool
|
|
|
|
// roundMu serializes VerifyMessage / StoreMessage / StoreBroadcastMessage
|
|
// calls against the live round.Session. Each party has its own Handler,
|
|
// so this only orders state mutations within a single party — concurrent
|
|
// VerifyMessage calls would race on shared round state (e.g. Pedersen
|
|
// parameters whose saferith.Nat.Cmp mutates limb storage even on equal
|
|
// values).
|
|
roundMu sync.Mutex
|
|
|
|
// Lifecycle management
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
closeOnce sync.Once
|
|
stopOnce sync.Once
|
|
closeDoneOnce sync.Once
|
|
|
|
// Worker pool
|
|
workers int
|
|
workerGroup sync.WaitGroup
|
|
|
|
// Lux logging
|
|
log log.Logger
|
|
|
|
// Prometheus metrics
|
|
metrics *Metrics
|
|
|
|
// Protocol info
|
|
protocolID string
|
|
sessionID []byte
|
|
|
|
// Performance tuning
|
|
config *Config
|
|
|
|
// Round finalization tracking
|
|
finalized sync.Map // round.Number -> bool
|
|
|
|
// Performance tracking
|
|
messagesProcessed uint64
|
|
roundsCompleted uint64
|
|
protocolStartTime time.Time
|
|
}
|
|
|
|
// roundWrapper wraps a round.Session to ensure atomic.Value type consistency
|
|
type roundWrapper struct {
|
|
round round.Session
|
|
}
|
|
|
|
// Config for handler - optimized for maximum performance
|
|
type Config struct {
|
|
// Worker pools
|
|
Workers int // CPU cores * 2 by default
|
|
PriorityWorkers int // 4 by default
|
|
|
|
// Channels
|
|
BufferSize int // 10000 by default
|
|
PriorityBuffer int // 1000 by default
|
|
|
|
// Timeouts
|
|
MessageTimeout time.Duration // 30s by default
|
|
RoundTimeout time.Duration // 60s by default
|
|
ProtocolTimeout time.Duration // 5m by default
|
|
|
|
// Performance
|
|
EnableBatching bool // true by default
|
|
BatchSize int // 100 by default
|
|
BatchTimeout time.Duration // 10ms by default
|
|
EnableCompression bool // true for large messages
|
|
CompressionThreshold int // 1KB by default
|
|
|
|
// Memory
|
|
EnablePooling bool // true by default
|
|
MaxMessageSize int // 10MB by default
|
|
|
|
// Reliability
|
|
RetryAttempts int // 3 by default
|
|
RetryBackoff time.Duration // 1s by default
|
|
}
|
|
|
|
// DefaultConfig returns the perfect configuration
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
Workers: runtime.NumCPU() * 2,
|
|
PriorityWorkers: 4,
|
|
BufferSize: 10000,
|
|
PriorityBuffer: 1000,
|
|
MessageTimeout: 30 * time.Second,
|
|
RoundTimeout: 60 * time.Second,
|
|
ProtocolTimeout: 5 * time.Minute,
|
|
EnableBatching: true,
|
|
BatchSize: 100,
|
|
BatchTimeout: 10 * time.Millisecond,
|
|
EnableCompression: true,
|
|
CompressionThreshold: 1024,
|
|
EnablePooling: true,
|
|
MaxMessageSize: 10 * 1024 * 1024, // 10MB
|
|
RetryAttempts: 3,
|
|
RetryBackoff: time.Second,
|
|
}
|
|
}
|
|
|
|
// Metrics for Prometheus monitoring
|
|
type Metrics struct {
|
|
// Counters
|
|
messagesReceived metric.Counter
|
|
messagesSent metric.Counter
|
|
messagesDropped metric.Counter
|
|
roundsCompleted metric.Counter
|
|
protocolsCompleted metric.Counter
|
|
protocolsFailed metric.Counter
|
|
|
|
// Gauges
|
|
activeWorkers metric.Gauge
|
|
queuedMessages metric.Gauge
|
|
currentRound metric.Gauge
|
|
memoryUsage metric.Gauge
|
|
|
|
// Histograms
|
|
messageLatency metric.Histogram
|
|
roundDuration metric.Histogram
|
|
protocolDuration metric.Histogram
|
|
queueWaitTime metric.Histogram
|
|
|
|
// Summaries
|
|
messageSize metric.Summary
|
|
batchSize metric.Summary
|
|
}
|
|
|
|
// NewHandler creates the perfect protocol handler
|
|
func NewHandler(
|
|
ctx context.Context,
|
|
logger log.Logger,
|
|
registry metric.Registerer,
|
|
create StartFunc,
|
|
sessionID []byte,
|
|
config *Config,
|
|
) (*Handler, error) {
|
|
if config == nil {
|
|
config = DefaultConfig()
|
|
}
|
|
|
|
if logger == nil || logger.IsZero() {
|
|
return nil, errors.New("logger is required")
|
|
}
|
|
|
|
// Create initial round
|
|
r, err := create(sessionID)
|
|
if err != nil {
|
|
logger.Error("failed to create initial round", log.Err(err))
|
|
return nil, fmt.Errorf("protocol: failed to create round: %w", err)
|
|
}
|
|
|
|
// Create metrics if registry provided
|
|
var metrics *Metrics
|
|
if registry != nil {
|
|
metrics = createMetrics(r.ProtocolID(), registry)
|
|
}
|
|
|
|
// Only add timeout if context doesn't already have one
|
|
var cancel context.CancelFunc
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && config.ProtocolTimeout > 0 {
|
|
ctx, cancel = context.WithTimeout(ctx, config.ProtocolTimeout)
|
|
} else {
|
|
// Create a cancel func anyway for cleanup
|
|
ctx, cancel = context.WithCancel(ctx)
|
|
}
|
|
|
|
h := &Handler{
|
|
messages: newMessageStore(),
|
|
broadcast: newMessageStore(),
|
|
out: make(chan *Message, config.BufferSize),
|
|
incoming: make(chan *Message, config.BufferSize),
|
|
priority: make(chan *Message, config.PriorityBuffer),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
done: make(chan struct{}),
|
|
workers: config.Workers,
|
|
log: logger,
|
|
metrics: metrics,
|
|
protocolID: r.ProtocolID(),
|
|
sessionID: sessionID,
|
|
config: config,
|
|
protocolStartTime: time.Now(),
|
|
}
|
|
|
|
// Store initial round with wrapper for atomic.Value type consistency
|
|
h.currentRound.Store(&roundWrapper{round: r})
|
|
h.rounds.Store(r.Number(), &roundWrapper{round: r})
|
|
|
|
logger.Info("starting protocol handler",
|
|
log.String("protocol", h.protocolID),
|
|
log.Int("workers", config.Workers),
|
|
log.Int("parties", r.N()),
|
|
log.Int("threshold", r.Threshold()))
|
|
|
|
// Start worker pools
|
|
h.startWorkers()
|
|
|
|
// Initialize first round
|
|
go h.initializeRound(r)
|
|
|
|
// Update metrics
|
|
if metrics != nil {
|
|
metrics.activeWorkers.Set(float64(config.Workers + config.PriorityWorkers))
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
|
|
func createMetrics(protocolID string, registry metric.Registerer) *Metrics {
|
|
m := &Metrics{
|
|
messagesReceived: metric.NewCounter(metric.CounterOpts{
|
|
Name: fmt.Sprintf("threshold_%s_messages_received_total", protocolID),
|
|
Help: "Total messages received",
|
|
}),
|
|
messagesSent: metric.NewCounter(metric.CounterOpts{
|
|
Name: fmt.Sprintf("threshold_%s_messages_sent_total", protocolID),
|
|
Help: "Total messages sent",
|
|
}),
|
|
messagesDropped: metric.NewCounter(metric.CounterOpts{
|
|
Name: fmt.Sprintf("threshold_%s_messages_dropped_total", protocolID),
|
|
Help: "Total messages dropped",
|
|
}),
|
|
roundsCompleted: metric.NewCounter(metric.CounterOpts{
|
|
Name: fmt.Sprintf("threshold_%s_rounds_completed_total", protocolID),
|
|
Help: "Total rounds completed",
|
|
}),
|
|
protocolsCompleted: metric.NewCounter(metric.CounterOpts{
|
|
Name: fmt.Sprintf("threshold_%s_protocols_completed_total", protocolID),
|
|
Help: "Total protocols completed",
|
|
}),
|
|
protocolsFailed: metric.NewCounter(metric.CounterOpts{
|
|
Name: fmt.Sprintf("threshold_%s_protocols_failed_total", protocolID),
|
|
Help: "Total protocols failed",
|
|
}),
|
|
activeWorkers: metric.NewGauge(metric.GaugeOpts{
|
|
Name: fmt.Sprintf("threshold_%s_active_workers", protocolID),
|
|
Help: "Active worker goroutines",
|
|
}),
|
|
queuedMessages: metric.NewGauge(metric.GaugeOpts{
|
|
Name: fmt.Sprintf("threshold_%s_queued_messages", protocolID),
|
|
Help: "Messages in queue",
|
|
}),
|
|
currentRound: metric.NewGauge(metric.GaugeOpts{
|
|
Name: fmt.Sprintf("threshold_%s_current_round", protocolID),
|
|
Help: "Current protocol round",
|
|
}),
|
|
memoryUsage: metric.NewGauge(metric.GaugeOpts{
|
|
Name: fmt.Sprintf("threshold_%s_memory_usage_bytes", protocolID),
|
|
Help: "Memory usage in bytes",
|
|
}),
|
|
messageLatency: metric.NewHistogram(metric.HistogramOpts{
|
|
Name: fmt.Sprintf("threshold_%s_message_latency_seconds", protocolID),
|
|
Help: "Message processing latency",
|
|
Buckets: metric.ExponentialBuckets(0.001, 2, 10),
|
|
}),
|
|
roundDuration: metric.NewHistogram(metric.HistogramOpts{
|
|
Name: fmt.Sprintf("threshold_%s_round_duration_seconds", protocolID),
|
|
Help: "Round completion duration",
|
|
Buckets: metric.ExponentialBuckets(0.01, 2, 10),
|
|
}),
|
|
protocolDuration: metric.NewHistogram(metric.HistogramOpts{
|
|
Name: fmt.Sprintf("threshold_%s_protocol_duration_seconds", protocolID),
|
|
Help: "Total protocol duration",
|
|
Buckets: metric.ExponentialBuckets(0.1, 2, 10),
|
|
}),
|
|
queueWaitTime: metric.NewHistogram(metric.HistogramOpts{
|
|
Name: fmt.Sprintf("threshold_%s_queue_wait_seconds", protocolID),
|
|
Help: "Queue wait time",
|
|
Buckets: metric.ExponentialBuckets(0.0001, 2, 10),
|
|
}),
|
|
messageSize: metric.NewSummary(metric.SummaryOpts{
|
|
Name: fmt.Sprintf("threshold_%s_message_size_bytes", protocolID),
|
|
Help: "Message size distribution",
|
|
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
|
}),
|
|
batchSize: metric.NewSummary(metric.SummaryOpts{
|
|
Name: fmt.Sprintf("threshold_%s_batch_size", protocolID),
|
|
Help: "Batch processing size",
|
|
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
|
}),
|
|
}
|
|
|
|
// Register all metrics
|
|
registry.MustRegister(
|
|
m.messagesReceived, m.messagesSent, m.messagesDropped,
|
|
m.roundsCompleted, m.protocolsCompleted, m.protocolsFailed,
|
|
m.activeWorkers, m.queuedMessages, m.currentRound, m.memoryUsage,
|
|
m.messageLatency, m.roundDuration, m.protocolDuration, m.queueWaitTime,
|
|
m.messageSize, m.batchSize,
|
|
)
|
|
|
|
return m
|
|
}
|
|
|
|
// startWorkers initializes all worker pools
|
|
func (h *Handler) startWorkers() {
|
|
h.log.Debug("starting worker pools",
|
|
log.Int("workers", h.workers),
|
|
log.Int("priority", h.config.PriorityWorkers))
|
|
|
|
// Start regular workers
|
|
for i := 0; i < h.workers; i++ {
|
|
h.workerGroup.Add(1)
|
|
go h.messageWorker(i, false)
|
|
}
|
|
|
|
// Start priority workers
|
|
for i := 0; i < h.config.PriorityWorkers; i++ {
|
|
h.workerGroup.Add(1)
|
|
go h.messageWorker(i, true)
|
|
}
|
|
|
|
// Start batch processor
|
|
if h.config.EnableBatching {
|
|
h.workerGroup.Add(1)
|
|
go h.batchProcessor()
|
|
}
|
|
|
|
// Start round processor
|
|
h.workerGroup.Add(1)
|
|
go func() {
|
|
defer h.workerGroup.Done()
|
|
h.roundProcessor()
|
|
}()
|
|
|
|
// Start metrics updater
|
|
if h.metrics != nil {
|
|
h.workerGroup.Add(1)
|
|
go func() {
|
|
defer h.workerGroup.Done()
|
|
h.metricsUpdater()
|
|
}()
|
|
}
|
|
}
|
|
|
|
// messageWorker processes messages with maximum efficiency
|
|
func (h *Handler) messageWorker(id int, isPriority bool) {
|
|
defer h.workerGroup.Done()
|
|
|
|
h.log.Debug("worker started",
|
|
log.Int("id", id),
|
|
log.Bool("priority", isPriority))
|
|
|
|
source := h.incoming
|
|
if isPriority {
|
|
source = h.priority
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-h.ctx.Done():
|
|
h.log.Debug("worker stopping", log.Int("id", id))
|
|
return
|
|
|
|
case msg := <-source:
|
|
start := time.Now()
|
|
h.processMessage(msg)
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.messageLatency.Observe(time.Since(start).Seconds())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// processMessage handles a single message with perfection
|
|
func (h *Handler) processMessage(msg *Message) {
|
|
if msg == nil {
|
|
return
|
|
}
|
|
|
|
atomic.AddUint64(&h.messagesProcessed, 1)
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.messagesReceived.Inc()
|
|
h.metrics.messageSize.Observe(float64(len(msg.Data)))
|
|
}
|
|
|
|
// Extra debug for p2p round 3
|
|
if msg.RoundNumber == 3 && !msg.Broadcast {
|
|
h.log.Debug("processMessage: p2p round 3 START",
|
|
log.String("from", string(msg.From)),
|
|
log.String("to", string(msg.To)))
|
|
}
|
|
|
|
h.log.Debug("processing message",
|
|
log.String("from", string(msg.From)),
|
|
log.String("to", string(msg.To)),
|
|
log.Uint16("round", uint16(msg.RoundNumber)),
|
|
log.Bool("broadcast", msg.Broadcast),
|
|
log.String("self", string(h.currentRound.Load().(*roundWrapper).round.SelfID())))
|
|
|
|
// Check if already errored or completed
|
|
if h.err.Load() != nil || h.result.Load() != nil {
|
|
h.log.Debug("dropping message - protocol finished")
|
|
return
|
|
}
|
|
|
|
// Handle abort messages
|
|
if msg.RoundNumber == 0 {
|
|
h.handleAbort(msg)
|
|
return
|
|
}
|
|
|
|
// Decompress if needed
|
|
if msg.Compressed {
|
|
msg = h.decompressMessage(msg)
|
|
if msg == nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Store message for any round (needed for buffering future rounds)
|
|
h.storeMessage(msg)
|
|
|
|
// Get current round
|
|
r := h.currentRound.Load().(*roundWrapper).round
|
|
if r.Number() != msg.RoundNumber {
|
|
h.log.Debug("message for different round (buffering)",
|
|
log.Uint16("msg_round", uint16(msg.RoundNumber)),
|
|
log.Uint16("current_round", uint16(r.Number())))
|
|
// Still try to advance in case this completes the current round
|
|
h.tryAdvanceRound()
|
|
return
|
|
}
|
|
|
|
// Verify and process message for current round
|
|
if msg.Broadcast {
|
|
h.verifyBroadcast(msg)
|
|
} else {
|
|
h.verifyNormal(msg)
|
|
}
|
|
|
|
// Try to advance round
|
|
h.tryAdvanceRound()
|
|
}
|
|
|
|
// batchProcessor handles batch message processing for maximum throughput
|
|
func (h *Handler) batchProcessor() {
|
|
defer h.workerGroup.Done()
|
|
|
|
ticker := time.NewTicker(h.config.BatchTimeout)
|
|
defer ticker.Stop()
|
|
|
|
batch := make([]*Message, 0, h.config.BatchSize)
|
|
|
|
for {
|
|
select {
|
|
case <-h.ctx.Done():
|
|
return
|
|
|
|
case msg := <-h.incoming:
|
|
batch = append(batch, msg)
|
|
|
|
if len(batch) >= h.config.BatchSize {
|
|
h.processBatch(batch)
|
|
batch = batch[:0]
|
|
}
|
|
|
|
case <-ticker.C:
|
|
if len(batch) > 0 {
|
|
h.processBatch(batch)
|
|
batch = batch[:0]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// processBatch processes multiple messages together
|
|
func (h *Handler) processBatch(batch []*Message) {
|
|
h.log.Debug("processing batch", log.Int("size", len(batch)))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.batchSize.Observe(float64(len(batch)))
|
|
}
|
|
|
|
for _, msg := range batch {
|
|
h.processMessage(msg)
|
|
}
|
|
}
|
|
|
|
// roundProcessor manages round advancement
|
|
func (h *Handler) roundProcessor() {
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
var lastRound round.Number = 0
|
|
var roundStartTime time.Time
|
|
|
|
for {
|
|
select {
|
|
case <-h.ctx.Done():
|
|
return
|
|
|
|
case <-ticker.C:
|
|
r := h.currentRound.Load().(*roundWrapper).round
|
|
|
|
// Track round transitions
|
|
if r.Number() != lastRound {
|
|
if lastRound > 0 {
|
|
atomic.AddUint64(&h.roundsCompleted, 1)
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.roundsCompleted.Inc()
|
|
h.metrics.roundDuration.Observe(time.Since(roundStartTime).Seconds())
|
|
}
|
|
}
|
|
|
|
h.log.Info("advanced to round", log.Uint16("round", uint16(r.Number())))
|
|
lastRound = r.Number()
|
|
roundStartTime = time.Now()
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.currentRound.Set(float64(r.Number()))
|
|
}
|
|
}
|
|
|
|
h.tryAdvanceRound()
|
|
|
|
case <-h.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// tryAdvanceRound attempts to advance to the next round
|
|
func (h *Handler) tryAdvanceRound() {
|
|
r := h.currentRound.Load().(*roundWrapper).round
|
|
|
|
h.log.Debug("tryAdvanceRound called",
|
|
log.Uint16("current_round", uint16(r.Number())),
|
|
log.String("self", string(r.SelfID())))
|
|
|
|
// First, retry any unprocessed messages that might have returned ErrNotReady
|
|
// This handles the case where round 3 messages arrive before round 2 completes
|
|
h.retryUnprocessedMessages(r.Number())
|
|
|
|
if !h.hasAllMessages(r) {
|
|
return
|
|
}
|
|
|
|
// Use compare-and-swap for lock-free round advancement
|
|
// The LoadOrStore returns the existing value if present, or stores and returns the new value
|
|
if finalized, loaded := h.finalized.LoadOrStore(r.Number(), true); loaded && finalized.(bool) {
|
|
h.log.Debug("round already finalized", log.Uint16("round", uint16(r.Number())))
|
|
// Check if current round has actually advanced
|
|
currentWrapper := h.currentRound.Load().(*roundWrapper)
|
|
if currentWrapper.round.Number() > r.Number() {
|
|
h.log.Debug("current round already advanced",
|
|
log.Uint16("checking", uint16(r.Number())),
|
|
log.Uint16("current", uint16(currentWrapper.round.Number())))
|
|
return // We're already past this round
|
|
}
|
|
// Check if we need to advance to the next round that was stored by initializeRound
|
|
nextRoundNum := r.Number() + 1
|
|
if nextRoundObj, ok := h.rounds.Load(nextRoundNum); ok {
|
|
nextRound := nextRoundObj.(*roundWrapper).round
|
|
if nextRound.Number() > r.Number() {
|
|
h.currentRound.Store(&roundWrapper{round: nextRound})
|
|
h.log.Info("advancing to next round (already initialized)",
|
|
log.Uint16("from", uint16(r.Number())),
|
|
log.Uint16("to", uint16(nextRound.Number())))
|
|
|
|
// CRITICAL: Check if the new round needs immediate initialization
|
|
// This was previously missed for BroadcastRounds when taking this path
|
|
broadcastRound, isBroadcastRound := nextRound.(round.BroadcastRound)
|
|
if isBroadcastRound {
|
|
bc := broadcastRound.BroadcastContent()
|
|
if bc != nil {
|
|
h.log.Debug("next round needs immediate initialization (BroadcastRound in already-finalized path)",
|
|
log.Uint16("round", uint16(nextRound.Number())))
|
|
go h.initializeRound(nextRound)
|
|
return
|
|
}
|
|
} else if nextRound.MessageContent() != nil {
|
|
h.log.Debug("next round needs immediate initialization (P2P-only in already-finalized path)",
|
|
log.Uint16("round", uint16(nextRound.Number())))
|
|
go h.initializeRound(nextRound)
|
|
return
|
|
}
|
|
// CRITICAL FIX: For "pure finalize" rounds (no BroadcastContent AND no MessageContent),
|
|
// we still need to call initializeRound so that Finalize is called to complete the protocol.
|
|
// Example: LSS sign round3 has no content but its Finalize returns the signature result.
|
|
h.log.Debug("next round is pure-finalize round (no content), initializing",
|
|
log.Uint16("round", uint16(nextRound.Number())))
|
|
go h.initializeRound(nextRound)
|
|
}
|
|
} else {
|
|
// Round is finalized but next round not stored yet - another goroutine is still
|
|
// running initializeRound/finalizeRound. Retry after a short delay to allow it to complete.
|
|
// But first check if the protocol has completed (in which case there's no next round).
|
|
select {
|
|
case <-h.done:
|
|
// Protocol completed, no retry needed
|
|
h.log.Debug("round finalized, protocol completed, no retry needed",
|
|
log.Uint16("round", uint16(r.Number())))
|
|
return
|
|
case <-h.ctx.Done():
|
|
// Context canceled, no retry needed
|
|
return
|
|
default:
|
|
// Check if a retry is already pending for this round to prevent goroutine accumulation
|
|
if _, alreadyPending := h.pendingRetries.LoadOrStore(r.Number(), true); alreadyPending {
|
|
// Another goroutine is already handling the retry, don't spawn another
|
|
return
|
|
}
|
|
|
|
// Protocol still running, schedule retry
|
|
h.log.Debug("round finalized but next round not yet stored, scheduling retry",
|
|
log.Uint16("round", uint16(r.Number())),
|
|
log.Uint16("expected_next", uint16(nextRoundNum)))
|
|
go func() {
|
|
defer h.pendingRetries.Delete(r.Number())
|
|
select {
|
|
case <-time.After(10 * time.Millisecond):
|
|
// Check again if protocol is done before retrying
|
|
select {
|
|
case <-h.done:
|
|
return
|
|
case <-h.ctx.Done():
|
|
return
|
|
default:
|
|
h.tryAdvanceRound()
|
|
}
|
|
case <-h.done:
|
|
return
|
|
case <-h.ctx.Done():
|
|
return
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
return // Already finalized this round
|
|
}
|
|
|
|
h.log.Debug("finalizing round", log.Uint16("round", uint16(r.Number())))
|
|
|
|
// Finalize round and get next round
|
|
nextRound := h.finalizeRound(r)
|
|
if nextRound == nil {
|
|
h.log.Debug("finalizeRound returned nil")
|
|
return
|
|
}
|
|
|
|
if nextRound.Number() == r.Number() {
|
|
// Round returned itself - not ready to advance yet
|
|
// This happens in LSS keygen round 1 when it doesn't have all broadcasts yet
|
|
h.log.Debug("round returned itself in finalizeRound, not advancing",
|
|
log.Uint16("round", uint16(r.Number())))
|
|
// Remove from finalized map so we can try again later
|
|
h.finalized.Delete(r.Number())
|
|
return
|
|
}
|
|
|
|
if nextRound.Number() > r.Number() {
|
|
h.currentRound.Store(&roundWrapper{round: nextRound})
|
|
h.rounds.Store(nextRound.Number(), &roundWrapper{round: nextRound})
|
|
|
|
h.log.Info("storing new currentRound",
|
|
log.Uint16("from", uint16(r.Number())),
|
|
log.Uint16("to", uint16(nextRound.Number())))
|
|
|
|
// First process any unverified messages from the previous round
|
|
if r.Number() > 0 {
|
|
h.processQueuedMessages(r.Number())
|
|
}
|
|
|
|
// Check if the new round needs immediate initialization
|
|
// This happens when a round has MessageContent (sends P2P messages) but doesn't
|
|
// expect any incoming messages initially (like LSS round 2)
|
|
// We need to finalize it immediately to send those messages
|
|
//
|
|
// Determine if the new round needs immediate initialization
|
|
// This is needed for:
|
|
// 1. P2P-only rounds (MessageContent != nil, no BroadcastContent) - to send P2P messages
|
|
// 2. BroadcastRounds - to send their broadcasts
|
|
//
|
|
// Without immediate initialization, BroadcastRounds would deadlock:
|
|
// handler waits for broadcasts, but broadcasts can only be sent by Finalize
|
|
needsImmediateInit := false
|
|
broadcastRound, isBroadcastRound := nextRound.(round.BroadcastRound)
|
|
h.log.Debug("checking round for immediate init",
|
|
log.Uint16("round", uint16(nextRound.Number())),
|
|
log.Bool("isBroadcastRound", isBroadcastRound),
|
|
log.Bool("hasMessageContent", nextRound.MessageContent() != nil))
|
|
if isBroadcastRound {
|
|
bc := broadcastRound.BroadcastContent()
|
|
h.log.Debug("BroadcastContent check",
|
|
log.Uint16("round", uint16(nextRound.Number())),
|
|
log.Bool("hasContent", bc != nil))
|
|
if bc != nil {
|
|
// This is a BroadcastRound - it needs to be initialized to SEND its broadcasts
|
|
needsImmediateInit = true
|
|
h.log.Debug("round needs immediate initialization (BroadcastRound)",
|
|
log.Uint16("round", uint16(nextRound.Number())))
|
|
}
|
|
} else if nextRound.MessageContent() != nil {
|
|
// This is a P2P-only round (like LSS keygen round 2)
|
|
// It needs to be initialized immediately to send messages
|
|
needsImmediateInit = true
|
|
h.log.Debug("round needs immediate initialization (P2P-only round)",
|
|
log.Uint16("round", uint16(nextRound.Number())))
|
|
}
|
|
|
|
if needsImmediateInit {
|
|
// Initialize the round immediately to send its messages
|
|
go h.initializeRound(nextRound)
|
|
} else {
|
|
// Process any queued messages for new round
|
|
go h.processQueuedMessages(nextRound.Number())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Accept accepts a message with non-blocking queue management
|
|
func (h *Handler) Accept(msg *Message) {
|
|
if msg == nil {
|
|
return
|
|
}
|
|
if h.metrics != nil {
|
|
h.metrics.queuedMessages.Inc()
|
|
}
|
|
|
|
// Debug log
|
|
// if msg.RoundNumber == 3 && !msg.Broadcast {
|
|
// fmt.Printf("Accept: p2p round 3 from %s to %s\n", msg.From, msg.To)
|
|
// }
|
|
|
|
// Try priority queue for important messages
|
|
if msg.RoundNumber == 0 || msg.Broadcast {
|
|
select {
|
|
case h.priority <- msg:
|
|
return
|
|
default:
|
|
// Fall through to regular queue
|
|
}
|
|
}
|
|
|
|
// Try regular queue
|
|
select {
|
|
case h.incoming <- msg:
|
|
|
|
case <-h.ctx.Done():
|
|
h.log.Debug("dropping message - context canceled")
|
|
|
|
default:
|
|
// Queue full, drop message
|
|
h.log.Warn("message queue full, dropping message",
|
|
log.String("from", string(msg.From)))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.messagesDropped.Inc()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Result returns the protocol result immediately if available
|
|
func (h *Handler) Result() (interface{}, error) {
|
|
// Check if we already have a result
|
|
if result := h.result.Load(); result != nil {
|
|
duration := time.Since(h.protocolStartTime)
|
|
h.log.Info("protocol completed successfully", log.Duration("duration", duration))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsCompleted.Inc()
|
|
h.metrics.protocolDuration.Observe(duration.Seconds())
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Check if we have an error
|
|
if err := h.err.Load(); err != nil {
|
|
e := err.(*Error)
|
|
h.log.Error("protocol failed", log.Err(e.Err))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsFailed.Inc()
|
|
}
|
|
|
|
return nil, *e
|
|
}
|
|
|
|
// Check if context was canceled
|
|
select {
|
|
case <-h.ctx.Done():
|
|
h.log.Error("protocol canceled")
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsFailed.Inc()
|
|
}
|
|
|
|
return nil, h.ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// Protocol not finished yet
|
|
return nil, errors.New("protocol: not finished")
|
|
}
|
|
|
|
// WaitForResult blocks until the protocol completes or times out
|
|
func (h *Handler) WaitForResult() (interface{}, error) {
|
|
timeout := h.config.ProtocolTimeout
|
|
if timeout == 0 {
|
|
timeout = 5 * time.Minute // Default timeout
|
|
}
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-timer.C:
|
|
h.log.Error("protocol timeout", log.Duration("timeout", h.config.ProtocolTimeout))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsFailed.Inc()
|
|
}
|
|
|
|
return nil, errors.New("protocol: timeout waiting for result")
|
|
|
|
case <-ticker.C:
|
|
if result := h.result.Load(); result != nil {
|
|
duration := time.Since(h.protocolStartTime)
|
|
h.log.Info("protocol completed successfully", log.Duration("duration", duration))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsCompleted.Inc()
|
|
h.metrics.protocolDuration.Observe(duration.Seconds())
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
if err := h.err.Load(); err != nil {
|
|
e := err.(*Error)
|
|
h.log.Error("protocol failed", log.Err(e.Err))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsFailed.Inc()
|
|
}
|
|
|
|
return nil, *e
|
|
}
|
|
|
|
case <-h.ctx.Done():
|
|
// When context is done, first check if we have a result
|
|
// This handles the race where protocol completes and cancels context
|
|
// but context Done fires before ticker case gets the result
|
|
if result := h.result.Load(); result != nil {
|
|
duration := time.Since(h.protocolStartTime)
|
|
h.log.Info("protocol completed successfully (caught on context done)", log.Duration("duration", duration))
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsCompleted.Inc()
|
|
h.metrics.protocolDuration.Observe(duration.Seconds())
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
h.log.Error("protocol canceled")
|
|
|
|
if h.metrics != nil {
|
|
h.metrics.protocolsFailed.Inc()
|
|
}
|
|
|
|
return nil, h.ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Listen returns the output channel
|
|
func (h *Handler) Listen() <-chan *Message {
|
|
return h.out
|
|
}
|
|
|
|
// Stop gracefully shuts down the handler
|
|
func (h *Handler) Stop() {
|
|
h.stopOnce.Do(func() {
|
|
h.log.Info("stopping protocol handler")
|
|
|
|
// Mark as stopped
|
|
h.stopped.Store(true)
|
|
|
|
// Cancel context to stop all workers
|
|
h.cancel()
|
|
|
|
// Wait for workers to finish
|
|
h.workerGroup.Wait()
|
|
|
|
// Close channels safely (out may already be closed by protocol completion).
|
|
// Take outMu so any in-flight sendRoundMessage drains before we close.
|
|
h.outMu.Lock()
|
|
h.outClosed.Store(true)
|
|
h.closeOnce.Do(func() {
|
|
close(h.out)
|
|
})
|
|
h.outMu.Unlock()
|
|
|
|
// Close other channels
|
|
close(h.incoming)
|
|
close(h.priority)
|
|
h.closeDoneOnce.Do(func() {
|
|
close(h.done)
|
|
})
|
|
|
|
h.log.Info("protocol handler stopped",
|
|
log.Uint64("messages_processed", h.messagesProcessed),
|
|
log.Uint64("rounds_completed", h.roundsCompleted))
|
|
})
|
|
}
|
|
|
|
// CanAccept checks if a message can be accepted
|
|
func (h *Handler) CanAccept(msg *Message) bool {
|
|
if msg == nil || msg.Data == nil {
|
|
return false
|
|
}
|
|
|
|
r := h.currentRound.Load().(*roundWrapper).round
|
|
|
|
// Check protocol and session ID
|
|
if msg.Protocol != r.ProtocolID() {
|
|
return false
|
|
}
|
|
|
|
if !bytes.Equal(msg.SSID, r.SSID()) {
|
|
return false
|
|
}
|
|
|
|
// Check if we're the intended recipient
|
|
if !msg.IsFor(r.SelfID()) {
|
|
return false
|
|
}
|
|
|
|
// Check sender is valid
|
|
if !r.PartyIDs().Contains(msg.From) {
|
|
return false
|
|
}
|
|
|
|
// Check round number is valid
|
|
if msg.RoundNumber > r.FinalRoundNumber() {
|
|
return false
|
|
}
|
|
|
|
if msg.RoundNumber < r.Number() && msg.RoundNumber > 0 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// finalize method removed - functionality integrated into NewHandler
|
|
|
|
// metricsUpdater periodically updates gauge metrics
|
|
func (h *Handler) metricsUpdater() {
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-h.ctx.Done():
|
|
return
|
|
|
|
case <-ticker.C:
|
|
// Update queue depth
|
|
h.metrics.queuedMessages.Set(float64(len(h.incoming) + len(h.priority)))
|
|
|
|
// Update memory usage
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
h.metrics.memoryUsage.Set(float64(m.Alloc))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper methods for perfect protocol execution...
|
|
|
|
func (h *Handler) initializeRound(r round.Session) {
|
|
if r == nil {
|
|
h.log.Debug("initializeRound called with nil round, ignoring")
|
|
return
|
|
}
|
|
h.log.Debug("initializing round", log.Uint16("round", uint16(r.Number())))
|
|
|
|
// Process any messages that might already be waiting BEFORE claiming finalized lock
|
|
h.processQueuedMessages(r.Number())
|
|
|
|
// Check if this round needs to wait for incoming messages before Finalize.
|
|
// There are three cases where we must wait:
|
|
//
|
|
// 1. FINAL round - aggregates data from all parties (e.g., SigmaShares in sign round 5)
|
|
// The final round's Finalize computes the final result using data from all parties.
|
|
//
|
|
// 2. Round with MessageContent (P2P messages) - StoreMessage populates state that Finalize uses
|
|
// Example: Sign round 3 - StoreMessage sets DeltaShareAlpha[from], Finalize reads DeltaShareAlpha[j]
|
|
// If we call Finalize before StoreMessage, we get nil pointer panics.
|
|
//
|
|
// 3. Round with BroadcastContent (broadcast messages) - StoreBroadcastMessage populates state
|
|
// Example: FROST round 2 - StoreBroadcastMessage sets ChainKeyCommitments, Finalize checks count
|
|
// If we call Finalize before StoreBroadcastMessage, we get "missing chain key commitments" errors.
|
|
//
|
|
// Note: Round 1 only SENDS broadcasts via Finalize, it doesn't RECEIVE broadcasts.
|
|
// Rounds 2+ that have BroadcastContent DO receive broadcasts from the previous round.
|
|
// So we only wait for broadcasts if r.Number() > 1.
|
|
isFinalRound := r.Number() == r.FinalRoundNumber()
|
|
hasIncomingP2P := r.MessageContent() != nil
|
|
hasIncomingBroadcast := false
|
|
if r.Number() > 1 {
|
|
if br, ok := r.(round.BroadcastRound); ok && br.BroadcastContent() != nil {
|
|
hasIncomingBroadcast = true
|
|
}
|
|
}
|
|
|
|
// Determine if we should wait for messages before calling Finalize.
|
|
//
|
|
// Key insight: Some rounds (like LSS round 2) use a "send-first" pattern where
|
|
// Finalize sends P2P messages first, then returns self to wait for replies.
|
|
// Other rounds (like CMP/FROST) receive messages from the previous round's Finalize
|
|
// and expect that data to be available when their Finalize is called.
|
|
//
|
|
// We can distinguish these cases by checking if ANY messages have arrived:
|
|
// - If messages have arrived: they're "in flight" from previous round → wait for all
|
|
// - If no messages yet: this might be a "send-first" round → call Finalize
|
|
shouldWait := false
|
|
|
|
if hasIncomingP2P {
|
|
// For P2P-only rounds (like LSS round 2), we should ALWAYS call Finalize first.
|
|
// The round's Finalize handles the "send-first, wait-for-replies" pattern:
|
|
// - First Finalize: sends shares, stores own share, returns self
|
|
// - Second Finalize: checks if all shares received, returns next round or self
|
|
//
|
|
// We previously waited if some P2P messages had arrived, but this caused a deadlock:
|
|
// - Party A waits for all shares before Finalize
|
|
// - But party A has shares that others need
|
|
// - Everyone waits, no one sends
|
|
//
|
|
// Now we always call Finalize for P2P rounds, and let the round logic decide.
|
|
// The round will return itself if it's not ready to advance.
|
|
h.log.Debug("P2P round - will call Finalize to send shares",
|
|
log.Uint16("round", uint16(r.Number())))
|
|
}
|
|
|
|
if hasIncomingBroadcast {
|
|
// For broadcast rounds, we must ALWAYS call Finalize first to send our own broadcast.
|
|
// Only wait if we've already sent our broadcast (check if it's stored).
|
|
// CRITICAL FIX: If we wait before sending our own broadcast, we deadlock:
|
|
// - Party A waits for B and C's broadcasts
|
|
// - Party B waits for A and C's broadcasts
|
|
// - No one sends, everyone waits
|
|
selfBroadcastKey := fmt.Sprintf("%d:%s", r.Number(), r.SelfID())
|
|
broadcasts := h.broadcast.LoadAll(r.Number())
|
|
hasSentOwnBroadcast := broadcasts[r.SelfID()] != nil
|
|
|
|
if hasSentOwnBroadcast && !h.hasAllMessages(r) {
|
|
// We've sent our broadcast, now wait for others
|
|
shouldWait = true
|
|
h.log.Debug("broadcast round: sent own broadcast, waiting for others",
|
|
log.Uint16("round", uint16(r.Number())),
|
|
log.String("self", string(r.SelfID())))
|
|
} else if !hasSentOwnBroadcast {
|
|
// Haven't sent our broadcast yet - MUST call Finalize first
|
|
h.log.Debug("broadcast round: need to send own broadcast first",
|
|
log.Uint16("round", uint16(r.Number())),
|
|
log.String("self", string(r.SelfID())))
|
|
}
|
|
_ = selfBroadcastKey // suppress unused warning
|
|
}
|
|
|
|
// Final round always waits for all messages
|
|
if isFinalRound && !h.hasAllMessages(r) {
|
|
shouldWait = true
|
|
}
|
|
|
|
if shouldWait {
|
|
h.log.Debug("round waiting for all messages",
|
|
log.Uint16("round", uint16(r.Number())),
|
|
log.Bool("isFinal", isFinalRound),
|
|
log.Bool("hasP2P", hasIncomingP2P),
|
|
log.Bool("hasBroadcast", hasIncomingBroadcast))
|
|
return
|
|
}
|
|
|
|
// Now we're ready to finalize - claim the lock
|
|
if finalized, loaded := h.finalized.LoadOrStore(r.Number(), true); loaded && finalized.(bool) {
|
|
h.log.Debug("round already being finalized, skipping",
|
|
log.Uint16("round", uint16(r.Number())))
|
|
return
|
|
}
|
|
|
|
// IMPORTANT: Call Finalize for rounds that:
|
|
// 1. Need to send broadcasts/P2P messages
|
|
// 2. Have all required incoming messages
|
|
//
|
|
// The round's Finalize method handles:
|
|
// 1. Sending broadcasts/P2P messages
|
|
// 2. Returning itself if waiting for incoming messages
|
|
// 3. Returning the next round when ready to advance
|
|
|
|
out := make(chan *round.Message, r.N()+1)
|
|
|
|
// Start a goroutine to call Finalize and close the channel
|
|
var nextRound round.Session
|
|
var finalizeErr error
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(out)
|
|
defer close(done)
|
|
// Hold roundMu so concurrent VerifyMessage / StoreMessage on the same
|
|
// round (or its shared state) does not race with Finalize's reads.
|
|
h.roundMu.Lock()
|
|
defer h.roundMu.Unlock()
|
|
nextRound, finalizeErr = r.Finalize(out)
|
|
}()
|
|
|
|
// Count messages for logging
|
|
messageCount := 0
|
|
|
|
// Process generated messages
|
|
for msg := range out {
|
|
messageCount++
|
|
h.sendRoundMessage(msg, r)
|
|
}
|
|
|
|
// Wait for Finalize to complete
|
|
<-done
|
|
|
|
if finalizeErr != nil {
|
|
h.handleError(finalizeErr, r.SelfID())
|
|
return
|
|
}
|
|
|
|
h.log.Debug("generated messages",
|
|
log.Uint16("round", uint16(r.Number())),
|
|
log.Int("count", messageCount))
|
|
|
|
// Store next round and advance if appropriate
|
|
if nextRound != nil {
|
|
// Check for protocol completion first
|
|
if output, ok := nextRound.(*round.Output); ok {
|
|
h.result.Store(output.Result)
|
|
h.log.Info("protocol completed in initialization",
|
|
log.String("self", string(r.SelfID())),
|
|
log.Uint16("final_round", uint16(r.Number())))
|
|
// Close done channel to signal completion to workers
|
|
h.closeDoneOnce.Do(func() {
|
|
close(h.done)
|
|
})
|
|
// Cancel context immediately to stop all workers
|
|
// This must happen before WaitForResult returns
|
|
h.cancel()
|
|
// Close output channel asynchronously to allow final message delivery
|
|
go func() {
|
|
time.Sleep(10 * time.Millisecond)
|
|
h.outMu.Lock()
|
|
h.outClosed.Store(true)
|
|
h.closeOnce.Do(func() {
|
|
close(h.out)
|
|
})
|
|
h.outMu.Unlock()
|
|
}()
|
|
return
|
|
}
|
|
|
|
// Check for protocol abort
|
|
if abort, ok := nextRound.(*round.Abort); ok {
|
|
h.handleError(abort.Err, abort.Culprits...)
|
|
return
|
|
}
|
|
|
|
if nextRound.Number() > r.Number() {
|
|
// Store the next round for later use but DON'T advance to it yet
|
|
// We need to wait for all round 1 messages to be processed first
|
|
h.rounds.Store(nextRound.Number(), &roundWrapper{round: nextRound})
|
|
// DON'T change currentRound yet - wait for tryAdvanceRound to do it
|
|
h.log.Info("stored next round after initialization",
|
|
log.Uint16("from", uint16(r.Number())),
|
|
log.Uint16("next", uint16(nextRound.Number())))
|
|
|
|
// Try to advance (which will check if we have all messages)
|
|
go func() {
|
|
time.Sleep(10 * time.Millisecond) // Small delay for message propagation
|
|
h.tryAdvanceRound()
|
|
}()
|
|
} else if nextRound.Number() == r.Number() {
|
|
// Round returned itself - not ready to advance yet
|
|
// This happens in some protocols when waiting for messages
|
|
h.log.Debug("round returned itself in initialization",
|
|
log.Uint16("round", uint16(r.Number())),
|
|
log.Bool("returned_self_in_initialize", true))
|
|
|
|
// IMPORTANT: Unmark as finalized so it can be finalized again later
|
|
// This is critical for protocols like LSS that return themselves when not ready
|
|
h.finalized.Delete(r.Number())
|
|
|
|
// CRITICAL FIX: Re-drive advancement immediately
|
|
// Process any messages that arrived while we were initializing
|
|
h.processQueuedMessages(r.Number())
|
|
// Try to advance now that our outbound is sent + inbound is queued
|
|
// Use a select with a timer to avoid blocking forever
|
|
go func() {
|
|
select {
|
|
case <-time.After(10 * time.Millisecond):
|
|
h.tryAdvanceRound()
|
|
case <-h.ctx.Done():
|
|
// Context canceled, don't try to advance
|
|
return
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
// safeSend sends a message to the output channel without blocking.
|
|
// Takes outMu to serialize with concurrent close paths (Stop, protocol-complete).
|
|
// Returns false if the channel is full, closed, or the handler has stopped.
|
|
func (h *Handler) safeSend(msg *Message) (sent bool) {
|
|
h.outMu.RLock()
|
|
defer h.outMu.RUnlock()
|
|
if h.outClosed.Load() {
|
|
return false
|
|
}
|
|
select {
|
|
case h.out <- msg:
|
|
sent = true
|
|
default:
|
|
sent = false
|
|
}
|
|
return
|
|
}
|
|
|
|
func (h *Handler) sendRoundMessage(msg *round.Message, r round.Session) {
|
|
h.log.Debug("sendRoundMessage",
|
|
log.String("from", string(r.SelfID())),
|
|
log.String("to", string(msg.To)),
|
|
log.Uint16("round", uint16(msg.Content.RoundNumber())),
|
|
log.Bool("broadcast", msg.Broadcast))
|
|
|
|
data, err := cbor.Marshal(msg.Content)
|
|
if err != nil {
|
|
h.handleError(err, r.SelfID())
|
|
return
|
|
}
|
|
|
|
// Compress if large
|
|
compressed := false
|
|
if h.config.EnableCompression && len(data) > h.config.CompressionThreshold {
|
|
data = h.compressData(data)
|
|
compressed = true
|
|
}
|
|
|
|
protocolMsg := &Message{
|
|
SSID: r.SSID(),
|
|
From: r.SelfID(),
|
|
To: msg.To,
|
|
Protocol: r.ProtocolID(),
|
|
RoundNumber: msg.Content.RoundNumber(),
|
|
Data: data,
|
|
Broadcast: msg.Broadcast,
|
|
Compressed: compressed,
|
|
}
|
|
|
|
if msg.Broadcast {
|
|
h.storeMessage(protocolMsg)
|
|
}
|
|
|
|
// Take the read lock so Stop / protocol-complete close paths cannot close
|
|
// h.out between the outClosed check and the chan send below.
|
|
h.outMu.RLock()
|
|
defer h.outMu.RUnlock()
|
|
if h.stopped.Load() || h.outClosed.Load() {
|
|
h.log.Debug("skipping send - handler stopped or channel closed")
|
|
return
|
|
}
|
|
|
|
select {
|
|
case h.out <- protocolMsg:
|
|
h.log.Debug("sent message to output channel",
|
|
log.String("from", string(protocolMsg.From)),
|
|
log.String("to", string(protocolMsg.To)),
|
|
log.Uint16("round", uint16(protocolMsg.RoundNumber)),
|
|
log.Bool("broadcast", protocolMsg.Broadcast))
|
|
if h.metrics != nil {
|
|
h.metrics.messagesSent.Inc()
|
|
}
|
|
case <-h.ctx.Done():
|
|
h.log.Debug("failed to send message - context canceled")
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleAbort(msg *Message) {
|
|
err := fmt.Errorf("aborted by %s: %s", msg.From, msg.Data)
|
|
h.log.Warn("protocol aborted", log.String("from", string(msg.From)))
|
|
h.handleError(err, msg.From)
|
|
}
|
|
|
|
func (h *Handler) handleError(err error, culprits ...party.ID) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
protocolErr := &Error{
|
|
Err: err,
|
|
Culprits: culprits,
|
|
}
|
|
|
|
// Try to set error atomically
|
|
if h.err.CompareAndSwap(nil, protocolErr) {
|
|
h.log.Error("protocol error - canceling context",
|
|
log.Err(err),
|
|
log.String("culprits", fmt.Sprintf("%v", culprits)))
|
|
|
|
// Send abort message using safeSend to handle race with channel close
|
|
if !h.outClosed.Load() {
|
|
r := h.currentRound.Load().(*roundWrapper).round
|
|
abortMsg := &Message{
|
|
SSID: r.SSID(),
|
|
From: r.SelfID(),
|
|
Protocol: r.ProtocolID(),
|
|
Data: []byte(err.Error()),
|
|
}
|
|
h.safeSend(abortMsg)
|
|
}
|
|
|
|
h.cancel()
|
|
|
|
// Close output channel after delay to signal protocol end
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
h.outMu.Lock()
|
|
h.outClosed.Store(true)
|
|
h.closeOnce.Do(func() {
|
|
close(h.out)
|
|
})
|
|
h.outMu.Unlock()
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (h *Handler) finalizeRound(r round.Session) round.Session {
|
|
// Check if we already have the next round stored (from initializeRound)
|
|
nextRoundNum := r.Number() + 1
|
|
if nextRoundObj, ok := h.rounds.Load(nextRoundNum); ok {
|
|
h.log.Debug("found existing next round",
|
|
log.Uint16("current", uint16(r.Number())),
|
|
log.Uint16("next", uint16(nextRoundNum)))
|
|
return nextRoundObj.(*roundWrapper).round
|
|
}
|
|
|
|
// We need to finalize this round
|
|
out := make(chan *round.Message, r.N()+1)
|
|
|
|
// Use goroutine like initializeRound does to allow async message generation
|
|
var nextRound round.Session
|
|
var err error
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(out)
|
|
defer close(done)
|
|
// Hold roundMu so concurrent VerifyMessage / StoreMessage on the same
|
|
// round (or its shared state — Pedersen params, etc.) does not race
|
|
// with Finalize's read of those values.
|
|
h.roundMu.Lock()
|
|
defer h.roundMu.Unlock()
|
|
nextRound, err = r.Finalize(out)
|
|
}()
|
|
|
|
// Count messages for logging
|
|
messageCount := 0
|
|
// Process generated messages
|
|
for msg := range out {
|
|
messageCount++
|
|
h.sendRoundMessage(msg, r)
|
|
}
|
|
|
|
// Wait for Finalize to complete
|
|
<-done
|
|
|
|
if err != nil {
|
|
h.handleError(err, r.SelfID())
|
|
return nil
|
|
}
|
|
|
|
h.log.Debug("finalized round messages",
|
|
log.Uint16("round", uint16(r.Number())),
|
|
log.Int("messages", messageCount),
|
|
log.Bool("returned_self", nextRound == r))
|
|
|
|
// CRITICAL FIX: If the round returns itself, immediately attempt to advance again
|
|
// in case inbound messages arrived while we were finalizing/sending
|
|
if nextRound == r {
|
|
h.log.Debug("round returned itself in finalize, re-driving advancement",
|
|
log.Bool("returned_self_in_finalize", true))
|
|
// Process any messages queued during finalize
|
|
h.processQueuedMessages(r.Number())
|
|
// Try to advance again with small delay for network propagation
|
|
go func() {
|
|
select {
|
|
case <-time.After(10 * time.Millisecond):
|
|
h.tryAdvanceRound()
|
|
case <-h.ctx.Done():
|
|
// Context canceled, don't try to advance
|
|
return
|
|
}
|
|
}()
|
|
return r
|
|
}
|
|
|
|
// Check for completion
|
|
switch result := nextRound.(type) {
|
|
case *round.Output:
|
|
h.result.Store(result.Result)
|
|
h.log.Info("protocol completed",
|
|
log.String("self", string(r.SelfID())),
|
|
log.Uint16("final_round", uint16(r.Number())))
|
|
// Close done channel to signal completion to workers
|
|
h.closeDoneOnce.Do(func() {
|
|
close(h.done)
|
|
})
|
|
// Close output channel to signal HandlerLoop that protocol is complete
|
|
// This is safe because we're done sending messages
|
|
go func() {
|
|
// Give a small delay to allow any final messages to be sent
|
|
time.Sleep(10 * time.Millisecond)
|
|
h.outMu.Lock()
|
|
h.outClosed.Store(true)
|
|
h.closeOnce.Do(func() {
|
|
close(h.out)
|
|
})
|
|
h.outMu.Unlock()
|
|
// Clean up all goroutines after closing the output channel
|
|
time.Sleep(10 * time.Millisecond)
|
|
h.cancel() // Cancel context to stop all workers
|
|
}()
|
|
return nil
|
|
|
|
case *round.Abort:
|
|
h.handleError(result.Err, result.Culprits...)
|
|
return nil
|
|
}
|
|
|
|
if nextRound != nil {
|
|
h.log.Debug("finalize returned next round",
|
|
log.Uint16("current", uint16(r.Number())),
|
|
log.Uint16("next", uint16(nextRound.Number())))
|
|
} else {
|
|
h.log.Debug("finalize returned nil")
|
|
}
|
|
|
|
return nextRound
|
|
}
|
|
|
|
func (h *Handler) verifyBroadcastForRound(msg *Message, roundNum round.Number) {
|
|
// Prevent concurrent verification of the same message
|
|
// This is critical because ZK proof verification is expensive
|
|
key := fmt.Sprintf("%d:%s", roundNum, msg.From)
|
|
if _, loaded := h.verifyingBroadcasts.LoadOrStore(key, true); loaded {
|
|
return // Already being verified by another goroutine
|
|
}
|
|
defer h.verifyingBroadcasts.Delete(key)
|
|
|
|
// Check if already processed (double-check after acquiring the verification lock)
|
|
if _, processed := h.processedBroadcasts.Load(key); processed {
|
|
return
|
|
}
|
|
|
|
// Verify a broadcast message for a specific round
|
|
roundObj, ok := h.rounds.Load(roundNum)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
r := roundObj.(*roundWrapper).round
|
|
broadcastRound, ok := r.(round.BroadcastRound)
|
|
if !ok {
|
|
h.handleError(errors.New("unexpected broadcast message"), msg.From)
|
|
return
|
|
}
|
|
|
|
// Unmarshal content
|
|
content := broadcastRound.BroadcastContent()
|
|
if err := cbor.Unmarshal(msg.Data, content); err != nil {
|
|
h.handleError(err, msg.From)
|
|
return
|
|
}
|
|
|
|
roundMsg := round.Message{
|
|
From: msg.From,
|
|
To: msg.To,
|
|
Content: content,
|
|
Broadcast: true,
|
|
}
|
|
|
|
h.roundMu.Lock()
|
|
defer h.roundMu.Unlock()
|
|
|
|
if err := broadcastRound.StoreBroadcastMessage(roundMsg); err != nil {
|
|
// If the round is not ready, don't treat as error - message remains queued
|
|
if err == round.ErrNotReady {
|
|
h.log.Debug("round not ready for broadcast message, will retry later",
|
|
log.String("from", string(msg.From)),
|
|
log.Uint16("round", uint16(roundNum)))
|
|
return
|
|
}
|
|
h.handleError(err, msg.From)
|
|
} else {
|
|
// Mark this broadcast as processed
|
|
key := fmt.Sprintf("%d:%s", roundNum, msg.From)
|
|
h.processedBroadcasts.Store(key, true)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) verifyNormalForRound(msg *Message, roundNum round.Number) {
|
|
// Prevent concurrent verification of the same message
|
|
key := fmt.Sprintf("%d:%s", roundNum, msg.From)
|
|
if _, loaded := h.verifyingMessages.LoadOrStore(key, true); loaded {
|
|
return // Already being verified by another goroutine
|
|
}
|
|
defer h.verifyingMessages.Delete(key)
|
|
|
|
// Check if already processed (double-check after acquiring the verification lock)
|
|
if _, processed := h.processedMessages.Load(key); processed {
|
|
return
|
|
}
|
|
|
|
// Verify a normal message for a specific round
|
|
roundObj, ok := h.rounds.Load(roundNum)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
r := roundObj.(*roundWrapper).round
|
|
|
|
// Check if we have required broadcast first
|
|
// Only check if BroadcastContent() is non-nil (some rounds embed BroadcastRound but don't use it)
|
|
// CRITICAL: We must check processedBroadcasts (set after StoreBroadcastMessage succeeds),
|
|
// not just if the broadcast message is queued. StoreBroadcastMessage populates round state
|
|
// (e.g., r.K[from]) that VerifyMessage depends on.
|
|
if br, ok := r.(round.BroadcastRound); ok && br.BroadcastContent() != nil {
|
|
key := fmt.Sprintf("%d:%s", r.Number(), msg.From)
|
|
if _, processed := h.processedBroadcasts.Load(key); !processed {
|
|
h.log.Debug("waiting for broadcast to be processed before normal message",
|
|
log.String("from", string(msg.From)),
|
|
log.Uint16("round", uint16(r.Number())))
|
|
return
|
|
}
|
|
}
|
|
|
|
// Unmarshal content
|
|
content := r.MessageContent()
|
|
if content == nil {
|
|
return
|
|
}
|
|
|
|
if err := cbor.Unmarshal(msg.Data, content); err != nil {
|
|
h.handleError(err, msg.From)
|
|
return
|
|
}
|
|
|
|
// Create round message
|
|
roundMsg := round.Message{
|
|
From: msg.From,
|
|
To: msg.To,
|
|
Content: content,
|
|
}
|
|
|
|
// Verify + Store under roundMu so concurrent verifications for different
|
|
// (from) pairs in the same round don't race on shared round state.
|
|
h.roundMu.Lock()
|
|
defer h.roundMu.Unlock()
|
|
|
|
if err := r.VerifyMessage(roundMsg); err != nil {
|
|
h.handleError(err, msg.From)
|
|
return
|
|
}
|
|
|
|
if err := r.StoreMessage(roundMsg); err != nil {
|
|
// If the round is not ready, don't treat as error - message remains queued
|
|
if err == round.ErrNotReady {
|
|
h.log.Debug("round not ready for p2p message, will retry later",
|
|
log.String("from", string(msg.From)),
|
|
log.Uint16("round", uint16(roundNum)))
|
|
return
|
|
}
|
|
h.handleError(err, msg.From)
|
|
} else {
|
|
// Mark this message as processed
|
|
key := fmt.Sprintf("%d:%s", roundNum, msg.From)
|
|
h.processedMessages.Store(key, true)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) verifyBroadcast(msg *Message) {
|
|
// Only verify messages for the current round
|
|
currentRound := h.currentRound.Load().(*roundWrapper).round
|
|
if msg.RoundNumber != currentRound.Number() {
|
|
h.log.Debug("skipping verification for different round",
|
|
log.Uint16("msg_round", uint16(msg.RoundNumber)),
|
|
log.Uint16("current_round", uint16(currentRound.Number())))
|
|
return
|
|
}
|
|
|
|
roundObj, ok := h.rounds.Load(msg.RoundNumber)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
r := roundObj.(*roundWrapper).round
|
|
broadcastRound, ok := r.(round.BroadcastRound)
|
|
if !ok {
|
|
h.handleError(errors.New("unexpected broadcast message"), msg.From)
|
|
return
|
|
}
|
|
|
|
// For now, skip broadcast hash verification (would implement properly)
|
|
// This needs proper integration with the hash package
|
|
|
|
// Unmarshal content
|
|
content := broadcastRound.BroadcastContent()
|
|
if err := cbor.Unmarshal(msg.Data, content); err != nil {
|
|
h.handleError(err, msg.From)
|
|
return
|
|
}
|
|
|
|
roundMsg := round.Message{
|
|
From: msg.From,
|
|
To: msg.To,
|
|
Content: content,
|
|
Broadcast: true,
|
|
}
|
|
|
|
h.roundMu.Lock()
|
|
defer h.roundMu.Unlock()
|
|
|
|
if err := broadcastRound.StoreBroadcastMessage(roundMsg); err != nil {
|
|
// If the round is not ready, don't treat as error - just skip for now
|
|
// The message will be retried when we process queued messages
|
|
if err == round.ErrNotReady {
|
|
h.log.Debug("round not ready for broadcast message, will retry later",
|
|
log.String("from", string(msg.From)),
|
|
log.Uint16("round", uint16(msg.RoundNumber)))
|
|
return
|
|
}
|
|
h.handleError(err, msg.From)
|
|
} else {
|
|
// Mark this broadcast as processed
|
|
key := fmt.Sprintf("%d:%s", msg.RoundNumber, msg.From)
|
|
h.processedBroadcasts.Store(key, true)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) verifyNormal(msg *Message) {
|
|
// Only verify messages for the current round
|
|
currentRound := h.currentRound.Load().(*roundWrapper).round
|
|
if msg.RoundNumber != currentRound.Number() {
|
|
h.log.Debug("skipping verification for different round",
|
|
log.Uint16("msg_round", uint16(msg.RoundNumber)),
|
|
log.Uint16("current_round", uint16(currentRound.Number())))
|
|
return
|
|
}
|
|
|
|
roundObj, ok := h.rounds.Load(msg.RoundNumber)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
r := roundObj.(*roundWrapper).round
|
|
|
|
// Check if we have required broadcast first
|
|
// Only check if BroadcastContent() is non-nil (some rounds embed BroadcastRound but don't use it)
|
|
// CRITICAL: We must check processedBroadcasts (set after StoreBroadcastMessage succeeds),
|
|
// not just if the broadcast message is queued. StoreBroadcastMessage populates round state
|
|
// (e.g., r.K[from]) that VerifyMessage depends on.
|
|
if br, isBroadcast := r.(round.BroadcastRound); isBroadcast && br.BroadcastContent() != nil {
|
|
key := fmt.Sprintf("%d:%s", msg.RoundNumber, msg.From)
|
|
if _, processed := h.processedBroadcasts.Load(key); !processed {
|
|
h.log.Debug("waiting for broadcast to be processed before normal message",
|
|
log.String("from", string(msg.From)),
|
|
log.Uint16("round", uint16(msg.RoundNumber)))
|
|
return // Wait for broadcast to be processed first
|
|
}
|
|
}
|
|
|
|
// Unmarshal content
|
|
content := r.MessageContent()
|
|
if content == nil {
|
|
return // Round doesn't expect messages
|
|
}
|
|
|
|
if err := cbor.Unmarshal(msg.Data, content); err != nil {
|
|
h.handleError(err, msg.From)
|
|
return
|
|
}
|
|
|
|
roundMsg := round.Message{
|
|
From: msg.From,
|
|
To: msg.To,
|
|
Content: content,
|
|
}
|
|
|
|
h.roundMu.Lock()
|
|
defer h.roundMu.Unlock()
|
|
|
|
if err := r.VerifyMessage(roundMsg); err != nil {
|
|
h.handleError(err, msg.From)
|
|
return
|
|
}
|
|
|
|
if err := r.StoreMessage(roundMsg); err != nil {
|
|
// If the round is not ready, don't treat as error - just skip for now
|
|
// The message will be retried when we process queued messages
|
|
if err == round.ErrNotReady {
|
|
h.log.Debug("round not ready for p2p message, will retry later",
|
|
log.String("from", string(msg.From)),
|
|
log.Uint16("round", uint16(msg.RoundNumber)))
|
|
return
|
|
}
|
|
h.handleError(err, msg.From)
|
|
} else {
|
|
// Mark this message as processed
|
|
key := fmt.Sprintf("%d:%s", msg.RoundNumber, msg.From)
|
|
h.processedMessages.Store(key, true)
|
|
}
|
|
}
|
|
|
|
// retryUnprocessedMessages retries messages that may have returned ErrNotReady
|
|
func (h *Handler) retryUnprocessedMessages(roundNum round.Number) {
|
|
// Process broadcasts that haven't been processed yet
|
|
broadcasts := h.broadcast.LoadAll(roundNum)
|
|
r := h.currentRound.Load().(*roundWrapper).round
|
|
for from, msg := range broadcasts {
|
|
if msg != nil && from != r.SelfID() {
|
|
// Check if already processed
|
|
key := fmt.Sprintf("%d:%s", roundNum, from)
|
|
if _, processed := h.processedBroadcasts.Load(key); !processed {
|
|
// Retry processing the broadcast message
|
|
h.verifyBroadcastForRound(msg, roundNum)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process normal messages that haven't been processed yet
|
|
messages := h.messages.LoadAll(roundNum)
|
|
for from, msg := range messages {
|
|
if msg != nil {
|
|
// Check if already processed
|
|
key := fmt.Sprintf("%d:%s", roundNum, from)
|
|
if _, processed := h.processedMessages.Load(key); !processed {
|
|
// Retry processing the normal message
|
|
h.verifyNormalForRound(msg, roundNum)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Handler) processQueuedMessages(roundNum round.Number) {
|
|
h.log.Debug("processing queued messages", log.Uint16("round", uint16(roundNum)))
|
|
|
|
// First process any messages from previous rounds to ensure we have all necessary data
|
|
if roundNum > 1 {
|
|
for prevRound := round.Number(1); prevRound < roundNum; prevRound++ {
|
|
// Process broadcasts from previous rounds
|
|
prevBroadcasts := h.broadcast.LoadAll(prevRound)
|
|
for _, msg := range prevBroadcasts {
|
|
if msg != nil && msg.From != h.currentRound.Load().(*roundWrapper).round.SelfID() {
|
|
// Temporarily set h.processingPreviousRound to allow verification
|
|
h.verifyBroadcastForRound(msg, prevRound)
|
|
}
|
|
}
|
|
|
|
// Process normal messages from previous rounds
|
|
prevMessages := h.messages.LoadAll(prevRound)
|
|
for _, msg := range prevMessages {
|
|
if msg != nil {
|
|
h.verifyNormalForRound(msg, prevRound)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now process messages for the current round with retry logic
|
|
// Retry up to 3 times to handle ErrNotReady cases
|
|
for retry := 0; retry < 3; retry++ {
|
|
anyRetried := false
|
|
|
|
// Process broadcasts first
|
|
broadcasts := h.broadcast.LoadAll(roundNum)
|
|
r := h.currentRound.Load().(*roundWrapper).round
|
|
|
|
for from, msg := range broadcasts {
|
|
if msg != nil && from != r.SelfID() {
|
|
// Check if already processed
|
|
key := fmt.Sprintf("%d:%s", roundNum, from)
|
|
if _, processed := h.processedBroadcasts.Load(key); !processed {
|
|
// Process the queued message directly since we know it's for the right round
|
|
h.verifyBroadcastForRound(msg, roundNum)
|
|
anyRetried = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then process normal messages
|
|
messages := h.messages.LoadAll(roundNum)
|
|
for from, msg := range messages {
|
|
if msg != nil {
|
|
// Check if already processed
|
|
key := fmt.Sprintf("%d:%s", roundNum, from)
|
|
if _, processed := h.processedMessages.Load(key); !processed {
|
|
// Process the queued message directly
|
|
h.verifyNormalForRound(msg, roundNum)
|
|
anyRetried = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// If nothing was retried, we're done
|
|
if !anyRetried {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Handler) hasAllMessages(r round.Session) bool {
|
|
number := r.Number()
|
|
|
|
// Check broadcasts - but only if BroadcastContent() returns non-nil
|
|
// Some rounds embed BroadcastRound but don't actually use broadcasts
|
|
if br, ok := r.(round.BroadcastRound); ok && br.BroadcastContent() != nil {
|
|
broadcasts := h.broadcast.LoadAll(number)
|
|
missingBroadcasts := []party.ID{}
|
|
unprocessedBroadcasts := []party.ID{}
|
|
|
|
for _, id := range r.PartyIDs() {
|
|
// Skip self - we send our own broadcast, we don't wait for it
|
|
// CRITICAL: This must be checked BEFORE the nil check to prevent livelock
|
|
// where a party waits for its own broadcast that hasn't been sent yet.
|
|
if id == r.SelfID() {
|
|
continue
|
|
}
|
|
if broadcasts[id] == nil {
|
|
missingBroadcasts = append(missingBroadcasts, id)
|
|
} else {
|
|
// Check if this broadcast has been processed by StoreBroadcastMessage
|
|
key := fmt.Sprintf("%d:%s", number, id)
|
|
if _, processed := h.processedBroadcasts.Load(key); !processed {
|
|
unprocessedBroadcasts = append(unprocessedBroadcasts, id)
|
|
} else {
|
|
h.log.Debug("have processed broadcast",
|
|
log.Uint16("round", uint16(number)),
|
|
log.String("from", string(id)),
|
|
log.String("self", string(r.SelfID())))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(missingBroadcasts) > 0 {
|
|
h.log.Debug("waiting for broadcasts",
|
|
log.Uint16("round", uint16(number)),
|
|
log.String("missing", fmt.Sprintf("%v", missingBroadcasts)),
|
|
log.String("self", string(r.SelfID())))
|
|
return false
|
|
}
|
|
|
|
if len(unprocessedBroadcasts) > 0 {
|
|
h.log.Debug("waiting for broadcast processing",
|
|
log.Uint16("round", uint16(number)),
|
|
log.String("unprocessed", fmt.Sprintf("%v", unprocessedBroadcasts)),
|
|
log.String("self", string(r.SelfID())))
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Check normal messages
|
|
if r.MessageContent() != nil {
|
|
messages := h.messages.LoadAll(number)
|
|
missingMessages := []party.ID{}
|
|
unprocessedMessages := []party.ID{}
|
|
|
|
for _, id := range r.OtherPartyIDs() {
|
|
if messages[id] == nil {
|
|
missingMessages = append(missingMessages, id)
|
|
} else {
|
|
// Check if this message has been processed by StoreMessage
|
|
key := fmt.Sprintf("%d:%s", number, id)
|
|
if _, processed := h.processedMessages.Load(key); !processed {
|
|
unprocessedMessages = append(unprocessedMessages, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(missingMessages) > 0 {
|
|
h.log.Debug("waiting for messages",
|
|
log.Uint16("round", uint16(number)),
|
|
log.String("missing", fmt.Sprintf("%v", missingMessages)))
|
|
return false
|
|
}
|
|
|
|
if len(unprocessedMessages) > 0 {
|
|
h.log.Debug("waiting for message processing",
|
|
log.Uint16("round", uint16(number)),
|
|
log.String("unprocessed", fmt.Sprintf("%v", unprocessedMessages)))
|
|
return false
|
|
}
|
|
}
|
|
|
|
h.log.Debug("have all messages and processed",
|
|
log.Uint16("round", uint16(number)),
|
|
log.String("self", string(r.SelfID())))
|
|
return true
|
|
}
|
|
|
|
func (h *Handler) storeMessage(msg *Message) {
|
|
if msg.Broadcast {
|
|
h.log.Debug("storing broadcast",
|
|
log.Uint16("round", uint16(msg.RoundNumber)),
|
|
log.String("from", string(msg.From)))
|
|
h.broadcast.Store(msg.RoundNumber, msg.From, msg)
|
|
} else {
|
|
h.log.Debug("storing message",
|
|
log.Uint16("round", uint16(msg.RoundNumber)),
|
|
log.String("from", string(msg.From)))
|
|
h.messages.Store(msg.RoundNumber, msg.From, msg)
|
|
}
|
|
}
|
|
|
|
// getBroadcastHash removed - not needed in optimized implementation
|
|
|
|
func (h *Handler) compressData(data []byte) []byte {
|
|
// Simple compression placeholder - would use gzip/zstd in production
|
|
return data
|
|
}
|
|
|
|
func (h *Handler) decompressMessage(msg *Message) *Message {
|
|
// Return a shallow copy with Compressed cleared. The same *Message can be
|
|
// observed by multiple parties' handlers in the test harness (broadcast
|
|
// sends share the slice element), so we must never mutate the input.
|
|
if msg == nil {
|
|
return nil
|
|
}
|
|
out := *msg
|
|
out.Compressed = false
|
|
return &out
|
|
}
|
|
|
|
// MessageStore provides zero-contention sharded message storage
|
|
type MessageStore struct {
|
|
shards [256]*messageShard // 256-way sharding for zero contention
|
|
}
|
|
|
|
type messageShard struct {
|
|
mu sync.RWMutex
|
|
data map[round.Number]map[party.ID]*Message
|
|
}
|
|
|
|
func newMessageStore() *MessageStore {
|
|
ms := &MessageStore{}
|
|
for i := range ms.shards {
|
|
ms.shards[i] = &messageShard{
|
|
data: make(map[round.Number]map[party.ID]*Message),
|
|
}
|
|
}
|
|
return ms
|
|
}
|
|
|
|
func (ms *MessageStore) getShard(roundNum round.Number) *messageShard {
|
|
// Perfect hash distribution
|
|
hash := uint(roundNum) * 2654435761 // Knuth's multiplicative hash
|
|
return ms.shards[hash%256]
|
|
}
|
|
|
|
func (ms *MessageStore) Store(roundNum round.Number, from party.ID, msg *Message) {
|
|
shard := ms.getShard(roundNum)
|
|
shard.mu.Lock()
|
|
defer shard.mu.Unlock()
|
|
|
|
if shard.data[roundNum] == nil {
|
|
shard.data[roundNum] = make(map[party.ID]*Message)
|
|
}
|
|
shard.data[roundNum][from] = msg
|
|
}
|
|
|
|
func (ms *MessageStore) Load(roundNum round.Number, from party.ID) (*Message, bool) {
|
|
shard := ms.getShard(roundNum)
|
|
shard.mu.RLock()
|
|
defer shard.mu.RUnlock()
|
|
|
|
if msgs, ok := shard.data[roundNum]; ok {
|
|
msg, exists := msgs[from]
|
|
return msg, exists
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (ms *MessageStore) LoadAll(roundNum round.Number) map[party.ID]*Message {
|
|
shard := ms.getShard(roundNum)
|
|
shard.mu.RLock()
|
|
defer shard.mu.RUnlock()
|
|
|
|
msgs := shard.data[roundNum]
|
|
if msgs == nil {
|
|
return nil
|
|
}
|
|
|
|
// Return copy to avoid concurrent modification
|
|
result := make(map[party.ID]*Message, len(msgs))
|
|
for k, v := range msgs {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|