diff --git a/CHANGELOG.md b/CHANGELOG.md index 616cf01a..f3875394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [#228](https://github.com/babylonlabs-io/finality-provider/pull/228) Save key name mapping in eotsd import commands * [#227](https://github.com/babylonlabs-io/finality-provider/pull/227) Fix FP submission loop * [#226](https://github.com/babylonlabs-io/finality-provider/pull/226) Update local fp before register +* [#233](https://github.com/babylonlabs-io/finality-provider/pull/233) Refactor CommitPubRand * [#234](https://github.com/babylonlabs-io/finality-provider/pull/234) eotsd ls command ## v0.13.1 diff --git a/Makefile b/Makefile index 913f25fc..9b83d057 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ build-docker: .PHONY: test test: - go test ./... + go test -v ./... test-e2e: go test -mod=readonly -failfast -timeout=25m -v $(PACKAGES_E2E) -count=1 --tags=e2e @@ -139,4 +139,4 @@ release: else release: @echo "Error: GITHUB_TOKEN is not defined. Please define it before running 'make release'." -endif \ No newline at end of file +endif diff --git a/docs/commit-pub-rand.md b/docs/commit-pub-rand.md index 81b66a6e..fd3d1215 100644 --- a/docs/commit-pub-rand.md +++ b/docs/commit-pub-rand.md @@ -42,27 +42,35 @@ committed before finality votes can be sent. Otherwise, the finality provider looses voting power for this height. To this end, when a finality provider is started, it runs a loop to periodically -check whether it needs to make a new commit. In particualar, -the following statement is checked: +check whether it needs to make a new commit and calculate the start height of +the next commit. In particular: ```go -if lastCommittedHeight < currentHeight + uint64(MinRandHeightGap) + tipHeightWithDelay := tipHeight + uint64(fp.cfg.TimestampingDelayBlocks) + var startHeight uint64 + switch { + case lastCommittedHeight < tipHeightWithDelay: + // the start height should consider the timestamping delay + // as it is only available to use after tip height + estimated timestamping delay + startHeight = tipHeightWithDelay + case lastCommittedHeight < tipHeightWithDelay+uint64(fp.cfg.NumPubRand): + startHeight = lastCommittedHeight + 1 + default: + // randomness is sufficient, do not need to make a commit ``` where: - `lastCommittedHeight` is the end height (`startHeight + numRand - 1`) from the latest public randomness commit recorded on the consumer chain -- `currentHeight` is the current height of the consumer chain -- `MinRandHeightGap` is a configuration value, which measures when to make a +- `tipHeight` is the current height of the consumer chain +- `TimestampingDelayBlocks` is a configuration value, which measures when to make a new commit +- `NumPubRand` is the number of randomness in a commit defined in the config. -If the statement is true, a new commit should be made to ensure sufficient -randomness is available for future blocks. +### Determining TimestampingDelayBlocks -### Determining MinRandHeightGap - -The value of `MinRandHeightGap` must account for BTC-timestamping +The value of `TimestampingDelayBlocks` must account for BTC-timestamping delays, which is needed to activate the randomness for a specific height after the committed epoch is BTC-timestamped. Here's an example: @@ -82,7 +90,7 @@ The BTC-timestamping protocol requires: Therefore, -- `MinRandHeightGap` should be > 6,000 to ensure randomness is always available +- `TimestampingDelayBlocks` should be around 6,000 - Recommended production value: > 10,000 to provide additional safety margin ### Determining Start Height @@ -125,7 +133,7 @@ reasons: Additionally, given that the end height of a commit equals to `startHeight + NumPubRand - 1`, we should ensure that the condition -`lastCommittedHeight > currentHeight + uint64(MinRandHeightGap)` can hold for +`lastCommittedHeight > tipHeight + uint64(TimestampingDelayBlocks)` can hold for a long period of time to avoid frequent commit of randomness. In real life, the value of `NumPubRand` should be much larger than -`MinRandHeightGap`, e.g., `NumPubRand = 2 * MinRandHeightGap`. +`TimestampingDelayBlocks`, e.g., `NumPubRand = 2 * TimestampingDelayBlocks`. diff --git a/eotsmanager/service/server.go b/eotsmanager/service/server.go index 999a4f87..9c4907a7 100644 --- a/eotsmanager/service/server.go +++ b/eotsmanager/service/server.go @@ -83,9 +83,7 @@ func (s *Server) RunUntilShutdown() error { return fmt.Errorf("failed to listen on %s: %w", listenAddr, err) } defer func() { - if err := lis.Close(); err != nil { - s.logger.Error(fmt.Sprintf("Failed to close network listener: %v", err)) - } + _ = lis.Close() }() grpcServer := grpc.NewServer() diff --git a/finality-provider/config/config.go b/finality-provider/config/config.go index 8040ad64..e3fba79e 100644 --- a/finality-provider/config/config.go +++ b/finality-provider/config/config.go @@ -27,9 +27,8 @@ const ( defaultConfigFileName = "fpd.conf" defaultNumPubRand = 70000 // support running of 1 week with block production time as 10s defaultNumPubRandMax = 100000 - defaultMinRandHeightGap = 35000 + defaultTimestampingDelayBlocks = 6000 // 100 BTC blocks * 600s / 10s defaultBatchSubmissionSize = 1000 - defaultStatusUpdateInterval = 20 * time.Second defaultRandomInterval = 30 * time.Second defaultSubmitRetryInterval = 1 * time.Second defaultSignatureSubmissionInterval = 1 * time.Second @@ -57,11 +56,10 @@ type Config struct { ChainType string `long:"chaintype" description:"the type of the consumer chain" choice:"babylon"` NumPubRand uint32 `long:"numPubRand" description:"The number of Schnorr public randomness for each commitment"` NumPubRandMax uint32 `long:"numpubrandmax" description:"The upper bound of the number of Schnorr public randomness for each commitment"` - MinRandHeightGap uint32 `long:"minrandheightgap" description:"The minimum gap between the last committed rand height and the current Babylon block height"` + TimestampingDelayBlocks uint32 `long:"timestampingdelayblocks" description:"The delay, measured in blocks, between a randomness commit submission and the randomness is BTC-timestamped"` MaxSubmissionRetries uint32 `long:"maxsubmissionretries" description:"The maximum number of retries to submit finality signature or public randomness"` EOTSManagerAddress string `long:"eotsmanageraddress" description:"The address of the remote EOTS manager; Empty if the EOTS manager is running locally"` BatchSubmissionSize uint32 `long:"batchsubmissionsize" description:"The size of a batch in one submission"` - StatusUpdateInterval time.Duration `long:"statusupdateinterval" description:"The interval between each update of finality-provider status"` RandomnessCommitInterval time.Duration `long:"randomnesscommitinterval" description:"The interval between each attempt to commit public randomness"` SubmissionRetryInterval time.Duration `long:"submissionretryinterval" description:"The interval between each attempt to submit finality signature or public randomness after a failure"` SignatureSubmissionInterval time.Duration `long:"signaturesubmissioninterval" description:"The interval between each finality signature(s) submission"` @@ -94,9 +92,8 @@ func DefaultConfigWithHome(homePath string) Config { PollerConfig: &pollerCfg, NumPubRand: defaultNumPubRand, NumPubRandMax: defaultNumPubRandMax, - MinRandHeightGap: defaultMinRandHeightGap, + TimestampingDelayBlocks: defaultTimestampingDelayBlocks, BatchSubmissionSize: defaultBatchSubmissionSize, - StatusUpdateInterval: defaultStatusUpdateInterval, RandomnessCommitInterval: defaultRandomInterval, SubmissionRetryInterval: defaultSubmitRetryInterval, SignatureSubmissionInterval: defaultSignatureSubmissionInterval, diff --git a/finality-provider/service/app.go b/finality-provider/service/app.go index cf49746f..c05734e9 100644 --- a/finality-provider/service/app.go +++ b/finality-provider/service/app.go @@ -7,7 +7,6 @@ import ( "sync" sdkmath "cosmossdk.io/math" - "github.com/avast/retry-go/v4" bbntypes "github.com/babylonlabs-io/babylon/types" bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" "github.com/btcsuite/btcd/btcec/v2" @@ -26,7 +25,6 @@ import ( "github.com/babylonlabs-io/finality-provider/finality-provider/store" fpkr "github.com/babylonlabs-io/finality-provider/keyring" "github.com/babylonlabs-io/finality-provider/metrics" - "github.com/babylonlabs-io/finality-provider/types" ) type FinalityProviderApp struct { @@ -253,18 +251,14 @@ func (app *FinalityProviderApp) SyncAllFinalityProvidersStatus() error { continue } - // power == 0 and slashed_height == 0, change to INACTIVE if the current status is ACTIVE - if oldStatus == proto.FinalityProviderStatus_ACTIVE { - app.fps.MustSetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_INACTIVE) - app.logger.Debug( - "the finality-provider status is changed to INACTIVE", - zap.String("fp_btc_pk", pkHex), - zap.String("old_status", oldStatus.String()), - ) + app.fps.MustSetFpStatus(fp.BtcPk, proto.FinalityProviderStatus_INACTIVE) - continue - } + app.logger.Debug( + "the finality-provider status is changed to INACTIVE", + zap.String("fp_btc_pk", pkHex), + zap.String("old_status", oldStatus.String()), + ) } return nil @@ -281,10 +275,9 @@ func (app *FinalityProviderApp) Start() error { return } - app.wg.Add(5) + app.wg.Add(4) go app.metricsUpdateLoop() go app.monitorCriticalErr() - go app.monitorStatusUpdate() go app.registrationLoop() go app.unjailFpLoop() }) @@ -550,32 +543,6 @@ func (app *FinalityProviderApp) setFinalityProviderJailed(fpi *FinalityProviderI } } -func (app *FinalityProviderApp) getLatestBlockWithRetry() (*types.BlockInfo, error) { - var ( - latestBlock *types.BlockInfo - err error - ) - - if err := retry.Do(func() error { - latestBlock, err = app.cc.QueryBestBlock() - if err != nil { - return err - } - return nil - }, RtyAtt, RtyDel, RtyErr, retry.OnRetry(func(n uint, err error) { - app.logger.Debug( - "failed to query the consumer chain for the latest block", - zap.Uint("attempt", n+1), - zap.Uint("max_attempts", RtyAttNum), - zap.Error(err), - ) - })); err != nil { - return nil, err - } - - return latestBlock, nil -} - // NOTE: this is not safe in production, so only used for testing purpose func (app *FinalityProviderApp) getFpPrivKey(fpPk []byte) (*btcec.PrivateKey, error) { record, err := app.eotsManager.KeyRecord(fpPk, "") diff --git a/finality-provider/service/app_test.go b/finality-provider/service/app_test.go index 15082174..82de8b3d 100644 --- a/finality-provider/service/app_test.go +++ b/finality-provider/service/app_test.go @@ -3,7 +3,6 @@ package service_test import ( "errors" "fmt" - btcstakingtypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" "math/rand" "os" "path/filepath" @@ -11,13 +10,14 @@ import ( "testing" "time" + btcstakingtypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + "github.com/babylonlabs-io/babylon/testutil/datagen" bbntypes "github.com/babylonlabs-io/babylon/types" finalitytypes "github.com/babylonlabs-io/babylon/x/finality/types" sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "go.uber.org/zap" "github.com/babylonlabs-io/finality-provider/clientcontroller" "github.com/babylonlabs-io/finality-provider/eotsmanager" @@ -44,7 +44,7 @@ func FuzzCreateFinalityProvider(f *testing.F) { f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) - logger := zap.NewNop() + logger := testutil.GetTestLogger(t) // create an EOTS manager eotsHomeDir := filepath.Join(t.TempDir(), "eots-home") eotsCfg := eotscfg.DefaultConfigWithHomePath(eotsHomeDir) @@ -170,7 +170,6 @@ func FuzzSyncFinalityProviderStatus(f *testing.F) { fpHomeDir := filepath.Join(t.TempDir(), "fp-home", pathSuffix) fpCfg := config.DefaultConfigWithHome(fpHomeDir) // no need for other intervals to run - fpCfg.StatusUpdateInterval = time.Minute * 10 fpCfg.SubmissionRetryInterval = time.Minute * 10 // Create fp app @@ -188,7 +187,7 @@ func FuzzSyncFinalityProviderStatus(f *testing.F) { case 1: expectedStatus = proto.FinalityProviderStatus_JAILED case 2: - expectedStatus = proto.FinalityProviderStatus_REGISTERED + expectedStatus = proto.FinalityProviderStatus_INACTIVE } } @@ -210,7 +209,6 @@ func FuzzUnjailFinalityProvider(f *testing.F) { fpHomeDir := filepath.Join(t.TempDir(), "fp-home", pathSuffix) fpCfg := config.DefaultConfigWithHome(fpHomeDir) // use shorter interval for the test to end faster - fpCfg.StatusUpdateInterval = time.Millisecond * 10 fpCfg.SubmissionRetryInterval = time.Millisecond * 10 blkInfo := &types.BlockInfo{Height: currentHeight} @@ -241,56 +239,12 @@ func FuzzUnjailFinalityProvider(f *testing.F) { }) } -func FuzzStatusUpdate(f *testing.F) { - testutil.AddRandomSeedsToFuzzer(f, 10) - f.Fuzz(func(t *testing.T, seed int64) { - r := rand.New(rand.NewSource(seed)) - - randomStartingHeight := uint64(r.Int63n(100) + 1) - currentHeight := randomStartingHeight + uint64(r.Int63n(10)+2) - mockClientController := testutil.PrepareMockedClientController(t, r, randomStartingHeight, currentHeight, 0) - - // setup mocks - votingPower := uint64(r.Intn(2)) - mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), currentHeight).Return(votingPower, nil).AnyTimes() - mockClientController.EXPECT().Close().Return(nil).AnyTimes() - mockClientController.EXPECT().QueryLatestFinalizedBlocks(gomock.Any()).Return(nil, nil).AnyTimes() - mockClientController.EXPECT().QueryFinalityProviderHighestVotedHeight(gomock.Any()).Return(uint64(0), nil).AnyTimes() - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).AnyTimes() - mockClientController.EXPECT().SubmitFinalitySig(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.TxResponse{TxHash: ""}, nil).AnyTimes() - mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, false, nil).AnyTimes() - - // Create randomized config - pathSuffix := datagen.GenRandomHexStr(r, 10) - fpHomeDir := filepath.Join(t.TempDir(), "fp-home", pathSuffix) - fpCfg := config.DefaultConfigWithHome(fpHomeDir) - // use shorter interval for the test to end faster - fpCfg.StatusUpdateInterval = time.Millisecond * 10 - fpCfg.SubmissionRetryInterval = time.Millisecond * 10 - - // Create fp app - app, fpPk, cleanup := startFPAppWithRegisteredFp(t, r, fpHomeDir, &fpCfg, mockClientController) - defer cleanup() - - err := app.StartFinalityProvider(fpPk, passphrase) - require.NoError(t, err) - fpIns, err := app.GetFinalityProviderInstance() - require.NoError(t, err) - - if votingPower > 0 { - waitForStatus(t, fpIns, proto.FinalityProviderStatus_ACTIVE) - } else if fpIns.GetStatus() == proto.FinalityProviderStatus_ACTIVE { - waitForStatus(t, fpIns, proto.FinalityProviderStatus_INACTIVE) - } - }) -} - func FuzzSaveAlreadyRegisteredFinalityProvider(f *testing.F) { testutil.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) - logger := zap.NewNop() + logger := testutil.GetTestLogger(t) // create an EOTS manager eotsHomeDir := filepath.Join(t.TempDir(), "eots-home") eotsCfg := eotscfg.DefaultConfigWithHomePath(eotsHomeDir) @@ -375,15 +329,8 @@ func FuzzSaveAlreadyRegisteredFinalityProvider(f *testing.F) { }) } -func waitForStatus(t *testing.T, fpIns *service.FinalityProviderInstance, s proto.FinalityProviderStatus) { - require.Eventually(t, - func() bool { - return fpIns.GetStatus() == s - }, eventuallyWaitTimeOut, eventuallyPollTime) -} - func startFPAppWithRegisteredFp(t *testing.T, r *rand.Rand, homePath string, cfg *config.Config, cc clientcontroller.ClientController) (*service.FinalityProviderApp, *bbntypes.BIP340PubKey, func()) { - logger := zap.NewNop() + logger := testutil.GetTestLogger(t) // create an EOTS manager eotsHomeDir := filepath.Join(t.TempDir(), "eots-home") eotsCfg := eotscfg.DefaultConfigWithHomePath(eotsHomeDir) diff --git a/finality-provider/service/benchmark_helper.go b/finality-provider/service/benchmark_helper.go index 092312d3..7174e635 100644 --- a/finality-provider/service/benchmark_helper.go +++ b/finality-provider/service/benchmark_helper.go @@ -2,9 +2,11 @@ package service import ( "fmt" - "github.com/babylonlabs-io/finality-provider/types" - "go.uber.org/zap" "time" + + "go.uber.org/zap" + + "github.com/babylonlabs-io/finality-provider/types" ) // CommitPubRandTiming - helper struct used to capture times for benchmark @@ -26,7 +28,7 @@ func (fp *FinalityProviderInstance) HelperCommitPubRand(tipHeight uint64) (*type case lastCommittedHeight == uint64(0): // the finality-provider has never submitted public rand before startHeight = tipHeight + 1 - case lastCommittedHeight < uint64(fp.cfg.MinRandHeightGap)+tipHeight: + case lastCommittedHeight < uint64(fp.cfg.TimestampingDelayBlocks)+tipHeight: // (should not use subtraction because they are in the type of uint64) // we are running out of the randomness startHeight = lastCommittedHeight + 1 diff --git a/finality-provider/service/chain_poller.go b/finality-provider/service/chain_poller.go index e59abfad..1b755e7a 100644 --- a/finality-provider/service/chain_poller.go +++ b/finality-provider/service/chain_poller.go @@ -143,18 +143,18 @@ func (cp *ChainPoller) blockWithRetry(height uint64) (*types.BlockInfo, error) { func (cp *ChainPoller) waitForActivation() { // ensure that the startHeight is no lower than the activated height for { + activatedHeight, err := cp.cc.QueryActivatedHeight() + if err != nil { + cp.logger.Debug("failed to query the consumer chain for the activated height", zap.Error(err)) + } else { + if cp.nextHeight < activatedHeight { + cp.nextHeight = activatedHeight + } + return + } select { case <-time.After(cp.cfg.PollInterval): - activatedHeight, err := cp.cc.QueryActivatedHeight() - if err != nil { - cp.logger.Debug("failed to query the consumer chain for the activated height", zap.Error(err)) - } else { - if cp.nextHeight < activatedHeight { - cp.nextHeight = activatedHeight - } - return - } - + continue case <-cp.quit: return } @@ -169,37 +169,39 @@ func (cp *ChainPoller) pollChain() { var failedCycles uint32 for { + // start polling in the first iteration + blockToRetrieve := cp.nextHeight + block, err := cp.blockWithRetry(blockToRetrieve) + if err != nil { + failedCycles++ + cp.logger.Debug( + "failed to query the consumer chain for the block", + zap.Uint32("current_failures", failedCycles), + zap.Uint64("block_to_retrieve", blockToRetrieve), + zap.Error(err), + ) + } else { + // no error and we got the header we wanted to get, bump the state and push + // notification about data + cp.nextHeight = blockToRetrieve + 1 + failedCycles = 0 + cp.metrics.RecordLastPolledHeight(block.Height) + + cp.logger.Info("the poller retrieved the block from the consumer chain", + zap.Uint64("height", block.Height)) + + // push the data to the channel + // Note: if the consumer is too slow -- the buffer is full + // the channel will block, and we will stop retrieving data from the node + cp.blockInfoChan <- block + } + + if failedCycles > maxFailedCycles { + cp.logger.Fatal("the poller has reached the max failed cycles, exiting") + } select { case <-time.After(cp.cfg.PollInterval): - blockToRetrieve := cp.nextHeight - block, err := cp.blockWithRetry(blockToRetrieve) - if err != nil { - failedCycles++ - cp.logger.Debug( - "failed to query the consumer chain for the block", - zap.Uint32("current_failures", failedCycles), - zap.Uint64("block_to_retrieve", blockToRetrieve), - zap.Error(err), - ) - } else { - // no error and we got the header we wanted to get, bump the state and push - // notification about data - cp.nextHeight = blockToRetrieve + 1 - failedCycles = 0 - cp.metrics.RecordLastPolledHeight(block.Height) - - cp.logger.Info("the poller retrieved the block from the consumer chain", - zap.Uint64("height", block.Height)) - - // push the data to the channel - // Note: if the consumer is too slow -- the buffer is full - // the channel will block, and we will stop retrieving data from the node - cp.blockInfoChan <- block - } - - if failedCycles > maxFailedCycles { - cp.logger.Fatal("the poller has reached the max failed cycles, exiting") - } + continue case req := <-cp.skipHeightChan: // no need to skip heights if the target height is not higher // than the next height to retrieve diff --git a/finality-provider/service/chain_poller_test.go b/finality-provider/service/chain_poller_test.go index b07a5e82..b7f37592 100644 --- a/finality-provider/service/chain_poller_test.go +++ b/finality-provider/service/chain_poller_test.go @@ -8,7 +8,6 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "go.uber.org/zap" fpcfg "github.com/babylonlabs-io/finality-provider/finality-provider/config" "github.com/babylonlabs-io/finality-provider/finality-provider/service" @@ -49,7 +48,7 @@ func FuzzChainPoller_Start(f *testing.F) { m := metrics.NewFpMetrics() pollerCfg := fpcfg.DefaultChainPollerConfig() pollerCfg.PollInterval = 10 * time.Millisecond - poller := service.NewChainPoller(zap.NewNop(), &pollerCfg, mockClientController, m) + poller := service.NewChainPoller(testutil.GetTestLogger(t), &pollerCfg, mockClientController, m) err := poller.Start(startHeight) require.NoError(t, err) defer func() { @@ -100,7 +99,7 @@ func FuzzChainPoller_SkipHeight(f *testing.F) { m := metrics.NewFpMetrics() pollerCfg := fpcfg.DefaultChainPollerConfig() pollerCfg.PollInterval = 1 * time.Second - poller := service.NewChainPoller(zap.NewNop(), &pollerCfg, mockClientController, m) + poller := service.NewChainPoller(testutil.GetTestLogger(t), &pollerCfg, mockClientController, m) // should expect error if the poller is not started err := poller.SkipToHeight(skipHeight) require.Error(t, err) diff --git a/finality-provider/service/event_loops.go b/finality-provider/service/event_loops.go index 2fcd6281..f3e8c42f 100644 --- a/finality-provider/service/event_loops.go +++ b/finality-provider/service/event_loops.go @@ -44,76 +44,6 @@ type UnjailFinalityProviderResponse struct { TxHash string } -// monitorStatusUpdate periodically check the status of the running finality provider and update -// it accordingly. We update the status by querying the latest voting power and the slashed_height. -// In particular, we perform the following status transitions (REGISTERED, ACTIVE, INACTIVE): -// 1. if power == 0 and status == ACTIVE, change to INACTIVE -// 2. if power > 0, change to ACTIVE -// NOTE: once error occurs, we log and continue as the status update is not critical to the entire program -func (app *FinalityProviderApp) monitorStatusUpdate() { - defer app.wg.Done() - - if app.config.StatusUpdateInterval == 0 { - app.logger.Info("the status update is disabled") - return - } - - statusUpdateTicker := time.NewTicker(app.config.StatusUpdateInterval) - defer statusUpdateTicker.Stop() - - for { - select { - case <-statusUpdateTicker.C: - fpi := app.fpIns - if fpi == nil { - continue - } - - latestBlock, err := app.getLatestBlockWithRetry() - if err != nil { - app.logger.Debug("failed to get the latest block", zap.Error(err)) - continue - } - oldStatus := fpi.GetStatus() - power, err := fpi.GetVotingPowerWithRetry(latestBlock.Height) - if err != nil { - app.logger.Debug( - "failed to get the voting power", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.Uint64("height", latestBlock.Height), - zap.Error(err), - ) - continue - } - // power > 0 (slashed_height must > 0), set status to ACTIVE - if power > 0 { - if oldStatus != proto.FinalityProviderStatus_ACTIVE { - fpi.MustSetStatus(proto.FinalityProviderStatus_ACTIVE) - app.logger.Debug( - "the finality-provider status is changed to ACTIVE", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.String("old_status", oldStatus.String()), - zap.Uint64("power", power), - ) - } - continue - } - // power == 0 and slashed_height == 0, change to INACTIVE if the current status is ACTIVE - if oldStatus == proto.FinalityProviderStatus_ACTIVE { - fpi.MustSetStatus(proto.FinalityProviderStatus_INACTIVE) - app.logger.Debug( - "the finality-provider status is changed to INACTIVE", - zap.String("fp_btc_pk", fpi.GetBtcPkHex()), - zap.String("old_status", oldStatus.String()), - ) - } - case <-app.quit: - app.logger.Info("exiting monitor fp status update loop") - return - } - } -} - // event loop for critical errors func (app *FinalityProviderApp) monitorCriticalErr() { defer app.wg.Done() @@ -269,14 +199,12 @@ func (app *FinalityProviderApp) metricsUpdateLoop() { defer updateTicker.Stop() for { + if app.fpIns != nil { + app.metrics.UpdateFpMetrics(app.fpIns.GetStoreFinalityProvider()) + } select { case <-updateTicker.C: - fps, err := app.fps.GetAllStoredFinalityProviders() - if err != nil { - app.logger.Error("failed to get finality-providers from the store", zap.Error(err)) - continue - } - app.metrics.UpdateFpMetrics(fps) + continue case <-app.quit: app.logger.Info("exiting metrics update loop") return diff --git a/finality-provider/service/fp_instance.go b/finality-provider/service/fp_instance.go index 71360687..03817441 100644 --- a/finality-provider/service/fp_instance.go +++ b/finality-provider/service/fp_instance.go @@ -112,14 +112,12 @@ func (fp *FinalityProviderInstance) Start() error { return fmt.Errorf("%w: %s", ErrFinalityProviderJailed, fp.GetBtcPkHex()) } - fp.logger.Info("Starting finality-provider instance", zap.String("pk", fp.GetBtcPkHex())) - startHeight, err := fp.DetermineStartHeight() if err != nil { return fmt.Errorf("failed to get the start height: %w", err) } - fp.logger.Info("starting the finality provider", + fp.logger.Info("starting the finality provider instance", zap.String("pk", fp.GetBtcPkHex()), zap.Uint64("height", startHeight)) poller := NewChainPoller(fp.logger, fp.cfg.PollerConfig, fp.cc, fp.metrics) @@ -175,7 +173,8 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { for { select { case <-time.After(fp.cfg.SignatureSubmissionInterval): - pollerBlocks := fp.getAllBlocksFromChan() + // start submission in the first iteration + pollerBlocks := fp.getBatchBlocksFromChan() if len(pollerBlocks) == 0 { continue } @@ -185,7 +184,18 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { zap.Uint64("start_height", pollerBlocks[0].Height), zap.Uint64("end_height", targetHeight), ) - res, err := fp.retrySubmitSigsUntilFinalized(pollerBlocks) + + processedBlocks, err := fp.processBlocksToVote(pollerBlocks) + if err != nil { + fp.reportCriticalErr(err) + continue + } + + if len(processedBlocks) == 0 { + continue + } + + res, err := fp.retrySubmitSigsUntilFinalized(processedBlocks) if err != nil { fp.metrics.IncrementFpTotalFailedVotes(fp.GetBtcPkHex()) if !errors.Is(err, ErrFinalityProviderShutDown) { @@ -207,7 +217,6 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { zap.Uint64("end_height", targetHeight), zap.String("tx_hash", res.TxHash), ) - case <-fp.quit: fp.logger.Info("the finality signature submission loop is closing") return @@ -215,21 +224,64 @@ func (fp *FinalityProviderInstance) finalitySigSubmissionLoop() { } } -func (fp *FinalityProviderInstance) getAllBlocksFromChan() []*types.BlockInfo { +// processBlocksToVote processes a batch a blocks and picks ones that need to vote +// it also updates the fp instance status according to the block's voting power +func (fp *FinalityProviderInstance) processBlocksToVote(blocks []*types.BlockInfo) ([]*types.BlockInfo, error) { + processedBlocks := make([]*types.BlockInfo, 0, len(blocks)) + + var power uint64 + var err error + for _, b := range blocks { + blk := *b + if blk.Height <= fp.GetLastVotedHeight() { + fp.logger.Debug( + "the block height is lower than last processed height", + zap.String("pk", fp.GetBtcPkHex()), + zap.Uint64("block_height", blk.Height), + zap.Uint64("last_voted_height", fp.GetLastVotedHeight()), + ) + continue + } + + // check whether the finality provider has voting power + power, err = fp.GetVotingPowerWithRetry(blk.Height) + if err != nil { + return nil, fmt.Errorf("failed to get voting power for height %d: %w", blk.Height, err) + } + if power == 0 { + fp.logger.Debug( + "the finality-provider does not have voting power", + zap.String("pk", fp.GetBtcPkHex()), + zap.Uint64("block_height", blk.Height), + ) + + // the finality provider does not have voting power + // and it will never will at this block, so continue + fp.metrics.IncrementFpTotalBlocksWithoutVotingPower(fp.GetBtcPkHex()) + continue + } + + processedBlocks = append(processedBlocks, &blk) + } + + // update fp status according to the power for the last block + if power > 0 && fp.GetStatus() != proto.FinalityProviderStatus_ACTIVE { + fp.MustSetStatus(proto.FinalityProviderStatus_ACTIVE) + } + + if power == 0 && fp.GetStatus() == proto.FinalityProviderStatus_ACTIVE { + fp.MustSetStatus(proto.FinalityProviderStatus_INACTIVE) + } + + return processedBlocks, nil +} + +func (fp *FinalityProviderInstance) getBatchBlocksFromChan() []*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) - } + pollerBlocks = append(pollerBlocks, b) if len(pollerBlocks) == int(fp.cfg.BatchSubmissionSize) { return pollerBlocks } @@ -242,47 +294,23 @@ func (fp *FinalityProviderInstance) getAllBlocksFromChan() []*types.BlockInfo { } } -func (fp *FinalityProviderInstance) shouldProcessBlock(b *types.BlockInfo) (bool, error) { - if b.Height <= fp.GetLastVotedHeight() { - fp.logger.Debug( - "the block height is lower than last processed height", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("block_height", b.Height), - zap.Uint64("last_voted_height", fp.GetLastVotedHeight()), - ) - return false, nil - } - - // check whether the finality provider has voting power - hasVp, err := fp.hasVotingPower(b) - 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.metrics.IncrementFpTotalBlocksWithoutVotingPower(fp.GetBtcPkHex()) - return false, nil - } - - return true, nil -} - func (fp *FinalityProviderInstance) randomnessCommitmentLoop() { defer fp.wg.Done() - commitRandTicker := time.NewTicker(fp.cfg.RandomnessCommitInterval) - defer commitRandTicker.Stop() - for { select { - case <-commitRandTicker.C: - tipBlock, err := fp.getLatestBlockWithRetry() + case <-time.After(fp.cfg.RandomnessCommitInterval): + // start randomness commit in the first iteration + should, startHeight, err := fp.ShouldCommitRandomness() if err != nil { fp.reportCriticalErr(err) continue } - txRes, err := fp.retryCommitPubRandUntilBlockFinalized(tipBlock) + if !should { + continue + } + + txRes, err := fp.CommitPubRand(startHeight) if err != nil { fp.metrics.IncrementFpTotalFailedRandomness(fp.GetBtcPkHex()) fp.reportCriticalErr(err) @@ -296,7 +324,6 @@ func (fp *FinalityProviderInstance) randomnessCommitmentLoop() { zap.String("tx_hash", txRes.TxHash), ) } - case <-fp.quit: fp.logger.Info("the randomness commitment loop is closing") return @@ -304,22 +331,61 @@ func (fp *FinalityProviderInstance) randomnessCommitmentLoop() { } } -func (fp *FinalityProviderInstance) hasVotingPower(b *types.BlockInfo) (bool, error) { - power, err := fp.GetVotingPowerWithRetry(b.Height) +// ShouldCommitRandomness determines whether a new randomness commit should be made +// Note: there's a delay from the commit is submitted to it is available to use due +// to timestamping. Therefore, the start height of the commit should consider an +// estimated delay. +// If randomness should be committed, start height of the commit will be returned +func (fp *FinalityProviderInstance) ShouldCommitRandomness() (bool, uint64, error) { + lastCommittedHeight, err := fp.GetLastCommittedHeight() if err != nil { - return false, err + return false, 0, fmt.Errorf("failed to get last committed height: %w", err) } - if power == 0 { + + tipBlock, err := fp.getLatestBlockWithRetry() + if err != nil { + return false, 0, fmt.Errorf("failed to get the last block: %w", err) + } + tipHeight := tipBlock.Height + + tipHeightWithDelay := tipHeight + uint64(fp.cfg.TimestampingDelayBlocks) + + var startHeight uint64 + switch { + case lastCommittedHeight < tipHeightWithDelay: + // the start height should consider the timestamping delay + // as it is only available to use after tip height + estimated timestamping delay + startHeight = tipHeightWithDelay + case lastCommittedHeight < tipHeightWithDelay+uint64(fp.cfg.NumPubRand): + startHeight = lastCommittedHeight + 1 + default: + // the randomness is sufficient, no need to make another commit fp.logger.Debug( - "the finality-provider does not have voting power", + "the finality-provider has sufficient public randomness, skip committing more", zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("block_height", b.Height), + zap.Uint64("tip_height", tipHeight), + zap.Uint64("last_committed_height", lastCommittedHeight), ) + return false, 0, nil + } - return false, nil + fp.logger.Debug( + "the finality-provider should commit randomness", + zap.String("pk", fp.GetBtcPkHex()), + zap.Uint64("tip_height", tipHeight), + zap.Uint64("last_committed_height", lastCommittedHeight), + ) + + activationBlkHeight, err := fp.cc.QueryFinalityActivationBlockHeight() + if err != nil { + return false, 0, err } - return true, nil + // make sure that the start height is at least the finality activation height + // and updated to generate the list with the same as the committed height. + startHeight = max(startHeight, activationBlkHeight) + + return true, startHeight, nil } func (fp *FinalityProviderInstance) reportCriticalErr(err error) { @@ -393,6 +459,7 @@ func (fp *FinalityProviderInstance) retrySubmitSigsUntilFinalized(targetBlocks [ select { case <-time.After(fp.cfg.SubmissionRetryInterval): // Continue to next retry iteration + continue case <-fp.quit: fp.logger.Debug("the finality-provider instance is closing", zap.String("pk", fp.GetBtcPkHex())) return nil, ErrFinalityProviderShutDown @@ -409,109 +476,8 @@ func (fp *FinalityProviderInstance) checkBlockFinalization(height uint64) (bool, return b.Finalized, nil } -// retryCommitPubRandUntilBlockFinalized periodically tries to commit public rand 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) retryCommitPubRandUntilBlockFinalized(targetBlock *types.BlockInfo) (*types.TxResponse, error) { - var failedCycles uint32 - - // we break the for loop if the block is finalized or the public rand is successfully committed - // 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 - // TODO: CommitPubRand also includes saving all inclusion proofs of public randomness - // this part should not be retried here. We need to separate the function into - // 1) determining the starting height to commit, 2) generating pub rand and inclusion - // proofs, and 3) committing public randomness. - res, err := fp.CommitPubRand(targetBlock.Height) - if err != nil { - if clientcontroller.IsUnrecoverable(err) { - return nil, err - } - fp.logger.Debug( - "failed to commit public randomness to the consumer chain", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint32("current_failures", failedCycles), - zap.Uint64("target_block_height", targetBlock.Height), - zap.Error(err), - ) - - failedCycles++ - if failedCycles > fp.cfg.MaxSubmissionRetries { - return nil, fmt.Errorf("reached max failed cycles with err: %w", err) - } - } else { - // the public randomness has been successfully submitted - return res, nil - } - select { - case <-time.After(fp.cfg.SubmissionRetryInterval): - // periodically query the index block to be later checked whether it is Finalized - finalized, err := fp.checkBlockFinalization(targetBlock.Height) - if err != nil { - return nil, fmt.Errorf("failed to query block finalization at height %v: %w", targetBlock.Height, err) - } - if finalized { - fp.logger.Debug( - "the block is already finalized, skip submission", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("target_height", targetBlock.Height), - ) - // returning nil here is to safely break the loop - // the error still exists - return nil, nil - } - - case <-fp.quit: - fp.logger.Debug("the finality-provider instance is closing", zap.String("pk", fp.GetBtcPkHex())) - return nil, nil - } - } -} - -// CommitPubRand generates a list of Schnorr rand pairs, -// commits the public randomness for the managed finality providers, -// and save the randomness pair to DB -// Note: -// - if there is no pubrand committed before, it will start from the tipHeight -// - if the tipHeight is too large, it will only commit fp.cfg.NumPubRand pairs -func (fp *FinalityProviderInstance) CommitPubRand(tipHeight uint64) (*types.TxResponse, error) { - lastCommittedHeight, err := fp.GetLastCommittedHeight() - if err != nil { - return nil, err - } - - var startHeight uint64 - switch { - case lastCommittedHeight == uint64(0): - // the finality-provider has never submitted public rand before - startHeight = tipHeight + 1 - case lastCommittedHeight < uint64(fp.cfg.MinRandHeightGap)+tipHeight: - // (should not use subtraction because they are in the type of uint64) - // we are running out of the randomness - startHeight = lastCommittedHeight + 1 - default: - fp.logger.Debug( - "the finality-provider has sufficient public randomness, skip committing more", - zap.String("pk", fp.GetBtcPkHex()), - zap.Uint64("block_height", tipHeight), - zap.Uint64("last_committed_height", lastCommittedHeight), - ) - return nil, nil - } - - return fp.commitPubRandPairs(startHeight) -} - -// it will commit fp.cfg.NumPubRand pairs of public randomness starting from startHeight -func (fp *FinalityProviderInstance) commitPubRandPairs(startHeight uint64) (*types.TxResponse, error) { - activationBlkHeight, err := fp.cc.QueryFinalityActivationBlockHeight() - if err != nil { - return nil, err - } - - // make sure that the start height is at least the finality activation height - // and updated to generate the list with the same as the committed height. - startHeight = max(startHeight, activationBlkHeight) +// CommitPubRand commits a list of randomness from given start height +func (fp *FinalityProviderInstance) CommitPubRand(startHeight uint64) (*types.TxResponse, error) { // generate a list of Schnorr randomness pairs // NOTE: currently, calling this will create and save a list of randomness // in case of failure, randomness that has been created will be overwritten @@ -608,7 +574,7 @@ func (fp *FinalityProviderInstance) TestCommitPubRandWithStartHeight(startHeight fp.logger.Info("Start committing pubrand from block height", zap.Uint64("start_height", startHeight)) for startHeight <= targetBlockHeight { - _, err = fp.commitPubRandPairs(startHeight) + _, err = fp.CommitPubRand(startHeight) if err != nil { return err } diff --git a/finality-provider/service/fp_instance_test.go b/finality-provider/service/fp_instance_test.go index 468d2fba..71612b49 100644 --- a/finality-provider/service/fp_instance_test.go +++ b/finality-provider/service/fp_instance_test.go @@ -13,7 +13,6 @@ import ( ftypes "github.com/babylonlabs-io/babylon/x/finality/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "go.uber.org/zap" "github.com/babylonlabs-io/finality-provider/clientcontroller" "github.com/babylonlabs-io/finality-provider/eotsmanager" @@ -42,7 +41,7 @@ func FuzzCommitPubRandList(f *testing.F) { expectedTxHash := testutil.GenRandomHexStr(r, 32) mockClientController.EXPECT(). - CommitPubRandList(fpIns.GetBtcPk(), startingBlock.Height+1, gomock.Any(), gomock.Any(), gomock.Any()). + CommitPubRandList(fpIns.GetBtcPk(), startingBlock.Height, gomock.Any(), gomock.Any(), gomock.Any()). Return(&types.TxResponse{TxHash: expectedTxHash}, nil).AnyTimes() mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).AnyTimes() res, err := fpIns.CommitPubRand(startingBlock.Height) @@ -65,7 +64,6 @@ func FuzzSubmitFinalitySigs(f *testing.F) { defer cleanUp() // commit pub rand - mockClientController.EXPECT().QueryLastCommittedPublicRand(gomock.Any(), uint64(1)).Return(nil, nil).Times(1) mockClientController.EXPECT().CommitPubRandList(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil).Times(1) _, err := fpIns.CommitPubRand(startingBlock.Height) require.NoError(t, err) @@ -148,7 +146,7 @@ func startFinalityProviderAppWithRegisteredFp( startingHeight uint64, numPubRand uint32, ) (*service.FinalityProviderApp, *service.FinalityProviderInstance, func()) { - logger := zap.NewNop() + logger := testutil.GetTestLogger(t) // create an EOTS manager eotsHomeDir := filepath.Join(t.TempDir(), "eots-home") eotsCfg := eotscfg.DefaultConfigWithHomePath(eotsHomeDir) diff --git a/finality-provider/service/server.go b/finality-provider/service/server.go index 3cd999c0..6d3dd46e 100644 --- a/finality-provider/service/server.go +++ b/finality-provider/service/server.go @@ -81,9 +81,7 @@ func (s *Server) RunUntilShutdown() error { return fmt.Errorf("failed to listen on %s: %w", listenAddr, err) } defer func() { - if err := lis.Close(); err != nil { - s.logger.Error(fmt.Sprintf("Failed to close network listener: %v", err)) - } + _ = lis.Close() }() grpcServer := grpc.NewServer() diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 01ce10eb..99754def 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -1,3 +1,6 @@ +//go:build e2e +// +build e2e + package e2etest import ( @@ -5,26 +8,25 @@ import ( "encoding/json" "errors" "fmt" - eotscmd "github.com/babylonlabs-io/finality-provider/eotsmanager/cmd/eotsd/daemon" - eotscfg "github.com/babylonlabs-io/finality-provider/eotsmanager/config" - "github.com/babylonlabs-io/finality-provider/finality-provider/store" - "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/jessevdk/go-flags" "log" "math/rand" "os" "testing" "time" - bbntypes "github.com/babylonlabs-io/babylon/types" - bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" - sdkmath "cosmossdk.io/math" "github.com/babylonlabs-io/babylon/testutil/datagen" + bbntypes "github.com/babylonlabs-io/babylon/types" + bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/jessevdk/go-flags" "github.com/stretchr/testify/require" + eotscmd "github.com/babylonlabs-io/finality-provider/eotsmanager/cmd/eotsd/daemon" + eotscfg "github.com/babylonlabs-io/finality-provider/eotsmanager/config" "github.com/babylonlabs-io/finality-provider/finality-provider/cmd/fpd/daemon" + "github.com/babylonlabs-io/finality-provider/finality-provider/store" "github.com/babylonlabs-io/finality-provider/types" ) diff --git a/itest/test_manager.go b/itest/test_manager.go index fc428a54..99d98317 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -257,8 +257,8 @@ func StartManagerWithFinalityProvider(t *testing.T, n int) (*TestManager, []*ser } func (tm *TestManager) Stop(t *testing.T) { - for _, fp := range tm.Fps { - err := fp.Stop() + for _, fpApp := range tm.Fps { + err := fpApp.Stop() require.NoError(t, err) } err := tm.manager.ClearResources() @@ -809,7 +809,7 @@ func defaultFpConfig(keyringDir, homeDir string) *fpcfg.Config { cfg.NumPubRand = 1000 cfg.NumPubRandMax = 1000 - cfg.MinRandHeightGap = 500 + cfg.TimestampingDelayBlocks = 0 cfg.BitcoinNetwork = "simnet" cfg.BTCNetParams = chaincfg.SimNetParams diff --git a/metrics/fp_collectors.go b/metrics/fp_collectors.go index b9a0e135..11e00e89 100644 --- a/metrics/fp_collectors.go +++ b/metrics/fp_collectors.go @@ -269,19 +269,17 @@ func (fm *FpMetrics) RecordFpRandomnessTime(fpBtcPkHex string) { fm.previousRandomnessByFp[fpBtcPkHex] = &now } -func (fm *FpMetrics) UpdateFpMetrics(fps []*store.StoredFinalityProvider) { +func (fm *FpMetrics) UpdateFpMetrics(fp *store.StoredFinalityProvider) { fm.mu.Lock() defer fm.mu.Unlock() - for _, fp := range fps { - fm.RecordFpStatus(fp.GetBIP340BTCPK().MarshalHex(), fp.Status) + fm.RecordFpStatus(fp.GetBIP340BTCPK().MarshalHex(), fp.Status) - if lastVoteTime, ok := fm.previousVoteByFp[fp.GetBIP340BTCPK().MarshalHex()]; ok { - fm.RecordFpSecondsSinceLastVote(fp.GetBIP340BTCPK().MarshalHex(), time.Since(*lastVoteTime).Seconds()) - } + if lastVoteTime, ok := fm.previousVoteByFp[fp.GetBIP340BTCPK().MarshalHex()]; ok { + fm.RecordFpSecondsSinceLastVote(fp.GetBIP340BTCPK().MarshalHex(), time.Since(*lastVoteTime).Seconds()) + } - if lastRandomnessTime, ok := fm.previousRandomnessByFp[fp.GetBIP340BTCPK().MarshalHex()]; ok { - fm.RecordFpSecondsSinceLastRandomness(fp.GetBIP340BTCPK().MarshalHex(), time.Since(*lastRandomnessTime).Seconds()) - } + if lastRandomnessTime, ok := fm.previousRandomnessByFp[fp.GetBIP340BTCPK().MarshalHex()]; ok { + fm.RecordFpSecondsSinceLastRandomness(fp.GetBIP340BTCPK().MarshalHex(), time.Since(*lastRandomnessTime).Seconds()) } } diff --git a/testutil/logger.go b/testutil/logger.go new file mode 100644 index 00000000..80aa29bd --- /dev/null +++ b/testutil/logger.go @@ -0,0 +1,17 @@ +package testutil + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func GetTestLogger(t *testing.T) *zap.Logger { + loggerConfig := zap.NewDevelopmentConfig() + loggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) + logger, err := loggerConfig.Build() + + require.NoError(t, err) + return logger +}