From cc5f0db9f0c29c9382c9ac74975ef9b84545e0c8 Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonlab@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:07:25 +1100 Subject: [PATCH] Sync params (#15) * replace custom type with type from babylon event --- Makefile | 3 +- cmd/babylon-staking-indexer/main.go | 2 +- config/config-docker.yml | 5 +- config/config-local.yml | 5 +- go.mod | 2 +- go.sum | 4 +- internal/clients/bbnclient/bbnclient.go | 74 +++++++++- internal/clients/bbnclient/interface.go | 2 + internal/clients/bbnclient/types.go | 48 ++++++ internal/clients/bbnclient/types/events.go | 52 ------- internal/config/config.go | 5 + internal/config/poller.go | 28 +--- internal/db/interface.go | 55 ++++++- internal/db/model/delegation.go | 6 +- internal/db/model/finality-provider.go | 12 +- internal/db/model/params.go | 7 + internal/db/model/setup.go | 4 + internal/db/params.go | 73 ++++++++++ internal/services/bootstrap.go | 2 +- internal/services/events.go | 3 +- internal/services/finality-provider.go | 8 +- internal/services/global-params.go | 53 +++++++ internal/services/service.go | 12 +- internal/services/subscription.go | 2 +- internal/types/error.go | 1 + internal/utils/poller/poller.go | 3 +- tests/mocks/mock_bbn_client.go | 161 +++++++++++++++++++++ tests/mocks/mock_btc_client.go | 52 +++++++ tests/mocks/mock_db_client.go | 52 ++++++- 29 files changed, 612 insertions(+), 124 deletions(-) create mode 100644 internal/clients/bbnclient/types.go delete mode 100644 internal/clients/bbnclient/types/events.go create mode 100644 internal/db/model/params.go create mode 100644 internal/db/params.go create mode 100644 internal/services/global-params.go create mode 100644 tests/mocks/mock_bbn_client.go create mode 100644 tests/mocks/mock_btc_client.go diff --git a/Makefile b/Makefile index 9170445..34854ac 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,8 @@ run-local: generate-mock-interface: cd internal/db && mockery --name=DbInterface --output=../../tests/mocks --outpkg=mocks --filename=mock_db_client.go - cd internal/btcclient && mockery --name=BtcInterface --output=../../tests/mocks --outpkg=mocks --filename=mock_btc_client.go + cd internal/clients/btcclient && mockery --name=BtcInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_btc_client.go + cd internal/clients/bbnclient && mockery --name=BbnInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_bbn_client.go test: ./bin/local-startup.sh; diff --git a/cmd/babylon-staking-indexer/main.go b/cmd/babylon-staking-indexer/main.go index 772546f..253aa17 100644 --- a/cmd/babylon-staking-indexer/main.go +++ b/cmd/babylon-staking-indexer/main.go @@ -55,7 +55,7 @@ func main() { log.Fatal().Err(err).Msg("error while creating queue manager") } - service := services.NewService(dbClient, btcClient, bbnClient, qm) + service := services.NewService(cfg, dbClient, btcClient, bbnClient, qm) if err != nil { log.Fatal().Err(err).Msg("error while creating delegation service") } diff --git a/config/config-docker.yml b/config/config-docker.yml index a35860b..058c97c 100644 --- a/config/config-docker.yml +++ b/config/config-docker.yml @@ -1,6 +1,3 @@ -poller: - interval: 5s - log-level: debug db: username: root password: example @@ -15,6 +12,8 @@ btc: bbn: rpc-addr: https://rpc.devnet.babylonchain.io:443 timeout: 30s +poller: + param-polling-interval: 60s queue: queue_user: user # can be replaced by values in .env file queue_password: password diff --git a/config/config-local.yml b/config/config-local.yml index 6ee4929..aa1647a 100644 --- a/config/config-local.yml +++ b/config/config-local.yml @@ -1,6 +1,3 @@ -poller: - interval: 5s - log-level: debug db: username: root password: example @@ -15,6 +12,8 @@ btc: bbn: rpc-addr: https://rpc.devnet.babylonchain.io:443 timeout: 30s +poller: + param-polling-interval: 10s queue: queue_user: user # can be replaced by values in .env file queue_password: password diff --git a/go.mod b/go.mod index da325dd..5ac1aad 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/babylonlabs-io/babylon-staking-indexer go 1.23.2 require ( - github.com/babylonlabs-io/babylon v0.12.0 + 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/cometbft/cometbft v0.38.7 diff --git a/go.sum b/go.sum index 9b4c9c8..b9b981a 100644 --- a/go.sum +++ b/go.sum @@ -272,8 +272,8 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX github.com/aws/aws-sdk-go v1.44.312 h1:llrElfzeqG/YOLFFKjg1xNpZCFJ2xraIi3PqSuP+95k= github.com/aws/aws-sdk-go v1.44.312/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/babylonlabs-io/babylon v0.12.0 h1:s2OTcxpk0RzrkVBnVTfnPdJVYDSqnm/33YKPQqEzNCE= -github.com/babylonlabs-io/babylon v0.12.0/go.mod h1:ZOrTde9vs2xoqGTFw4xhupu2CMulnpywiuk0eh4kPOw= +github.com/babylonlabs-io/babylon v0.12.1 h1:Qfmrq3pdDEZGq6DtMXxwiQjx0HD+t+U0cXQzsJfX15U= +github.com/babylonlabs-io/babylon v0.12.1/go.mod h1:ZOrTde9vs2xoqGTFw4xhupu2CMulnpywiuk0eh4kPOw= github.com/babylonlabs-io/staking-queue-client v0.4.1 h1:AW+jtrNxZYN/isRx+njqjHbUU9CzhF42Ke6roK+0N3I= github.com/babylonlabs-io/staking-queue-client v0.4.1/go.mod h1:n3fr3c+9LNiJlyETmcrVk94Zn76rAADhGZKxX+rVf+Q= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= diff --git a/internal/clients/bbnclient/bbnclient.go b/internal/clients/bbnclient/bbnclient.go index 608a972..5922adc 100644 --- a/internal/clients/bbnclient/bbnclient.go +++ b/internal/clients/bbnclient/bbnclient.go @@ -2,14 +2,16 @@ package bbnclient import ( "context" + "fmt" "net/http" - - ctypes "github.com/cometbft/cometbft/rpc/core/types" - "github.com/rs/zerolog/log" + "strings" "github.com/babylonlabs-io/babylon-staking-indexer/internal/types" "github.com/babylonlabs-io/babylon/client/config" "github.com/babylonlabs-io/babylon/client/query" + bbntypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + ctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/rs/zerolog/log" ) type BbnClient struct { @@ -28,7 +30,9 @@ func (c *BbnClient) GetLatestBlockNumber(ctx context.Context) (int64, *types.Err status, err := c.queryClient.RPCClient.Status(ctx) if err != nil { return 0, types.NewErrorWithMsg( - http.StatusInternalServerError, types.InternalServiceError, err.Error(), + http.StatusInternalServerError, + types.InternalServiceError, + fmt.Sprintf("failed to get latest block number by fetching status: %s", err.Error()), ) } return status.SyncInfo.LatestBlockHeight, nil @@ -38,13 +42,71 @@ func (c *BbnClient) GetBlockResults(ctx context.Context, blockHeight int64) (*ct resp, err := c.queryClient.RPCClient.BlockResults(ctx, &blockHeight) if err != nil { return nil, types.NewErrorWithMsg( - http.StatusInternalServerError, types.InternalServiceError, err.Error(), + http.StatusInternalServerError, + types.InternalServiceError, + fmt.Sprintf("failed to get block results for block %d: %s", blockHeight, err.Error()), ) } - return resp, nil } +func (c *BbnClient) GetCheckpointParams(ctx context.Context) (*CheckpointParams, *types.Error) { + params, err := c.queryClient.BTCCheckpointParams() + if err != nil { + return nil, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.ClientRequestError, + fmt.Sprintf("failed to get checkpoint params: %s", err.Error()), + ) + } + if err := params.Params.Validate(); err != nil { + return nil, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.ValidationError, + fmt.Sprintf("failed to validate checkpoint params: %s", err.Error()), + ) + } + return ¶ms.Params, nil +} + +func (c *BbnClient) GetAllStakingParams(ctx context.Context) (map[uint32]*StakingParams, *types.Error) { + allParams := make(map[uint32]*StakingParams) // Map to store versioned staking parameters + version := uint32(0) + + for { + params, err := c.queryClient.BTCStakingParamsByVersion(version) + if err != nil { + if strings.Contains(err.Error(), bbntypes.ErrParamsNotFound.Error()) { + // Break the loop if an error occurs (indicating no more versions) + break + } + return nil, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.ClientRequestError, + fmt.Sprintf("failed to get staking params for version %d: %s", version, err.Error()), + ) + } + if err := params.Params.Validate(); err != nil { + return nil, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.ValidationError, + fmt.Sprintf("failed to validate staking params for version %d: %s", version, err.Error()), + ) + } + allParams[version] = FromBbnStakingParams(params.Params) + version++ + } + if len(allParams) == 0 { + return nil, types.NewErrorWithMsg( + http.StatusNotFound, + types.NotFound, + "no staking params found", + ) + } + + return allParams, nil +} + func (c *BbnClient) getBlockResults(ctx context.Context, blockHeight *int64) (*ctypes.ResultBlockResults, *types.Error) { resp, err := c.queryClient.RPCClient.BlockResults(ctx, blockHeight) if err != nil { diff --git a/internal/clients/bbnclient/interface.go b/internal/clients/bbnclient/interface.go index b793a77..34c0de1 100644 --- a/internal/clients/bbnclient/interface.go +++ b/internal/clients/bbnclient/interface.go @@ -8,6 +8,8 @@ import ( ) type BbnInterface interface { + GetCheckpointParams(ctx context.Context) (*CheckpointParams, *types.Error) + GetAllStakingParams(ctx context.Context) (map[uint32]*StakingParams, *types.Error) GetLatestBlockNumber(ctx context.Context) (int64, *types.Error) GetBlockResults( ctx context.Context, blockHeight int64, diff --git a/internal/clients/bbnclient/types.go b/internal/clients/bbnclient/types.go new file mode 100644 index 0000000..5b3a359 --- /dev/null +++ b/internal/clients/bbnclient/types.go @@ -0,0 +1,48 @@ +package bbnclient + +import ( + "encoding/hex" + + checkpointtypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" + stakingtypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" +) + +// StakingParams represents the staking parameters of the BBN chain +// Reference: https://github.com/babylonlabs-io/babylon/blob/main/proto/babylon/btcstaking/v1/params.proto +type StakingParams struct { + CovenantPks []string + CovenantQuorum uint32 + MinStakingValueSat int64 + MaxStakingValueSat int64 + MinStakingTimeBlocks uint32 + MaxStakingTimeBlocks uint32 + SlashingPkScript string + MinSlashingTxFeeSat int64 + SlashingRate string + MinUnbondingTimeBlocks uint32 + UnbondingFeeSat int64 + MinCommissionRate string + MaxActiveFinalityProviders uint32 + DelegationCreationBaseGasFee uint64 +} + +func FromBbnStakingParams(params stakingtypes.Params) *StakingParams { + return &StakingParams{ + CovenantPks: params.CovenantPksHex(), + CovenantQuorum: params.CovenantQuorum, + MinStakingValueSat: params.MinStakingValueSat, + MaxStakingValueSat: params.MaxStakingValueSat, + MinStakingTimeBlocks: params.MinStakingTimeBlocks, + MaxStakingTimeBlocks: params.MaxStakingTimeBlocks, + SlashingPkScript: hex.EncodeToString(params.SlashingPkScript), + MinSlashingTxFeeSat: params.MinSlashingTxFeeSat, + SlashingRate: params.SlashingRate.String(), + MinUnbondingTimeBlocks: params.MinUnbondingTimeBlocks, + UnbondingFeeSat: params.UnbondingFeeSat, + MinCommissionRate: params.MinCommissionRate.String(), + MaxActiveFinalityProviders: params.MaxActiveFinalityProviders, + DelegationCreationBaseGasFee: params.DelegationCreationBaseGasFee, + } +} + +type CheckpointParams = checkpointtypes.Params diff --git a/internal/clients/bbnclient/types/events.go b/internal/clients/bbnclient/types/events.go deleted file mode 100644 index 904cd8f..0000000 --- a/internal/clients/bbnclient/types/events.go +++ /dev/null @@ -1,52 +0,0 @@ -package bbntypes - -// Below are temporary types while waiting for core to fix the event type - -// EventFinalityProviderCreated is the event emitted when a finality provider is created -type EventFinalityProviderCreated struct { - // btc_pk is the Bitcoin secp256k1 PK of this finality provider - // the PK follows encoding in BIP-340 spec - BtcPk string `protobuf:"bytes,1,opt,name=btc_pk,proto3" json:"btc_pk,omitempty"` - // addr is the address to receive commission from delegations. - Addr string `protobuf:"bytes,2,opt,name=addr,proto3" json:"addr,omitempty"` - // commission defines the commission rate of the finality provider. - Commission string `protobuf:"bytes,3,opt,name=commission,proto3" json:"commission,omitempty"` - // description defines the description terms for the finality provider. - // moniker defines a human-readable name for the validator. - Moniker string `protobuf:"bytes,1,opt,name=moniker,proto3" json:"moniker,omitempty"` - // identity defines an optional identity signature (ex. UPort or Keybase). - Identity string `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` - // website defines an optional website link. - Website string `protobuf:"bytes,3,opt,name=website,proto3" json:"website,omitempty"` - // security_contact defines an optional email for security contact. - SecurityContact string `protobuf:"bytes,4,opt,name=security_contact,json=securityContact,proto3" json:"security_contact,omitempty"` - // details define other optional details. - Details string `protobuf:"bytes,5,opt,name=details,proto3" json:"details,omitempty"` -} - -type EventFinalityProviderStateChange struct { - // btc_pk is the BTC public key of the finality provider - BtcPk string `protobuf:"bytes,1,opt,name=btc_pk,json=btcPk,proto3" json:"btc_pk,omitempty"` - // new_state is the new state that the finality provider - // is transitioned to - NewState string `protobuf:"bytes,2,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` -} - -// EventFinalityProviderEdited is the event emitted when a finality provider is edited -type EventFinalityProviderEdited struct { - // btc_pk is the Bitcoin secp256k1 PK of this finality provider - // the PK follows encoding in BIP-340 spec - BtcPk string `protobuf:"bytes,1,opt,name=btc_pk,proto3" json:"btc_pk,omitempty"` - // commission defines the commission rate of the finality provider. - Commission string `protobuf:"bytes,2,opt,name=commission,proto3" json:"commission,omitempty"` - // moniker defines a human-readable name for the validator. - Moniker string `protobuf:"bytes,1,opt,name=moniker,proto3" json:"moniker,omitempty"` - // identity defines an optional identity signature (ex. UPort or Keybase). - Identity string `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` - // website defines an optional website link. - Website string `protobuf:"bytes,3,opt,name=website,proto3" json:"website,omitempty"` - // security_contact defines an optional email for security contact. - SecurityContact string `protobuf:"bytes,4,opt,name=security_contact,json=securityContact,proto3" json:"security_contact,omitempty"` - // details define other optional details. - Details string `protobuf:"bytes,5,opt,name=details,proto3" json:"details,omitempty"` -} diff --git a/internal/config/config.go b/internal/config/config.go index 423826d..0b9b7a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,7 @@ type Config struct { Db DbConfig `mapstructure:"db"` Btc BtcConfig `mapstructure:"btc"` Bbn bbnconfig.BabylonQueryConfig `mapstructure:"bbn"` + Poller PollerConfig `mapstructure:"poller"` Queue queue.QueueConfig `mapstructure:"queue"` Metrics MetricsConfig `mapstructure:"metrics"` } @@ -42,6 +43,10 @@ func (cfg *Config) Validate() error { return err } + if err := cfg.Poller.Validate(); err != nil { + return err + } + return nil } diff --git a/internal/config/poller.go b/internal/config/poller.go index a551e40..d266062 100644 --- a/internal/config/poller.go +++ b/internal/config/poller.go @@ -2,39 +2,17 @@ package config import ( "errors" - "fmt" "time" - - "github.com/rs/zerolog" ) type PollerConfig struct { - Interval time.Duration `mapstructure:"interval"` - LogLevel string `mapstructure:"log-level"` + ParamPollingInterval time.Duration `mapstructure:"param-polling-interval"` } func (cfg *PollerConfig) Validate() error { - if cfg.Interval < 0 { - return errors.New("poll interval cannot be negative") - } - - if err := cfg.ValidateServiceLogLevel(); err != nil { - return err + if cfg.ParamPollingInterval < 0 { + return errors.New("param-polling-interval must be positive") } return nil } - -func (cfg *PollerConfig) ValidateServiceLogLevel() error { - // If log level is not set, we don't need to validate it, a default value will be used in service - if cfg.LogLevel == "" { - return nil - } - - if parsedLevel, err := zerolog.ParseLevel(cfg.LogLevel); err != nil { - return fmt.Errorf("invalid log level: %w", err) - } else if parsedLevel < zerolog.DebugLevel || parsedLevel > zerolog.FatalLevel { - return fmt.Errorf("only log levels from debug to fatal are supported") - } - return nil -} diff --git a/internal/db/interface.go b/internal/db/interface.go index d797836..d96cc62 100644 --- a/internal/db/interface.go +++ b/internal/db/interface.go @@ -3,21 +3,74 @@ package db import ( "context" + "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/bbnclient" "github.com/babylonlabs-io/babylon-staking-indexer/internal/db/model" ) type DbInterface interface { + /** + * Ping checks the database connection. + * @param ctx The context + * @return An error if the operation failed + */ Ping(ctx context.Context) error + /** + * SaveNewFinalityProvider saves a new finality provider to the database. + * If the finality provider already exists, DuplicateKeyError will be returned. + * @param ctx The context + * @param fpDoc The finality provider details + * @return An error if the operation failed + */ SaveNewFinalityProvider( ctx context.Context, fpDoc *model.FinalityProviderDetails, ) error + /** + * UpdateFinalityProviderState updates the finality provider state. + * @param ctx The context + * @param btcPk The BTC public key + * @param newState The new state + * @return An error if the operation failed + */ UpdateFinalityProviderState( ctx context.Context, btcPk string, newState string, ) error + /** + * UpdateFinalityProviderDetailsFromEvent updates the finality provider details based on the event. + * Only the fields that are not empty in the event will be updated. + * @param ctx The context + * @param detailsToUpdate The finality provider details to update + * @return An error if the operation failed + */ UpdateFinalityProviderDetailsFromEvent( ctx context.Context, detailsToUpdate *model.FinalityProviderDetails, ) error + /** + * GetFinalityProviderByBtcPk retrieves the finality provider details by the BTC public key. + * If the finality provider does not exist, a NotFoundError will be returned. + * @param ctx The context + * @param btcPk The BTC public key + * @return The finality provider details or an error + */ GetFinalityProviderByBtcPk( ctx context.Context, btcPk string, - ) (model.FinalityProviderDetails, error) + ) (*model.FinalityProviderDetails, error) + /** + * SaveStakingParams saves the staking parameters to the database. + * @param ctx The context + * @param version The version of the staking parameters + * @param params The staking parameters + * @return An error if the operation failed + */ + SaveStakingParams( + ctx context.Context, version uint32, params *bbnclient.StakingParams, + ) error + /** + * SaveCheckpointParams saves the checkpoint parameters to the database. + * @param ctx The context + * @param params The checkpoint parameters + * @return An error if the operation failed + */ + SaveCheckpointParams( + ctx context.Context, params *bbnclient.CheckpointParams, + ) error } diff --git a/internal/db/model/delegation.go b/internal/db/model/delegation.go index 0acd434..901e93f 100644 --- a/internal/db/model/delegation.go +++ b/internal/db/model/delegation.go @@ -2,14 +2,10 @@ package model import ( "github.com/babylonlabs-io/babylon-staking-indexer/internal/types" - "go.mongodb.org/mongo-driver/bson/primitive" ) -const DelegationCollection = "delegation" - type DelegationDocument struct { - ID primitive.ObjectID `bson:"_id"` - StakingTxHashHex string `bson:"staking_tx_hash_hex"` + StakingTxHashHex string `bson:"_id"` // Primary key State types.DelegationState `bson:"state"` // TODO: Placeholder for more fields } diff --git a/internal/db/model/finality-provider.go b/internal/db/model/finality-provider.go index d62dc8d..14f2d43 100644 --- a/internal/db/model/finality-provider.go +++ b/internal/db/model/finality-provider.go @@ -1,7 +1,6 @@ package model import ( - types "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/bbnclient/types" bbntypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" ) @@ -23,10 +22,10 @@ type Description struct { } func FromEventFinalityProviderCreated( - event *types.EventFinalityProviderCreated, + event *bbntypes.EventFinalityProviderCreated, ) *FinalityProviderDetails { return &FinalityProviderDetails{ - BtcPk: event.BtcPk, + BtcPk: event.BtcPkHex, BabylonAddress: event.Addr, Description: Description{ Moniker: event.Moniker, @@ -36,16 +35,15 @@ func FromEventFinalityProviderCreated( Details: event.Details, }, Commission: event.Commission, - // TODO: Below to be updated once BBN used string type for state - State: bbntypes.FinalityProviderStatus_name[int32(bbntypes.FinalityProviderStatus_STATUS_INACTIVE)], + State: bbntypes.FinalityProviderStatus_FINALITY_PROVIDER_STATUS_INACTIVE.String(), } } func FromEventFinalityProviderEdited( - event *types.EventFinalityProviderEdited, + event *bbntypes.EventFinalityProviderEdited, ) *FinalityProviderDetails { return &FinalityProviderDetails{ - BtcPk: event.BtcPk, + BtcPk: event.BtcPkHex, Description: Description{ Moniker: event.Moniker, Identity: event.Identity, diff --git a/internal/db/model/params.go b/internal/db/model/params.go new file mode 100644 index 0000000..69c9ec3 --- /dev/null +++ b/internal/db/model/params.go @@ -0,0 +1,7 @@ +package model + +type GlobalParamsDocument struct { + Type string `bson:"type"` + Version uint32 `bson:"version"` + Params interface{} `bson:"params"` +} diff --git a/internal/db/model/setup.go b/internal/db/model/setup.go index 763127e..a4edde5 100644 --- a/internal/db/model/setup.go +++ b/internal/db/model/setup.go @@ -15,6 +15,8 @@ import ( const ( FinalityProviderDetailsCollection = "finality_provider_details" + DelegationCollection = "delegation" + GlobalParamsCollection = "global_params" ) type index struct { @@ -24,6 +26,8 @@ type index struct { var collections = map[string][]index{ FinalityProviderDetailsCollection: {{Indexes: map[string]int{}}}, + DelegationCollection: {{Indexes: map[string]int{}}}, + GlobalParamsCollection: {{Indexes: map[string]int{}}}, } func Setup(ctx context.Context, cfg *config.Config) error { diff --git a/internal/db/params.go b/internal/db/params.go new file mode 100644 index 0000000..c089c5f --- /dev/null +++ b/internal/db/params.go @@ -0,0 +1,73 @@ +package db + +import ( + "context" + "fmt" + + "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/bbnclient" + "github.com/babylonlabs-io/babylon-staking-indexer/internal/db/model" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" +) + +const ( + // CHECKPOINT_PARAMS_VERSION is the version of the checkpoint params + // the value is hardcoded to 0 as the checkpoint params are not expected to change + // However, we keep the versioning in place for future compatibility and + // maintain the same pattern as other global params + CHECKPOINT_PARAMS_VERSION = 0 + CHECKPOINT_PARAMS_TYPE = "CHECKPOINT" + STAKING_PARAMS_TYPE = "STAKING" +) + +func (db *Database) SaveStakingParams( + ctx context.Context, version uint32, params *bbnclient.StakingParams, +) error { + collection := db.client.Database(db.dbName). + Collection(model.GlobalParamsCollection) + + filter := bson.M{ + "type": STAKING_PARAMS_TYPE, + "version": version, + } + + update := bson.M{ + "$setOnInsert": &model.GlobalParamsDocument{ + Type: STAKING_PARAMS_TYPE, + Version: version, + Params: params, + }, + } + + _, err := collection.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true)) + if err != nil { + return fmt.Errorf("failed to save staking params: %w", err) + } + return nil +} + +func (db *Database) SaveCheckpointParams( + ctx context.Context, params *bbnclient.CheckpointParams, +) error { + collection := db.client.Database(db.dbName). + Collection(model.GlobalParamsCollection) + + filter := bson.M{ + "type": CHECKPOINT_PARAMS_TYPE, + "version": CHECKPOINT_PARAMS_VERSION, + } + + update := bson.M{ + "$setOnInsert": &model.GlobalParamsDocument{ + Type: CHECKPOINT_PARAMS_TYPE, + Version: CHECKPOINT_PARAMS_VERSION, + Params: params, + }, + } + + _, err := collection.UpdateOne(ctx, filter, update, options.Update().SetUpsert(true)) + if err != nil { + return fmt.Errorf("failed to save checkpoint params: %w", err) + } + return nil +} diff --git a/internal/services/bootstrap.go b/internal/services/bootstrap.go index a2b8c06..5a157bf 100644 --- a/internal/services/bootstrap.go +++ b/internal/services/bootstrap.go @@ -21,7 +21,7 @@ const ( // height and processing events. If any errors occur during the process, // it will retry with exponential backoff, up to a maximum of maxRetries. // The method runs asynchronously to allow non-blocking operation. -func (s *Service) bootstrapBbn(ctx context.Context) { +func (s *Service) BootstrapBbn(ctx context.Context) { go func() { bootstrapCtx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/internal/services/events.go b/internal/services/events.go index 71afb86..84992d8 100644 --- a/internal/services/events.go +++ b/internal/services/events.go @@ -36,7 +36,7 @@ func NewBbnEvent(category EventCategory, event abcitypes.Event) BbnEvent { // startBbnEventProcessor continuously listens for events from the channel and // processes them in the main thread -func (s *Service) startBbnEventProcessor(ctx context.Context) { +func (s *Service) StartBbnEventProcessor(ctx context.Context) { for event := range s.bbnEventProcessor { if event.Event.Type == "" { log.Warn().Msg("Empty event received, skipping") @@ -67,7 +67,6 @@ func (s *Service) processBbnTxEvent(ctx context.Context, event abcitypes.Event) s.processNewFinalityProviderEvent(ctx, event) case EventFinalityProviderEditedType: s.processFinalityProviderEditedEvent(ctx, event) - } } diff --git a/internal/services/finality-provider.go b/internal/services/finality-provider.go index 2448f8f..6520067 100644 --- a/internal/services/finality-provider.go +++ b/internal/services/finality-provider.go @@ -5,11 +5,11 @@ import ( "fmt" "net/http" - bbntypes "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/bbnclient/types" "github.com/babylonlabs-io/babylon-staking-indexer/internal/db" "github.com/babylonlabs-io/babylon-staking-indexer/internal/db/model" "github.com/babylonlabs-io/babylon-staking-indexer/internal/types" "github.com/babylonlabs-io/babylon-staking-indexer/internal/utils/state" + bbntypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" abcitypes "github.com/cometbft/cometbft/abci/types" ) @@ -76,7 +76,7 @@ func (s *Service) processFinalityProviderEditedEvent( func (s *Service) processFinalityProviderStateChangeEvent( ctx context.Context, event abcitypes.Event, ) *types.Error { - finalityProviderStateChange, err := parseEvent[bbntypes.EventFinalityProviderStateChange]( + finalityProviderStateChange, err := parseEvent[bbntypes.EventFinalityProviderStatusChange]( EventFinalityProviderStateChangeType, event, ) if err != nil { @@ -132,7 +132,7 @@ func validateFinalityProviderCreatedEvent( func validateFinalityProviderEditedEvent( fpEdited *bbntypes.EventFinalityProviderEdited, ) *types.Error { - if fpEdited.BtcPk == "" { + if fpEdited.BtcPkHex == "" { return types.NewErrorWithMsg( http.StatusInternalServerError, types.InternalServiceError, @@ -144,7 +144,7 @@ func validateFinalityProviderEditedEvent( } func validateFinalityProviderStateChangeEvent( - fpStateChange *bbntypes.EventFinalityProviderStateChange, + fpStateChange *bbntypes.EventFinalityProviderStatusChange, ) *types.Error { if fpStateChange.BtcPk == "" { return types.NewErrorWithMsg( diff --git a/internal/services/global-params.go b/internal/services/global-params.go new file mode 100644 index 0000000..22bc0c3 --- /dev/null +++ b/internal/services/global-params.go @@ -0,0 +1,53 @@ +package services + +import ( + "context" + "fmt" + + "github.com/babylonlabs-io/babylon-staking-indexer/internal/types" + "github.com/babylonlabs-io/babylon-staking-indexer/internal/utils/poller" +) + +func (s *Service) SyncGlobalParams(ctx context.Context) { + paramsPoller := poller.NewPoller( + s.cfg.Poller.ParamPollingInterval, + s.fetchAndSaveParams, + ) + go paramsPoller.Start(ctx) +} + +func (s *Service) fetchAndSaveParams(ctx context.Context) *types.Error { + checkpointParams, err := s.bbn.GetCheckpointParams(ctx) + if err != nil { + // TODO: Add metrics and replace internal service error with a more specific + // error code so that the poller can catch and emit the error metrics + return types.NewInternalServiceError( + fmt.Errorf("failed to get checkpoint params: %w", err), + ) + } + if err := s.db.SaveCheckpointParams(ctx, checkpointParams); err != nil { + return types.NewInternalServiceError( + fmt.Errorf("failed to save checkpoint params: %w", err), + ) + } + + allStakingParams, err := s.bbn.GetAllStakingParams(ctx) + if err != nil { + return types.NewInternalServiceError( + fmt.Errorf("failed to get staking params: %w", err), + ) + } + for version, params := range allStakingParams { + if params == nil { + return types.NewInternalServiceError( + fmt.Errorf("nil staking params for version %d", version), + ) + } + if err := s.db.SaveStakingParams(ctx, version, params); err != nil { + return types.NewInternalServiceError( + fmt.Errorf("failed to save staking params: %w", err), + ) + } + } + return nil +} diff --git a/internal/services/service.go b/internal/services/service.go index e4138d9..ac26459 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -5,11 +5,13 @@ import ( "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/bbnclient" "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/btcclient" + "github.com/babylonlabs-io/babylon-staking-indexer/internal/config" "github.com/babylonlabs-io/babylon-staking-indexer/internal/db" "github.com/babylonlabs-io/babylon-staking-indexer/internal/queue" ) type Service struct { + cfg *config.Config db db.DbInterface btc btcclient.BtcInterface bbn bbnclient.BbnInterface @@ -18,6 +20,7 @@ type Service struct { } func NewService( + cfg *config.Config, db db.DbInterface, btc btcclient.BtcInterface, bbn bbnclient.BbnInterface, @@ -25,6 +28,7 @@ func NewService( ) *Service { eventProcessor := make(chan BbnEvent, eventProcessorSize) return &Service{ + cfg: cfg, db: db, btc: btc, bbn: bbn, @@ -34,10 +38,12 @@ func NewService( } func (s *Service) StartIndexerSync(ctx context.Context) { + // Sync global parameters + s.SyncGlobalParams(ctx) // Start the bootstrap process - s.bootstrapBbn(ctx) + s.BootstrapBbn(ctx) // Start the websocket event subscription process - s.subscribeToBbnEvents(ctx) + s.SubscribeToBbnEvents(ctx) // Keep processing events in the main thread - s.startBbnEventProcessor(ctx) + s.StartBbnEventProcessor(ctx) } diff --git a/internal/services/subscription.go b/internal/services/subscription.go index 8a010a5..0e3e05c 100644 --- a/internal/services/subscription.go +++ b/internal/services/subscription.go @@ -3,7 +3,7 @@ package services import "context" // TODO: Placeholder for subscribing to BBN events via websocket -func (s *Service) subscribeToBbnEvents(ctx context.Context) { +func (s *Service) SubscribeToBbnEvents(ctx context.Context) { go func() { for { select { diff --git a/internal/types/error.go b/internal/types/error.go index be4b085..9322f8a 100644 --- a/internal/types/error.go +++ b/internal/types/error.go @@ -20,6 +20,7 @@ const ( Forbidden ErrorCode = "FORBIDDEN" UnprocessableEntity ErrorCode = "UNPROCESSABLE_ENTITY" RequestTimeout ErrorCode = "REQUEST_TIMEOUT" + ClientRequestError ErrorCode = "CLIENT_REQUEST_ERROR" ) // ApiError represents an error with an HTTP status code and an application-specific error code. diff --git a/internal/utils/poller/poller.go b/internal/utils/poller/poller.go index 7d5c50f..f17aee6 100644 --- a/internal/utils/poller/poller.go +++ b/internal/utils/poller/poller.go @@ -2,9 +2,9 @@ package poller import ( "context" - "go/types" "time" + "github.com/babylonlabs-io/babylon-staking-indexer/internal/types" "github.com/rs/zerolog/log" ) @@ -36,6 +36,7 @@ func (p *Poller) Start(ctx context.Context) { log.Info().Msg("Poller stopped due to context cancellation") return case <-p.quit: + log.Info().Msg("Poller stopped") ticker.Stop() // Stop the ticker return } diff --git a/tests/mocks/mock_bbn_client.go b/tests/mocks/mock_bbn_client.go new file mode 100644 index 0000000..92fdd8b --- /dev/null +++ b/tests/mocks/mock_bbn_client.go @@ -0,0 +1,161 @@ +// Code generated by mockery v2.41.0. DO NOT EDIT. + +package mocks + +import ( + bbnclient "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/bbnclient" + btccheckpointtypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" + + context "context" + + coretypes "github.com/cometbft/cometbft/rpc/core/types" + + mock "github.com/stretchr/testify/mock" + + types "github.com/babylonlabs-io/babylon-staking-indexer/internal/types" +) + +// BbnInterface is an autogenerated mock type for the BbnInterface type +type BbnInterface struct { + mock.Mock +} + +// GetAllStakingParams provides a mock function with given fields: ctx +func (_m *BbnInterface) GetAllStakingParams(ctx context.Context) (map[uint32]*bbnclient.StakingParams, *types.Error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetAllStakingParams") + } + + var r0 map[uint32]*bbnclient.StakingParams + var r1 *types.Error + if rf, ok := ret.Get(0).(func(context.Context) (map[uint32]*bbnclient.StakingParams, *types.Error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) map[uint32]*bbnclient.StakingParams); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[uint32]*bbnclient.StakingParams) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) *types.Error); ok { + r1 = rf(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*types.Error) + } + } + + return r0, r1 +} + +// GetBlockResults provides a mock function with given fields: ctx, blockHeight +func (_m *BbnInterface) GetBlockResults(ctx context.Context, blockHeight int64) (*coretypes.ResultBlockResults, *types.Error) { + ret := _m.Called(ctx, blockHeight) + + if len(ret) == 0 { + panic("no return value specified for GetBlockResults") + } + + var r0 *coretypes.ResultBlockResults + var r1 *types.Error + if rf, ok := ret.Get(0).(func(context.Context, int64) (*coretypes.ResultBlockResults, *types.Error)); ok { + return rf(ctx, blockHeight) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) *coretypes.ResultBlockResults); ok { + r0 = rf(ctx, blockHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.ResultBlockResults) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) *types.Error); ok { + r1 = rf(ctx, blockHeight) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*types.Error) + } + } + + return r0, r1 +} + +// GetCheckpointParams provides a mock function with given fields: ctx +func (_m *BbnInterface) GetCheckpointParams(ctx context.Context) (*btccheckpointtypes.Params, *types.Error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetCheckpointParams") + } + + var r0 *btccheckpointtypes.Params + var r1 *types.Error + if rf, ok := ret.Get(0).(func(context.Context) (*btccheckpointtypes.Params, *types.Error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *btccheckpointtypes.Params); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*btccheckpointtypes.Params) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) *types.Error); ok { + r1 = rf(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*types.Error) + } + } + + return r0, r1 +} + +// GetLatestBlockNumber provides a mock function with given fields: ctx +func (_m *BbnInterface) GetLatestBlockNumber(ctx context.Context) (int64, *types.Error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetLatestBlockNumber") + } + + var r0 int64 + var r1 *types.Error + if rf, ok := ret.Get(0).(func(context.Context) (int64, *types.Error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context) *types.Error); ok { + r1 = rf(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*types.Error) + } + } + + return r0, r1 +} + +// NewBbnInterface creates a new instance of BbnInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBbnInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *BbnInterface { + mock := &BbnInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/mocks/mock_btc_client.go b/tests/mocks/mock_btc_client.go new file mode 100644 index 0000000..21a8239 --- /dev/null +++ b/tests/mocks/mock_btc_client.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.41.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// BtcInterface is an autogenerated mock type for the BtcInterface type +type BtcInterface struct { + mock.Mock +} + +// GetBlockCount provides a mock function with given fields: +func (_m *BtcInterface) GetBlockCount() (int64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetBlockCount") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func() (int64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewBtcInterface creates a new instance of BtcInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBtcInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *BtcInterface { + mock := &BtcInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/tests/mocks/mock_db_client.go b/tests/mocks/mock_db_client.go index 31c89ff..1efd07f 100644 --- a/tests/mocks/mock_db_client.go +++ b/tests/mocks/mock_db_client.go @@ -5,9 +5,13 @@ package mocks import ( context "context" + bbnclient "github.com/babylonlabs-io/babylon-staking-indexer/internal/clients/bbnclient" + mock "github.com/stretchr/testify/mock" model "github.com/babylonlabs-io/babylon-staking-indexer/internal/db/model" + + types "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" ) // DbInterface is an autogenerated mock type for the DbInterface type @@ -16,22 +20,24 @@ type DbInterface struct { } // GetFinalityProviderByBtcPk provides a mock function with given fields: ctx, btcPk -func (_m *DbInterface) GetFinalityProviderByBtcPk(ctx context.Context, btcPk string) (model.FinalityProviderDetails, error) { +func (_m *DbInterface) GetFinalityProviderByBtcPk(ctx context.Context, btcPk string) (*model.FinalityProviderDetails, error) { ret := _m.Called(ctx, btcPk) if len(ret) == 0 { panic("no return value specified for GetFinalityProviderByBtcPk") } - var r0 model.FinalityProviderDetails + var r0 *model.FinalityProviderDetails var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (model.FinalityProviderDetails, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) (*model.FinalityProviderDetails, error)); ok { return rf(ctx, btcPk) } - if rf, ok := ret.Get(0).(func(context.Context, string) model.FinalityProviderDetails); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) *model.FinalityProviderDetails); ok { r0 = rf(ctx, btcPk) } else { - r0 = ret.Get(0).(model.FinalityProviderDetails) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.FinalityProviderDetails) + } } if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { @@ -61,6 +67,24 @@ func (_m *DbInterface) Ping(ctx context.Context) error { return r0 } +// SaveCheckpointParams provides a mock function with given fields: ctx, params +func (_m *DbInterface) SaveCheckpointParams(ctx context.Context, params *types.Params) error { + ret := _m.Called(ctx, params) + + if len(ret) == 0 { + panic("no return value specified for SaveCheckpointParams") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Params) error); ok { + r0 = rf(ctx, params) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SaveNewFinalityProvider provides a mock function with given fields: ctx, fpDoc func (_m *DbInterface) SaveNewFinalityProvider(ctx context.Context, fpDoc *model.FinalityProviderDetails) error { ret := _m.Called(ctx, fpDoc) @@ -79,6 +103,24 @@ func (_m *DbInterface) SaveNewFinalityProvider(ctx context.Context, fpDoc *model return r0 } +// SaveStakingParams provides a mock function with given fields: ctx, version, params +func (_m *DbInterface) SaveStakingParams(ctx context.Context, version uint32, params *bbnclient.StakingParams) error { + ret := _m.Called(ctx, version, params) + + if len(ret) == 0 { + panic("no return value specified for SaveStakingParams") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uint32, *bbnclient.StakingParams) error); ok { + r0 = rf(ctx, version, params) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateFinalityProviderDetailsFromEvent provides a mock function with given fields: ctx, detailsToUpdate func (_m *DbInterface) UpdateFinalityProviderDetailsFromEvent(ctx context.Context, detailsToUpdate *model.FinalityProviderDetails) error { ret := _m.Called(ctx, detailsToUpdate)