Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: unbonding state transitions #27

Merged
merged 15 commits into from
Oct 23, 2024
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
gusin13 marked this conversation as resolved.
Show resolved Hide resolved

if dbErr := s.db.UpdateBTCDelegationState(
ctx, unbondedEarlyEvent.StakingTxHash, types.StateUnbonding,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have a method in the utils to map the BBN state into web state? i.e types.DelegationState(unbondedEarlyEvent.NewState) seems ok to me, but not sure why it's removed?

Copy link
Collaborator Author

@gusin13 gusin13 Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the state sent in Babylon event is Unbonded, types.DelegationState(unbondedEarlyEvent.NewState) would give us Unbonded

but the indexer needs to treat this as Unbonding

); 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
Loading