Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix: circular deps finalized head #35

Merged
merged 44 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ffcea68
fix: circular dep finalized head
parketh Oct 31, 2024
3c177a4
feat: implement batch insert
parketh Oct 31, 2024
4a40c4d
fix: query internal db in QueryBlockRangeBabylonFinalized
parketh Oct 31, 2024
1b23efa
test: add batch insert test case
parketh Oct 31, 2024
353ecc6
feat: improve inline docs and error handling
parketh Nov 1, 2024
e60a22e
feat: db block range query
parketh Nov 1, 2024
0215d28
feat: validate configs
parketh Nov 2, 2024
a8905ef
feat: add back QueryIsBlockBabylonFinalized api
parketh Nov 2, 2024
7b09dc3
feat: add back QueryIsBlockBabylonFinalized api
parketh Nov 2, 2024
3110e9a
debug: log close db
parketh Nov 2, 2024
88b5825
fix: add back FG activation checks
parketh Nov 2, 2024
d3c04fb
debug: concurrent db read
parketh Nov 2, 2024
4e79917
feat: prevent concurrent db r/w
parketh Nov 2, 2024
d294ff8
Revert "feat: prevent concurrent db r/w"
parketh Nov 2, 2024
94662b6
Revert "debug: concurrent db read"
parketh Nov 2, 2024
93ff4e0
fix: downgrade cors
parketh Nov 2, 2024
f361f09
debug: disable http server
parketh Nov 2, 2024
e840f8a
debug: reenable http server
parketh Nov 2, 2024
62752d5
fix: remove duplicate db close
parketh Nov 2, 2024
c32bf01
Revert "fix: remove duplicate db close"
parketh Nov 2, 2024
829f2ee
debug: disable api routes
parketh Nov 2, 2024
c21cc6d
Revert "debug: disable api routes"
parketh Nov 2, 2024
b19c286
debug: getactivatedtimestamp
parketh Nov 2, 2024
eeffbec
debug: revert early return
parketh Nov 2, 2024
e4e4059
debug: revert error msgs
parketh Nov 2, 2024
95b3da8
feat: refactor processHeight
parketh Nov 3, 2024
da09e72
docs: update inline
parketh Nov 3, 2024
3239860
fix: activation height check
parketh Nov 3, 2024
8ef172b
fix: opt btc rpc params
parketh Nov 3, 2024
a164918
add todos: config validation
parketh Nov 3, 2024
0430d35
nit: rename batch heights
parketh Nov 3, 2024
9a820aa
chore: remove circle ci
parketh Nov 3, 2024
c53d102
fix: grpc method
parketh Nov 3, 2024
7247af0
feat: simplify error handling
parketh Nov 3, 2024
ab1ddac
nit: move up range check
parketh Nov 3, 2024
a9f148c
chore: publish docker ci for test branch
parketh Nov 3, 2024
ae8d80f
fix: lint errors
parketh Nov 3, 2024
d29abad
fix: gomock version
parketh Nov 3, 2024
6a4c287
fix: gosec errors
parketh Nov 3, 2024
0a26e66
fix: lint errors
parketh Nov 3, 2024
b58f7db
add back error logs
parketh Nov 3, 2024
a467d81
nit: return early for non-consecutive blocks
parketh Nov 3, 2024
3d85e2f
remove `TestFinalityGadget`
bap2pecs Nov 4, 2024
0409cd8
fix: close error channel
parketh Nov 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions .circleci/config.yml

This file was deleted.

2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
branches:
- 'chore/ci-publish-docker'
- 'main'
# TODO: remove temp branch
- 'fix/circular-deps-finalized-head'
tags:
- '*'

Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 11 additions & 1 deletion cmd/opfgd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
PollInterval = "10s"
bap2pecs marked this conversation as resolved.
Show resolved Hide resolved
BatchSize = 10
46 changes: 46 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"fmt"
"time"

"github.com/spf13/viper"
Expand All @@ -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")
}
bap2pecs marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
bap2pecs marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("batch-size must be greater than 0")
}

return nil
}

func Load(configPath string) (*Config, error) {
Expand All @@ -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
}
154 changes: 90 additions & 64 deletions db/bbolt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
bap2pecs marked this conversation as resolved.
Show resolved Hide resolved
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
bap2pecs marked this conversation as resolved.
Show resolved Hide resolved
}
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))
bap2pecs marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible that block h is finalised but block h+1 is not? I think it's not possiible as BTC staking enforces consecutive finalisation. So wdyt of maintaining the last finalised height in DB and accessing that value, instead of iterating over all these blocks?

Copy link
Contributor

@bap2pecs bap2pecs Nov 4, 2024

Choose a reason for hiding this comment

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

Would it be possible that block h is finalised but block h+1 is not?

no it's not possible (we can enforce it in bbolt.go as well but can go into another pr)

So wdyt of maintaining the last finalised height in DB and accessing that value, instead of iterating over all these blocks?

good point. we already maintain fist and last finalized heights. we can use them

const (
	blocksBucket          = "blocks"
	blockHeightsBucket    = "block_heights"
	indexerBucket         = "indexer"
	earliestBlockKey      = "earliest"
	latestBlockKey        = "latest"
	activatedTimestampKey = "activated_timestamp"
)

Copy link
Member

Choose a reason for hiding this comment

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

Yes, if using last finalised height then the logic can be greatly simplified. Can be done in a separate PR though

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
bap2pecs marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
Expand Down Expand Up @@ -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()
}

Expand Down
Loading
Loading