diff --git a/btcstaking-tracker/stakingeventwatcher/expected_babylon_client.go b/btcstaking-tracker/stakingeventwatcher/expected_babylon_client.go index 63a47ba..8c157d2 100644 --- a/btcstaking-tracker/stakingeventwatcher/expected_babylon_client.go +++ b/btcstaking-tracker/stakingeventwatcher/expected_babylon_client.go @@ -2,19 +2,31 @@ package stakingeventwatcher import ( "context" + sdkerrors "cosmossdk.io/errors" "fmt" - - btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" - - "cosmossdk.io/errors" + "github.com/avast/retry-go/v4" + btclctypes "github.com/babylonlabs-io/babylon/x/btclightclient/types" + "github.com/babylonlabs-io/vigilante/config" + "github.com/cosmos/cosmos-sdk/client" + "strings" + "time" + + "errors" bbnclient "github.com/babylonlabs-io/babylon/client/client" bbn "github.com/babylonlabs-io/babylon/types" + btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" btcstakingtypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/cosmos/cosmos-sdk/types/query" ) +var ( + ErrHeaderNotKnownToBabylon = errors.New("btc header not known to babylon") + ErrHeaderOnBabylonLCFork = errors.New("btc header is on babylon btc light client fork") + ErrBabylonBtcLightClientNotReady = errors.New("babylon btc light client is not ready to receive delegation") +) + type Delegation struct { StakingTx *wire.MsgTx StakingOutputIdx uint32 @@ -23,6 +35,10 @@ type Delegation struct { HasProof bool } +type BabylonParams struct { + ConfirmationTimeBlocks uint32 // K-deep +} + type BabylonNodeAdapter interface { DelegationsByStatus(status btcstakingtypes.BTCDelegationStatus, offset uint64, limit uint64) ([]Delegation, error) IsDelegationActive(stakingTxHash chainhash.Hash) (bool, error) @@ -31,17 +47,21 @@ type BabylonNodeAdapter interface { inclusionProof *btcstakingtypes.InclusionProof) error BtcClientTipHeight() (uint32, error) ActivateDelegation(ctx context.Context, stakingTxHash chainhash.Hash, proof *btcctypes.BTCSpvProof) error + QueryHeaderDepth(headerHash *chainhash.Hash) (uint32, error) + Params() (*BabylonParams, error) } type BabylonClientAdapter struct { babylonClient *bbnclient.Client + cfg *config.BTCStakingTrackerConfig } var _ BabylonNodeAdapter = (*BabylonClientAdapter)(nil) -func NewBabylonClientAdapter(babylonClient *bbnclient.Client) *BabylonClientAdapter { +func NewBabylonClientAdapter(babylonClient *bbnclient.Client, cfg *config.BTCStakingTrackerConfig) *BabylonClientAdapter { return &BabylonClientAdapter{ babylonClient: babylonClient, + cfg: cfg, } } @@ -127,7 +147,7 @@ func (bca *BabylonClientAdapter) ReportUnbonding( StakeSpendingTxInclusionProof: inclusionProof, } - resp, err := bca.babylonClient.ReliablySendMsg(ctx, &msg, []*errors.Error{}, []*errors.Error{}) + resp, err := bca.babylonClient.ReliablySendMsg(ctx, &msg, []*sdkerrors.Error{}, []*sdkerrors.Error{}) if err != nil && resp != nil { return fmt.Errorf("msg MsgBTCUndelegate failed exeuction with code %d and error %w", resp.Code, err) } @@ -161,7 +181,7 @@ func (bca *BabylonClientAdapter) ActivateDelegation( StakingTxInclusionProof: btcstakingtypes.NewInclusionProofFromSpvProof(proof), } - resp, err := bca.babylonClient.ReliablySendMsg(ctx, &msg, []*errors.Error{}, []*errors.Error{}) + resp, err := bca.babylonClient.ReliablySendMsg(ctx, &msg, []*sdkerrors.Error{}, []*sdkerrors.Error{}) if err != nil && resp != nil { return fmt.Errorf("msg MsgAddBTCDelegationInclusionProof failed exeuction with code %d and error %w", resp.Code, err) @@ -173,3 +193,52 @@ func (bca *BabylonClientAdapter) ActivateDelegation( return nil } + +func (bca *BabylonClientAdapter) QueryHeaderDepth(headerHash *chainhash.Hash) (uint32, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + clientCtx := client.Context{Client: bca.babylonClient.RPCClient} + queryClient := btclctypes.NewQueryClient(clientCtx) + + var response *btclctypes.QueryHeaderDepthResponse + if err := retry.Do(func() error { + depthResponse, err := queryClient.HeaderDepth(ctx, &btclctypes.QueryHeaderDepthRequest{Hash: headerHash.String()}) + if err != nil { + return err + } + response = depthResponse + return nil + }, + retry.Attempts(5), + retry.MaxDelay(bca.cfg.RetrySubmitUnbondingTxInterval), + retry.MaxJitter(bca.cfg.RetryJitter), + retry.LastErrorOnly(true)); err != nil { + if strings.Contains(err.Error(), btclctypes.ErrHeaderDoesNotExist.Error()) { + return 0, fmt.Errorf("%s: %w", err.Error(), ErrHeaderNotKnownToBabylon) + } + return 0, err + } + + return response.Depth, nil +} + +func (bca *BabylonClientAdapter) Params() (*BabylonParams, error) { + var bccParams *btcctypes.Params + if err := retry.Do(func() error { + response, err := bca.babylonClient.BTCCheckpointParams() + if err != nil { + return err + } + bccParams = &response.Params + + return nil + }, retry.Attempts(5), + retry.MaxDelay(bca.cfg.RetrySubmitUnbondingTxInterval), + retry.MaxJitter(bca.cfg.RetryJitter), + retry.LastErrorOnly(true)); err != nil { + return nil, err + } + + return &BabylonParams{ConfirmationTimeBlocks: bccParams.BtcConfirmationDepth}, nil +} diff --git a/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go b/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go index 2daedc3..99f1e77 100644 --- a/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go +++ b/btcstaking-tracker/stakingeventwatcher/mock_babylon_client.go @@ -112,6 +112,36 @@ func (mr *MockBabylonNodeAdapterMockRecorder) IsDelegationVerified(stakingTxHash return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDelegationVerified", reflect.TypeOf((*MockBabylonNodeAdapter)(nil).IsDelegationVerified), stakingTxHash) } +// Params mocks base method. +func (m *MockBabylonNodeAdapter) Params() (*BabylonParams, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Params") + ret0, _ := ret[0].(*BabylonParams) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Params indicates an expected call of Params. +func (mr *MockBabylonNodeAdapterMockRecorder) Params() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Params", reflect.TypeOf((*MockBabylonNodeAdapter)(nil).Params)) +} + +// QueryHeaderDepth mocks base method. +func (m *MockBabylonNodeAdapter) QueryHeaderDepth(headerHash *chainhash.Hash) (uint32, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryHeaderDepth", headerHash) + ret0, _ := ret[0].(uint32) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryHeaderDepth indicates an expected call of QueryHeaderDepth. +func (mr *MockBabylonNodeAdapterMockRecorder) QueryHeaderDepth(headerHash interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryHeaderDepth", reflect.TypeOf((*MockBabylonNodeAdapter)(nil).QueryHeaderDepth), headerHash) +} + // ReportUnbonding mocks base method. func (m *MockBabylonNodeAdapter) ReportUnbonding(ctx context.Context, stakingTxHash chainhash.Hash, stakeSpendingTx *wire.MsgTx, inclusionProof *types0.InclusionProof) error { m.ctrl.T.Helper() diff --git a/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go b/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go index 2a873e1..8b21115 100644 --- a/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go +++ b/btcstaking-tracker/stakingeventwatcher/stakingeventwatcher.go @@ -577,6 +577,12 @@ func (sew *StakingEventWatcher) handlerVerifiedDelegations() { // checkBtcForStakingTx gets a snapshot of current Delegations in cache // checks if staking tx is in BTC, generates a proof and invokes sending of MsgAddBTCDelegationInclusionProof func (sew *StakingEventWatcher) checkBtcForStakingTx() { + params, err := sew.babylonNodeAdapter.Params() + if err != nil { + sew.logger.Errorf("error getting tx params %v", err) + return + } + for del := range sew.pendingTracker.DelegationsIter() { if del.ActivationInProgress { continue @@ -602,13 +608,17 @@ func (sew *StakingEventWatcher) checkBtcForStakingTx() { continue } - go sew.activateBtcDelegation(txHash, proof) + go sew.activateBtcDelegation(txHash, proof, details.Block.BlockHash(), params.ConfirmationTimeBlocks) } } // activateBtcDelegation invokes bbn client and send MsgAddBTCDelegationInclusionProof func (sew *StakingEventWatcher) activateBtcDelegation( - stakingTxHash chainhash.Hash, proof *btcctypes.BTCSpvProof) { + stakingTxHash chainhash.Hash, + proof *btcctypes.BTCSpvProof, + inclusionBlockHash chainhash.Hash, + requiredDepth uint32, +) { ctx, cancel := sew.quitContext() defer cancel() @@ -618,6 +628,8 @@ func (sew *StakingEventWatcher) activateBtcDelegation( sew.logger.Debugf("skipping tx %s is not in pending tracker, err: %v", stakingTxHash, err) } + sew.waitForRequiredDepth(ctx, stakingTxHash, &inclusionBlockHash, requiredDepth) + _ = retry.Do(func() error { verified, err := sew.babylonNodeAdapter.IsDelegationVerified(stakingTxHash) if err != nil { @@ -652,6 +664,45 @@ func (sew *StakingEventWatcher) activateBtcDelegation( ) } +func (sew *StakingEventWatcher) waitForRequiredDepth( + ctx context.Context, + stakingTxHash chainhash.Hash, + inclusionBlockHash *chainhash.Hash, + requiredDepth uint32, +) { + var depth uint32 + _ = retry.Do(func() error { + var err error + depth, err = sew.babylonNodeAdapter.QueryHeaderDepth(inclusionBlockHash) + if err != nil { + // If the header is not known to babylon, or it is on LCFork, then most probably + // lc is not up to date, we should retry sending delegation after some time. + if errors.Is(err, ErrHeaderNotKnownToBabylon) || errors.Is(err, ErrHeaderOnBabylonLCFork) { + return fmt.Errorf("btc light client error %s: %w", err.Error(), ErrBabylonBtcLightClientNotReady) + } + + return fmt.Errorf("error while getting delegation data: %w", err) + } + + if depth < requiredDepth { + return fmt.Errorf("btc lc not ready, required depth: %d, current depth: %d: %w", + requiredDepth, depth, ErrBabylonBtcLightClientNotReady) + } + + return nil + }, + retry.Context(ctx), + retryForever, + fixedDelyTypeWithJitter, + retry.MaxDelay(sew.cfg.RetrySubmitUnbondingTxInterval), + retry.MaxJitter(sew.cfg.RetryJitter), + retry.OnRetry(func(n uint, err error) { + sew.logger.Debugf("waiting for staking tx: %s to be k-deep. Current[%d], required[%d]. "+ + "Attempt: %d. Err: %v", stakingTxHash, depth, requiredDepth, n, err) + }), + ) +} + func (sew *StakingEventWatcher) latency(method string) func() { startTime := time.Now() return func() { diff --git a/btcstaking-tracker/tracker.go b/btcstaking-tracker/tracker.go index 85152ba..a799d12 100644 --- a/btcstaking-tracker/tracker.go +++ b/btcstaking-tracker/tracker.go @@ -68,7 +68,7 @@ func NewBTCStakingTracker( logger := parentLogger.With(zap.String("module", "btcstaking-tracker")) // watcher routine - babylonAdapter := uw.NewBabylonClientAdapter(bbnClient) + babylonAdapter := uw.NewBabylonClientAdapter(bbnClient, cfg) watcher := uw.NewStakingEventWatcher( btcNotifier, btcClient,