diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 4f07150..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,42 +0,0 @@ -version: 2.1 - -orbs: - go: circleci/go@1.7.3 - aws-ecr: circleci/aws-ecr@8.2.1 - -jobs: - build_lint_test: - machine: - image: ubuntu-2204:2024.01.1 - resource_class: large - steps: - - go/install: - version: "1.21.4" - - checkout - - run: - name: Print Go environment - command: "go env" - - go/load-cache: - key: go-mod-v6-{{ checksum "go.sum" }} - - add_ssh_keys - - go/mod-download - - go/save-cache: - key: go-mod-v6-{{ checksum "go.sum" }} - path: "/home/circleci/.go_workspace/pkg/mod" - - run: - name: Build cli app - command: make build - - run: - name: Lint - command: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.55.2 - ./bin/golangci-lint run --timeout 5m0s - - run: - name: Run tests - command: | - make test - -workflows: - CI: - jobs: - - build_lint_test \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4bcb5f1..984b3b6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,6 +5,8 @@ on: branches: - 'chore/ci-publish-docker' - 'main' + # TODO: remove temp branch + - 'fix/circular-deps-finalized-head' tags: - '*' diff --git a/Makefile b/Makefile index b67ce38..d98bc54 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ GIT_ROOT := $(shell git rev-parse --show-toplevel) mock-gen: @which mockgen > /dev/null || CGO_ENABLED=0 go install go.uber.org/mock/mockgen@latest mockgen -source=db/interface.go -package mocks -destination $(MOCKS_DIR)/db_mock.go + mockgen -source=finalitygadget/interface.go -package mocks -destination $(MOCKS_DIR)/finalitygadget_mock.go mockgen -source=finalitygadget/expected_clients.go -package mocks -destination $(MOCKS_DIR)/expected_clients_mock.go test: diff --git a/cmd/opfgd/start.go b/cmd/opfgd/start.go index 18b94f0..dceb2de 100644 --- a/cmd/opfgd/start.go +++ b/cmd/opfgd/start.go @@ -55,7 +55,12 @@ func runStartCmd(ctx client.Context, cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("failed to create DB handler: %w", err) } - defer db.Close() + defer func() { + logger.Info("Closing DB...") + if dbErr := db.Close(); dbErr != nil { + logger.Error("Error closing DB", zap.Error(dbErr)) + } + }() err = db.CreateInitialSchema() if err != nil { return fmt.Errorf("create initial buckets error: %w", err) @@ -88,6 +93,11 @@ func runStartCmd(ctx client.Context, cmd *cobra.Command, args []string) error { } }() + // Start finality gadget + if err := fg.Startup(fgCtx); err != nil { + logger.Fatal("Error starting finality gadget", zap.Error(err)) + } + // Run finality gadget in a separate goroutine go func() { if err := fg.ProcessBlocks(fgCtx); err != nil { diff --git a/config.toml.example b/config.toml.example index c4ece2f..fef69a0 100644 --- a/config.toml.example +++ b/config.toml.example @@ -9,4 +9,5 @@ BBNChainID = "euphrates-0.4.0" BBNRPCAddress = "https://rpc-euphrates.devnet.babylonchain.io" GRPCListener = "0.0.0.0:50051" HTTPListener = "0.0.0.0:8080" -PollInterval = "10s" \ No newline at end of file +PollInterval = "10s" +BatchSize = 10 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 7af7dd4..002b882 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "time" "github.com/spf13/viper" @@ -19,6 +20,47 @@ type Config struct { HTTPListener string `long:"http-listener" description:"host:port to listen for HTTP connections"` BitcoinDisableTLS bool `long:"bitcoin-disable-tls" description:"disable TLS for RPC connections"` PollInterval time.Duration `long:"retry-interval" description:"interval in seconds to recheck Babylon finality of block"` + BatchSize uint64 `long:"batch-size" description:"number of blocks to process in a batch"` +} + +func (c *Config) Validate() error { + // Required fields + if c.L2RPCHost == "" { + return fmt.Errorf("l2-rpc-host is required") + } + if c.BitcoinRPCHost == "" { + return fmt.Errorf("bitcoin-rpc-host is required") + } + if c.FGContractAddress == "" { + return fmt.Errorf("fg-contract-address is required") + } + if c.BBNChainID == "" { + return fmt.Errorf("bbn-chain-id is required") + } + if c.BBNRPCAddress == "" { + return fmt.Errorf("bbn-rpc-address is required") + } + // TODO: add some default values if missing + if c.DBFilePath == "" { + return fmt.Errorf("db-file-path is required") + } + if c.GRPCListener == "" { + return fmt.Errorf("grpc-listener is required") + } + if c.HTTPListener == "" { + return fmt.Errorf("http-listener is required") + } + + // Numeric validations + // TODO: add more validations (max batch size, min poll interval) + if c.PollInterval <= 0 { + return fmt.Errorf("poll-interval must be positive") + } + if c.BatchSize == 0 { + return fmt.Errorf("batch-size must be greater than 0") + } + + return nil } func Load(configPath string) (*Config, error) { @@ -34,5 +76,9 @@ func Load(configPath string) (*Config, error) { return nil, err } + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + return &config, nil } diff --git a/db/bbolt.go b/db/bbolt.go index 9873cd8..9f7636d 100644 --- a/db/bbolt.go +++ b/db/bbolt.go @@ -64,80 +64,72 @@ func (bb *BBoltHandler) CreateInitialSchema() error { }) } -func (bb *BBoltHandler) InsertBlock(block *types.Block) error { - bb.logger.Info("Inserting block to DB", zap.Uint64("block_height", block.BlockHeight)) - - // Store mapping number -> block - err := bb.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(blocksBucket)) - key := bb.itob(block.BlockHeight) - blockBytes, err := json.Marshal(block) - if err != nil { - return err - } - return b.Put(key, blockBytes) - }) - if err != nil { - bb.logger.Error("Error inserting block", zap.Error(err)) - return err +func (bb *BBoltHandler) InsertBlocks(blocks []*types.Block) error { + if len(blocks) == 0 { + return nil } - // Store mapping hash -> number - err = bb.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(blockHeightsBucket)) - return b.Put([]byte(block.BlockHash), bb.itob(block.BlockHeight)) - }) - if err != nil { - bb.logger.Error("Error inserting block", zap.Error(err)) - return err - } + bb.logger.Info("Batch inserting blocks to DB", zap.Int("count", len(blocks))) + + // Single transaction for all operations + return bb.db.Update(func(tx *bolt.Tx) error { + blocksBucket := tx.Bucket([]byte(blocksBucket)) + heightsBucket := tx.Bucket([]byte(blockHeightsBucket)) + indexBucket := tx.Bucket([]byte(indexerBucket)) + + var minHeight, maxHeight uint64 = math.MaxUint64, 0 + + // Insert all blocks + for _, block := range blocks { + // Update min/max heights + if block.BlockHeight < minHeight { + minHeight = block.BlockHeight + } + if block.BlockHeight > maxHeight { + maxHeight = block.BlockHeight + } + + // Store block data + blockBytes, err := json.Marshal(block) + if err != nil { + bb.logger.Error("Error inserting block", zap.Error(err)) + return err + } + if err := blocksBucket.Put(bb.itob(block.BlockHeight), blockBytes); err != nil { + return err + } - // Get current earliest block - // If it is unset, update earliest block - earliestBlock, err := bb.QueryEarliestFinalizedBlock() - if earliestBlock == nil || errors.Is(err, types.ErrBlockNotFound) { - err = bb.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(indexerBucket)) - return b.Put([]byte(earliestBlockKey), bb.itob(block.BlockHeight)) - }) - if err != nil { - bb.logger.Error("Error updating earliest block", zap.Error(err)) - return err + // Store height mapping + if err := heightsBucket.Put([]byte(block.BlockHash), bb.itob(block.BlockHeight)); err != nil { + bb.logger.Error("Error inserting height mapping", zap.Error(err)) + return err + } } - } - if err != nil { - bb.logger.Error("Error getting earliest block", zap.Error(err)) - return err - } - // Get current latest block - latestBlock, err := bb.QueryLatestFinalizedBlock() - if latestBlock == nil { - latestBlock = &types.Block{BlockHeight: 0} - } - if err != nil { - bb.logger.Error("Error getting latest block", zap.Error(err)) - return err - } + // Update earliest block if needed + earliestBytes := indexBucket.Get([]byte(earliestBlockKey)) + if earliestBytes == nil { + if err := indexBucket.Put([]byte(earliestBlockKey), bb.itob(minHeight)); err != nil { + bb.logger.Error("Error inserting earliest block", zap.Error(err)) + return err + } + } - // Update latest block if it's the latest - err = bb.db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(indexerBucket)) - if err != nil { - bb.logger.Error("Error getting latest block", zap.Error(err)) - return err + // Update latest block if needed + latestBytes := indexBucket.Get([]byte(latestBlockKey)) + var currentLatest uint64 + if latestBytes != nil { + currentLatest = bb.btoi(latestBytes) } - if latestBlock.BlockHeight < block.BlockHeight { - return b.Put([]byte(latestBlockKey), bb.itob(block.BlockHeight)) + if maxHeight > currentLatest { + if err := indexBucket.Put([]byte(latestBlockKey), bb.itob(maxHeight)); err != nil { + bb.logger.Error("Error inserting latest block", zap.Error(err)) + return err + } } + return nil }) - if err != nil { - bb.logger.Error("Error updating latest block", zap.Error(err)) - return err - } - - return nil } func (bb *BBoltHandler) GetBlockByHeight(height uint64) (*types.Block, error) { @@ -174,6 +166,39 @@ func (bb *BBoltHandler) GetBlockByHash(hash string) (*types.Block, error) { return bb.GetBlockByHeight(blockHeight) } +func (bb *BBoltHandler) QueryIsBlockRangeFinalizedByHeight(startHeight, endHeight uint64) ([]bool, error) { + if startHeight > endHeight { + return nil, types.ErrInvalidBlockRange + } + + // Create result slice with size of the range + len := endHeight - startHeight + 1 + results := make([]bool, len) + + err := bb.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(blocksBucket)) + + // Check each height in the range + for i := uint64(0); i < len; i++ { + height := startHeight + i + blockExists := bucket.Get(bb.itob(height)) != nil + // break early if block not found, as we only store consecutive blocks + if !blockExists { + break + } + results[i] = blockExists + } + + return nil + }) + + if err != nil { + return nil, err + } + + return results, nil +} + func (bb *BBoltHandler) QueryIsBlockFinalizedByHeight(height uint64) (bool, error) { _, err := bb.GetBlockByHeight(height) if err != nil { @@ -275,6 +300,7 @@ func (bb *BBoltHandler) SaveActivatedTimestamp(timestamp uint64) error { } func (bb *BBoltHandler) Close() error { + bb.logger.Info("Closing DB...") return bb.db.Close() } diff --git a/db/bbolt_test.go b/db/bbolt_test.go index 362aa48..f1822dc 100644 --- a/db/bbolt_test.go +++ b/db/bbolt_test.go @@ -48,25 +48,62 @@ func setupDB(t *testing.T) (*BBoltHandler, func()) { return db, cleanup } -func TestInsertBlock(t *testing.T) { +func TestInsertBlocks(t *testing.T) { handler, cleanup := setupDB(t) defer cleanup() - block := &types.Block{ - BlockHeight: 1, - BlockHash: "0x123", - BlockTimestamp: 1000, + // Create test blocks + blocks := []*types.Block{ + { + BlockHeight: 1, + BlockHash: "0x123", + BlockTimestamp: 1000, + }, + { + BlockHeight: 2, + BlockHash: "0x456", + BlockTimestamp: 1050, + }, + { + BlockHeight: 3, + BlockHash: "0x789", + BlockTimestamp: 1100, + }, } - err := handler.InsertBlock(block) + // Test batch insert + err := handler.InsertBlocks(blocks) assert.NoError(t, err) - // Verify block was inserted - retrievedBlock, err := handler.GetBlockByHeight(block.BlockHeight) + // Verify all blocks were inserted correctly + for _, block := range blocks { + // Check by height + retrievedBlock, blockErr := handler.GetBlockByHeight(block.BlockHeight) + assert.NoError(t, blockErr) + assert.Equal(t, block.BlockHeight, retrievedBlock.BlockHeight) + assert.Equal(t, block.BlockHash, retrievedBlock.BlockHash) + assert.Equal(t, block.BlockTimestamp, retrievedBlock.BlockTimestamp) + + // Check by hash + retrievedBlock, err = handler.GetBlockByHash(block.BlockHash) + assert.NoError(t, err) + assert.Equal(t, block.BlockHeight, retrievedBlock.BlockHeight) + assert.Equal(t, block.BlockHash, retrievedBlock.BlockHash) + assert.Equal(t, block.BlockTimestamp, retrievedBlock.BlockTimestamp) + } + + // Verify earliest and latest blocks + earliest, err := handler.QueryEarliestFinalizedBlock() + assert.NoError(t, err) + assert.Equal(t, uint64(1), earliest.BlockHeight) + + latest, err := handler.QueryLatestFinalizedBlock() + assert.NoError(t, err) + assert.Equal(t, uint64(3), latest.BlockHeight) + + // Test empty slice + err = handler.InsertBlocks([]*types.Block{}) assert.NoError(t, err) - assert.Equal(t, block.BlockHeight, retrievedBlock.BlockHeight) - assert.Equal(t, block.BlockHash, retrievedBlock.BlockHash) - assert.Equal(t, block.BlockTimestamp, retrievedBlock.BlockTimestamp) } func TestGetBlockByHeight(t *testing.T) { @@ -79,7 +116,7 @@ func TestGetBlockByHeight(t *testing.T) { BlockHash: "0x123", BlockTimestamp: 1000, } - err := handler.InsertBlock(block) + err := handler.InsertBlocks([]*types.Block{block}) assert.NoError(t, err) // Retrieve block by height @@ -109,7 +146,7 @@ func TestGetBlockByHash(t *testing.T) { BlockHash: "0x123", BlockTimestamp: 1000, } - err := handler.InsertBlock(block) + err := handler.InsertBlocks([]*types.Block{block}) assert.NoError(t, err) // Retrieve block by hash @@ -129,6 +166,88 @@ func TestGetBlockByHashForNonExistentBlock(t *testing.T) { assert.Equal(t, types.ErrBlockNotFound, err) } +func TestQueryIsBlockRangeFinalizedByHeight(t *testing.T) { + handler, cleanup := setupDB(t) + defer cleanup() + + // Insert some test blocks + blocks := []*types.Block{ + { + BlockHeight: 1, + BlockHash: "0x123", + BlockTimestamp: 1000, + }, + { + BlockHeight: 2, + BlockHash: "0x456", + BlockTimestamp: 1050, + }, + { + BlockHeight: 3, + BlockHash: "0x789", + BlockTimestamp: 1100, + }, + } + err := handler.InsertBlocks(blocks) + assert.NoError(t, err) + + testCases := []struct { + name string + expected []bool + expectErr bool + endHeight uint64 + startHeight uint64 + }{ + { + name: "single block exists", + startHeight: 1, + endHeight: 1, + expected: []bool{true}, + expectErr: false, + }, + { + name: "multiple blocks exist", + startHeight: 1, + endHeight: 3, + expected: []bool{true, true, true}, + expectErr: false, + }, + { + name: "no blocks exist in range", + startHeight: 4, + endHeight: 5, + expected: []bool{false, false}, + expectErr: false, + }, + { + name: "mixed existing and non-existing blocks", + startHeight: 2, + endHeight: 4, + expected: []bool{true, true, false}, + expectErr: false, + }, + { + name: "invalid range (end < start)", + startHeight: 2, + endHeight: 1, + expected: nil, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + results, err := handler.QueryIsBlockRangeFinalizedByHeight(tc.startHeight, tc.endHeight) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, results) + } + }) + } +} + func TestQueryIsBlockFinalizedByHeight(t *testing.T) { handler, cleanup := setupDB(t) defer cleanup() @@ -139,7 +258,7 @@ func TestQueryIsBlockFinalizedByHeight(t *testing.T) { BlockHash: "0x123", BlockTimestamp: 1000, } - err := handler.InsertBlock(block) + err := handler.InsertBlocks([]*types.Block{block}) assert.NoError(t, err) // Retrieve block status by height @@ -167,7 +286,7 @@ func TestQueryIsBlockFinalizedByHash(t *testing.T) { BlockHash: "0x123", BlockTimestamp: 1000, } - err := handler.InsertBlock(block) + err := handler.InsertBlocks([]*types.Block{block}) assert.NoError(t, err) // Retrieve block status by hash @@ -205,11 +324,7 @@ func TestQueryEarliestFinalizedBlock(t *testing.T) { BlockHash: "0x789", BlockTimestamp: 1100, } - err := handler.InsertBlock(first) - assert.NoError(t, err) - err = handler.InsertBlock(second) - assert.NoError(t, err) - err = handler.InsertBlock(third) + err := handler.InsertBlocks([]*types.Block{first, second, third}) assert.NoError(t, err) // Query earliest consecutively finalized block @@ -235,9 +350,7 @@ func TestQueryLatestFinalizedBlock(t *testing.T) { BlockHash: "0x456", BlockTimestamp: 1050, } - err := handler.InsertBlock(first) - assert.NoError(t, err) - err = handler.InsertBlock(second) + err := handler.InsertBlocks([]*types.Block{first, second}) assert.NoError(t, err) // Retrieve latest block diff --git a/db/interface.go b/db/interface.go index 559a363..7cb7900 100644 --- a/db/interface.go +++ b/db/interface.go @@ -4,10 +4,11 @@ import "github.com/babylonlabs-io/finality-gadget/types" type IDatabaseHandler interface { CreateInitialSchema() error - InsertBlock(block *types.Block) error + InsertBlocks(block []*types.Block) error GetBlockByHeight(height uint64) (*types.Block, error) GetBlockByHash(hash string) (*types.Block, error) QueryIsBlockFinalizedByHeight(height uint64) (bool, error) + QueryIsBlockRangeFinalizedByHeight(startHeight, endHeight uint64) ([]bool, error) QueryIsBlockFinalizedByHash(hash string) (bool, error) QueryEarliestFinalizedBlock() (*types.Block, error) QueryLatestFinalizedBlock() (*types.Block, error) diff --git a/finalitygadget/finalitygadget.go b/finalitygadget/finalitygadget.go index 50a8622..32399a5 100644 --- a/finalitygadget/finalitygadget.go +++ b/finalitygadget/finalitygadget.go @@ -40,6 +40,7 @@ type FinalityGadget struct { pollInterval time.Duration lastProcessedHeight uint64 + batchSize uint64 } ////////////////////////////// @@ -107,6 +108,7 @@ func NewFinalityGadget(cfg *config.Config, db db.IDatabaseHandler, logger *zap.L l2Client: l2Client, db: db, pollInterval: cfg.PollInterval, + batchSize: cfg.BatchSize, lastProcessedHeight: lastProcessedHeight, logger: logger, }, nil @@ -116,7 +118,8 @@ func NewFinalityGadget(cfg *config.Config, db db.IDatabaseHandler, logger *zap.L // METHODS ////////////////////////////// -/* QueryIsBlockBabylonFinalized checks if the given L2 block is finalized by the Babylon finality gadget +// TODO: make this method internal once fully tested. External services should query the database instead. +/* QueryIsBlockBabylonFinalizedFromBabylon checks if the given L2 block is finalized by querying the Babylon node * * - if the finality gadget is not enabled, always return true * - else, check if the given L2 block is finalized @@ -132,7 +135,7 @@ func NewFinalityGadget(cfg *config.Config, db db.IDatabaseHandler, logger *zap.L * - calculate voted voting power * - check if the voted voting power is more than 2/3 of the total voting power */ -func (fg *FinalityGadget) QueryIsBlockBabylonFinalized(block *types.Block) (bool, error) { +func (fg *FinalityGadget) QueryIsBlockBabylonFinalizedFromBabylon(block *types.Block) (bool, error) { // check if the finality gadget is enabled // if not, always return true to pass through op derivation pipeline isEnabled, err := fg.cwClient.QueryIsEnabled() @@ -207,8 +210,38 @@ func (fg *FinalityGadget) QueryIsBlockBabylonFinalized(block *types.Block) (bool return true, nil } -/* QueryBlockRangeBabylonFinalized searches for a row of consecutive finalized blocks in the block range, and returns - * the last finalized block height +// QueryIsBlockBabylonFinalized queries the finality status of a given block height from the internal db +func (fg *FinalityGadget) QueryIsBlockBabylonFinalized(block *types.Block) (bool, error) { + // check if the finality gadget is enabled + // if not, always return true to pass through op derivation pipeline + isEnabled, err := fg.cwClient.QueryIsEnabled() + if err != nil { + return false, err + } + if !isEnabled { + return true, nil + } + + // convert the L2 timestamp to BTC height + btcblockHeight, err := fg.btcClient.GetBlockHeightByTimestamp(block.BlockTimestamp) + if err != nil { + return false, err + } + + // check whether the btc staking is activated + btcStakingActivatedTimestamp, err := fg.QueryBtcStakingActivatedTimestamp() + if err != nil { + return false, err + } + if btcblockHeight < btcStakingActivatedTimestamp { + return false, types.ErrBtcStakingNotActivated + } + + // query the finality status of the block from internal db + return fg.db.QueryIsBlockFinalizedByHeight(block.BlockHeight) +} + +/* QueryBlockRangeBabylonFinalized searches the internal db and returns the last consecutively finalized block in the block range * * Example: if give block range 1-10, and block 1-5 are finalized, and block 6-10 are not finalized, then return 5 * @@ -233,14 +266,20 @@ func (fg *FinalityGadget) QueryBlockRangeBabylonFinalized( return nil, fmt.Errorf("blocks are not consecutive") } } + + // query the finality status of block range from internal db + startHeight := queryBlocks[0].BlockHeight + endHeight := queryBlocks[len(queryBlocks)-1].BlockHeight + isFinalizedArr, err := fg.db.QueryIsBlockRangeFinalizedByHeight(startHeight, endHeight) + if err != nil { + return nil, err + } + + // find the last finalized block in the range var finalizedBlockHeight *uint64 - for _, block := range queryBlocks { - isFinalized, err := fg.QueryIsBlockBabylonFinalized(block) - if err != nil { - return finalizedBlockHeight, err - } + for i, isFinalized := range isFinalizedArr { if isFinalized { - finalizedBlockHeight = &block.BlockHeight + finalizedBlockHeight = &queryBlocks[i].BlockHeight } else { break } @@ -249,6 +288,7 @@ func (fg *FinalityGadget) QueryBlockRangeBabylonFinalized( if finalizedBlockHeight == nil { return nil, nil } + return finalizedBlockHeight, nil } @@ -384,8 +424,16 @@ func (fg *FinalityGadget) QueryLatestFinalizedBlock() (*types.Block, error) { return fg.db.QueryLatestFinalizedBlock() } -// This function process blocks indefinitely, starting from the last finalized block. -func (fg *FinalityGadget) ProcessBlocks(ctx context.Context) error { +// This function is run once at startup and starts the FG from the last finalized block. +// Note that starting FG before the chain has ETH finalized its first block will cause a panic. +// The intended startup order for new chains is: +// 1. Start the OP chain with `babylonFinalityGadgetRpc` in rollup configs set to an empty string +// 2. Wait for the chain to finalize its first block +// 3. Integrate FG with it disabled on CW contract +// 3. Restart OP chain after setting `babylonFinalityGadgetRpc` +// 4. Enable FG on CW contract (for network with multiple nodes, enable after majority of nodes upgrade) +func (fg *FinalityGadget) Startup(ctx context.Context) error { + fg.logger.Info("Starting up finality gadget...") // Start polling for new blocks at set interval ticker := time.NewTicker(fg.pollInterval) defer ticker.Stop() @@ -395,11 +443,12 @@ func (fg *FinalityGadget) ProcessBlocks(ctx context.Context) error { case <-ctx.Done(): return nil case <-ticker.C: + // query rpc for latest eth finalized block + // at this point, FG is disabled so the derivation pipeline passes through latestFinalizedBlock, err := fg.l2Client.HeaderByNumber(ctx, big.NewInt(ethrpc.FinalizedBlockNumber.Int64())) if err != nil { return fmt.Errorf("error fetching latest finalized L2 block: %w", err) } - latestFinalizedHeight := latestFinalizedBlock.Number.Uint64() latestFinalizedBlockTime := latestFinalizedBlock.Time @@ -413,47 +462,75 @@ func (fg *FinalityGadget) ProcessBlocks(ctx context.Context) error { return fmt.Errorf("error querying BTC staking activation timestamp: %w", err) } - // only process blocks after the btc staking is activated + // throw error if btc staking activated before the first block was finalized (see startup order above) + if latestFinalizedHeight == 0 && btcStakingActivatedTimestamp < latestFinalizedBlockTime { + return fmt.Errorf("BTC staking activated before the first finalized block") + } + + // skip blocks before btc staking is activated if latestFinalizedBlockTime < btcStakingActivatedTimestamp { fg.logger.Info("Skipping block before BTC staking activation", zap.Uint64("block_height", latestFinalizedHeight)) - fg.lastProcessedHeight = latestFinalizedHeight continue } - // at FG startup, this can avoid indexing from blocks that's not activated yet - // TODO: we can add a flag fullSync - // true: sync from the first btc finalized block (convertL2BlockHeight(btcStakingActivatedTimestamp)) - // false: sync from the last btc finalized block - if fg.lastProcessedHeight == 0 { - fg.lastProcessedHeight = latestFinalizedHeight - 1 + // otherwise, startup the FG at latest finalized block + // note we set `lastProcessedHeight` to the prev block to ensure the latest height is also processed + fg.logger.Info("Starting finality gadget from block", zap.Uint64("block_height", latestFinalizedHeight)) + fg.lastProcessedHeight = latestFinalizedHeight - 1 + + return nil + } + } +} + +// This function process blocks indefinitely, starting from the last finalized block. +func (fg *FinalityGadget) ProcessBlocks(ctx context.Context) error { + fg.logger.Info("Processing blocks...") + // Start polling for new blocks at set interval + ticker := time.NewTicker(fg.pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + // get latest block + latestBlock, err := fg.l2Client.HeaderByNumber(ctx, big.NewInt(ethrpc.LatestBlockNumber.Int64())) + if err != nil { + return fmt.Errorf("error fetching latest L2 block: %w", err) } - // if the latest finalized block is greater than the last processed block, process it - if latestFinalizedHeight > fg.lastProcessedHeight { - fg.logger.Info("Processing block", zap.Uint64("block_height", latestFinalizedHeight)) - if err := fg.handleBlock(ctx, latestFinalizedHeight); err != nil { - return fmt.Errorf("error processing block %d: %w", latestFinalizedHeight, err) + // if the last processed block is less than the latest block, process all intervening blocks + if fg.lastProcessedHeight < latestBlock.Number.Uint64() { + fg.logger.Info("Processing new blocks", zap.Uint64("start_height", fg.lastProcessedHeight+1), zap.Uint64("end_height", latestBlock.Number.Uint64())) + if err := fg.processBlocksTillHeight(ctx, latestBlock.Number.Uint64()); err != nil { + return fmt.Errorf("error processing block %d: %w", latestBlock.Number.Uint64(), err) } } } } } -func (fg *FinalityGadget) InsertBlock(block *types.Block) error { +func (fg *FinalityGadget) insertBlocks(blocks []*types.Block) error { // Lock mutex fg.mutex.Lock() - // Store block in DB - err := fg.db.InsertBlock(&types.Block{ - BlockHeight: block.BlockHeight, - BlockHash: normalizeBlockHash(block.BlockHash), - BlockTimestamp: block.BlockTimestamp, - }) - if err != nil { - return err + defer fg.mutex.Unlock() + + // Normalize block hashes + normalizedBlocks := make([]*types.Block, len(blocks)) + for i, block := range blocks { + normalizedBlocks[i] = &types.Block{ + BlockHeight: block.BlockHeight, + BlockHash: normalizeBlockHash(block.BlockHash), + BlockTimestamp: block.BlockTimestamp, + } } - // Unlock mutex - fg.mutex.Unlock() + // Store blocks in DB + if err := fg.db.InsertBlocks(normalizedBlocks); err != nil { + return fmt.Errorf("failed to batch insert blocks: %w", err) + } return nil } @@ -499,43 +576,103 @@ func (fg *FinalityGadget) queryBlockByHeight(blockNumber int64) (*types.Block, e }, nil } -func (fg *FinalityGadget) handleBlock(ctx context.Context, latestFinalizedHeight uint64) error { - for height := fg.lastProcessedHeight + 1; height <= latestFinalizedHeight; height++ { +// Process blocks in batches of size `fg.batchSize` until the latest height +func (fg *FinalityGadget) processBlocksTillHeight(ctx context.Context, latestHeight uint64) error { + for batchStartHeight := fg.lastProcessedHeight + 1; batchStartHeight <= latestHeight; { select { case <-ctx.Done(): return nil default: - if height > math.MaxInt64 { - return fmt.Errorf("block height %d exceeds maximum int64 value", height) + // Calculate batch start and end heights + batchEndHeight := batchStartHeight + fg.batchSize - 1 + if batchEndHeight > latestHeight { + batchEndHeight = latestHeight } - block, err := fg.queryBlockByHeight(int64(height)) - if err != nil { - return fmt.Errorf("error getting block at height %d: %w", height, err) + fg.logger.Info("Processing batch of blocks", zap.Uint64("batch_start_height", batchStartHeight), zap.Uint64("batch_end_height", batchEndHeight)) + + // Create batch of blocks to check in parallel + results := make(chan *types.Block, batchEndHeight-batchStartHeight+1) + errors := make(chan error, batchEndHeight-batchStartHeight+1) + var wg sync.WaitGroup + + // Query batch in parallel + for height := batchStartHeight; height <= batchEndHeight; height++ { + wg.Add(1) + go func(h uint64) { + defer wg.Done() + block, err := fg.processHeight(h) + results <- block + errors <- err + }(height) } - // Check the block is babylon finalized using sdk client - isFinal, err := fg.QueryIsBlockBabylonFinalized(block) - if err != nil && !errors.Is(err, types.ErrBtcStakingNotActivated) { - return fmt.Errorf("error checking block %d: %v", block.BlockHeight, err) + // Close results channel once all goroutines complete + go func() { + wg.Wait() + close(results) + close(errors) + }() + + // Extract and handle error (if any) + for err := range errors { + if err != nil { + return err + } } - // If not finalized, throw error - if !isFinal { - return fmt.Errorf("block %d should be finalized according to client but is not", block.BlockHeight) + + // Extract blocks and find last consecutive finalized block + var finalizedBlocks []*types.Block + var lastFinalizedHeight uint64 + for block := range results { + if block == nil { + break + } + finalizedBlocks = append(finalizedBlocks, block) + lastFinalizedHeight = block.BlockHeight } - // If finalised, store the block in DB - err = fg.InsertBlock(block) - if err != nil { - return fmt.Errorf("error storing block %d: %v", block.BlockHeight, err) + // If no blocks were finalized, wait for next poll + if lastFinalizedHeight < batchStartHeight { + return nil + } + + // Batch insert all consecutive finalized blocks + if err := fg.insertBlocks(finalizedBlocks); err != nil { + return fmt.Errorf("error storing blocks: %w", err) } - fg.lastProcessedHeight = block.BlockHeight - fg.logger.Info("Inserted new finalized block", zap.Uint64("block_height", block.BlockHeight)) + fg.lastProcessedHeight = lastFinalizedHeight + + // Update start height for next batch + batchStartHeight = lastFinalizedHeight + 1 } } return nil } +func (fg *FinalityGadget) processHeight(height uint64) (*types.Block, error) { + // Fetch block from rpc + if height > math.MaxInt64 { + return nil, fmt.Errorf("block height %d exceeds maximum int64 value", height) + } + block, err := fg.queryBlockByHeight(int64(height)) + if err != nil { + return nil, fmt.Errorf("error getting block at height %d: %w", height, err) + } + + // Check finalization + isFinalized, err := fg.QueryIsBlockBabylonFinalizedFromBabylon(block) + if err != nil { + return nil, fmt.Errorf("error checking is block %d finalized from babylon: %w", height, err) + } + + if !isFinalized { + return nil, nil + } + + return block, nil +} + // Query the BTC staking activation timestamp from bbnClient // returns math.MaxUint64, ErrBtcStakingNotActivated if the BTC staking is not activated func (fg *FinalityGadget) queryBtcStakingActivationTimestamp() (uint64, error) { diff --git a/finalitygadget/finalitygadget_test.go b/finalitygadget/finalitygadget_test.go index 2c96484..3f4fd47 100644 --- a/finalitygadget/finalitygadget_test.go +++ b/finalitygadget/finalitygadget_test.go @@ -17,6 +17,8 @@ import ( "go.uber.org/zap" ) +// TODO: add `QueryIsBlockBabylonFinalizedFromBabylon` as test fn once removed from interface + func TestFinalityGadgetDisabled(t *testing.T) { ctl := gomock.NewController(t) @@ -24,14 +26,14 @@ func TestFinalityGadgetDisabled(t *testing.T) { mockCwClient := mocks.NewMockICosmWasmClient(ctl) mockCwClient.EXPECT().QueryIsEnabled().Return(false, nil).Times(1) - mockFinalityGadget := &FinalityGadget{ + mockTestFinalityGadget := &FinalityGadget{ cwClient: mockCwClient, bbnClient: nil, btcClient: nil, } // check QueryIsBlockBabylonFinalized always returns true when finality gadget is not enabled - res, err := mockFinalityGadget.QueryIsBlockBabylonFinalized(&types.Block{}) + res, err := mockTestFinalityGadget.QueryIsBlockBabylonFinalizedFromBabylon(&types.Block{}) require.NoError(t, err) require.True(t, res) } @@ -185,7 +187,7 @@ func TestQueryIsBlockBabylonFinalized(t *testing.T) { btcClient: mockBTCClient, } - res, err := mockFinalityGadget.QueryIsBlockBabylonFinalized(tc.block) + res, err := mockFinalityGadget.QueryIsBlockBabylonFinalizedFromBabylon(tc.block) require.Equal(t, tc.expectResult, res) require.Equal(t, tc.expectedErr, err) }) @@ -196,28 +198,76 @@ func TestQueryBlockRangeBabylonFinalized(t *testing.T) { rng := rand.New(rand.NewSource(time.Now().UnixNano())) l2BlockTime := uint64(2) - blockA, blockAWithHashTrimmed := testutil.RandomL2Block(rng) - blockB, blockBWithHashTrimmed := testutil.GenL2Block(rng, &blockA, l2BlockTime, 1) - blockC, blockCWithHashTrimmed := testutil.GenL2Block(rng, &blockB, l2BlockTime, 1) - blockD, blockDWithHashTrimmed := testutil.GenL2Block(rng, &blockC, l2BlockTime, 300) // 10 minutes later - blockE, blockEWithHashTrimmed := testutil.GenL2Block(rng, &blockD, l2BlockTime, 1) - blockF, blockFWithHashTrimmed := testutil.GenL2Block(rng, &blockE, l2BlockTime, 300) - blockG, blockGWithHashTrimmed := testutil.GenL2Block(rng, &blockF, l2BlockTime, 1) + blockA, _ := testutil.RandomL2Block(rng) + blockB, _ := testutil.GenL2Block(rng, &blockA, l2BlockTime, 1) + blockC, _ := testutil.GenL2Block(rng, &blockB, l2BlockTime, 1) + blockD, _ := testutil.GenL2Block(rng, &blockC, l2BlockTime, 300) + blockE, _ := testutil.GenL2Block(rng, &blockD, l2BlockTime, 1) testCases := []struct { - name string - expectedErr error - expectResult *uint64 - queryBlocks []*types.Block + queryBlocks []*types.Block + expRes *uint64 + expErr error + expDbErr error + name string + expDbRes []bool + queryDb bool }{ - {"empty query blocks", fmt.Errorf("no blocks provided"), nil, []*types.Block{}}, - {"single block with finalized", nil, &blockA.BlockHeight, []*types.Block{&blockA}}, - {"single block with error", fmt.Errorf("RPC rate limit error"), nil, []*types.Block{&blockD}}, - {"non-consecutive blocks", fmt.Errorf("blocks are not consecutive"), nil, []*types.Block{&blockA, &blockD}}, - {"the first two blocks are finalized and the last block has error", fmt.Errorf("RPC rate limit error"), &blockB.BlockHeight, []*types.Block{&blockA, &blockB, &blockC}}, - {"all consecutive blocks are finalized", nil, &blockB.BlockHeight, []*types.Block{&blockA, &blockB}}, - {"none of the block is finalized and the first block has error", fmt.Errorf("RPC rate limit error"), nil, []*types.Block{&blockD, &blockE}}, - {"none of the block is finalized and the second block has error", nil, nil, []*types.Block{&blockF, &blockG}}, + { + name: "empty query blocks", + queryBlocks: []*types.Block{}, + queryDb: false, + expErr: fmt.Errorf("no blocks provided"), + expRes: nil, + }, + { + name: "single block with finalized", + queryBlocks: []*types.Block{&blockA}, + queryDb: true, + expErr: nil, + expDbRes: []bool{true}, + expRes: &blockA.BlockHeight, + }, + { + name: "single block with error", + queryBlocks: []*types.Block{&blockD}, + queryDb: true, + expErr: fmt.Errorf("database error"), + expDbErr: fmt.Errorf("database error"), + expRes: nil, + }, + { + name: "non-consecutive blocks", + queryBlocks: []*types.Block{&blockA, &blockD}, + queryDb: false, + expErr: fmt.Errorf("blocks are not consecutive"), + expRes: nil, + }, + { + name: "all consecutive blocks are finalized", + queryBlocks: []*types.Block{&blockA, &blockB}, + queryDb: true, + expErr: nil, + expDbRes: []bool{true, true}, + expRes: &blockB.BlockHeight, + }, + { + name: "first two blocks finalized, third has error", + queryBlocks: []*types.Block{&blockA, &blockB, &blockC}, + queryDb: true, + expErr: fmt.Errorf("database error"), + expDbErr: fmt.Errorf("database error"), + expDbRes: []bool{true, true}, + expRes: nil, + }, + { + name: "none of the blocks are finalized", + queryBlocks: []*types.Block{&blockD, &blockE}, + queryDb: true, + expErr: nil, + expDbRes: []bool{false}, + expRes: nil, + }, } for _, tc := range testCases { @@ -225,40 +275,42 @@ func TestQueryBlockRangeBabylonFinalized(t *testing.T) { ctl := gomock.NewController(t) defer ctl.Finish() - mockCwClient := mocks.NewMockICosmWasmClient(ctl) - mockBTCClient := mocks.NewMockIBitcoinClient(ctl) - mockBBNClient := mocks.NewMockIBabylonClient(ctl) - mockFinalityGadget := &FinalityGadget{ - cwClient: mockCwClient, - bbnClient: mockBBNClient, - btcClient: mockBTCClient, + mockDbHandler := mocks.NewMockIDatabaseHandler(ctl) + + // Setup mock DB responses + if len(tc.queryBlocks) > 0 && tc.queryDb { + if tc.expDbErr != nil { + mockDbHandler.EXPECT(). + QueryIsBlockRangeFinalizedByHeight( + tc.queryBlocks[0].BlockHeight, + tc.queryBlocks[len(tc.queryBlocks)-1].BlockHeight, + ). + Return(nil, tc.expDbErr). + Times(1) + } else { + mockDbHandler.EXPECT(). + QueryIsBlockRangeFinalizedByHeight( + tc.queryBlocks[0].BlockHeight, + tc.queryBlocks[len(tc.queryBlocks)-1].BlockHeight, + ). + Return(tc.expDbRes, nil). + Times(1) + } } - mockCwClient.EXPECT().QueryIsEnabled().Return(true, nil).AnyTimes() - mockCwClient.EXPECT().QueryConsumerId().Return("consumer-chain-id", nil).AnyTimes() - mockCwClient.EXPECT().QueryListOfVotedFinalityProviders(&blockAWithHashTrimmed).Return([]string{"pk1", "pk2", "pk3"}, nil).AnyTimes() - mockCwClient.EXPECT().QueryListOfVotedFinalityProviders(&blockBWithHashTrimmed).Return([]string{"pk1", "pk2", "pk3"}, nil).AnyTimes() - mockCwClient.EXPECT().QueryListOfVotedFinalityProviders(&blockCWithHashTrimmed).Return([]string{"pk1", "pk2", "pk3"}, nil).AnyTimes() - mockCwClient.EXPECT().QueryListOfVotedFinalityProviders(&blockDWithHashTrimmed).Return([]string{"pk3"}, nil).AnyTimes() - mockCwClient.EXPECT().QueryListOfVotedFinalityProviders(&blockEWithHashTrimmed).Return([]string{"pk1"}, nil).AnyTimes() - mockCwClient.EXPECT().QueryListOfVotedFinalityProviders(&blockFWithHashTrimmed).Return([]string{"pk2"}, nil).AnyTimes() - mockCwClient.EXPECT().QueryListOfVotedFinalityProviders(&blockGWithHashTrimmed).Return([]string{"pk3"}, nil).AnyTimes() - - mockBTCClient.EXPECT().GetBlockHeightByTimestamp(blockA.BlockTimestamp).Return(uint64(111), nil).AnyTimes() - mockBTCClient.EXPECT().GetBlockHeightByTimestamp(blockB.BlockTimestamp).Return(uint64(111), nil).AnyTimes() - mockBTCClient.EXPECT().GetBlockHeightByTimestamp(blockC.BlockTimestamp).Return(uint64(111), fmt.Errorf("RPC rate limit error")).AnyTimes() - mockBTCClient.EXPECT().GetBlockHeightByTimestamp(blockD.BlockTimestamp).Return(uint64(112), fmt.Errorf("RPC rate limit error")).AnyTimes() - mockBTCClient.EXPECT().GetBlockHeightByTimestamp(blockE.BlockTimestamp).Return(uint64(112), nil).AnyTimes() - mockBTCClient.EXPECT().GetBlockHeightByTimestamp(blockF.BlockTimestamp).Return(uint64(113), nil).AnyTimes() - mockBTCClient.EXPECT().GetBlockHeightByTimestamp(blockG.BlockTimestamp).Return(uint64(113), fmt.Errorf("RPC rate limit error")).AnyTimes() - - mockBBNClient.EXPECT().QueryEarliestActiveDelBtcHeight(gomock.Any()).Return(uint64(1), nil).AnyTimes() - mockBBNClient.EXPECT().QueryAllFpBtcPubKeys("consumer-chain-id").Return([]string{"pk1", "pk2", "pk3"}, nil).AnyTimes() - mockBBNClient.EXPECT().QueryMultiFpPower([]string{"pk1", "pk2", "pk3"}, gomock.Any()).Return(map[string]uint64{"pk1": 100, "pk2": 200, "pk3": 300}, nil).AnyTimes() + mockFinalityGadget := &FinalityGadget{ + db: mockDbHandler, + } res, err := mockFinalityGadget.QueryBlockRangeBabylonFinalized(tc.queryBlocks) - require.Equal(t, tc.expectResult, res) - require.Equal(t, tc.expectedErr, err) + fmt.Println("res", res, "err", err) + require.Equal(t, tc.expRes, res) + if tc.expErr != nil { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expErr.Error()) + } else { + require.NoError(t, err) + } }) } } @@ -274,7 +326,8 @@ func TestInsertBlock(t *testing.T) { ctl := gomock.NewController(t) mockDbHandler := mocks.NewMockIDatabaseHandler(ctl) // note: the block hash is normalized before passing to the db handler - mockDbHandler.EXPECT().InsertBlock(normalizedBlock(block)).Return(nil).Times(1) + blocks := []*types.Block{normalizedBlock(block)} + mockDbHandler.EXPECT().InsertBlocks(blocks).Return(nil).Times(1) mockDbHandler.EXPECT().GetBlockByHeight(block.BlockHeight).Return(block, nil).Times(1) mockFinalityGadget := &FinalityGadget{ @@ -282,7 +335,7 @@ func TestInsertBlock(t *testing.T) { } // insert block - err := mockFinalityGadget.InsertBlock(block) + err := mockFinalityGadget.insertBlocks(blocks) require.NoError(t, err) // verify block was inserted @@ -293,6 +346,115 @@ func TestInsertBlock(t *testing.T) { require.Equal(t, block.BlockTimestamp, retrievedBlock.BlockTimestamp) } +func TestBatchInsertBlocks(t *testing.T) { + // Setup mock controller + ctl := gomock.NewController(t) + defer ctl.Finish() + + // Create a larger set of test blocks + numBlocks := 25 // Testing with 25 blocks + blocks := make([]*types.Block, numBlocks) + for i := 0; i < numBlocks; i++ { + blocks[i] = &types.Block{ + BlockHeight: uint64(i + 1), + BlockHash: fmt.Sprintf("0x%x", i+1000), // unique hash for each block + BlockTimestamp: uint64(1000 + i*100), // increasing timestamps + } + } + + // Create normalized versions of the blocks + normalizedBlocks := make([]*types.Block, len(blocks)) + for i, block := range blocks { + normalizedBlocks[i] = &types.Block{ + BlockHeight: block.BlockHeight, + BlockHash: normalizeBlockHash(block.BlockHash), + BlockTimestamp: block.BlockTimestamp, + } + } + + testCases := []struct { + name string + blocks []*types.Block + batchSize uint64 + }{ + { + name: "small batch size", + batchSize: 5, + blocks: blocks, + }, + { + name: "medium batch size", + batchSize: 10, + blocks: blocks, + }, + { + name: "large batch size", + batchSize: 20, + blocks: blocks, + }, + { + name: "batch size larger than number of blocks", + batchSize: 30, + blocks: blocks, + }, + { + name: "single block batch", + batchSize: 1, + blocks: blocks[:1], // Test with just one block + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Setup mock database handler + mockDbHandler := mocks.NewMockIDatabaseHandler(ctl) + + // Create normalized versions specific to this test case + tcNormalizedBlocks := make([]*types.Block, len(tc.blocks)) + for i, block := range tc.blocks { + tcNormalizedBlocks[i] = &types.Block{ + BlockHeight: block.BlockHeight, + BlockHash: normalizeBlockHash(block.BlockHash), + BlockTimestamp: block.BlockTimestamp, + } + } + + // Expect batch insert call with normalized blocks + if len(tc.blocks) > 0 { + mockDbHandler.EXPECT().InsertBlocks(tcNormalizedBlocks).Return(nil).Times(1) + } + + // Setup verification calls for each block + for i, block := range tc.blocks { + mockDbHandler.EXPECT(). + GetBlockByHeight(block.BlockHeight). + Return(tcNormalizedBlocks[i], nil). + Times(1) + } + + // Create finality gadget instance with mock DB and specified batch size + mockFinalityGadget := &FinalityGadget{ + db: mockDbHandler, + batchSize: tc.batchSize, + } + + // Test batch insert + err := mockFinalityGadget.insertBlocks(tc.blocks) + require.NoError(t, err) + + // Verify each block was inserted correctly + for _, block := range tc.blocks { + retrievedBlock, err := mockFinalityGadget.GetBlockByHeight(block.BlockHeight) + require.NoError(t, err) + require.NotNil(t, retrievedBlock) + require.Equal(t, block.BlockHeight, retrievedBlock.BlockHeight) + require.Equal(t, normalizeBlockHash(block.BlockHash), retrievedBlock.BlockHash) + require.Equal(t, block.BlockTimestamp, retrievedBlock.BlockTimestamp) + } + }) + } +} + func TestGetBlockByHeight(t *testing.T) { block := &types.Block{ BlockHeight: 1, @@ -304,7 +466,8 @@ func TestGetBlockByHeight(t *testing.T) { ctl := gomock.NewController(t) mockDbHandler := mocks.NewMockIDatabaseHandler(ctl) // note: the block hash is normalized before passing to the db handler - mockDbHandler.EXPECT().InsertBlock(normalizedBlock(block)).Return(nil).Times(1) + blocks := []*types.Block{normalizedBlock(block)} + mockDbHandler.EXPECT().InsertBlocks(blocks).Return(nil).Times(1) mockDbHandler.EXPECT().GetBlockByHeight(block.BlockHeight).Return(block, nil).Times(1) mockFinalityGadget := &FinalityGadget{ @@ -312,7 +475,7 @@ func TestGetBlockByHeight(t *testing.T) { } // insert block - err := mockFinalityGadget.InsertBlock(block) + err := mockFinalityGadget.insertBlocks(blocks) require.NoError(t, err) // fetch block by height @@ -350,7 +513,8 @@ func TestGetBlockByHashWith0xPrefix(t *testing.T) { ctl := gomock.NewController(t) mockDbHandler := mocks.NewMockIDatabaseHandler(ctl) // note: the block hash is normalized before passing to the db handler - mockDbHandler.EXPECT().InsertBlock(normalizedBlock(block)).Return(nil).Times(1) + blocks := []*types.Block{normalizedBlock(block)} + mockDbHandler.EXPECT().InsertBlocks(blocks).Return(nil).Times(1) mockDbHandler.EXPECT().GetBlockByHash(normalizeBlockHash(block.BlockHash)).Return(block, nil).Times(2) mockFinalityGadget := &FinalityGadget{ @@ -358,7 +522,7 @@ func TestGetBlockByHashWith0xPrefix(t *testing.T) { } // insert block - err := mockFinalityGadget.InsertBlock(block) + err := mockFinalityGadget.insertBlocks(blocks) require.NoError(t, err) // fetch block by hash including 0x prefix @@ -387,7 +551,8 @@ func TestGetBlockByHashWithout0xPrefix(t *testing.T) { ctl := gomock.NewController(t) mockDbHandler := mocks.NewMockIDatabaseHandler(ctl) // note: the block hash is normalized before passing to the db handler - mockDbHandler.EXPECT().InsertBlock(normalizedBlock(block)).Return(nil).Times(1) + blocks := []*types.Block{normalizedBlock(block)} + mockDbHandler.EXPECT().InsertBlocks(blocks).Return(nil).Times(1) mockDbHandler.EXPECT().GetBlockByHash(normalizeBlockHash(block.BlockHash)).Return(block, nil).Times(2) mockFinalityGadget := &FinalityGadget{ @@ -395,7 +560,7 @@ func TestGetBlockByHashWithout0xPrefix(t *testing.T) { } // insert block - err := mockFinalityGadget.InsertBlock(block) + err := mockFinalityGadget.insertBlocks(blocks) require.NoError(t, err) // fetch block by hash including 0x prefix @@ -539,8 +704,8 @@ func TestQueryLatestFinalizedBlock(t *testing.T) { // mock db and finality gadget ctl := gomock.NewController(t) mockDbHandler := mocks.NewMockIDatabaseHandler(ctl) - mockDbHandler.EXPECT().InsertBlock(normalizedFirst).Return(nil).Times(1) - mockDbHandler.EXPECT().InsertBlock(normalizedSecond).Return(nil).Times(1) + blocks := []*types.Block{normalizedFirst, normalizedSecond} + mockDbHandler.EXPECT().InsertBlocks(blocks).Return(nil).Times(1) mockDbHandler.EXPECT().QueryLatestFinalizedBlock().Return(normalizedSecond, nil).Times(1) mockFinalityGadget := &FinalityGadget{ @@ -548,9 +713,7 @@ func TestQueryLatestFinalizedBlock(t *testing.T) { } // insert two blocks - err := mockFinalityGadget.InsertBlock(first) - require.NoError(t, err) - err = mockFinalityGadget.InsertBlock(second) + err := mockFinalityGadget.insertBlocks(blocks) require.NoError(t, err) // fetch latest block diff --git a/finalitygadget/interface.go b/finalitygadget/interface.go index 85f0a48..1b2be50 100644 --- a/finalitygadget/interface.go +++ b/finalitygadget/interface.go @@ -3,7 +3,8 @@ package finalitygadget import "github.com/babylonlabs-io/finality-gadget/types" type IFinalityGadget interface { - /* QueryIsBlockBabylonFinalized checks if the given L2 block is finalized by the Babylon finality gadget + // TODO: make this method internal once fully tested. External services should query the database instead. + /* QueryIsBlockBabylonFinalizedFromBabylon checks if the given L2 block is finalized by the Babylon finality gadget * * - if the finality gadget is not enabled, always return true * - else, check if the given L2 block is finalized @@ -19,6 +20,9 @@ type IFinalityGadget interface { * - calculate voted voting power * - check if the voted voting power is more than 2/3 of the total voting power */ + QueryIsBlockBabylonFinalizedFromBabylon(block *types.Block) (bool, error) + + // QueryIsBlockBabylonFinalized queries the finality status of a given block height from the internal db QueryIsBlockBabylonFinalized(block *types.Block) (bool, error) /* QueryBlockRangeBabylonFinalized searches for a row of consecutive finalized blocks in the block range, and returns @@ -55,9 +59,6 @@ type IFinalityGadget interface { */ QueryBtcStakingActivatedTimestamp() (uint64, error) - // InsertBlock inserts a btc finalized block into the local db - InsertBlock(block *types.Block) error - // GetBlockByHeight returns the btc finalized block at given height by querying the local db GetBlockByHeight(height uint64) (*types.Block, error) diff --git a/go.mod b/go.mod index 2fd3719..50cdffc 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/ethereum/go-ethereum v1.13.15 github.com/jsternberg/zap-logfmt v1.3.0 github.com/lightningnetwork/lnd v0.16.4-beta.rc1 - github.com/rs/cors v1.11.0 + github.com/rs/cors v1.8.3 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 558b8a1..e910f10 100644 --- a/go.sum +++ b/go.sum @@ -1044,8 +1044,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= +github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= diff --git a/server/rpcserver.go b/server/rpcserver.go index 0181c40..cecee5b 100644 --- a/server/rpcserver.go +++ b/server/rpcserver.go @@ -35,7 +35,7 @@ func (r *rpcServer) RegisterWithGrpcServer(grpcServer *grpc.Server) error { return nil } -// QueryIsBlockBabylonFinalized is an RPC method that returns the finality status of a block by querying Babylon chain. +// QueryIsBlockBabylonFinalized is an RPC method that returns the finality status of a block by querying the internal db. func (r *rpcServer) QueryIsBlockBabylonFinalized(ctx context.Context, req *proto.QueryIsBlockBabylonFinalizedRequest) (*proto.QueryIsBlockFinalizedResponse, error) { isFinalized, err := r.fg.QueryIsBlockBabylonFinalized(&types.Block{ BlockHash: req.Block.BlockHash, @@ -49,6 +49,20 @@ func (r *rpcServer) QueryIsBlockBabylonFinalized(ctx context.Context, req *proto return &proto.QueryIsBlockFinalizedResponse{IsFinalized: isFinalized}, nil } +// QueryIsBlockBabylonFinalizedFromBabylon is an RPC method that returns the finality status of a block by querying Babylon chain. +func (r *rpcServer) QueryIsBlockBabylonFinalizedFromBabylon(ctx context.Context, req *proto.QueryIsBlockBabylonFinalizedRequest) (*proto.QueryIsBlockFinalizedResponse, error) { + isFinalized, err := r.fg.QueryIsBlockBabylonFinalizedFromBabylon(&types.Block{ + BlockHash: req.Block.BlockHash, + BlockHeight: req.Block.BlockHeight, + BlockTimestamp: req.Block.BlockTimestamp, + }) + if err != nil { + return nil, err + } + + return &proto.QueryIsBlockFinalizedResponse{IsFinalized: isFinalized}, nil +} + // QueryBlockRangeBabylonFinalized is an RPC method that returns the latest Babylon finalized block in a range by querying Babylon chain. func (r *rpcServer) QueryBlockRangeBabylonFinalized(ctx context.Context, req *proto.QueryBlockRangeBabylonFinalizedRequest) (*proto.QueryBlockRangeBabylonFinalizedResponse, error) { blocks := make([]*types.Block, 0, len(req.Blocks)) diff --git a/testutil/mocks/db_mock.go b/testutil/mocks/db_mock.go index ced1ab2..0049f9e 100644 --- a/testutil/mocks/db_mock.go +++ b/testutil/mocks/db_mock.go @@ -20,6 +20,7 @@ import ( type MockIDatabaseHandler struct { ctrl *gomock.Controller recorder *MockIDatabaseHandlerMockRecorder + isgomock struct{} } // MockIDatabaseHandlerMockRecorder is the mock recorder for MockIDatabaseHandler. @@ -112,18 +113,18 @@ func (mr *MockIDatabaseHandlerMockRecorder) GetBlockByHeight(height any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHeight", reflect.TypeOf((*MockIDatabaseHandler)(nil).GetBlockByHeight), height) } -// InsertBlock mocks base method. -func (m *MockIDatabaseHandler) InsertBlock(block *types.Block) error { +// InsertBlocks mocks base method. +func (m *MockIDatabaseHandler) InsertBlocks(block []*types.Block) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertBlock", block) + ret := m.ctrl.Call(m, "InsertBlocks", block) ret0, _ := ret[0].(error) return ret0 } -// InsertBlock indicates an expected call of InsertBlock. -func (mr *MockIDatabaseHandlerMockRecorder) InsertBlock(block any) *gomock.Call { +// InsertBlocks indicates an expected call of InsertBlocks. +func (mr *MockIDatabaseHandlerMockRecorder) InsertBlocks(block any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlock", reflect.TypeOf((*MockIDatabaseHandler)(nil).InsertBlock), block) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertBlocks", reflect.TypeOf((*MockIDatabaseHandler)(nil).InsertBlocks), block) } // QueryEarliestFinalizedBlock mocks base method. @@ -171,6 +172,21 @@ func (mr *MockIDatabaseHandlerMockRecorder) QueryIsBlockFinalizedByHeight(height return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryIsBlockFinalizedByHeight", reflect.TypeOf((*MockIDatabaseHandler)(nil).QueryIsBlockFinalizedByHeight), height) } +// QueryIsBlockRangeFinalizedByHeight mocks base method. +func (m *MockIDatabaseHandler) QueryIsBlockRangeFinalizedByHeight(startHeight, endHeight uint64) ([]bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryIsBlockRangeFinalizedByHeight", startHeight, endHeight) + ret0, _ := ret[0].([]bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryIsBlockRangeFinalizedByHeight indicates an expected call of QueryIsBlockRangeFinalizedByHeight. +func (mr *MockIDatabaseHandlerMockRecorder) QueryIsBlockRangeFinalizedByHeight(startHeight, endHeight any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryIsBlockRangeFinalizedByHeight", reflect.TypeOf((*MockIDatabaseHandler)(nil).QueryIsBlockRangeFinalizedByHeight), startHeight, endHeight) +} + // QueryLatestFinalizedBlock mocks base method. func (m *MockIDatabaseHandler) QueryLatestFinalizedBlock() (*types.Block, error) { m.ctrl.T.Helper() diff --git a/testutil/mocks/expected_clients_mock.go b/testutil/mocks/expected_clients_mock.go index a561e2f..9a77e33 100644 --- a/testutil/mocks/expected_clients_mock.go +++ b/testutil/mocks/expected_clients_mock.go @@ -25,6 +25,7 @@ import ( type MockIBitcoinClient struct { ctrl *gomock.Controller recorder *MockIBitcoinClientMockRecorder + isgomock struct{} } // MockIBitcoinClientMockRecorder is the mock recorder for MockIBitcoinClient. @@ -123,6 +124,7 @@ func (mr *MockIBitcoinClientMockRecorder) GetBlockTimestampByHeight(height any) type MockIBabylonClient struct { ctrl *gomock.Controller recorder *MockIBabylonClientMockRecorder + isgomock struct{} } // MockIBabylonClientMockRecorder is the mock recorder for MockIBabylonClient. @@ -206,6 +208,7 @@ func (mr *MockIBabylonClientMockRecorder) QueryMultiFpPower(fpPubkeyHexList, btc type MockICosmWasmClient struct { ctrl *gomock.Controller recorder *MockICosmWasmClientMockRecorder + isgomock struct{} } // MockICosmWasmClientMockRecorder is the mock recorder for MockICosmWasmClient. @@ -274,6 +277,7 @@ func (mr *MockICosmWasmClientMockRecorder) QueryListOfVotedFinalityProviders(que type MockIEthL2Client struct { ctrl *gomock.Controller recorder *MockIEthL2ClientMockRecorder + isgomock struct{} } // MockIEthL2ClientMockRecorder is the mock recorder for MockIEthL2Client. diff --git a/testutil/mocks/finalitygadget_mock.go b/testutil/mocks/finalitygadget_mock.go new file mode 100644 index 0000000..2ac15db --- /dev/null +++ b/testutil/mocks/finalitygadget_mock.go @@ -0,0 +1,206 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: finalitygadget/interface.go +// +// Generated by this command: +// +// mockgen -source=finalitygadget/interface.go -package mocks -destination ./testutil/mocks/finalitygadget_mock.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + types "github.com/babylonlabs-io/finality-gadget/types" + gomock "go.uber.org/mock/gomock" +) + +// MockIFinalityGadget is a mock of IFinalityGadget interface. +type MockIFinalityGadget struct { + ctrl *gomock.Controller + recorder *MockIFinalityGadgetMockRecorder + isgomock struct{} +} + +// MockIFinalityGadgetMockRecorder is the mock recorder for MockIFinalityGadget. +type MockIFinalityGadgetMockRecorder struct { + mock *MockIFinalityGadget +} + +// NewMockIFinalityGadget creates a new mock instance. +func NewMockIFinalityGadget(ctrl *gomock.Controller) *MockIFinalityGadget { + mock := &MockIFinalityGadget{ctrl: ctrl} + mock.recorder = &MockIFinalityGadgetMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIFinalityGadget) EXPECT() *MockIFinalityGadgetMockRecorder { + return m.recorder +} + +// GetBlockByHash mocks base method. +func (m *MockIFinalityGadget) GetBlockByHash(hash string) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlockByHash", hash) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockByHash indicates an expected call of GetBlockByHash. +func (mr *MockIFinalityGadgetMockRecorder) GetBlockByHash(hash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHash", reflect.TypeOf((*MockIFinalityGadget)(nil).GetBlockByHash), hash) +} + +// GetBlockByHeight mocks base method. +func (m *MockIFinalityGadget) GetBlockByHeight(height uint64) (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBlockByHeight", height) + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockByHeight indicates an expected call of GetBlockByHeight. +func (mr *MockIFinalityGadgetMockRecorder) GetBlockByHeight(height any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHeight", reflect.TypeOf((*MockIFinalityGadget)(nil).GetBlockByHeight), height) +} + +// QueryBlockRangeBabylonFinalized mocks base method. +func (m *MockIFinalityGadget) QueryBlockRangeBabylonFinalized(queryBlocks []*types.Block) (*uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryBlockRangeBabylonFinalized", queryBlocks) + ret0, _ := ret[0].(*uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryBlockRangeBabylonFinalized indicates an expected call of QueryBlockRangeBabylonFinalized. +func (mr *MockIFinalityGadgetMockRecorder) QueryBlockRangeBabylonFinalized(queryBlocks any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryBlockRangeBabylonFinalized", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryBlockRangeBabylonFinalized), queryBlocks) +} + +// QueryBtcStakingActivatedTimestamp mocks base method. +func (m *MockIFinalityGadget) QueryBtcStakingActivatedTimestamp() (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryBtcStakingActivatedTimestamp") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryBtcStakingActivatedTimestamp indicates an expected call of QueryBtcStakingActivatedTimestamp. +func (mr *MockIFinalityGadgetMockRecorder) QueryBtcStakingActivatedTimestamp() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryBtcStakingActivatedTimestamp", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryBtcStakingActivatedTimestamp)) +} + +// QueryChainSyncStatus mocks base method. +func (m *MockIFinalityGadget) QueryChainSyncStatus() (*types.ChainSyncStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryChainSyncStatus") + ret0, _ := ret[0].(*types.ChainSyncStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryChainSyncStatus indicates an expected call of QueryChainSyncStatus. +func (mr *MockIFinalityGadgetMockRecorder) QueryChainSyncStatus() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryChainSyncStatus", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryChainSyncStatus)) +} + +// QueryIsBlockBabylonFinalized mocks base method. +func (m *MockIFinalityGadget) QueryIsBlockBabylonFinalized(block *types.Block) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryIsBlockBabylonFinalized", block) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryIsBlockBabylonFinalized indicates an expected call of QueryIsBlockBabylonFinalized. +func (mr *MockIFinalityGadgetMockRecorder) QueryIsBlockBabylonFinalized(block any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryIsBlockBabylonFinalized", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryIsBlockBabylonFinalized), block) +} + +// QueryIsBlockBabylonFinalizedFromBabylon mocks base method. +func (m *MockIFinalityGadget) QueryIsBlockBabylonFinalizedFromBabylon(block *types.Block) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryIsBlockBabylonFinalizedFromBabylon", block) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryIsBlockBabylonFinalizedFromBabylon indicates an expected call of QueryIsBlockBabylonFinalizedFromBabylon. +func (mr *MockIFinalityGadgetMockRecorder) QueryIsBlockBabylonFinalizedFromBabylon(block any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryIsBlockBabylonFinalizedFromBabylon", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryIsBlockBabylonFinalizedFromBabylon), block) +} + +// QueryIsBlockFinalizedByHash mocks base method. +func (m *MockIFinalityGadget) QueryIsBlockFinalizedByHash(hash string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryIsBlockFinalizedByHash", hash) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryIsBlockFinalizedByHash indicates an expected call of QueryIsBlockFinalizedByHash. +func (mr *MockIFinalityGadgetMockRecorder) QueryIsBlockFinalizedByHash(hash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryIsBlockFinalizedByHash", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryIsBlockFinalizedByHash), hash) +} + +// QueryIsBlockFinalizedByHeight mocks base method. +func (m *MockIFinalityGadget) QueryIsBlockFinalizedByHeight(height uint64) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryIsBlockFinalizedByHeight", height) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryIsBlockFinalizedByHeight indicates an expected call of QueryIsBlockFinalizedByHeight. +func (mr *MockIFinalityGadgetMockRecorder) QueryIsBlockFinalizedByHeight(height any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryIsBlockFinalizedByHeight", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryIsBlockFinalizedByHeight), height) +} + +// QueryLatestFinalizedBlock mocks base method. +func (m *MockIFinalityGadget) QueryLatestFinalizedBlock() (*types.Block, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryLatestFinalizedBlock") + ret0, _ := ret[0].(*types.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryLatestFinalizedBlock indicates an expected call of QueryLatestFinalizedBlock. +func (mr *MockIFinalityGadgetMockRecorder) QueryLatestFinalizedBlock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryLatestFinalizedBlock", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryLatestFinalizedBlock)) +} + +// QueryTransactionStatus mocks base method. +func (m *MockIFinalityGadget) QueryTransactionStatus(txHash string) (*types.TransactionInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryTransactionStatus", txHash) + ret0, _ := ret[0].(*types.TransactionInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryTransactionStatus indicates an expected call of QueryTransactionStatus. +func (mr *MockIFinalityGadgetMockRecorder) QueryTransactionStatus(txHash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTransactionStatus", reflect.TypeOf((*MockIFinalityGadget)(nil).QueryTransactionStatus), txHash) +} diff --git a/types/errors.go b/types/errors.go index f7a9197..4ea7c43 100644 --- a/types/errors.go +++ b/types/errors.go @@ -4,6 +4,7 @@ import "errors" var ( ErrBlockNotFound = errors.New("block not found") + ErrInvalidBlockRange = errors.New("invalid block range") ErrNoFpHasVotingPower = errors.New("no FP has voting power for the consumer chain") ErrBtcStakingNotActivated = errors.New("BTC staking is not activated for the consumer chain") ErrActivatedTimestampNotFound = errors.New("BTC staking activated timestamp not found")