diff --git a/finality-provider/service/fp_instance.go b/finality-provider/service/fp_instance.go index f6d2c9e0..9ada689b 100644 --- a/finality-provider/service/fp_instance.go +++ b/finality-provider/service/fp_instance.go @@ -1,7 +1,6 @@ package service import ( - "encoding/hex" "encoding/json" "errors" "fmt" @@ -196,71 +195,6 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { for { select { - case b := <-fp.poller.GetBlockInfoChan(): - fp.logger.Debug( - "the finality-provider received a new block, start processing", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("height", b.Height), - zap.String("block_hash", hex.EncodeToString(b.Hash)), - ) - - // check whether the block has been processed before - if fp.hasProcessed(b.Height) { - continue - } - // check whether the finality provider has voting power - hasVp, err := fp.hasVotingPower(b.Height) - if err != nil { - fp.reportCriticalErr(err) - continue - } - if !hasVp { - // the finality provider does not have voting power - // and it will never will at this block - fp.MustSetLastProcessedHeight(b.Height) - fp.metrics.IncrementFpTotalBlocksWithoutVotingPower(fp.GetBtcPkHex()) - continue - } - // check whether the randomness has been committed - // the retry will end if max retry times is reached - // or the target block is finalized - isFinalized, err := fp.retryCheckRandomnessUntilBlockFinalized(b) - if err != nil { - if !errors.Is(err, ErrFinalityProviderShutDown) { - fp.reportCriticalErr(err) - } - break - } - // the block is finalized, no need to submit finality signature - if isFinalized { - fp.MustSetLastProcessedHeight(b.Height) - continue - } - - // use the copy of the block to avoid the impact to other receivers - nextBlock := *b - res, err := fp.retrySubmitFinalitySignatureUntilBlockFinalized(&nextBlock) - if err != nil { - fp.metrics.IncrementFpTotalFailedVotes(fp.GetBtcPkHex()) - if !errors.Is(err, ErrFinalityProviderShutDown) { - fp.reportCriticalErr(err) - } - continue - } - if res == nil { - // this can happen when a finality signature is not needed - // either if the block is already submitted or the signature - // is already submitted - continue - } - fp.logger.Info( - "successfully submitted a finality signature to the consumer chain", - zap.String("consumer_id", string(fp.GetChainID())), - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("height", b.Height), - zap.String("tx_hash", res.TxHash), - ) - case targetBlock := <-fp.laggingTargetChan: res, err := fp.tryFastSync(targetBlock) fp.isLagging.Store(false) @@ -308,10 +242,96 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { case <-fp.quit: fp.logger.Info("the finality signature submission loop is closing") return + default: + pollerBlocks := fp.getAllBlocksFromChan() + if len(pollerBlocks) == 0 { + continue + } + targetHeight := pollerBlocks[len(pollerBlocks)-1].Height + fp.logger.Debug("the finality-provider received new block(s), start processing", + zap.String("pk", fp.GetBtcPkHex()), + zap.Uint64("start_height", pollerBlocks[0].Height), + zap.Uint64("end_height", targetHeight), + ) + res, err := fp.retrySubmitFinalitySignatureUntilBlocksFinalized(pollerBlocks) + if err != nil { + fp.metrics.IncrementFpTotalFailedVotes(fp.GetBtcPkHex()) + if !errors.Is(err, ErrFinalityProviderShutDown) { + fp.reportCriticalErr(err) + } + continue + } + if res == nil { + // this can happen when a finality signature is not needed + // either if the block is already submitted or the signature + // is already submitted + continue + } + fp.logger.Info( + "successfully submitted the finality signature to the consumer chain", + zap.String("consumer_id", string(fp.GetChainID())), + zap.String("pk", fp.GetBtcPkHex()), + zap.Uint64("start_height", pollerBlocks[0].Height), + zap.Uint64("end_height", targetHeight), + zap.String("tx_hash", res.TxHash), + ) } } } +func (fp *FinalityProviderInstance) getAllBlocksFromChan() []*types.BlockInfo { + var pollerBlocks []*types.BlockInfo + for { + select { + case b := <-fp.poller.GetBlockInfoChan(): + shouldProcess, err := fp.shouldProcessBlock(b) + if err != nil { + if !errors.Is(err, ErrFinalityProviderShutDown) { + fp.reportCriticalErr(err) + } + break + } + if shouldProcess { + pollerBlocks = append(pollerBlocks, b) + } + default: + return pollerBlocks + } + } +} + +func (fp *FinalityProviderInstance) shouldProcessBlock(b *types.BlockInfo) (bool, error) { + // check whether the block has been processed before + if fp.hasProcessed(b.Height) { + return false, nil + } + // check whether the finality provider has voting power + hasVp, err := fp.hasVotingPower(b.Height) + if err != nil { + return false, err + } + if !hasVp { + // the finality provider does not have voting power + // and it will never will at this block + fp.MustSetLastProcessedHeight(b.Height) + fp.metrics.IncrementFpTotalBlocksWithoutVotingPower(fp.GetBtcPkHex()) + return false, nil + } + // check whether the randomness has been committed + // the retry will end if max retry times is reached + // or the target block is finalized + isFinalized, err := fp.retryCheckRandomnessUntilBlockFinalized(b) + if err != nil { + return false, err + } + // the block is finalized, no need to submit finality signature + if isFinalized { + fp.MustSetLastProcessedHeight(b.Height) + return false, nil + } + return true, nil +} + func (fp *FinalityProviderInstance) randomnessCommitmentLoop(startHeight uint64) { defer fp.wg.Done() @@ -601,23 +621,29 @@ func (fp *FinalityProviderInstance) retryCheckRandomnessUntilBlockFinalized(targ } } -// retrySubmitFinalitySignatureUntilBlockFinalized periodically tries to submit finality signature until success or the block is finalized +// retrySubmitFinalitySignatureUntilBlocksFinalized periodically tries to submit finality signature until success or the block is finalized // error will be returned if maximum retries have been reached or the query to the consumer chain fails -func (fp *FinalityProviderInstance) retrySubmitFinalitySignatureUntilBlockFinalized(targetBlock *types.BlockInfo) (*types.TxResponse, error) { +func (fp *FinalityProviderInstance) retrySubmitFinalitySignatureUntilBlocksFinalized(targetBlocks []*types.BlockInfo) (*types.TxResponse, error) { var failedCycles uint32 - + targetHeight := targetBlocks[len(targetBlocks)-1].Height // we break the for loop if the block is finalized or the signature is successfully submitted // error will be returned if maximum retries have been reached or the query to the consumer chain fails for { // error will be returned if max retries have been reached - res, err := fp.SubmitFinalitySignature(targetBlock) + var res *types.TxResponse + var err error + if len(targetBlocks) == 1 { + res, err = fp.SubmitFinalitySignature(targetBlocks[0]) + } else { + res, err = fp.SubmitBatchFinalitySignatures(targetBlocks) + } if err != nil { - fp.logger.Debug( "failed to submit finality signature to the consumer chain", zap.String("pk", fp.GetBtcPkHex()), zap.Uint32("current_failures", failedCycles), - zap.Uint64("target_block_height", targetBlock.Height), + zap.Uint64("target_start_height", targetBlocks[0].Height), + zap.Uint64("target_end_height", targetHeight), zap.Error(err), ) @@ -640,15 +666,15 @@ func (fp *FinalityProviderInstance) retrySubmitFinalitySignatureUntilBlockFinali select { case <-time.After(fp.cfg.SubmissionRetryInterval): // periodically query the index block to be later checked whether it is Finalized - finalized, err := fp.consumerCon.QueryIsBlockFinalized(targetBlock.Height) + finalized, err := fp.consumerCon.QueryIsBlockFinalized(targetHeight) if err != nil { - return nil, fmt.Errorf("failed to query block finalization at height %v: %w", targetBlock.Height, err) + return nil, fmt.Errorf("failed to query block finalization at height %v: %w", targetHeight, err) } if finalized { fp.logger.Debug( "the block is already finalized, skip submission", zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("target_height", targetBlock.Height), + zap.Uint64("target_height", targetHeight), ) // TODO: returning nil here is to safely break the loop // the error still exists diff --git a/go.mod b/go.mod index 2c2ecbca..2f5643dc 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/protolambda/ctxlock v0.1.0 // indirect github.com/shamaton/msgpack/v2 v2.2.0 // indirect github.com/wlynxg/anet v0.0.4 // indirect + nhooyr.io/websocket v1.8.17 // indirect ) require ( @@ -433,7 +434,6 @@ require ( modernc.org/sqlite v1.20.3 // indirect modernc.org/strutil v1.1.3 // indirect modernc.org/token v1.0.1 // indirect - nhooyr.io/websocket v1.8.6 // indirect pgregory.net/rapid v1.1.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 414afc34..e97a585b 100644 --- a/go.sum +++ b/go.sum @@ -612,11 +612,8 @@ github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -650,14 +647,8 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -665,16 +656,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= -github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -1106,8 +1089,6 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -1623,12 +1604,9 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -2507,8 +2485,9 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= -nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/itest/babylon/babylon_test_manager.go b/itest/babylon/babylon_test_manager.go index 70aa8dcc..a19a3aa5 100644 --- a/itest/babylon/babylon_test_manager.go +++ b/itest/babylon/babylon_test_manager.go @@ -192,7 +192,8 @@ func StartManagerWithFinalityProvider(t *testing.T, n int) (*TestManager, []*ser // goes back to old key in app cfg.BabylonConfig.Key = oldKey - cc, err := clientcontroller.NewClientController(cfg, zap.NewNop()) + logger := createLogger(t, zapcore.DebugLevel) + cc, err := clientcontroller.NewClientController(cfg, logger) require.NoError(t, err) app.UpdateClientController(cc) diff --git a/itest/opstackl2/op_test_manager.go b/itest/opstackl2/op_test_manager.go index 586b3182..aa1b0428 100644 --- a/itest/opstackl2/op_test_manager.go +++ b/itest/opstackl2/op_test_manager.go @@ -71,7 +71,7 @@ func StartOpL2ConsumerManager(t *testing.T, numOfConsumerFPs uint8) *OpL2Consume testDir, err := e2eutils.BaseDir("fpe2etest") require.NoError(t, err) - logger := createLogger(t, zapcore.DebugLevel) + logger := createLogger(t, zapcore.InfoLevel) // generate covenant committee covenantQuorum := 2 @@ -361,6 +361,8 @@ func createBaseFpConfig(fpHomeDir string, index int, logger *zap.Logger) *fpcfg. cfg.RandomnessCommitInterval = 2 * time.Second cfg.NumPubRand = 64 cfg.MinRandHeightGap = 1000 + cfg.FastSyncGap = 60 + cfg.FastSyncLimit = 100 return cfg } diff --git a/tools/go.mod b/tools/go.mod index f60730f9..145eceff 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -220,7 +220,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect - nhooyr.io/websocket v1.8.6 // indirect + nhooyr.io/websocket v1.8.17 // indirect pgregory.net/rapid v1.1.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/tools/go.sum b/tools/go.sum index d98c0fe2..a0f7b68c 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1776,6 +1776,8 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=