From f0332931a50a9de17d00b73b4cd514ca3f7e0933 Mon Sep 17 00:00:00 2001 From: 0xrlawrence Date: Mon, 31 Aug 2026 17:04:10 +0800 Subject: [PATCH] fix: stop verifying a batch once a proof is rejected ProcessNewBatchLogV3 returns as soon as a proof fails, but the worker goroutines kept verifying the rest of the batch, which is the expensive part. Close an abandon channel on the failure path so workers stop picking up further proofs. Two related cleanups in the same function: - results is sized from the batch length, so a zero-length batch made it unbuffered. Reject empty batches up front instead. - The DisabledVerifiers error path pushed a value into results and then returned, so nothing could ever read it. That send was both dead and, with an unbuffered results, the one place that could have blocked forever. Drop it. Co-Authored-By: Claude Opus 5 --- operator/pkg/operator.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/operator/pkg/operator.go b/operator/pkg/operator.go index 44c25ef7d..786fca158 100644 --- a/operator/pkg/operator.go +++ b/operator/pkg/operator.go @@ -331,13 +331,18 @@ func (o *Operator) ProcessNewBatchLogV3(newBatchLog *servicemanager.ContractAlig } verificationDataBatchLen := len(verificationDataBatch) + if verificationDataBatchLen == 0 { + // results is sized from this length, so an empty batch would leave it + // unbuffered and block the error path below forever. + return fmt.Errorf("batch contains no verification data") + } + results := make(chan bool, verificationDataBatchLen) jobs := make(chan VerificationData, verificationDataBatchLen) disabledVerifiersBitmap, err := o.avsReader.DisabledVerifiers() if err != nil { o.Logger.Errorf("Could not check verifiers status: %s", err) - results <- false return err } @@ -346,18 +351,31 @@ func (o *Operator) ProcessNewBatchLogV3(newBatchLog *servicemanager.ContractAlig maxWorkers = 1 } + // Closed as soon as the batch is known to be invalid, so the workers stop + // verifying proofs whose results can no longer change the outcome. + abandon := make(chan struct{}) + var abandonOnce sync.Once + stopVerifying := func() { abandonOnce.Do(func() { close(abandon) }) } + defer stopVerifying() + var wg sync.WaitGroup for i := 0; i < maxWorkers; i++ { wg.Add(1) go func() { defer wg.Done() for data := range jobs { + select { + case <-abandon: + return + default: + } o.verify(data, disabledVerifiersBitmap, results) o.metrics.IncOperatorTaskResponses() } }() } + // jobs is buffered to the full batch length, so these sends never block. for _, verificationData := range verificationDataBatch { jobs <- verificationData } @@ -370,6 +388,7 @@ func (o *Operator) ProcessNewBatchLogV3(newBatchLog *servicemanager.ContractAlig for result := range results { if !result { + stopVerifying() return fmt.Errorf("invalid proof") } }