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: granular states #69

Merged
merged 11 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions internal/db/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,22 @@ func (db *Database) SaveNewBTCDelegation(
}

func (db *Database) UpdateBTCDelegationState(
ctx context.Context, stakingTxHash string, newState types.DelegationState,
ctx context.Context,
stakingTxHash string,
newState types.DelegationState,
subState *types.DelegationSubState,
) error {
filter := bson.M{"_id": stakingTxHash}
updateFields := bson.M{
"state": newState.String(),
}

if subState != nil {
updateFields["sub_state"] = subState.String()
}

update := bson.M{
"$set": bson.M{
"state": newState.String(),
},
"$set": updateFields,
}

res := db.client.Database(db.dbName).
Expand Down
10 changes: 8 additions & 2 deletions internal/db/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ type DbInterface interface {
* @return An error if the operation failed
*/
UpdateBTCDelegationState(
ctx context.Context, stakingTxHash string, newState types.DelegationState,
ctx context.Context,
stakingTxHash string,
newState types.DelegationState,
subState *types.DelegationSubState,
) error
/**
* SaveBTCDelegationUnbondingCovenantSignature saves a BTC delegation
Expand Down Expand Up @@ -160,7 +163,10 @@ type DbInterface interface {
* @return An error if the operation failed
*/
SaveNewTimeLockExpire(
ctx context.Context, stakingTxHashHex string, expireHeight uint32, txType string,
ctx context.Context,
stakingTxHashHex string,
expireHeight uint32,
subState types.DelegationSubState,
) error
/**
* FindExpiredDelegations finds the expired delegations.
Expand Down
1 change: 1 addition & 0 deletions internal/db/model/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type BTCDelegationDetails struct {
StartHeight uint32 `bson:"start_height"`
EndHeight uint32 `bson:"end_height"`
State types.DelegationState `bson:"state"`
SubState types.DelegationSubState `bson:"sub_state,omitempty"`
ParamsVersion uint32 `bson:"params_version"`
UnbondingTime uint32 `bson:"unbonding_time"`
UnbondingTx string `bson:"unbonding_tx"`
Expand Down
18 changes: 11 additions & 7 deletions internal/db/model/timelock.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package model

import "github.com/babylonlabs-io/babylon-staking-indexer/internal/types"

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

func NewTimeLockDocument(stakingTxHashHex string, expireHeight uint32, txType string) *TimeLockDocument {
func NewTimeLockDocument(
stakingTxHashHex string, expireHeight uint32, subState types.DelegationSubState,
) *TimeLockDocument {
return &TimeLockDocument{
StakingTxHashHex: stakingTxHashHex,
ExpireHeight: expireHeight,
TxType: txType,
StakingTxHashHex: stakingTxHashHex,
ExpireHeight: expireHeight,
DelegationSubState: subState,
}
}
9 changes: 6 additions & 3 deletions internal/db/timelock.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import (
"fmt"

"github.com/babylonlabs-io/babylon-staking-indexer/internal/db/model"
"github.com/babylonlabs-io/babylon-staking-indexer/internal/types"
"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,
ctx context.Context,
stakingTxHashHex string,
expireHeight uint32,
subState types.DelegationSubState,
) error {
tlDoc := model.NewTimeLockDocument(stakingTxHashHex, expireHeight, txType)
tlDoc := model.NewTimeLockDocument(stakingTxHashHex, expireHeight, subState)
_, err := db.client.Database(db.dbName).
Collection(model.TimeLockCollection).
InsertOne(ctx, tlDoc)
Expand Down
21 changes: 16 additions & 5 deletions internal/services/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ func (s *Service) processCovenantQuorumReachedEvent(
}

if dbErr := s.db.UpdateBTCDelegationState(
ctx, covenantQuorumReachedEvent.StakingTxHash, newState,
ctx,
covenantQuorumReachedEvent.StakingTxHash,
newState,
nil,
); dbErr != nil {
return types.NewError(
http.StatusInternalServerError,
Expand Down Expand Up @@ -205,12 +208,14 @@ func (s *Service) processBTCDelegationInclusionProofReceivedEvent(
}

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

Expand Down Expand Up @@ -260,13 +265,15 @@ func (s *Service) processBTCDelegationUnbondedEarlyEvent(
)
}

subState := types.SubStateEarlyUnbonding

// Save timelock expire
unbondingExpireHeight := uint32(unbondingStartHeight) + delegation.UnbondingTime
if err := s.db.SaveNewTimeLockExpire(
ctx,
delegation.StakingTxHashHex,
unbondingExpireHeight,
types.EarlyUnbondingTxType.String(),
subState,
); err != nil {
return types.NewError(
http.StatusInternalServerError,
Expand All @@ -280,6 +287,7 @@ func (s *Service) processBTCDelegationUnbondedEarlyEvent(
ctx,
unbondedEarlyEvent.StakingTxHash,
types.StateUnbonding,
&subState,
); err != nil {
return types.NewError(
http.StatusInternalServerError,
Expand Down Expand Up @@ -330,12 +338,14 @@ func (s *Service) processBTCDelegationExpiredEvent(
return err
}

subState := types.SubStateTimelock

// Save timelock expire
if err := s.db.SaveNewTimeLockExpire(
ctx,
delegation.StakingTxHashHex,
delegation.EndHeight,
types.ExpiredTxType.String(),
subState,
); err != nil {
return types.NewError(
http.StatusInternalServerError,
Expand All @@ -349,6 +359,7 @@ func (s *Service) processBTCDelegationExpiredEvent(
ctx,
delegation.StakingTxHashHex,
types.StateUnbonding,
&subState,
); err != nil {
return types.NewError(
http.StatusInternalServerError,
Expand Down
7 changes: 6 additions & 1 deletion internal/services/expiry_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ func (s *Service) checkExpiry(ctx context.Context) *types.Error {
return consumerErr
}

if err := s.db.UpdateBTCDelegationState(ctx, delegation.StakingTxHashHex, types.StateWithdrawable); err != nil {
if err := s.db.UpdateBTCDelegationState(
ctx,
delegation.StakingTxHashHex,
types.StateWithdrawable,
&tlDoc.DelegationSubState,
); err != nil {
log.Error().Err(err).Msg("Error updating BTC delegation state to withdrawable")
return types.NewInternalServiceError(
fmt.Errorf("failed to update BTC delegation state to withdrawable: %w", err),
Expand Down
78 changes: 63 additions & 15 deletions internal/services/watch_btc_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func (s *Service) watchForSpendStakingTx(
quitCtx,
spendDetail.SpendingTx,
spendDetail.SpenderInputIndex,
uint32(spendDetail.SpendingHeight),
delegation,
); err != nil {
log.Error().Err(err).Msg("failed to handle spending staking transaction")
Expand Down Expand Up @@ -62,6 +63,7 @@ func (s *Service) watchForSpendUnbondingTx(
if err := s.handleSpendingUnbondingTransaction(
quitCtx,
spendDetail.SpendingTx,
uint32(spendDetail.SpendingHeight),
spendDetail.SpenderInputIndex,
delegation,
); err != nil {
Expand All @@ -79,6 +81,7 @@ func (s *Service) watchForSpendUnbondingTx(
func (s *Service) watchForSpendSlashingChange(
spendEvent *notifier.SpendEvent,
delegation *model.BTCDelegationDetails,
subState types.DelegationSubState,
) {
defer s.wg.Done()
quitCtx, cancel := s.quitContext()
Expand All @@ -103,10 +106,12 @@ func (s *Service) watchForSpendSlashingChange(
}

// Update to withdrawn state
delegationSubState := subState
if err := s.db.UpdateBTCDelegationState(
quitCtx,
delegation.StakingTxHashHex,
types.StateSlashedWithdrawn,
types.StateWithdrawn,
&delegationSubState,
); err != nil {
log.Error().Err(err).Msg("failed to update delegation state")
return
Expand All @@ -121,8 +126,9 @@ func (s *Service) watchForSpendSlashingChange(

func (s *Service) handleSpendingStakingTransaction(
ctx context.Context,
tx *wire.MsgTx,
spendingTx *wire.MsgTx,
spendingInputIdx uint32,
spendingHeight uint32,
delegation *model.BTCDelegationDetails,
) error {
params, err := s.db.GetStakingParams(ctx, delegation.ParamsVersion)
Expand All @@ -131,7 +137,7 @@ func (s *Service) handleSpendingStakingTransaction(
}

// First try to validate as unbonding tx
isUnbonding, err := s.IsValidUnbondingTx(tx, delegation, params)
isUnbonding, err := s.IsValidUnbondingTx(spendingTx, delegation, params)
if err != nil {
return fmt.Errorf("failed to validate unbonding tx: %w", err)
}
Expand All @@ -141,10 +147,10 @@ func (s *Service) handleSpendingStakingTransaction(
}

// Try to validate as withdrawal transaction
withdrawalErr := s.validateWithdrawalTxFromStaking(tx, spendingInputIdx, delegation, params)
withdrawalErr := s.validateWithdrawalTxFromStaking(spendingTx, spendingInputIdx, delegation, params)
if withdrawalErr == nil {
// It's a valid withdrawal, process it
return s.handleWithdrawal(ctx, delegation)
return s.handleWithdrawal(ctx, delegation, types.SubStateTimelock)
}

// If it's not a valid withdrawal, check if it's a valid slashing
Expand All @@ -153,7 +159,7 @@ func (s *Service) handleSpendingStakingTransaction(
}

// Try to validate as slashing transaction
if err := s.validateSlashingTxFromStaking(tx, spendingInputIdx, delegation, params); err != nil {
if err := s.validateSlashingTxFromStaking(spendingTx, spendingInputIdx, delegation, params); err != nil {
if errors.Is(err, types.ErrInvalidSlashingTx) {
// Neither withdrawal nor slashing - this is an invalid spend
return fmt.Errorf("transaction is neither valid unbonding, withdrawal, nor slashing: %w", err)
Expand All @@ -162,12 +168,19 @@ func (s *Service) handleSpendingStakingTransaction(
}

// It's a valid slashing tx, watch for spending change output
return s.startWatchingSlashingChange(ctx, tx, delegation)
return s.startWatchingSlashingChange(
ctx,
spendingTx,
spendingHeight,
delegation,
types.SubStateTimelockSlashing,
)
}

func (s *Service) handleSpendingUnbondingTransaction(
ctx context.Context,
tx *wire.MsgTx,
spendingTx *wire.MsgTx,
spendingHeight uint32,
spendingInputIdx uint32,
delegation *model.BTCDelegationDetails,
) error {
Expand All @@ -177,10 +190,10 @@ func (s *Service) handleSpendingUnbondingTransaction(
}

// First try to validate as withdrawal transaction
withdrawalErr := s.validateWithdrawalTxFromUnbonding(tx, delegation, spendingInputIdx, params)
withdrawalErr := s.validateWithdrawalTxFromUnbonding(spendingTx, delegation, spendingInputIdx, params)
if withdrawalErr == nil {
// It's a valid withdrawal, process it
return s.handleWithdrawal(ctx, delegation)
return s.handleWithdrawal(ctx, delegation, types.SubStateEarlyUnbonding)
}

// If it's not a valid withdrawal, check if it's a valid slashing
Expand All @@ -189,7 +202,7 @@ func (s *Service) handleSpendingUnbondingTransaction(
}

// Try to validate as slashing transaction
if err := s.validateSlashingTxFromUnbonding(tx, delegation, spendingInputIdx, params); err != nil {
if err := s.validateSlashingTxFromUnbonding(spendingTx, delegation, spendingInputIdx, params); err != nil {
if errors.Is(err, types.ErrInvalidSlashingTx) {
// Neither withdrawal nor slashing - this is an invalid spend
return fmt.Errorf("transaction is neither valid withdrawal nor slashing: %w", err)
Expand All @@ -198,10 +211,20 @@ func (s *Service) handleSpendingUnbondingTransaction(
}

// It's a valid slashing tx, watch for spending change output
return s.startWatchingSlashingChange(ctx, tx, delegation)
return s.startWatchingSlashingChange(
ctx,
spendingTx,
spendingHeight,
delegation,
types.SubStateEarlyUnbondingSlashing,
)
}

func (s *Service) handleWithdrawal(ctx context.Context, delegation *model.BTCDelegationDetails) error {
func (s *Service) handleWithdrawal(
ctx context.Context,
delegation *model.BTCDelegationDetails,
subState types.DelegationSubState,
) error {
delegationState, err := s.db.GetBTCDelegationState(ctx, delegation.StakingTxHashHex)
if err != nil {
return fmt.Errorf("failed to get delegation state: %w", err)
Expand All @@ -217,10 +240,17 @@ func (s *Service) handleWithdrawal(ctx context.Context, delegation *model.BTCDel
ctx,
delegation.StakingTxHashHex,
types.StateWithdrawn,
&subState,
)
}

func (s *Service) startWatchingSlashingChange(ctx context.Context, slashingTx *wire.MsgTx, delegation *model.BTCDelegationDetails) error {
func (s *Service) startWatchingSlashingChange(
ctx context.Context,
slashingTx *wire.MsgTx,
spendingHeight uint32,
delegation *model.BTCDelegationDetails,
subState types.DelegationSubState,
) error {
// Create outpoint for the change output (index 1)
changeOutpoint := wire.OutPoint{
Hash: slashingTx.TxHash(),
Expand All @@ -237,8 +267,26 @@ func (s *Service) startWatchingSlashingChange(ctx context.Context, slashingTx *w
return fmt.Errorf("failed to register spend ntfn for slashing change output: %w", err)
}

// TODO: confirm if this is correct
// in btc-staker it is max(w, minUnbondingTime)
stakingParams, err := s.db.GetStakingParams(ctx, delegation.ParamsVersion)
if err != nil {
return fmt.Errorf("failed to get staking params: %w", err)
}
slashingChangeTimelockExpireHeight := spendingHeight + stakingParams.MinUnbondingTimeBlocks

// Save timelock expire to mark it as Withdrawn (sub state - timelock_slashing/early_unbonding_slashing)
if err := s.db.SaveNewTimeLockExpire(
ctx,
delegation.StakingTxHashHex,
slashingChangeTimelockExpireHeight,
subState,
); err != nil {
return fmt.Errorf("failed to save timelock expire: %w", err)
}

s.wg.Add(1)
go s.watchForSpendSlashingChange(spendEv, delegation)
go s.watchForSpendSlashingChange(spendEv, delegation, subState)

return nil
}
Expand Down
Loading
Loading