Skip to content

Commit

Permalink
feat: unbonding state transitions (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
gusin13 authored Oct 23, 2024
1 parent 4bd50fe commit 22ed334
Show file tree
Hide file tree
Showing 15 changed files with 336 additions and 40 deletions.
2 changes: 2 additions & 0 deletions config/config-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ bbn:
timeout: 30s
poller:
param-polling-interval: 60s
expiry-checker-polling-interval: 10s
expired-delegations-limit: 100
queue:
queue_user: user # can be replaced by values in .env file
queue_password: password
Expand Down
2 changes: 2 additions & 0 deletions config/config-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ bbn:
timeout: 30s
poller:
param-polling-interval: 10s
expiry-checker-polling-interval: 10s
expired-delegations-limit: 100
queue:
queue_user: user # can be replaced by values in .env file
queue_password: password
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.22.3
require (
github.com/babylonlabs-io/babylon v0.12.1
github.com/babylonlabs-io/staking-queue-client v0.4.1
github.com/btcsuite/btcd v0.24.2
github.com/btcsuite/btcd v0.24.3-0.20241011125836-24eb815168f4
github.com/cometbft/cometbft v0.38.7
github.com/cosmos/cosmos-sdk v0.50.6
github.com/cosmos/gogoproto v1.7.0
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,9 @@ github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d/go.mod h1:f1iKL6Z
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd v0.24.3-0.20241011125836-24eb815168f4 h1:wks2KaK25+5rr3BBmD2euEhinRViLv3jJpYZImxwCnM=
github.com/btcsuite/btcd v0.24.3-0.20241011125836-24eb815168f4/go.mod h1:zHK7t7sw8XbsCkD64WePHE3r3k9/XoGAcf6mXV14c64=
github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
Expand Down
14 changes: 12 additions & 2 deletions internal/config/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ import (
)

type PollerConfig struct {
ParamPollingInterval time.Duration `mapstructure:"param-polling-interval"`
ParamPollingInterval time.Duration `mapstructure:"param-polling-interval"`
ExpiryCheckerPollingInterval time.Duration `mapstructure:"expiry-checker-polling-interval"`
ExpiredDelegationsLimit uint64 `mapstructure:"expired-delegations-limit"`
}

func (cfg *PollerConfig) Validate() error {
if cfg.ParamPollingInterval < 0 {
if cfg.ParamPollingInterval <= 0 {
return errors.New("param-polling-interval must be positive")
}

if cfg.ExpiryCheckerPollingInterval <= 0 {
return errors.New("expiry-checker-polling-interval must be positive")
}

if cfg.ExpiredDelegationsLimit <= 0 {
return errors.New("expired-delegations-limit must be positive")
}

return nil
}
26 changes: 26 additions & 0 deletions internal/db/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,30 @@ type DbInterface interface {
GetBTCDelegationByStakingTxHash(
ctx context.Context, stakingTxHash string,
) (*model.BTCDelegationDetails, error)
/**
* SaveNewTimeLockExpire saves a new timelock expire to the database.
* If the timelock expire already exists, DuplicateKeyError will be returned.
* @param ctx The context
* @param stakingTxHashHex The staking tx hash hex
* @param expireHeight The expire height
* @param txType The transaction type
* @return An error if the operation failed
*/
SaveNewTimeLockExpire(
ctx context.Context, stakingTxHashHex string, expireHeight uint32, txType string,
) error
/**
* FindExpiredDelegations finds the expired delegations.
* @param ctx The context
* @param btcTipHeight The BTC tip height
* @return The expired delegations or an error
*/
FindExpiredDelegations(ctx context.Context, btcTipHeight, limit uint64) ([]model.TimeLockDocument, error)
/**
* DeleteExpiredDelegation deletes an expired delegation.
* @param ctx The context
* @param id The ID of the expired delegation
* @return An error if the operation failed
*/
DeleteExpiredDelegation(ctx context.Context, stakingTxHashHex string) error
}
2 changes: 2 additions & 0 deletions internal/db/model/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
const (
FinalityProviderDetailsCollection = "finality_provider_details"
BTCDelegationDetailsCollection = "btc_delegation_details"
TimeLockCollection = "timelock"
GlobalParamsCollection = "global_params"
)

Expand All @@ -27,6 +28,7 @@ type index struct {
var collections = map[string][]index{
FinalityProviderDetailsCollection: {{Indexes: map[string]int{}}},
BTCDelegationDetailsCollection: {{Indexes: map[string]int{}}},
TimeLockCollection: {{Indexes: map[string]int{}}},
GlobalParamsCollection: {{Indexes: map[string]int{}}},
}

Expand Down
15 changes: 15 additions & 0 deletions internal/db/model/timelock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package model

type TimeLockDocument struct {
StakingTxHashHex string `bson:"_id"` // Primary key
ExpireHeight uint32 `bson:"expire_height"`
TxType string `bson:"tx_type"`
}

func NewTimeLockDocument(stakingTxHashHex string, expireHeight uint32, txType string) *TimeLockDocument {
return &TimeLockDocument{
StakingTxHashHex: stakingTxHashHex,
ExpireHeight: expireHeight,
TxType: txType,
}
}
73 changes: 73 additions & 0 deletions internal/db/timelock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package db

import (
"context"
"errors"
"fmt"

"github.com/babylonlabs-io/babylon-staking-indexer/internal/db/model"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

func (db *Database) SaveNewTimeLockExpire(
ctx context.Context, stakingTxHashHex string,
expireHeight uint32, txType string,
) error {
tlDoc := model.NewTimeLockDocument(stakingTxHashHex, expireHeight, txType)
_, err := db.client.Database(db.dbName).
Collection(model.TimeLockCollection).
InsertOne(ctx, tlDoc)
if err != nil {
var writeErr mongo.WriteException
if errors.As(err, &writeErr) {
for _, e := range writeErr.WriteErrors {
if mongo.IsDuplicateKeyError(e) {
return &DuplicateKeyError{
Key: tlDoc.StakingTxHashHex,
Message: "timelock already exists",
}
}
}
}
return err
}
return nil
}

func (db *Database) FindExpiredDelegations(ctx context.Context, btcTipHeight, limit uint64) ([]model.TimeLockDocument, error) {
client := db.client.Database(db.dbName).Collection(model.TimeLockCollection)
filter := bson.M{"expire_height": bson.M{"$lte": btcTipHeight}}

opts := options.Find().SetLimit(int64(limit))
cursor, err := client.Find(ctx, filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)

var delegations []model.TimeLockDocument
if err = cursor.All(ctx, &delegations); err != nil {
return nil, err
}

return delegations, nil
}

func (db *Database) DeleteExpiredDelegation(ctx context.Context, stakingTxHashHex string) error {
client := db.client.Database(db.dbName).Collection(model.TimeLockCollection)
filter := bson.M{"_id": stakingTxHashHex}

result, err := client.DeleteOne(ctx, filter)
if err != nil {
return fmt.Errorf("failed to delete expired delegation with stakingTxHashHex %v: %w", stakingTxHashHex, err)
}

// Check if any document was deleted
if result.DeletedCount == 0 {
return fmt.Errorf("no expired delegation found with stakingTxHashHex %v", stakingTxHashHex)
}

return nil
}
79 changes: 50 additions & 29 deletions internal/services/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,17 @@ func (s *Service) processNewBTCDelegationEvent(
return err
}

if err := s.db.SaveNewBTCDelegation(
if dbErr := s.db.SaveNewBTCDelegation(
ctx, model.FromEventBTCDelegationCreated(newDelegation),
); err != nil {
); dbErr != nil {
if db.IsDuplicateKeyError(err) {
// BTC delegation already exists, ignore the event
return nil
}
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to save new BTC delegation: %w", err),
fmt.Errorf("failed to save new BTC delegation: %w", dbErr),
)
}

Expand All @@ -65,13 +65,13 @@ func (s *Service) processCovenantQuorumReachedEvent(
return err
}

if err := s.db.UpdateBTCDelegationState(
if dbErr := s.db.UpdateBTCDelegationState(
ctx, covenantQuorumReachedEvent.StakingTxHash, types.DelegationState(covenantQuorumReachedEvent.NewState),
); err != nil {
); dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to update BTC delegation state: %w", err),
fmt.Errorf("failed to update BTC delegation state: %w", dbErr),
)
}

Expand All @@ -92,13 +92,13 @@ func (s *Service) processBTCDelegationInclusionProofReceivedEvent(
return err
}

if err := s.db.UpdateBTCDelegationDetails(
if dbErr := s.db.UpdateBTCDelegationDetails(
ctx, inclusionProofEvent.StakingTxHash, model.FromEventBTCDelegationInclusionProofReceived(inclusionProofEvent),
); err != nil {
); dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to update BTC delegation state: %w", err),
fmt.Errorf("failed to update BTC delegation state: %w", dbErr),
)
}

Expand All @@ -119,13 +119,16 @@ func (s *Service) processBTCDelegationUnbondedEarlyEvent(
return err
}

if err := s.db.UpdateBTCDelegationState(
ctx, unbondedEarlyEvent.StakingTxHash, types.DelegationState(unbondedEarlyEvent.NewState),
); err != nil {
// TODO: save timelock expire, need to figure out what will be the expire height in this case.
// https://github.com/babylonlabs-io/babylon-staking-indexer/issues/28

if dbErr := s.db.UpdateBTCDelegationState(
ctx, unbondedEarlyEvent.StakingTxHash, types.StateUnbonding,
); dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to update BTC delegation state: %w", err),
fmt.Errorf("failed to update BTC delegation state: %w", dbErr),
)
}

Expand All @@ -146,13 +149,31 @@ func (s *Service) processBTCDelegationExpiredEvent(
return err
}

if err := s.db.UpdateBTCDelegationState(
ctx, expiredEvent.StakingTxHash, types.DelegationState(expiredEvent.NewState),
); err != nil {
delegation, dbErr := s.db.GetBTCDelegationByStakingTxHash(ctx, expiredEvent.StakingTxHash)
if dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", dbErr),
)
}
if dbErr := s.db.SaveNewTimeLockExpire(
ctx, delegation.StakingTxHashHex, delegation.EndHeight, types.ExpiredTxType.String(),
); dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to save timelock expire: %w", dbErr),
)
}

if dbErr := s.db.UpdateBTCDelegationState(
ctx, expiredEvent.StakingTxHash, types.StateUnbonding,
); dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to update BTC delegation state: %w", err),
fmt.Errorf("failed to update BTC delegation state: %w", dbErr),
)
}

Expand Down Expand Up @@ -190,12 +211,12 @@ func (s *Service) validateCovenantQuorumReachedEvent(ctx context.Context, event
}

// Fetch the current delegation state from the database
delegation, err := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if err != nil {
delegation, dbErr := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", err),
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", dbErr),
)
}

Expand Down Expand Up @@ -247,12 +268,12 @@ func (s *Service) validateBTCDelegationInclusionProofReceivedEvent(ctx context.C
}

// Fetch the current delegation state from the database
delegation, err := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if err != nil {
delegation, dbErr := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", err),
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", dbErr),
)
}

Expand Down Expand Up @@ -312,12 +333,12 @@ func (s *Service) validateBTCDelegationUnbondedEarlyEvent(ctx context.Context, e
}

// Fetch the current delegation state from the database
delegation, err := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if err != nil {
delegation, dbErr := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", err),
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", dbErr),
)
}

Expand Down Expand Up @@ -349,12 +370,12 @@ func (s *Service) validateBTCDelegationExpiredEvent(ctx context.Context, event *
}

// Fetch the current delegation state from the database
delegation, err := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if err != nil {
delegation, dbErr := s.db.GetBTCDelegationByStakingTxHash(ctx, event.StakingTxHash)
if dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
types.InternalServiceError,
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", err),
fmt.Errorf("failed to get BTC delegation by staking tx hash: %w", dbErr),
)
}

Expand Down
Loading

0 comments on commit 22ed334

Please sign in to comment.