From 710451a05f4127f52c4ba7f4cfd6f301363946b4 Mon Sep 17 00:00:00 2001 From: Gurjot Singh <111540954+gusin13@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:25:56 +0530 Subject: [PATCH] fix: decoding issues in unbonding tx (#28) --- cmd/staking-expiry-checker/main.go | 10 +-- go.mod | 2 +- go.sum | 4 +- internal/services/params.go | 20 ----- internal/services/pollers.go | 3 - internal/services/service.go | 20 +++-- internal/services/watch_btc_events.go | 118 ++++++++++---------------- params/params.go | 26 ++++++ 8 files changed, 92 insertions(+), 111 deletions(-) delete mode 100644 internal/services/params.go create mode 100644 params/params.go diff --git a/cmd/staking-expiry-checker/main.go b/cmd/staking-expiry-checker/main.go index bd7171c..09826cd 100644 --- a/cmd/staking-expiry-checker/main.go +++ b/cmd/staking-expiry-checker/main.go @@ -14,7 +14,7 @@ import ( "github.com/babylonlabs-io/staking-expiry-checker/internal/config" "github.com/babylonlabs-io/staking-expiry-checker/internal/db" "github.com/babylonlabs-io/staking-expiry-checker/internal/services" - "github.com/babylonlabs-io/staking-expiry-checker/internal/types" + "github.com/babylonlabs-io/staking-expiry-checker/params" ) func init() { @@ -36,11 +36,11 @@ func main() { log.Fatal().Err(err).Msg(fmt.Sprintf("error while loading config file: %s", cfgPath)) } - paramsPath := cli.GetGlobalParamsPath() - params, err := types.NewGlobalParams(paramsPath) + paramsRetriever, err := params.NewGlobalParamsRetriever(cli.GetGlobalParamsPath()) if err != nil { - log.Fatal().Err(err).Msg(fmt.Sprintf("error while loading global params file: %s", paramsPath)) + log.Fatal().Err(err).Msg("failed to initialize params retriever") } + versionedParams := paramsRetriever.VersionedParams() // Create context with signal handling ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) @@ -68,7 +68,7 @@ func main() { } // Create service - service := services.NewService(cfg, params, dbClient, btcNotifier, btcClient) + service := services.NewService(cfg, versionedParams, dbClient, btcNotifier, btcClient) if err := service.RunUntilShutdown(ctx); err != nil { log.Fatal().Err(err).Msg("failed to start service") } diff --git a/go.mod b/go.mod index c66d5c5..9722d30 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.1 require ( github.com/babylonlabs-io/babylon v0.18.1 + github.com/babylonlabs-io/networks/parameters v0.2.2 github.com/btcsuite/btcd v0.24.3-0.20241011125836-24eb815168f4 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcutil v1.1.6 @@ -33,7 +34,6 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aead/siphash v1.0.1 // indirect - github.com/babylonlabs-io/networks/parameters v0.2.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect github.com/btcsuite/btcd/btcutil/psbt v1.1.8 // indirect diff --git a/go.sum b/go.sum index 81fa90a..2153682 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3Tju github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/babylonlabs-io/babylon v0.18.1 h1:ID8BDDHc+snOnW2nqjP7OMf3a//5mvjbEmiAoJAtZlc= github.com/babylonlabs-io/babylon v0.18.1/go.mod h1:sT+KG2U+M0tDMNZZ2L5CwlXX0OpagGEs56BiWXqaZFw= -github.com/babylonlabs-io/networks/parameters v0.2.3 h1:T1nigYrU61GWSpJZko3Gylt3T3eHHoxXLWkhw7s3uz0= -github.com/babylonlabs-io/networks/parameters v0.2.3/go.mod h1:iEJVOzaLsE33vpP7J4u+CRGfkSIfErUAwRmgCFCBpyI= +github.com/babylonlabs-io/networks/parameters v0.2.2 h1:TCu39fZvjX5f6ZZrjhYe54M6wWxglNewuKu56yE+zrc= +github.com/babylonlabs-io/networks/parameters v0.2.2/go.mod h1:iEJVOzaLsE33vpP7J4u+CRGfkSIfErUAwRmgCFCBpyI= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= diff --git a/internal/services/params.go b/internal/services/params.go deleted file mode 100644 index 4ff8f0c..0000000 --- a/internal/services/params.go +++ /dev/null @@ -1,20 +0,0 @@ -package services - -import ( - "github.com/babylonlabs-io/staking-expiry-checker/internal/types" -) - -// GetVersionedGlobalParamsByHeight returns the versioned global params -// for a particular bitcoin height -func (s *Service) GetVersionedGlobalParamsByHeight(height uint64) *types.VersionedGlobalParams { - // Iterate the list in reverse (i.e. decreasing ActivationHeight) - // and identify the first element that has an activation height below - // the specified BTC height. - for i := len(s.params.Versions) - 1; i >= 0; i-- { - paramsVersion := s.params.Versions[i] - if paramsVersion.ActivationHeight <= height { - return paramsVersion - } - } - return nil -} diff --git a/internal/services/pollers.go b/internal/services/pollers.go index 56c08fe..b1fc286 100644 --- a/internal/services/pollers.go +++ b/internal/services/pollers.go @@ -28,9 +28,6 @@ func (s *Service) processBTCSubscriber(ctx context.Context) *types.Error { // Process each delegation for _, delegation := range delegations { if s.trackedSubs.IsSubscribed(delegation.StakingTxHashHex) { - log.Debug(). - Str("stakingTxHash", delegation.StakingTxHashHex). - Msg("Delegation already subscribed, skipping") continue } diff --git a/internal/services/service.go b/internal/services/service.go index 67ebcda..5f55974 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/babylonlabs-io/networks/parameters/parser" "github.com/babylonlabs-io/staking-expiry-checker/internal/btcclient" "github.com/babylonlabs-io/staking-expiry-checker/internal/config" "github.com/babylonlabs-io/staking-expiry-checker/internal/db" @@ -19,9 +20,9 @@ type Service struct { wg sync.WaitGroup quit chan struct{} - cfg *config.Config - btcNotifier notifier.ChainNotifier - params *types.GlobalParams + cfg *config.Config + btcNotifier notifier.ChainNotifier + paramsVersions *parser.ParsedGlobalParams // interfaces db db.DbInterface @@ -37,7 +38,7 @@ type Service struct { func NewService( cfg *config.Config, - params *types.GlobalParams, + paramsVersions *parser.ParsedGlobalParams, db db.DbInterface, btcNotifier notifier.ChainNotifier, btc btcclient.BtcInterface, @@ -46,7 +47,7 @@ func NewService( quit: make(chan struct{}), cfg: cfg, btcNotifier: btcNotifier, - params: params, + paramsVersions: paramsVersions, db: db, btc: btc, trackedSubs: NewTrackedSubscriptions(), @@ -138,3 +139,12 @@ func (s *Service) startBTCSubscriberPoller(ctx context.Context) { } } } + +func (s *Service) getVersionedParams(height uint64) (*parser.ParsedVersionedGlobalParams, error) { + params := s.paramsVersions.GetVersionedGlobalParamsByHeight(height) + if params == nil { + return nil, fmt.Errorf("the params for height %d does not exist", height) + } + + return params, nil +} diff --git a/internal/services/watch_btc_events.go b/internal/services/watch_btc_events.go index 7faa727..30ea075 100644 --- a/internal/services/watch_btc_events.go +++ b/internal/services/watch_btc_events.go @@ -10,6 +10,7 @@ import ( "github.com/babylonlabs-io/babylon/btcstaking" bbn "github.com/babylonlabs-io/babylon/types" + "github.com/babylonlabs-io/networks/parameters/parser" "github.com/babylonlabs-io/staking-expiry-checker/internal/db/model" "github.com/babylonlabs-io/staking-expiry-checker/internal/observability/metrics" "github.com/babylonlabs-io/staking-expiry-checker/internal/types" @@ -106,24 +107,16 @@ func (s *Service) handleSpendingStakingTransaction( return fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", err) } - paramsVersion := s.GetVersionedGlobalParamsByHeight(delegation.StakingTx.StartHeight) - if paramsVersion == nil { - log.Ctx(ctx).Error().Msg("failed to get global params") - return types.NewErrorWithMsg( - http.StatusInternalServerError, types.InternalServiceError, - "failed to get global params based on the staking tx height", - ) + paramsFromStakingTxHeight, err := s.getVersionedParams(delegation.StakingTx.StartHeight) + if err != nil { + return fmt.Errorf("failed to get versioned params from staking tx height: %w", err) } // First try to validate as unbonding tx isUnbonding, err := s.IsValidUnbondingTx( spendingTx, - delegation.StakingTx.TxHex, - delegation.StakerPkHex, - delegation.FinalityProviderPkHex, - uint32(delegation.StakingTx.OutputIndex), - uint16(delegation.StakingTx.TimeLock), - paramsVersion, + delegation, + paramsFromStakingTxHeight, ) if err != nil { if errors.Is(err, types.ErrInvalidUnbondingTx) { @@ -161,7 +154,7 @@ func (s *Service) handleSpendingStakingTransaction( delegation.StakingTxHashHex, unbondingStartHeight, unbondingTxTimestamp, - paramsVersion.UnbondingTime, + uint64(paramsFromStakingTxHeight.UnbondingTime), // valid unbonding tx always has one output uint64(0), unbondingTxHex, @@ -174,9 +167,9 @@ func (s *Service) handleSpendingStakingTransaction( } // Try to validate as withdrawal transaction - withdrawalErr := s.validateWithdrawalTxFromStaking(spendingTx, spendingInputIdx, delegation, paramsVersion) + withdrawalErr := s.validateWithdrawalTxFromStaking(spendingTx, spendingInputIdx, delegation, paramsFromStakingTxHeight) if withdrawalErr != nil { - if errors.Is(err, types.ErrInvalidWithdrawalTx) { + if errors.Is(withdrawalErr, types.ErrInvalidWithdrawalTx) { metrics.IncrementInvalidStakingWithdrawalTxCounter() log.Error(). Err(withdrawalErr). @@ -186,6 +179,10 @@ func (s *Service) handleSpendingStakingTransaction( return nil } + log.Error(). + Err(withdrawalErr). + Str("staking_tx", delegation.StakingTxHashHex). + Msg("failed to validate withdrawal tx from staking") metrics.IncrementFailedVerifyingStakingWithdrawalTxCounter() return err } @@ -207,17 +204,13 @@ func (s *Service) handleSpendingUnbondingTransaction( return fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", err) } - paramsVersion := s.GetVersionedGlobalParamsByHeight(delegation.StakingTx.StartHeight) - if paramsVersion == nil { - log.Ctx(ctx).Error().Msg("failed to get global params") - return types.NewErrorWithMsg( - http.StatusInternalServerError, types.InternalServiceError, - "failed to get global params based on the staking tx height", - ) + paramsFromStakingTxHeight, err := s.getVersionedParams(delegation.StakingTx.StartHeight) + if err != nil { + return err } // First try to validate as withdrawal transaction - withdrawalErr := s.validateWithdrawalTxFromUnbonding(spendingTx, delegation, spendingInputIdx, paramsVersion) + withdrawalErr := s.validateWithdrawalTxFromUnbonding(spendingTx, delegation, spendingInputIdx, paramsFromStakingTxHeight) if withdrawalErr != nil { if errors.Is(withdrawalErr, types.ErrInvalidWithdrawalTx) { metrics.IncrementInvalidUnbondingWithdrawalTxCounter() @@ -245,14 +238,10 @@ func (s *Service) handleSpendingUnbondingTransaction( // but is invalid func (s *Service) IsValidUnbondingTx( tx *wire.MsgTx, - stakingTxHex, - stakerPkHex, - finalityProviderPkHex string, - stakingOutputIdx uint32, - stakingTimeLock uint16, - params *types.VersionedGlobalParams, + delegation *model.DelegationDocument, + params *parser.ParsedVersionedGlobalParams, ) (bool, error) { - stakingTx, err := utils.DeserializeBtcTransactionFromHex(stakingTxHex) + stakingTx, err := utils.DeserializeBtcTransactionFromHex(delegation.StakingTx.TxHex) if err != nil { return false, fmt.Errorf("failed to deserialize staking tx: %w", err) } @@ -267,45 +256,36 @@ func (s *Service) IsValidUnbondingTx( if !tx.TxIn[0].PreviousOutPoint.Hash.IsEqual(&stakingTxHash) { return false, nil } - if tx.TxIn[0].PreviousOutPoint.Index != stakingOutputIdx { + if tx.TxIn[0].PreviousOutPoint.Index != uint32(delegation.StakingTx.OutputIndex) { return false, nil } - stakerPk, err := bbn.NewBIP340PubKeyFromHex(stakerPkHex) + stakerPk, err := bbn.NewBIP340PubKeyFromHex(delegation.StakerPkHex) if err != nil { return false, fmt.Errorf("failed to convert staker btc pkh to a public key: %w", err) } - fpPKBIP340, err := bbn.NewBIP340PubKeyFromHex(finalityProviderPkHex) + fpPKBIP340, err := bbn.NewBIP340PubKeyFromHex(delegation.FinalityProviderPkHex) if err != nil { return false, fmt.Errorf("failed to convert finality provider pk hex to a public key: %w", err) } fpPK := fpPKBIP340.MustToBTCPK() - covPks := make([]*btcec.PublicKey, len(params.CovenantPks)) - for i, hex := range params.CovenantPks { - covPk, err := bbn.NewBIP340PubKeyFromHex(hex) - if err != nil { - return false, fmt.Errorf("failed to convert finality provider pk hex to a public key: %w", err) - } - covPks[i] = covPk.MustToBTCPK() - } - btcParams, err := utils.GetBTCParams(s.cfg.Btc.NetParams) if err != nil { return false, fmt.Errorf("invalid BTC network params: %w", err) } - stakingValue := btcutil.Amount(stakingTx.TxOut[stakingOutputIdx].Value) + stakingValue := btcutil.Amount(stakingTx.TxOut[delegation.StakingTx.OutputIndex].Value) // 3. re-build the unbonding path script and check whether the script from // the witness matches stakingInfo, err := btcstaking.BuildStakingInfo( stakerPk.MustToBTCPK(), []*btcec.PublicKey{fpPK}, - covPks, - uint32(params.CovenantQuorum), - uint16(stakingTimeLock), + params.CovenantPks, + params.CovenantQuorum, + uint16(delegation.StakingTx.TimeLock), stakingValue, btcParams, ) @@ -326,6 +306,12 @@ func (s *Service) IsValidUnbondingTx( if !bytes.Equal(unbondingPathInfo.GetPkScriptPath(), scriptFromWitness) { // not unbonding tx as it does not unlock the unbonding path + log.Debug(). + Str("staking_tx", delegation.StakingTxHashHex). + Str("spending_tx", tx.TxHash().String()). + Str("unbonding_path", hex.EncodeToString(unbondingPathInfo.GetPkScriptPath())). + Str("script_from_witness", hex.EncodeToString(scriptFromWitness)). + Msg("pk script from witness does not match unbonding path") return false, nil } @@ -348,9 +334,9 @@ func (s *Service) IsValidUnbondingTx( unbondingInfo, err := btcstaking.BuildUnbondingInfo( stakerPk.MustToBTCPK(), []*btcec.PublicKey{fpPK}, - covPks, - uint32(params.CovenantQuorum), - uint16(params.UnbondingTime), + params.CovenantPks, + params.CovenantQuorum, + params.UnbondingTime, expectedUnbondingOutputValue, btcParams, ) @@ -372,7 +358,7 @@ func (s *Service) validateWithdrawalTxFromStaking( tx *wire.MsgTx, spendingInputIdx uint32, delegation *model.DelegationDocument, - params *types.VersionedGlobalParams, + params *parser.ParsedVersionedGlobalParams, ) error { stakerPk, err := bbn.NewBIP340PubKeyFromHex(delegation.StakerPkHex) if err != nil { @@ -385,15 +371,6 @@ func (s *Service) validateWithdrawalTxFromStaking( } fpPK := fpPKBIP340.MustToBTCPK() - covPks := make([]*btcec.PublicKey, len(params.CovenantPks)) - for i, hex := range params.CovenantPks { - covPk, err := bbn.NewBIP340PubKeyFromHex(hex) - if err != nil { - return fmt.Errorf("failed to convert finality provider pk hex to a public key: %w", err) - } - covPks[i] = covPk.MustToBTCPK() - } - btcParams, err := utils.GetBTCParams(s.cfg.Btc.NetParams) if err != nil { return fmt.Errorf("invalid BTC network params: %w", err) @@ -411,8 +388,8 @@ func (s *Service) validateWithdrawalTxFromStaking( stakingInfo, err := btcstaking.BuildStakingInfo( stakerPk.MustToBTCPK(), []*btcec.PublicKey{fpPK}, - covPks, - uint32(params.CovenantQuorum), + params.CovenantPks, + params.CovenantQuorum, uint16(delegation.StakingTx.TimeLock), stakingValue, btcParams, @@ -444,7 +421,7 @@ func (s *Service) validateWithdrawalTxFromUnbonding( tx *wire.MsgTx, delegation *model.DelegationDocument, spendingInputIdx uint32, - params *types.VersionedGlobalParams, + params *parser.ParsedVersionedGlobalParams, ) error { stakerPk, err := bbn.NewBIP340PubKeyFromHex(delegation.StakerPkHex) if err != nil { @@ -457,15 +434,6 @@ func (s *Service) validateWithdrawalTxFromUnbonding( } fpPK := fpPKBIP340.MustToBTCPK() - covPks := make([]*btcec.PublicKey, len(params.CovenantPks)) - for i, hex := range params.CovenantPks { - covPk, err := bbn.NewBIP340PubKeyFromHex(hex) - if err != nil { - return fmt.Errorf("failed to convert finality provider pk hex to a public key: %w", err) - } - covPks[i] = covPk.MustToBTCPK() - } - btcParams, err := utils.GetBTCParams(s.cfg.Btc.NetParams) if err != nil { return fmt.Errorf("invalid BTC network params: %w", err) @@ -484,9 +452,9 @@ func (s *Service) validateWithdrawalTxFromUnbonding( unbondingInfo, err := btcstaking.BuildUnbondingInfo( stakerPk.MustToBTCPK(), []*btcec.PublicKey{fpPK}, - covPks, - uint32(params.CovenantQuorum), - uint16(params.UnbondingTime), + params.CovenantPks, + params.CovenantQuorum, + params.UnbondingTime, expectedUnbondingOutputValue, btcParams, ) diff --git a/params/params.go b/params/params.go new file mode 100644 index 0000000..be420a6 --- /dev/null +++ b/params/params.go @@ -0,0 +1,26 @@ +package params + +import ( + "github.com/babylonlabs-io/networks/parameters/parser" +) + +type ParamsRetriever interface { + VersionedParams() *parser.ParsedGlobalParams +} + +type GlobalParamsRetriever struct { + paramsVersions *parser.ParsedGlobalParams +} + +func NewGlobalParamsRetriever(filePath string) (*GlobalParamsRetriever, error) { + parsedGlobalParams, err := parser.NewParsedGlobalParamsFromFile(filePath) + if err != nil { + return nil, err + } + + return &GlobalParamsRetriever{paramsVersions: parsedGlobalParams}, nil +} + +func (lp *GlobalParamsRetriever) VersionedParams() *parser.ParsedGlobalParams { + return lp.paramsVersions +}