From 61f7f0261708efbcf829998734c688372281a78c Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 4 Jun 2024 14:20:51 -0500 Subject: [PATCH 01/21] save local new files to remote --- zetaclient/chains/evm/base/logger.go | 21 ++ zetaclient/chains/evm/base/observer.go | 252 ++++++++++++++++++++ zetaclient/chains/evm/base/observer_test.go | 11 + zetaclient/chains/evm/base/signer.go | 1 + 4 files changed, 285 insertions(+) create mode 100644 zetaclient/chains/evm/base/logger.go create mode 100644 zetaclient/chains/evm/base/observer.go create mode 100644 zetaclient/chains/evm/base/observer_test.go create mode 100644 zetaclient/chains/evm/base/signer.go diff --git a/zetaclient/chains/evm/base/logger.go b/zetaclient/chains/evm/base/logger.go new file mode 100644 index 0000000000..c675450d2d --- /dev/null +++ b/zetaclient/chains/evm/base/logger.go @@ -0,0 +1,21 @@ +package base + +import "github.com/rs/zerolog" + +// ObserverLogger is the base logger for chain observers +type ObserverLogger struct { + // the parent logger for the chain observer + Chain zerolog.Logger + + // the logger for inbound transactions + Inbound zerolog.Logger + + // the logger for outbound transactions + Outbound zerolog.Logger + + // the logger for the chain's gas price + GasPrice zerolog.Logger + + // the logger for the compliance check + Compliance zerolog.Logger +} diff --git a/zetaclient/chains/evm/base/observer.go b/zetaclient/chains/evm/base/observer.go new file mode 100644 index 0000000000..29fc185256 --- /dev/null +++ b/zetaclient/chains/evm/base/observer.go @@ -0,0 +1,252 @@ +package base + +import ( + "fmt" + "os" + "strconv" + "sync/atomic" + + lru "github.com/hashicorp/golang-lru" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" +) + +// Observer is the base chain observer +type Observer struct { + // the external chain + chain chains.Chain + + // the external chain parameters + chainParams observertypes.ChainParams + + // zetacore context + zetacoreContext *context.ZetacoreContext + + // zetacore client + zetacoreClient interfaces.ZetacoreClient + + // tss signer + tss interfaces.TSSSigner + + // the latest block height of external chain + lastBlock uint64 + + // the last successfully scanned block height + lastBlockScanned uint64 + + // lru cache for chain blocks + blockCache *lru.Cache + + // observer database for persistency + db *gorm.DB + + // the channel to stop the observer + stop chan struct{} + + // telemetry server + ts *metrics.TelemetryServer +} + +// NewObserver creates a new base observer +func NewObserver( + chain chains.Chain, + chainParams observertypes.ChainParams, + zetacoreContext *context.ZetacoreContext, + zetacoreClient interfaces.ZetacoreClient, + tss interfaces.TSSSigner, + dbPath string, + ts *metrics.TelemetryServer, +) (*Observer, error) { + ob := Observer{ + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + lastBlock: 0, + lastBlockScanned: 0, + stop: make(chan struct{}), + ts: ts, + } + + // create block cache + var err error + ob.blockCache, err = lru.New(1000) + if err != nil { + return nil, err + } + + // open database + err = ob.OpenDB(dbPath) + if err != nil { + return nil, err + } + + return &ob, nil +} + +// Chain returns the chain for the observer +func (ob *Observer) Chain() chains.Chain { + return ob.chain +} + +// WithChain attaches a new chain to the observer +func (ob *Observer) WithChain(chain chains.Chain) *Observer { + ob.chain = chain + return ob +} + +// ChainParams returns the chain params for the observer +func (ob *Observer) ChainParams() observertypes.ChainParams { + return ob.chainParams +} + +// WithChainParams attaches a new chain params to the observer +func (ob *Observer) WithChainParams(params observertypes.ChainParams) *Observer { + ob.chainParams = params + return ob +} + +// ZetacoreContext returns the zetacore context for the observer +func (ob *Observer) ZetacoreContext() *context.ZetacoreContext { + return ob.zetacoreContext +} + +// WithZetacoreClient attaches a new zetacore client to the observer +func (ob *Observer) WithZetacoreClient(client interfaces.ZetacoreClient) *Observer { + ob.zetacoreClient = client + return ob +} + +// GetLastBlock get external last block height +func (ob *Observer) GetLastBlock() uint64 { + return atomic.LoadUint64(&ob.lastBlock) +} + +// SetLastBlock set external last block height +func (ob *Observer) SetLastBlock(height uint64) { + atomic.StoreUint64(&ob.lastBlock, height) +} + +// GetLastBlockScanned get last block scanned (not necessarily caught up with external block; could be slow/paused) +func (ob *Observer) GetLastBlockScanned() uint64 { + height := atomic.LoadUint64(&ob.lastBlockScanned) + return height +} + +// SetLastBlockScanned set last block scanned (not necessarily caught up with external block; could be slow/paused) +func (ob *Observer) SetLastBlockScanned(blockNumber uint64) { + atomic.StoreUint64(&ob.lastBlockScanned, blockNumber) + metrics.LastScannedBlockNumber.WithLabelValues(ob.chain.ChainName.String()).Set(float64(blockNumber)) +} + +// BlockCache returns the block cache for the observer +func (ob *Observer) BlockCache() *lru.Cache { + return ob.blockCache +} + +// WithBlockCache attaches a new block cache to the observer +func (ob *Observer) WithBlockCache(cache *lru.Cache) *Observer { + ob.blockCache = cache + return ob +} + +// Stop returns the stop channel for the observer +func (ob *Observer) Stop() chan struct{} { + return ob.stop +} + +// TelemetryServer returns the telemetry server for the observer +func (ob *Observer) TelemetryServer() *metrics.TelemetryServer { + return ob.ts +} + +// OpenDB open sql database in the given path +func (ob *Observer) OpenDB(dbPath string) error { + if dbPath != "" { + // create db path if not exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) + if err != nil { + return errors.Wrap(err, "error creating db path") + } + } + + // open db by chain name + chainName := ob.chain.ChainName.String() + path := fmt.Sprintf("%s/%s", dbPath, chainName) + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("error opening observer db for chain: %s", chainName)) + } + + // migrate db + err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + &clienttypes.LastBlockSQLType{}) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("error migrating observer db for chain: %s", chainName)) + } + ob.db = db + } + return nil +} + +// LoadLastBlockScanned loads last scanned block from environment variable or from database +// The last scanned block is the height from which the observer should start scanning for inbound transactions +func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool, err error) { + // get environment variable + envvar := ob.chain.ChainName.String() + "_SCAN_FROM" + scanFromBlock := os.Getenv(envvar) + + // load from environment variable if set + if scanFromBlock != "" { + logger.Info(). + Msgf("LoadLastBlockScanned: envvar %s is set; scan from block %s", envvar, scanFromBlock) + if scanFromBlock == clienttypes.EnvVarLatest { + return true, nil + } + blockNumber, err := strconv.ParseUint(scanFromBlock, 10, 64) + if err != nil { + return false, err + } + ob.SetLastBlockScanned(blockNumber) + return false, nil + } + + // load from DB otherwise. If not found, start from latest block + blockNumber, err := ob.ReadLastBlockScannedFromDB() + if err != nil { + logger.Info().Msgf("LoadLastBlockScanned: chain %d starts scanning from latest block", ob.chain.ChainId) + return true, nil + } + ob.SetLastBlockScanned(blockNumber) + logger.Info(). + Msgf("LoadLastBlockScanned: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockScanned()) + + return false, nil +} + +// WriteLastBlockScannedToDB saves the last scanned block to the database +func (ob *Observer) WriteLastBlockScannedToDB(lastScannedBlock uint64) error { + return ob.db.Save(clienttypes.ToLastBlockSQLType(lastScannedBlock)).Error +} + +// ReadLastBlockScannedFromDB reads the last scanned block from the database +func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { + var lastBlock clienttypes.LastBlockSQLType + if err := ob.db.First(&lastBlock, clienttypes.LastBlockNumID).Error; err != nil { + // not found + return 0, err + } + return lastBlock.Num, nil +} diff --git a/zetaclient/chains/evm/base/observer_test.go b/zetaclient/chains/evm/base/observer_test.go new file mode 100644 index 0000000000..49e0890e7c --- /dev/null +++ b/zetaclient/chains/evm/base/observer_test.go @@ -0,0 +1,11 @@ +package base_test + +import ( + "testing" +) + +func TestNewObserver(t *testing.T) { +} + +func TestChain(t *testing.T) { +} diff --git a/zetaclient/chains/evm/base/signer.go b/zetaclient/chains/evm/base/signer.go new file mode 100644 index 0000000000..22f1111448 --- /dev/null +++ b/zetaclient/chains/evm/base/signer.go @@ -0,0 +1 @@ +package base From 695caa7d7d39ae332efa2de7a700cc063e86bcc0 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 4 Jun 2024 17:30:36 -0500 Subject: [PATCH 02/21] initiated base observer --- zetaclient/chains/evm/base/observer.go | 112 ++++++----- zetaclient/chains/evm/base/observer_test.go | 195 +++++++++++++++++++- 2 files changed, 259 insertions(+), 48 deletions(-) diff --git a/zetaclient/chains/evm/base/observer.go b/zetaclient/chains/evm/base/observer.go index 29fc185256..1865560fd5 100644 --- a/zetaclient/chains/evm/base/observer.go +++ b/zetaclient/chains/evm/base/observer.go @@ -20,6 +20,11 @@ import ( clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) +const ( + // DefaultBlockCacheSize is the default size of the block cache + DefaultBlockCacheSize = 1000 +) + // Observer is the base chain observer type Observer struct { // the external chain @@ -63,6 +68,7 @@ func NewObserver( zetacoreContext *context.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, + blockCacheSize int, dbPath string, ts *metrics.TelemetryServer, ) (*Observer, error) { @@ -80,15 +86,15 @@ func NewObserver( // create block cache var err error - ob.blockCache, err = lru.New(1000) + ob.blockCache, err = lru.New(blockCacheSize) if err != nil { - return nil, err + return nil, errors.Wrap(err, "error creating block cache") } // open database err = ob.OpenDB(dbPath) if err != nil { - return nil, err + return nil, errors.Wrap(err, fmt.Sprintf("error opening observer db for chain: %s", chain.ChainName)) } return &ob, nil @@ -121,32 +127,44 @@ func (ob *Observer) ZetacoreContext() *context.ZetacoreContext { return ob.zetacoreContext } +// ZetacoreClient returns the zetacore client for the observer +func (ob *Observer) ZetacoreClient() interfaces.ZetacoreClient { + return ob.zetacoreClient +} + // WithZetacoreClient attaches a new zetacore client to the observer func (ob *Observer) WithZetacoreClient(client interfaces.ZetacoreClient) *Observer { ob.zetacoreClient = client return ob } -// GetLastBlock get external last block height -func (ob *Observer) GetLastBlock() uint64 { +// Tss returns the tss signer for the observer +func (ob *Observer) TSS() interfaces.TSSSigner { + return ob.tss +} + +// LastBlock get external last block height +func (ob *Observer) LastBlock() uint64 { return atomic.LoadUint64(&ob.lastBlock) } -// SetLastBlock set external last block height -func (ob *Observer) SetLastBlock(height uint64) { - atomic.StoreUint64(&ob.lastBlock, height) +// WithLastBlock set external last block height +func (ob *Observer) WithLastBlock(lastBlock uint64) *Observer { + atomic.StoreUint64(&ob.lastBlock, lastBlock) + return ob } -// GetLastBlockScanned get last block scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) GetLastBlockScanned() uint64 { +// LastBlockScanned get last block scanned (not necessarily caught up with external block; could be slow/paused) +func (ob *Observer) LastBlockScanned() uint64 { height := atomic.LoadUint64(&ob.lastBlockScanned) return height } -// SetLastBlockScanned set last block scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) SetLastBlockScanned(blockNumber uint64) { +// WithLastBlockScanned set last block scanned (not necessarily caught up with external block; could be slow/paused) +func (ob *Observer) WithLastBlockScanned(blockNumber uint64) *Observer { atomic.StoreUint64(&ob.lastBlockScanned, blockNumber) metrics.LastScannedBlockNumber.WithLabelValues(ob.chain.ChainName.String()).Set(float64(blockNumber)) + return ob } // BlockCache returns the block cache for the observer @@ -170,37 +188,6 @@ func (ob *Observer) TelemetryServer() *metrics.TelemetryServer { return ob.ts } -// OpenDB open sql database in the given path -func (ob *Observer) OpenDB(dbPath string) error { - if dbPath != "" { - // create db path if not exist - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return errors.Wrap(err, "error creating db path") - } - } - - // open db by chain name - chainName := ob.chain.ChainName.String() - path := fmt.Sprintf("%s/%s", dbPath, chainName) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("error opening observer db for chain: %s", chainName)) - } - - // migrate db - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("error migrating observer db for chain: %s", chainName)) - } - ob.db = db - } - return nil -} - // LoadLastBlockScanned loads last scanned block from environment variable or from database // The last scanned block is the height from which the observer should start scanning for inbound transactions func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool, err error) { @@ -219,7 +206,7 @@ func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool if err != nil { return false, err } - ob.SetLastBlockScanned(blockNumber) + ob.WithLastBlockScanned(blockNumber) return false, nil } @@ -229,9 +216,9 @@ func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool logger.Info().Msgf("LoadLastBlockScanned: chain %d starts scanning from latest block", ob.chain.ChainId) return true, nil } - ob.SetLastBlockScanned(blockNumber) + ob.WithLastBlockScanned(blockNumber) logger.Info(). - Msgf("LoadLastBlockScanned: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockScanned()) + Msgf("LoadLastBlockScanned: chain %d starts scanning from block %d", ob.chain.ChainId, ob.LastBlockScanned()) return false, nil } @@ -245,8 +232,39 @@ func (ob *Observer) WriteLastBlockScannedToDB(lastScannedBlock uint64) error { func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { var lastBlock clienttypes.LastBlockSQLType if err := ob.db.First(&lastBlock, clienttypes.LastBlockNumID).Error; err != nil { - // not found + // record not found return 0, err } return lastBlock.Num, nil } + +// OpenDB open sql database in the given path +func (ob *Observer) OpenDB(dbPath string) error { + if dbPath != "" { + // create db path if not exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) + if err != nil { + return errors.Wrap(err, "error creating db path") + } + } + + // open db by chain name + chainName := ob.chain.ChainName.String() + path := fmt.Sprintf("%s/%s", dbPath, chainName) + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { + return errors.Wrap(err, "error opening db") + } + + // migrate db + err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + &clienttypes.LastBlockSQLType{}) + if err != nil { + return errors.Wrap(err, "error migrating db") + } + ob.db = db + } + return nil +} diff --git a/zetaclient/chains/evm/base/observer_test.go b/zetaclient/chains/evm/base/observer_test.go index 49e0890e7c..402995367f 100644 --- a/zetaclient/chains/evm/base/observer_test.go +++ b/zetaclient/chains/evm/base/observer_test.go @@ -1,11 +1,204 @@ package base_test import ( + "os" "testing" + + lru "github.com/hashicorp/golang-lru" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" + observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/evm/base" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) +// create a temporary path for database +func tempDbPath(t *testing.T) string { + // Create a temporary file to get a unique name + tempPath, err := os.MkdirTemp("", "tempdb-") + require.NoError(t, err) + return tempPath +} + +// createObserver creates a new observer for testing +func createObserver(t *testing.T) *base.Observer { + // constructor parameters + chain := chains.Ethereum + chainParams := *sample.ChainParams(chain.ChainId) + zetacoreContext := context.NewZetacoreContext(config.NewConfig()) + zetacoreClient := mocks.NewMockZetacoreClient() + tss := mocks.NewTSSMainnet() + blockCacheSize := base.DefaultBlockCacheSize + dbPath := tempDbPath(t) + + // create observer + ob, err := base.NewObserver(chain, chainParams, zetacoreContext, zetacoreClient, tss, blockCacheSize, dbPath, nil) + require.NoError(t, err) + + return ob +} + func TestNewObserver(t *testing.T) { + // constructor parameters + chain := chains.Ethereum + chainParams := *sample.ChainParams(chain.ChainId) + zetacoreContext := context.NewZetacoreContext(config.NewConfig()) + zetacoreClient := mocks.NewMockZetacoreClient() + tss := mocks.NewTSSMainnet() + blockCacheSize := base.DefaultBlockCacheSize + dbPath := tempDbPath(t) + + // test cases + tests := []struct { + name string + chain chains.Chain + chainParams observertypes.ChainParams + zetacoreContext *context.ZetacoreContext + zetacoreClient interfaces.ZetacoreClient + tss interfaces.TSSSigner + blockCacheSize int + dbPath string + fail bool + message string + }{ + { + name: "should be able to create new observer", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + dbPath: dbPath, + fail: false, + }, + { + name: "should return error on invalid block cache size", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: 0, + dbPath: dbPath, + fail: true, + message: "error creating block cache", + }, + { + name: "should return error on invalid db path", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + dbPath: "/invalid/123db", + fail: true, + message: "error opening observer db", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ob, err := base.NewObserver(tt.chain, tt.chainParams, tt.zetacoreContext, tt.zetacoreClient, tt.tss, tt.blockCacheSize, tt.dbPath, nil) + if tt.fail { + require.ErrorContains(t, err, tt.message) + require.Nil(t, ob) + return + } + + require.NoError(t, err) + require.NotNil(t, ob) + }) + } } -func TestChain(t *testing.T) { +func TestObserverSetters(t *testing.T) { + t.Run("should be able to update chain", func(t *testing.T) { + ob := createObserver(t) + + // update chain + newChain := chains.BscMainnet + ob = ob.WithChain(chains.BscMainnet) + require.Equal(t, newChain, ob.Chain()) + }) + t.Run("should be able to update chain params", func(t *testing.T) { + ob := createObserver(t) + + // update chain params + newChainParams := *sample.ChainParams(chains.BscMainnet.ChainId) + ob = ob.WithChainParams(newChainParams) + require.True(t, observertypes.ChainParamsEqual(newChainParams, ob.ChainParams())) + }) + t.Run("should be able to update zetacore client", func(t *testing.T) { + ob := createObserver(t) + + // update zetacore client + newZetacoreClient := mocks.NewMockZetacoreClient() + ob = ob.WithZetacoreClient(newZetacoreClient) + require.Equal(t, newZetacoreClient, ob.ZetacoreClient()) + }) + t.Run("should be able to update last block", func(t *testing.T) { + ob := createObserver(t) + + // update last block + newLastBlock := uint64(100) + ob = ob.WithLastBlock(newLastBlock) + require.Equal(t, newLastBlock, ob.LastBlock()) + }) + t.Run("should be able to update last block scanned", func(t *testing.T) { + ob := createObserver(t) + + // update last block scanned + newLastBlockScanned := uint64(100) + ob = ob.WithLastBlockScanned(newLastBlockScanned) + require.Equal(t, newLastBlockScanned, ob.LastBlockScanned()) + }) + t.Run("should be able to update block cache", func(t *testing.T) { + ob := createObserver(t) + + // update block cache + newBlockCache, err := lru.New(200) + require.NoError(t, err) + + ob = ob.WithBlockCache(newBlockCache) + require.Equal(t, newBlockCache, ob.BlockCache()) + }) +} + +func TestOpenDB(t *testing.T) { + ob := createObserver(t) + dbPath := tempDbPath(t) + + t.Run("should be able to open db", func(t *testing.T) { + err := ob.OpenDB(dbPath) + require.NoError(t, err) + }) + t.Run("should return error on invalid db path", func(t *testing.T) { + err := ob.OpenDB("/invalid/123db") + require.ErrorContains(t, err, "error creating db path") + }) +} + +func TestReadWriteLastBlockScannedToDB(t *testing.T) { + t.Run("should be able to write and read last block scanned to db", func(t *testing.T) { + ob := createObserver(t) + err := ob.WriteLastBlockScannedToDB(100) + require.NoError(t, err) + + lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() + require.NoError(t, err) + require.EqualValues(t, 100, lastBlockScanned) + }) + t.Run("should return error when last block scanned not found in db", func(t *testing.T) { + ob := createObserver(t) + lastScannedBlock, err := ob.ReadLastBlockScannedFromDB() + require.Error(t, err) + require.Zero(t, lastScannedBlock) + }) } From 1500138335847bb401978bd5f276fda4dec6a8f3 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 4 Jun 2024 17:47:57 -0500 Subject: [PATCH 03/21] move base to chains folder --- zetaclient/chains/{evm => }/base/logger.go | 0 zetaclient/chains/{evm => }/base/observer.go | 0 zetaclient/chains/{evm => }/base/observer_test.go | 2 +- zetaclient/chains/{evm => }/base/signer.go | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename zetaclient/chains/{evm => }/base/logger.go (100%) rename zetaclient/chains/{evm => }/base/observer.go (100%) rename zetaclient/chains/{evm => }/base/observer_test.go (99%) rename zetaclient/chains/{evm => }/base/signer.go (100%) diff --git a/zetaclient/chains/evm/base/logger.go b/zetaclient/chains/base/logger.go similarity index 100% rename from zetaclient/chains/evm/base/logger.go rename to zetaclient/chains/base/logger.go diff --git a/zetaclient/chains/evm/base/observer.go b/zetaclient/chains/base/observer.go similarity index 100% rename from zetaclient/chains/evm/base/observer.go rename to zetaclient/chains/base/observer.go diff --git a/zetaclient/chains/evm/base/observer_test.go b/zetaclient/chains/base/observer_test.go similarity index 99% rename from zetaclient/chains/evm/base/observer_test.go rename to zetaclient/chains/base/observer_test.go index 402995367f..5f3b0ec966 100644 --- a/zetaclient/chains/evm/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -9,7 +9,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/chains/evm/base" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" diff --git a/zetaclient/chains/evm/base/signer.go b/zetaclient/chains/base/signer.go similarity index 100% rename from zetaclient/chains/evm/base/signer.go rename to zetaclient/chains/base/signer.go From 2cdff62c54bd19b1317321954f4485923310c4d2 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Tue, 4 Jun 2024 23:36:54 -0500 Subject: [PATCH 04/21] moved logger to base package --- cmd/zetaclientd/main.go | 64 ------------ cmd/zetaclientd/start.go | 9 +- cmd/zetaclientd/utils.go | 26 ++--- zetaclient/chains/base/logger.go | 87 ++++++++++++++++- zetaclient/chains/base/logger_test.go | 97 +++++++++++++++++++ zetaclient/chains/base/observer.go | 8 +- zetaclient/chains/base/observer_test.go | 25 +++-- zetaclient/chains/base/signer.go | 49 ++++++++++ .../chains/bitcoin/observer/live_test.go | 4 +- .../chains/bitcoin/observer/observer.go | 8 +- .../chains/bitcoin/observer/observer_test.go | 6 +- zetaclient/chains/bitcoin/signer/signer.go | 8 +- .../chains/bitcoin/signer/signer_test.go | 8 +- zetaclient/chains/evm/observer/observer.go | 8 +- .../chains/evm/observer/observer_test.go | 4 +- zetaclient/chains/evm/signer/signer.go | 12 +-- zetaclient/chains/evm/signer/signer_test.go | 6 +- zetaclient/common/logger.go | 20 ---- zetaclient/context/zetacore_context_test.go | 8 +- 19 files changed, 306 insertions(+), 151 deletions(-) create mode 100644 zetaclient/chains/base/logger_test.go delete mode 100644 zetaclient/common/logger.go diff --git a/cmd/zetaclientd/main.go b/cmd/zetaclientd/main.go index fdac54a7d0..99aaf55c10 100644 --- a/cmd/zetaclientd/main.go +++ b/cmd/zetaclientd/main.go @@ -3,24 +3,15 @@ package main import ( "math/rand" "os" - "path/filepath" "time" ecdsakeygen "github.com/binance-chain/tss-lib/ecdsa/keygen" "github.com/cosmos/cosmos-sdk/server" svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" "github.com/cosmos/cosmos-sdk/types" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "github.com/zeta-chain/zetacore/app" "github.com/zeta-chain/zetacore/cmd" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - "github.com/zeta-chain/zetacore/zetaclient/config" -) - -const ( - ComplianceLogFile = "compliance.log" ) var ( @@ -53,58 +44,3 @@ func SetupConfigForTest() { rand.Seed(time.Now().UnixNano()) } - -func InitLogger(cfg config.Config) (clientcommon.ClientLogger, error) { - // open compliance log file - file, err := OpenComplianceLogFile(cfg) - if err != nil { - return clientcommon.DefaultLoggers(), err - } - - var logger zerolog.Logger - var loggerCompliance zerolog.Logger - switch cfg.LogFormat { - case "json": - logger = zerolog.New(os.Stdout).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() - loggerCompliance = zerolog.New(file).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() - case "text": - logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}). - Level(zerolog.Level(cfg.LogLevel)). - With(). - Timestamp(). - Logger() - loggerCompliance = zerolog.New(file).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() - default: - logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}) - loggerCompliance = zerolog.New(file).With().Timestamp().Logger() - } - - if cfg.LogSampler { - logger = logger.Sample(&zerolog.BasicSampler{N: 5}) - } - log.Logger = logger // set global logger - - return clientcommon.ClientLogger{ - Std: log.Logger, - Compliance: loggerCompliance, - }, nil -} - -func OpenComplianceLogFile(cfg config.Config) (*os.File, error) { - // use zetacore home as default - logPath := cfg.ZetaCoreHome - if cfg.ComplianceConfig.LogPath != "" { - logPath = cfg.ComplianceConfig.LogPath - } - - // clean file name - name := filepath.Join(logPath, ComplianceLogFile) - name, err := filepath.Abs(name) - if err != nil { - return nil, err - } - name = filepath.Clean(name) - - // open (or create) compliance log file - return os.OpenFile(name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) -} diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 9c964ea26e..c815b616c0 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -24,6 +24,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/authz" "github.com/zeta-chain/zetacore/pkg/constant" observerTypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" @@ -61,7 +62,7 @@ func start(_ *cobra.Command, _ []string) error { if err != nil { return err } - loggers, err := InitLogger(cfg) + logger, err := base.InitLogger(cfg) if err != nil { log.Error().Err(err).Msg("InitLogger failed") return err @@ -76,7 +77,7 @@ func start(_ *cobra.Command, _ []string) error { } } - masterLogger := loggers.Std + masterLogger := logger.Std startLogger := masterLogger.With().Str("module", "startup").Logger() // Wait until zetacore is up @@ -267,7 +268,7 @@ func start(_ *cobra.Command, _ []string) error { } // CreateSignerMap: This creates a map of all signers for each chain . Each signer is responsible for signing transactions for a particular chain - signerMap, err := CreateSignerMap(appContext, tss, loggers, telemetryServer) + signerMap, err := CreateSignerMap(appContext, tss, logger, telemetryServer) if err != nil { log.Error().Err(err).Msg("CreateSignerMap") return err @@ -281,7 +282,7 @@ func start(_ *cobra.Command, _ []string) error { dbpath := filepath.Join(userDir, ".zetaclient/chainobserver") // Creates a map of all chain observers for each chain. Each chain observer is responsible for observing events on the chain and processing them. - observerMap, err := CreateChainObserverMap(appContext, zetacoreClient, tss, dbpath, loggers, telemetryServer) + observerMap, err := CreateChainObserverMap(appContext, zetacoreClient, tss, dbpath, logger, telemetryServer) if err != nil { startLogger.Err(err).Msg("CreateChainObserverMap") return err diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 7c37c00380..521c6dc858 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -5,12 +5,12 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/zeta-chain/zetacore/zetaclient/authz" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" btcsigner "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/signer" evmobserver "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" evmsigner "github.com/zeta-chain/zetacore/zetaclient/chains/evm/signer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" @@ -57,7 +57,7 @@ func CreateZetacoreClient( func CreateSignerMap( appContext *context.AppContext, tss interfaces.TSSSigner, - loggers clientcommon.ClientLogger, + logger base.Logger, ts *metrics.TelemetryServer, ) (map[int64]interfaces.ChainSigner, error) { coreContext := appContext.ZetacoreContext() @@ -70,7 +70,7 @@ func CreateSignerMap( } evmChainParams, found := coreContext.GetEVMChainParams(evmConfig.Chain.ChainId) if !found { - loggers.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) + logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } mpiAddress := ethcommon.HexToAddress(evmChainParams.ConnectorContractAddress) @@ -84,10 +84,10 @@ func CreateSignerMap( mpiAddress, erc20CustodyAddress, coreContext, - loggers, + logger, ts) if err != nil { - loggers.Std.Error().Err(err).Msgf("NewEVMSigner error for chain %s", evmConfig.Chain.String()) + logger.Std.Error().Err(err).Msgf("NewEVMSigner error for chain %s", evmConfig.Chain.String()) continue } signerMap[evmConfig.Chain.ChainId] = signer @@ -95,9 +95,9 @@ func CreateSignerMap( // BTC signer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - signer, err := btcsigner.NewSigner(btcConfig, tss, loggers, ts, coreContext) + signer, err := btcsigner.NewSigner(btcConfig, tss, logger, ts, coreContext) if err != nil { - loggers.Std.Error().Err(err).Msgf("NewBTCSigner error for chain %s", btcChain.String()) + logger.Std.Error().Err(err).Msgf("NewBTCSigner error for chain %s", btcChain.String()) } else { signerMap[btcChain.ChainId] = signer } @@ -112,7 +112,7 @@ func CreateChainObserverMap( zetacoreClient *zetacore.Client, tss interfaces.TSSSigner, dbpath string, - loggers clientcommon.ClientLogger, + logger base.Logger, ts *metrics.TelemetryServer, ) (map[int64]interfaces.ChainObserver, error) { observerMap := make(map[int64]interfaces.ChainObserver) @@ -123,12 +123,12 @@ func CreateChainObserverMap( } _, found := appContext.ZetacoreContext().GetEVMChainParams(evmConfig.Chain.ChainId) if !found { - loggers.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) + logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } - co, err := evmobserver.NewObserver(appContext, zetacoreClient, tss, dbpath, loggers, evmConfig, ts) + co, err := evmobserver.NewObserver(appContext, zetacoreClient, tss, dbpath, logger, evmConfig, ts) if err != nil { - loggers.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) + logger.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) continue } observerMap[evmConfig.Chain.ChainId] = co @@ -136,9 +136,9 @@ func CreateChainObserverMap( // BTC observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - co, err := btcobserver.NewObserver(appContext, btcChain, zetacoreClient, tss, dbpath, loggers, btcConfig, ts) + co, err := btcobserver.NewObserver(appContext, btcChain, zetacoreClient, tss, dbpath, logger, btcConfig, ts) if err != nil { - loggers.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) + logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) } else { observerMap[btcChain.ChainId] = co diff --git a/zetaclient/chains/base/logger.go b/zetaclient/chains/base/logger.go index c675450d2d..a6810cc984 100644 --- a/zetaclient/chains/base/logger.go +++ b/zetaclient/chains/base/logger.go @@ -1,6 +1,33 @@ package base -import "github.com/rs/zerolog" +import ( + "os" + "path/filepath" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/zeta-chain/zetacore/zetaclient/config" +) + +const ( + ComplianceLogFile = "compliance.log" +) + +// Logger contains the base loggers +type Logger struct { + Std zerolog.Logger + Compliance zerolog.Logger +} + +// DefaultLoggers creates default base loggers for tests +func DefaultLogger() Logger { + return Logger{ + Std: log.Logger, + Compliance: log.Logger, + } +} // ObserverLogger is the base logger for chain observers type ObserverLogger struct { @@ -19,3 +46,61 @@ type ObserverLogger struct { // the logger for the compliance check Compliance zerolog.Logger } + +// InitLogger initializes the base loggers +func InitLogger(cfg config.Config) (Logger, error) { + // open compliance log file + file, err := openComplianceLogFile(cfg) + if err != nil { + return DefaultLogger(), err + } + + // create loggers based on configured level and format + var std zerolog.Logger + var compliance zerolog.Logger + switch cfg.LogFormat { + case "json": + std = zerolog.New(os.Stdout).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() + compliance = zerolog.New(file).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() + case "text": + std = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}). + Level(zerolog.Level(cfg.LogLevel)). + With(). + Timestamp(). + Logger() + compliance = zerolog.New(file).Level(zerolog.Level(cfg.LogLevel)).With().Timestamp().Logger() + default: + std = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}) + compliance = zerolog.New(file).With().Timestamp().Logger() + } + + if cfg.LogSampler { + std = std.Sample(&zerolog.BasicSampler{N: 5}) + } + log.Logger = std // set global logger + + return Logger{ + Std: std, + Compliance: compliance, + }, nil +} + +// openComplianceLogFile opens the compliance log file +func openComplianceLogFile(cfg config.Config) (*os.File, error) { + // use zetacore home as default + logPath := cfg.ZetaCoreHome + if cfg.ComplianceConfig.LogPath != "" { + logPath = cfg.ComplianceConfig.LogPath + } + + // clean file name + name := filepath.Join(logPath, ComplianceLogFile) + name, err := filepath.Abs(name) + if err != nil { + return nil, err + } + name = filepath.Clean(name) + + // open (or create) compliance log file + return os.OpenFile(name, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) +} diff --git a/zetaclient/chains/base/logger_test.go b/zetaclient/chains/base/logger_test.go new file mode 100644 index 0000000000..07c2859b0e --- /dev/null +++ b/zetaclient/chains/base/logger_test.go @@ -0,0 +1,97 @@ +package base_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/config" +) + +func TestInitLogger(t *testing.T) { + // test cases + tests := []struct { + name string + t *testing.T + cfg config.Config + fail bool + }{ + { + name: "should be able to initialize json formatted logger", + cfg: config.Config{ + LogFormat: "json", + LogLevel: 1, // zerolog.InfoLevel, + ComplianceConfig: config.ComplianceConfig{ + LogPath: createTempDir(t), + }, + }, + fail: false, + }, + { + name: "should be able to initialize plain text logger", + cfg: config.Config{ + LogFormat: "text", + LogLevel: 2, // zerolog.WarnLevel, + ComplianceConfig: config.ComplianceConfig{ + LogPath: createTempDir(t), + }, + }, + fail: false, + }, + { + name: "should be able to initialize default formatted logger", + cfg: config.Config{ + LogFormat: "unknown", + LogLevel: 3, // zerolog.ErrorLevel, + ComplianceConfig: config.ComplianceConfig{ + LogPath: createTempDir(t), + }, + }, + fail: false, + }, + { + name: "should be able to initialize sampled logger", + cfg: config.Config{ + LogFormat: "json", + LogLevel: 4, // zerolog.DebugLevel, + LogSampler: true, + ComplianceConfig: config.ComplianceConfig{ + LogPath: createTempDir(t), + }, + }, + }, + { + name: "should fail on invalid compliance log path", + cfg: config.Config{ + LogFormat: "json", + LogLevel: 1, // zerolog.InfoLevel, + ComplianceConfig: config.ComplianceConfig{ + LogPath: "/invalid/123path", + }, + }, + fail: true, + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // init logger + logger, err := base.InitLogger(tt.cfg) + + // check if error is expected + if tt.fail { + require.Error(t, err) + return + } + + // check if logger is initialized + require.NoError(t, err) + + // should be able to print log + logger.Std.Info().Msg("print standard log") + logger.Compliance.Info().Msg("print compliance log") + }) + } +} diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 1865560fd5..e36a2aa924 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -9,14 +9,14 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/zetaclient/metrics" "gorm.io/driver/sqlite" "gorm.io/gorm" + "github.com/zeta-chain/zetacore/pkg/chains" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) @@ -25,7 +25,7 @@ const ( DefaultBlockCacheSize = 1000 ) -// Observer is the base chain observer +// Observer is the base chain observer for external chains type Observer struct { // the external chain chain chains.Chain diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 5f3b0ec966..007dca4199 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -6,6 +6,7 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -16,10 +17,9 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -// create a temporary path for database -func tempDbPath(t *testing.T) string { - // Create a temporary file to get a unique name - tempPath, err := os.MkdirTemp("", "tempdb-") +// create a temporary directory for testing +func createTempDir(t *testing.T) string { + tempPath, err := os.MkdirTemp("", "tempdir-") require.NoError(t, err) return tempPath } @@ -33,7 +33,7 @@ func createObserver(t *testing.T) *base.Observer { zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize - dbPath := tempDbPath(t) + dbPath := createTempDir(t) // create observer ob, err := base.NewObserver(chain, chainParams, zetacoreContext, zetacoreClient, tss, blockCacheSize, dbPath, nil) @@ -50,7 +50,7 @@ func TestNewObserver(t *testing.T) { zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize - dbPath := tempDbPath(t) + dbPath := createTempDir(t) // test cases tests := []struct { @@ -105,7 +105,16 @@ func TestNewObserver(t *testing.T) { // run tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ob, err := base.NewObserver(tt.chain, tt.chainParams, tt.zetacoreContext, tt.zetacoreClient, tt.tss, tt.blockCacheSize, tt.dbPath, nil) + ob, err := base.NewObserver( + tt.chain, + tt.chainParams, + tt.zetacoreContext, + tt.zetacoreClient, + tt.tss, + tt.blockCacheSize, + tt.dbPath, + nil, + ) if tt.fail { require.ErrorContains(t, err, tt.message) require.Nil(t, ob) @@ -173,7 +182,7 @@ func TestObserverSetters(t *testing.T) { func TestOpenDB(t *testing.T) { ob := createObserver(t) - dbPath := tempDbPath(t) + dbPath := createTempDir(t) t.Run("should be able to open db", func(t *testing.T) { err := ob.OpenDB(dbPath) diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index 22f1111448..1e768d21ea 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -1 +1,50 @@ package base + +import ( + "github.com/rs/zerolog" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" +) + +// Signer is the base chain signer for external chains +type Signer struct { + // the external chain + chain chains.Chain + + // zetacore context + zetacoreContext *context.ZetacoreContext + + // tss signer + tss interfaces.TSSSigner + + // telemetry server + ts *metrics.TelemetryServer + + // the standard logger + logger zerolog.Logger + + // the compliance logger + loggerCompliance zerolog.Logger +} + +// NewSigner creates a new base signer +func NewSigner( + chain chains.Chain, + zetacoreContext *context.ZetacoreContext, + tss interfaces.TSSSigner, + logger zerolog.Logger, + loggerCompliance zerolog.Logger, + ts *metrics.TelemetryServer, +) *Signer { + return &Signer{ + chain: chain, + zetacoreContext: zetacoreContext, + tss: tss, + logger: logger, + loggerCompliance: loggerCompliance, + ts: ts, + } +} diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/observer/live_test.go index 51bedbf1ef..dd1053f620 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/observer/live_test.go @@ -21,8 +21,8 @@ import ( "github.com/stretchr/testify/suite" "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/testutils" @@ -49,7 +49,7 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { } appContext := clientcontext.NewAppContext(&clientcontext.ZetacoreContext{}, config.Config{}) client, err := NewObserver(appContext, chains.BitcoinRegtest, nil, tss, tempSQLiteDbPath, - clientcommon.DefaultLoggers(), config.BTCConfig{}, nil) + base.DefaultLogger(), config.BTCConfig{}, nil) suite.Require().NoError(err) suite.rpcClient, err = getRPCClient(18332) suite.Require().NoError(err) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 91f0d695f4..dcb81a47f3 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -28,9 +28,9 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" @@ -138,7 +138,7 @@ func NewObserver( zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, - loggers clientcommon.ClientLogger, + logger base.Logger, btcCfg config.BTCConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { @@ -158,14 +158,14 @@ func NewObserver( ob.Mu = &sync.Mutex{} - chainLogger := loggers.Std.With().Str("chain", chain.ChainName.String()).Logger() + chainLogger := logger.Std.With().Str("chain", chain.ChainName.String()).Logger() ob.logger = Logger{ Chain: chainLogger, Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), UTXOS: chainLogger.With().Str("module", "WatchUTXOS").Logger(), GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: loggers.Compliance, + Compliance: logger.Compliance, } ob.zetacoreClient = zetacoreClient diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index dfba6b3dbc..2d2494dd16 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -14,7 +14,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" @@ -75,11 +75,11 @@ func TestNewBitcoinObserver(t *testing.T) { chain := chains.BitcoinMainnet zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewMockTSS(chains.BitcoinTestnet, sample.EthAddress().String(), "") - loggers := clientcommon.ClientLogger{} + logger := base.Logger{} btcCfg := cfg.BitcoinConfig ts := metrics.NewTelemetryServer() - client, err := NewObserver(appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, loggers, btcCfg, ts) + client, err := NewObserver(appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, logger, btcCfg, ts) require.ErrorContains(t, err, "btc chains params not initialized") require.Nil(t, client) }) diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 0c51fc7204..1215b32bb3 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -21,10 +21,10 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" @@ -56,7 +56,7 @@ type Signer struct { func NewSigner( cfg config.BTCConfig, tssSigner interfaces.TSSSigner, - loggers clientcommon.ClientLogger, + logger base.Logger, ts *metrics.TelemetryServer, coreContext *context.ZetacoreContext) (*Signer, error) { connCfg := &rpcclient.ConnConfig{ @@ -75,8 +75,8 @@ func NewSigner( return &Signer{ tssSigner: tssSigner, rpcClient: client, - logger: loggers.Std.With().Str("chain", "BTC").Str("module", "BTCSigner").Logger(), - loggerCompliance: loggers.Compliance, + logger: logger.Std.With().Str("chain", "BTC").Str("module", "BTCSigner").Logger(), + loggerCompliance: logger.Compliance, ts: ts, coreContext: coreContext, }, nil diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index 6d048c7bba..74628211a4 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -18,8 +18,8 @@ import ( . "gopkg.in/check.v1" "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" @@ -52,7 +52,7 @@ func (s *BTCSignerSuite) SetUpTest(c *C) { s.btcSigner, err = NewSigner( config.BTCConfig{}, tss, - clientcommon.DefaultLoggers(), + base.DefaultLogger(), &metrics.TelemetryServer{}, context.NewZetacoreContext(cfg)) c.Assert(err, IsNil) @@ -233,7 +233,7 @@ func TestAddWithdrawTxOutputs(t *testing.T) { signer, err := NewSigner( config.BTCConfig{}, mocks.NewTSSMainnet(), - clientcommon.DefaultLoggers(), + base.DefaultLogger(), &metrics.TelemetryServer{}, nil, ) @@ -396,7 +396,7 @@ func TestNewBTCSigner(t *testing.T) { btcSigner, err := NewSigner( config.BTCConfig{}, tss, - clientcommon.DefaultLoggers(), + base.DefaultLogger(), &metrics.TelemetryServer{}, context.NewZetacoreContext(cfg)) require.NoError(t, err) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 4a38a62d12..272f0a06d9 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -30,9 +30,9 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" @@ -93,7 +93,7 @@ func NewObserver( zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, - loggers clientcommon.ClientLogger, + logger base.Logger, evmCfg config.EVMConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { @@ -101,13 +101,13 @@ func NewObserver( ts: ts, } - chainLogger := loggers.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() + chainLogger := logger.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() ob.logger = Logger{ Chain: chainLogger, Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: loggers.Compliance, + Compliance: logger.Compliance, } ob.coreContext = appContext.ZetacoreContext() diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index bfc85e15e1..0601c083f2 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -15,9 +15,9 @@ import ( "github.com/zeta-chain/zetacore/testutil/sample" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" @@ -83,7 +83,7 @@ func MockEVMObserver( appCtx, evmCfg := getAppContext(chain, ¶ms) // create chain observer - client, err := observer.NewObserver(appCtx, zetacoreClient, tss, "", common.ClientLogger{}, evmCfg, nil) + client, err := observer.NewObserver(appCtx, zetacoreClient, tss, "", base.Logger{}, evmCfg, nil) require.NoError(t, err) client.WithEvmClient(evmClient) client.WithEvmJSONRPC(evmJSONRPC) diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index 8ab5608959..447ea9d924 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -27,10 +27,10 @@ import ( crosschainkeeper "github.com/zeta-chain/zetacore/x/crosschain/keeper" "github.com/zeta-chain/zetacore/x/crosschain/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/compliance" clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" @@ -47,7 +47,7 @@ type Signer struct { chain *chains.Chain tssSigner interfaces.TSSSigner ethSigner ethtypes.Signer - logger clientcommon.ClientLogger + logger base.Logger ts *metrics.TelemetryServer coreContext *clientcontext.ZetacoreContext @@ -69,7 +69,7 @@ func NewSigner( zetaConnectorAddress ethcommon.Address, erc20CustodyAddress ethcommon.Address, coreContext *clientcontext.ZetacoreContext, - loggers clientcommon.ClientLogger, + logger base.Logger, ts *metrics.TelemetryServer, ) (*Signer, error) { client, ethSigner, err := getEVMRPC(endpoint) @@ -95,9 +95,9 @@ func NewSigner( zetaConnectorAddress: zetaConnectorAddress, er20CustodyAddress: erc20CustodyAddress, coreContext: coreContext, - logger: clientcommon.ClientLogger{ - Std: loggers.Std.With().Str("chain", chain.ChainName.String()).Str("module", "EVMSigner").Logger(), - Compliance: loggers.Compliance, + logger: base.Logger{ + Std: logger.Std.With().Str("chain", chain.ChainName.String()).Str("module", "EVMSigner").Logger(), + Compliance: logger.Compliance, }, ts: ts, mu: &sync.Mutex{}, diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index 6039c44803..580486d709 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -12,8 +12,8 @@ import ( "github.com/zeta-chain/zetacore/pkg/constant" "github.com/zeta-chain/zetacore/testutil/sample" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" - "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" @@ -32,7 +32,7 @@ var ( func getNewEvmSigner() (*Signer, error) { mpiAddress := ConnectorAddress erc20CustodyAddress := ERC20CustodyAddress - logger := common.ClientLogger{} + logger := base.Logger{} ts := &metrics.TelemetryServer{} cfg := config.NewConfig() return NewSigner( @@ -49,7 +49,7 @@ func getNewEvmSigner() (*Signer, error) { } func getNewEvmChainObserver() (*observer.Observer, error) { - logger := common.ClientLogger{} + logger := base.Logger{} ts := &metrics.TelemetryServer{} cfg := config.NewConfig() tss := mocks.NewTSSMainnet() diff --git a/zetaclient/common/logger.go b/zetaclient/common/logger.go deleted file mode 100644 index ebe773abfc..0000000000 --- a/zetaclient/common/logger.go +++ /dev/null @@ -1,20 +0,0 @@ -package common - -import ( - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -// ClientLogger is a struct that contains the logger for a chain observer -type ClientLogger struct { - Std zerolog.Logger - Compliance zerolog.Logger -} - -// DefaultLoggers returns the default loggers for a chain observer -func DefaultLoggers() ClientLogger { - return ClientLogger{ - Std: log.Logger, - Compliance: log.Logger, - } -} diff --git a/zetaclient/context/zetacore_context_test.go b/zetaclient/context/zetacore_context_test.go index 0ee7d5bc8a..d8117e124b 100644 --- a/zetaclient/context/zetacore_context_test.go +++ b/zetaclient/context/zetacore_context_test.go @@ -4,13 +4,13 @@ import ( "testing" "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/testutil/sample" lightclienttypes "github.com/zeta-chain/zetacore/x/lightclient/types" observertypes "github.com/zeta-chain/zetacore/x/observer/types" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/config" context "github.com/zeta-chain/zetacore/zetaclient/context" ) @@ -179,7 +179,6 @@ func TestUpdateZetacoreContext(t *testing.T) { ChainId: 3, } tssPubKeyToUpdate := "tsspubkeytest" - loggers := clientcommon.DefaultLoggers() crosschainFlags := sample.CrosschainFlags() verificationFlags := sample.HeaderSupportedChains() @@ -193,7 +192,7 @@ func TestUpdateZetacoreContext(t *testing.T) { *crosschainFlags, verificationFlags, false, - loggers.Std, + log.Logger, ) // assert keygen updated @@ -285,7 +284,6 @@ func TestUpdateZetacoreContext(t *testing.T) { crosschainFlags := sample.CrosschainFlags() verificationFlags := sample.HeaderSupportedChains() require.NotNil(t, crosschainFlags) - loggers := clientcommon.DefaultLoggers() zetaContext.Update( &keyGenToUpdate, enabledChainsToUpdate, @@ -295,7 +293,7 @@ func TestUpdateZetacoreContext(t *testing.T) { *crosschainFlags, verificationFlags, false, - loggers.Std, + log.Logger, ) // assert keygen updated From d2d19f11424f819c7097010b038d24e21a55e53d Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 5 Jun 2024 23:25:10 -0500 Subject: [PATCH 05/21] added base signer and logger --- zetaclient/chains/base/logger.go | 5 +- zetaclient/chains/base/observer.go | 181 +++++++++++++-------- zetaclient/chains/base/observer_test.go | 143 ++++++++++++++-- zetaclient/chains/base/signer.go | 76 ++++++--- zetaclient/chains/base/signer_test.go | 65 ++++++++ zetaclient/chains/evm/observer/observer.go | 2 +- zetaclient/types/defs.go | 5 - 7 files changed, 362 insertions(+), 115 deletions(-) create mode 100644 zetaclient/chains/base/signer_test.go delete mode 100644 zetaclient/types/defs.go diff --git a/zetaclient/chains/base/logger.go b/zetaclient/chains/base/logger.go index a6810cc984..eeffcfab3b 100644 --- a/zetaclient/chains/base/logger.go +++ b/zetaclient/chains/base/logger.go @@ -29,7 +29,7 @@ func DefaultLogger() Logger { } } -// ObserverLogger is the base logger for chain observers +// ObserverLogger contains the loggers for chain observers type ObserverLogger struct { // the parent logger for the chain observer Chain zerolog.Logger @@ -43,6 +43,9 @@ type ObserverLogger struct { // the logger for the chain's gas price GasPrice zerolog.Logger + // the logger for block headers + Headers zerolog.Logger + // the logger for the compliance check Compliance zerolog.Logger } diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index e36a2aa924..0f148b6a87 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -21,47 +21,50 @@ import ( ) const ( + // EnvVarLatestBlock is the environment variable to indicate latest block height + EnvVarLatestBlock = "latest" + // DefaultBlockCacheSize is the default size of the block cache DefaultBlockCacheSize = 1000 ) -// Observer is the base chain observer for external chains +// Observer is the base observer. type Observer struct { - // the external chain + // chain contains static information about the observed chain chain chains.Chain - // the external chain parameters + // chainParams contains the dynamic chain parameters of the observed chain chainParams observertypes.ChainParams - // zetacore context + // coreContext contains context data of ZetaChain zetacoreContext *context.ZetacoreContext - // zetacore client + // zetacoreClient is the client to interact with ZetaChain zetacoreClient interfaces.ZetacoreClient - // tss signer + // tss is the TSS signer tss interfaces.TSSSigner - // the latest block height of external chain + // lastBlock is the last block height of the observed chain lastBlock uint64 - // the last successfully scanned block height + // lastBlockScanned is the last block height scanned by the observer lastBlockScanned uint64 - // lru cache for chain blocks + // blockCache is the cache for blocks blockCache *lru.Cache - // observer database for persistency + // db is the database to persist data db *gorm.DB - // the channel to stop the observer - stop chan struct{} + // logger contains the loggers used by observer + logger ObserverLogger - // telemetry server - ts *metrics.TelemetryServer + // stop is the channel to signal the observer to stop + stop chan struct{} } -// NewObserver creates a new base observer +// NewObserver creates a new base observer. func NewObserver( chain chains.Chain, chainParams observertypes.ChainParams, @@ -70,7 +73,7 @@ func NewObserver( tss interfaces.TSSSigner, blockCacheSize int, dbPath string, - ts *metrics.TelemetryServer, + logger Logger, ) (*Observer, error) { ob := Observer{ chain: chain, @@ -81,9 +84,11 @@ func NewObserver( lastBlock: 0, lastBlockScanned: 0, stop: make(chan struct{}), - ts: ts, } + // setup loggers + ob.WithLogger(logger) + // create block cache var err error ob.blockCache, err = lru.New(blockCacheSize) @@ -100,106 +105,168 @@ func NewObserver( return &ob, nil } -// Chain returns the chain for the observer +// Chain returns the chain for the observer. func (ob *Observer) Chain() chains.Chain { return ob.chain } -// WithChain attaches a new chain to the observer +// WithChain attaches a new chain to the observer. func (ob *Observer) WithChain(chain chains.Chain) *Observer { ob.chain = chain return ob } -// ChainParams returns the chain params for the observer +// ChainParams returns the chain params for the observer. func (ob *Observer) ChainParams() observertypes.ChainParams { return ob.chainParams } -// WithChainParams attaches a new chain params to the observer +// WithChainParams attaches a new chain params to the observer. func (ob *Observer) WithChainParams(params observertypes.ChainParams) *Observer { ob.chainParams = params return ob } -// ZetacoreContext returns the zetacore context for the observer +// ZetacoreContext returns the zetacore context for the observer. func (ob *Observer) ZetacoreContext() *context.ZetacoreContext { return ob.zetacoreContext } -// ZetacoreClient returns the zetacore client for the observer +// WithZetacoreContext attaches a new zetacore context to the observer. +func (ob *Observer) WithZetacoreContext(context *context.ZetacoreContext) *Observer { + ob.zetacoreContext = context + return ob +} + +// ZetacoreClient returns the zetacore client for the observer. func (ob *Observer) ZetacoreClient() interfaces.ZetacoreClient { return ob.zetacoreClient } -// WithZetacoreClient attaches a new zetacore client to the observer +// WithZetacoreClient attaches a new zetacore client to the observer. func (ob *Observer) WithZetacoreClient(client interfaces.ZetacoreClient) *Observer { ob.zetacoreClient = client return ob } -// Tss returns the tss signer for the observer +// Tss returns the tss signer for the observer. func (ob *Observer) TSS() interfaces.TSSSigner { return ob.tss } -// LastBlock get external last block height +// WithTSS attaches a new tss signer to the observer. +func (ob *Observer) WithTSS(tss interfaces.TSSSigner) *Observer { + ob.tss = tss + return ob +} + +// LastBlock get external last block height. func (ob *Observer) LastBlock() uint64 { return atomic.LoadUint64(&ob.lastBlock) } -// WithLastBlock set external last block height +// WithLastBlock set external last block height. func (ob *Observer) WithLastBlock(lastBlock uint64) *Observer { atomic.StoreUint64(&ob.lastBlock, lastBlock) return ob } -// LastBlockScanned get last block scanned (not necessarily caught up with external block; could be slow/paused) +// LastBlockScanned get last block scanned (not necessarily caught up with the chain; could be slow/paused). func (ob *Observer) LastBlockScanned() uint64 { height := atomic.LoadUint64(&ob.lastBlockScanned) return height } -// WithLastBlockScanned set last block scanned (not necessarily caught up with external block; could be slow/paused) +// WithLastBlockScanned set last block scanned (not necessarily caught up with the chain; could be slow/paused). func (ob *Observer) WithLastBlockScanned(blockNumber uint64) *Observer { atomic.StoreUint64(&ob.lastBlockScanned, blockNumber) metrics.LastScannedBlockNumber.WithLabelValues(ob.chain.ChainName.String()).Set(float64(blockNumber)) return ob } -// BlockCache returns the block cache for the observer +// BlockCache returns the block cache for the observer. func (ob *Observer) BlockCache() *lru.Cache { return ob.blockCache } -// WithBlockCache attaches a new block cache to the observer +// WithBlockCache attaches a new block cache to the observer. func (ob *Observer) WithBlockCache(cache *lru.Cache) *Observer { ob.blockCache = cache return ob } -// Stop returns the stop channel for the observer +// DB returns the database for the observer. +func (ob *Observer) DB() *gorm.DB { + return ob.db +} + +// Logger returns the logger for the observer. +func (ob *Observer) Logger() ObserverLogger { + return ob.logger +} + +// WithLogger attaches a new logger to the observer. +func (ob *Observer) WithLogger(logger Logger) *Observer { + chainLogger := logger.Std.With().Int64("chain", ob.chain.ChainId).Logger() + ob.logger = ObserverLogger{ + Chain: chainLogger, + Inbound: chainLogger.With().Str("module", "inbound").Logger(), + Outbound: chainLogger.With().Str("module", "outbound").Logger(), + GasPrice: chainLogger.With().Str("module", "gasprice").Logger(), + Headers: chainLogger.With().Str("module", "headers").Logger(), + Compliance: logger.Compliance, + } + return ob +} + +// Stop returns the stop channel for the observer. func (ob *Observer) Stop() chan struct{} { return ob.stop } -// TelemetryServer returns the telemetry server for the observer -func (ob *Observer) TelemetryServer() *metrics.TelemetryServer { - return ob.ts +// OpenDB open sql database in the given path. +func (ob *Observer) OpenDB(dbPath string) error { + if dbPath != "" { + // create db path if not exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) + if err != nil { + return errors.Wrap(err, "error creating db path") + } + } + + // open db by chain name + chainName := ob.chain.ChainName.String() + path := fmt.Sprintf("%s/%s", dbPath, chainName) + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { + return errors.Wrap(err, "error opening db") + } + + // migrate db + err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + &clienttypes.LastBlockSQLType{}) + if err != nil { + return errors.Wrap(err, "error migrating db") + } + ob.db = db + } + return nil } -// LoadLastBlockScanned loads last scanned block from environment variable or from database -// The last scanned block is the height from which the observer should start scanning for inbound transactions +// LoadLastBlockScanned loads last scanned block from environment variable or from database. +// The last scanned block is the height from which the observer should continue scanning. func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool, err error) { // get environment variable - envvar := ob.chain.ChainName.String() + "_SCAN_FROM" + envvar := EnvVarLatestBlockByChain(ob.chain) scanFromBlock := os.Getenv(envvar) // load from environment variable if set if scanFromBlock != "" { logger.Info(). Msgf("LoadLastBlockScanned: envvar %s is set; scan from block %s", envvar, scanFromBlock) - if scanFromBlock == clienttypes.EnvVarLatest { + if scanFromBlock == EnvVarLatestBlock { return true, nil } blockNumber, err := strconv.ParseUint(scanFromBlock, 10, 64) @@ -223,12 +290,12 @@ func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool return false, nil } -// WriteLastBlockScannedToDB saves the last scanned block to the database +// WriteLastBlockScannedToDB saves the last scanned block to the database. func (ob *Observer) WriteLastBlockScannedToDB(lastScannedBlock uint64) error { return ob.db.Save(clienttypes.ToLastBlockSQLType(lastScannedBlock)).Error } -// ReadLastBlockScannedFromDB reads the last scanned block from the database +// ReadLastBlockScannedFromDB reads the last scanned block from the database. func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { var lastBlock clienttypes.LastBlockSQLType if err := ob.db.First(&lastBlock, clienttypes.LastBlockNumID).Error; err != nil { @@ -238,33 +305,7 @@ func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { return lastBlock.Num, nil } -// OpenDB open sql database in the given path -func (ob *Observer) OpenDB(dbPath string) error { - if dbPath != "" { - // create db path if not exist - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return errors.Wrap(err, "error creating db path") - } - } - - // open db by chain name - chainName := ob.chain.ChainName.String() - path := fmt.Sprintf("%s/%s", dbPath, chainName) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) - if err != nil { - return errors.Wrap(err, "error opening db") - } - - // migrate db - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return errors.Wrap(err, "error migrating db") - } - ob.db = db - } - return nil +// EnvVarLatestBlock returns the environment variable for the latest block by chain. +func EnvVarLatestBlockByChain(chain chains.Chain) string { + return chain.ChainName.String() + "_SCAN_FROM" } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 007dca4199..13d724d497 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -5,6 +5,7 @@ import ( "testing" lru "github.com/hashicorp/golang-lru" + "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" @@ -25,7 +26,7 @@ func createTempDir(t *testing.T) string { } // createObserver creates a new observer for testing -func createObserver(t *testing.T) *base.Observer { +func createObserver(t *testing.T, dbPath string) *base.Observer { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) @@ -33,10 +34,19 @@ func createObserver(t *testing.T) *base.Observer { zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize - dbPath := createTempDir(t) // create observer - ob, err := base.NewObserver(chain, chainParams, zetacoreContext, zetacoreClient, tss, blockCacheSize, dbPath, nil) + logger := base.DefaultLogger() + ob, err := base.NewObserver( + chain, + chainParams, + zetacoreContext, + zetacoreClient, + tss, + blockCacheSize, + dbPath, + logger, + ) require.NoError(t, err) return ob @@ -113,7 +123,7 @@ func TestNewObserver(t *testing.T) { tt.tss, tt.blockCacheSize, tt.dbPath, - nil, + base.DefaultLogger(), ) if tt.fail { require.ErrorContains(t, err, tt.message) @@ -127,9 +137,11 @@ func TestNewObserver(t *testing.T) { } } -func TestObserverSetters(t *testing.T) { +func TestObserverGetterAndSetter(t *testing.T) { + dbPath := createTempDir(t) + t.Run("should be able to update chain", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update chain newChain := chains.BscMainnet @@ -137,23 +149,39 @@ func TestObserverSetters(t *testing.T) { require.Equal(t, newChain, ob.Chain()) }) t.Run("should be able to update chain params", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update chain params newChainParams := *sample.ChainParams(chains.BscMainnet.ChainId) ob = ob.WithChainParams(newChainParams) require.True(t, observertypes.ChainParamsEqual(newChainParams, ob.ChainParams())) }) + t.Run("should be able to update zetacore context", func(t *testing.T) { + ob := createObserver(t, dbPath) + + // update zetacore context + newZetacoreContext := context.NewZetacoreContext(config.NewConfig()) + ob = ob.WithZetacoreContext(newZetacoreContext) + require.Equal(t, newZetacoreContext, ob.ZetacoreContext()) + }) t.Run("should be able to update zetacore client", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update zetacore client newZetacoreClient := mocks.NewMockZetacoreClient() ob = ob.WithZetacoreClient(newZetacoreClient) require.Equal(t, newZetacoreClient, ob.ZetacoreClient()) }) + t.Run("should be able to update tss", func(t *testing.T) { + ob := createObserver(t, dbPath) + + // update tss + newTSS := mocks.NewTSSAthens3() + ob = ob.WithTSS(newTSS) + require.Equal(t, newTSS, ob.TSS()) + }) t.Run("should be able to update last block", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update last block newLastBlock := uint64(100) @@ -161,7 +189,7 @@ func TestObserverSetters(t *testing.T) { require.Equal(t, newLastBlock, ob.LastBlock()) }) t.Run("should be able to update last block scanned", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update last block scanned newLastBlockScanned := uint64(100) @@ -169,7 +197,7 @@ func TestObserverSetters(t *testing.T) { require.Equal(t, newLastBlockScanned, ob.LastBlockScanned()) }) t.Run("should be able to update block cache", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update block cache newBlockCache, err := lru.New(200) @@ -178,11 +206,29 @@ func TestObserverSetters(t *testing.T) { ob = ob.WithBlockCache(newBlockCache) require.Equal(t, newBlockCache, ob.BlockCache()) }) + t.Run("should be able to get database", func(t *testing.T) { + ob := createObserver(t, dbPath) + + db := ob.DB() + require.NotNil(t, db) + }) + t.Run("should be able to get logger", func(t *testing.T) { + ob := createObserver(t, dbPath) + logger := ob.Logger() + + // should be able to print log + logger.Chain.Info().Msg("print chain log") + logger.Inbound.Info().Msg("print inbound log") + logger.Outbound.Info().Msg("print outbound log") + logger.GasPrice.Info().Msg("print gasprice log") + logger.Headers.Info().Msg("print headers log") + logger.Compliance.Info().Msg("print compliance log") + }) } func TestOpenDB(t *testing.T) { - ob := createObserver(t) dbPath := createTempDir(t) + ob := createObserver(t, dbPath) t.Run("should be able to open db", func(t *testing.T) { err := ob.OpenDB(dbPath) @@ -194,9 +240,75 @@ func TestOpenDB(t *testing.T) { }) } +func TestLoadLastBlockScanned(t *testing.T) { + chain := chains.Ethereum + envvar := base.EnvVarLatestBlockByChain(chain) + + t.Run("should be able to load last block scanned", func(t *testing.T) { + // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) + ob.WriteLastBlockScannedToDB(100) + + // read last block scanned + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + require.NoError(t, err) + require.EqualValues(t, 100, ob.LastBlockScanned()) + require.False(t, fromLatest) + }) + t.Run("should use latest block if last block scanned not found", func(t *testing.T) { + // create empty db + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) + + // read last block scanned + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + require.NoError(t, err) + require.True(t, fromLatest) + }) + t.Run("should overwrite last block scanned if env var is set", func(t *testing.T) { + // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) + ob.WriteLastBlockScannedToDB(100) + + // set env var + os.Setenv(envvar, "101") + + // read last block scanned + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + require.NoError(t, err) + require.EqualValues(t, 101, ob.LastBlockScanned()) + require.False(t, fromLatest) + + // set env var to 'latest' + os.Setenv(envvar, base.EnvVarLatestBlock) + + // read last block scanned + fromLatest, err = ob.LoadLastBlockScanned(log.Logger) + require.NoError(t, err) + require.True(t, fromLatest) + }) + t.Run("should return error on invalid env var", func(t *testing.T) { + // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) + + // set invalid env var + os.Setenv(envvar, "invalid") + + // read last block scanned + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + require.Error(t, err) + require.False(t, fromLatest) + }) +} + func TestReadWriteLastBlockScannedToDB(t *testing.T) { t.Run("should be able to write and read last block scanned to db", func(t *testing.T) { - ob := createObserver(t) + // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) err := ob.WriteLastBlockScannedToDB(100) require.NoError(t, err) @@ -205,7 +317,10 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { require.EqualValues(t, 100, lastBlockScanned) }) t.Run("should return error when last block scanned not found in db", func(t *testing.T) { - ob := createObserver(t) + // create empty db + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) + lastScannedBlock, err := ob.ReadLastBlockScannedFromDB() require.Error(t, err) require.Zero(t, lastScannedBlock) diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index 1e768d21ea..622e1a47a8 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -1,33 +1,24 @@ package base import ( - "github.com/rs/zerolog" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/zetaclient/metrics" ) -// Signer is the base chain signer for external chains +// Signer is the base signer type Signer struct { - // the external chain + // chain contains static information about the external chain chain chains.Chain - // zetacore context + // zetacoreContext is the Zetacore client to interact with ZetaChain zetacoreContext *context.ZetacoreContext - // tss signer + // tss is the TSS signer tss interfaces.TSSSigner - // telemetry server - ts *metrics.TelemetryServer - - // the standard logger - logger zerolog.Logger - - // the compliance logger - loggerCompliance zerolog.Logger + // logger contains the loggers used by signer + logger Logger } // NewSigner creates a new base signer @@ -35,16 +26,53 @@ func NewSigner( chain chains.Chain, zetacoreContext *context.ZetacoreContext, tss interfaces.TSSSigner, - logger zerolog.Logger, - loggerCompliance zerolog.Logger, - ts *metrics.TelemetryServer, + logger Logger, ) *Signer { return &Signer{ - chain: chain, - zetacoreContext: zetacoreContext, - tss: tss, - logger: logger, - loggerCompliance: loggerCompliance, - ts: ts, + chain: chain, + zetacoreContext: zetacoreContext, + tss: tss, + logger: Logger{ + Std: logger.Std.With().Int64("chain", chain.ChainId).Str("module", "signer").Logger(), + Compliance: logger.Compliance, + }, } } + +// Chain returns the chain for the signer +func (s *Signer) Chain() chains.Chain { + return s.chain +} + +// WithChain attaches a new chain to the signer +func (s *Signer) WithChain(chain chains.Chain) *Signer { + s.chain = chain + return s +} + +// ZetacoreContext returns the zetacore context for the signer +func (s *Signer) ZetacoreContext() *context.ZetacoreContext { + return s.zetacoreContext +} + +// WithZetacoreContext attaches a new zetacore context to the signer +func (s *Signer) WithZetacoreContext(context *context.ZetacoreContext) *Signer { + s.zetacoreContext = context + return s +} + +// Tss returns the tss signer for the signer +func (s *Signer) TSS() interfaces.TSSSigner { + return s.tss +} + +// WithTSS attaches a new tss signer to the signer +func (s *Signer) WithTSS(tss interfaces.TSSSigner) *Signer { + s.tss = tss + return s +} + +// Logger returns the logger for the signer +func (s *Signer) Logger() Logger { + return s.logger +} diff --git a/zetaclient/chains/base/signer_test.go b/zetaclient/chains/base/signer_test.go new file mode 100644 index 0000000000..7c3628637b --- /dev/null +++ b/zetaclient/chains/base/signer_test.go @@ -0,0 +1,65 @@ +package base_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" +) + +// createSigner creates a new signer for testing +func createSigner(_ *testing.T) *base.Signer { + // constructor parameters + chain := chains.Ethereum + zetacoreContext := context.NewZetacoreContext(config.NewConfig()) + tss := mocks.NewTSSMainnet() + logger := base.DefaultLogger() + + // create signer + return base.NewSigner(chain, zetacoreContext, tss, logger) +} + +func TestNewSigner(t *testing.T) { + signer := createSigner(t) + require.NotNil(t, signer) +} + +func TestSignerGetterAndSetter(t *testing.T) { + t.Run("should be able to update chain", func(t *testing.T) { + signer := createSigner(t) + + // update chain + newChain := chains.BscMainnet + signer = signer.WithChain(chains.BscMainnet) + require.Equal(t, newChain, signer.Chain()) + }) + t.Run("should be able to update zetacore context", func(t *testing.T) { + signer := createSigner(t) + + // update zetacore context + newZetacoreContext := context.NewZetacoreContext(config.NewConfig()) + signer = signer.WithZetacoreContext(newZetacoreContext) + require.Equal(t, newZetacoreContext, signer.ZetacoreContext()) + }) + t.Run("should be able to update tss", func(t *testing.T) { + signer := createSigner(t) + + // update tss + newTSS := mocks.NewTSSAthens3() + signer = signer.WithTSS(newTSS) + require.Equal(t, newTSS, signer.TSS()) + }) + t.Run("should be able to get logger", func(t *testing.T) { + ob := createSigner(t) + logger := ob.Logger() + + // should be able to print log + logger.Std.Info().Msg("print standard log") + logger.Compliance.Info().Msg("print compliance log") + }) +} diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 272f0a06d9..5de30619bf 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -581,7 +581,7 @@ func (ob *Observer) LoadLastScannedBlock() error { if scanFromBlock != "" { ob.logger.Chain.Info(). Msgf("LoadLastScannedBlock: envvar %s is set; scan from block %s", envvar, scanFromBlock) - if scanFromBlock == clienttypes.EnvVarLatest { + if scanFromBlock == base.EnvVarLatestBlock { header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) if err != nil { return err diff --git a/zetaclient/types/defs.go b/zetaclient/types/defs.go deleted file mode 100644 index 8bc79913fe..0000000000 --- a/zetaclient/types/defs.go +++ /dev/null @@ -1,5 +0,0 @@ -package types - -const ( - EnvVarLatest = "latest" -) From f6784ee9005b1e8a3d59f9f03d888a01c010b0b2 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 17 Jun 2024 10:33:55 -0500 Subject: [PATCH 06/21] added changelog entry --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 1dfe34192d..bee21632cb 100644 --- a/changelog.md +++ b/changelog.md @@ -46,6 +46,7 @@ * [2269](https://github.com/zeta-chain/node/pull/2269) - refactor MsgUpdateCrosschainFlags into MsgEnableCCTX, MsgDisableCCTX and MsgUpdateGasPriceIncreaseFlags * [2306](https://github.com/zeta-chain/node/pull/2306) - refactor zetaclient outbound transaction signing logic * [2296](https://github.com/zeta-chain/node/pull/2296) - move `testdata` package to `testutil` to organize test-related utilities +* [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs ### Tests From 13e62511672318cbd4e6044587c33b89072d3544 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Wed, 19 Jun 2024 09:23:05 -0500 Subject: [PATCH 07/21] integrated base signer into evm/bitcoin; integrated base observer into evm --- cmd/zetaclientd/debug.go | 5 - cmd/zetaclientd/start.go | 4 +- cmd/zetaclientd/utils.go | 67 ++- zetaclient/chains/base/logger_test.go | 9 +- zetaclient/chains/base/observer.go | 134 ++++-- zetaclient/chains/base/observer_test.go | 195 +++++--- zetaclient/chains/base/signer.go | 24 +- zetaclient/chains/base/signer_test.go | 11 +- .../chains/bitcoin/observer/live_test.go | 3 +- .../chains/bitcoin/observer/observer.go | 9 +- .../chains/bitcoin/observer/observer_test.go | 5 +- zetaclient/chains/bitcoin/signer/signer.go | 78 ++-- .../bitcoin/signer/signer_keysign_test.go | 3 +- .../chains/bitcoin/signer/signer_test.go | 28 +- zetaclient/chains/evm/observer/inbound.go | 210 ++++----- zetaclient/chains/evm/observer/observer.go | 427 ++++++------------ .../chains/evm/observer/observer_test.go | 220 ++++++++- zetaclient/chains/evm/observer/outbound.go | 62 +-- .../chains/evm/signer/outbound_data_test.go | 2 +- zetaclient/chains/evm/signer/signer.go | 120 ++--- zetaclient/chains/evm/signer/signer_test.go | 57 ++- zetaclient/chains/interfaces/interfaces.go | 5 +- .../supplychecker/zeta_supply_checker.go | 2 +- zetaclient/testutils/mocks/evm_rpc.go | 64 ++- zetaclient/testutils/mocks/tss_signer.go | 13 +- zetaclient/testutils/os.go | 15 + zetaclient/tss/tss_signer.go | 12 +- 27 files changed, 1045 insertions(+), 739 deletions(-) create mode 100644 zetaclient/testutils/os.go diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index 881997d0ec..4e10b24c25 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "io" "strconv" "strings" "sync" @@ -13,7 +12,6 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/onrik/ethrpc" - "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/zeta-chain/zetacore/pkg/chains" @@ -61,7 +59,6 @@ func DebugCmd() *cobra.Command { } inboundHash := args[0] var ballotIdentifier string - chainLogger := zerolog.New(io.Discard).Level(zerolog.Disabled) // create a new zetacore client client, err := zetacore.NewClient( @@ -93,7 +90,6 @@ func DebugCmd() *cobra.Command { Mu: &sync.Mutex{}, } evmObserver.WithZetacoreClient(client) - evmObserver.WithLogger(chainLogger) var ethRPC *ethrpc.EthRPC var client *ethclient.Client coinType := coin.CoinType_Cmd @@ -172,7 +168,6 @@ func DebugCmd() *cobra.Command { Mu: &sync.Mutex{}, } btcObserver.WithZetacoreClient(client) - btcObserver.WithLogger(chainLogger) btcObserver.WithChain(*chains.GetChainFromChainID(chainID)) connCfg := &rpcclient.ConnConfig{ Host: cfg.BitcoinConfig.RPCHost, diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index c815b616c0..1337fb4408 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -268,7 +268,7 @@ func start(_ *cobra.Command, _ []string) error { } // CreateSignerMap: This creates a map of all signers for each chain . Each signer is responsible for signing transactions for a particular chain - signerMap, err := CreateSignerMap(appContext, tss, logger, telemetryServer) + signerMap, err := CreateSignerMap(appContext, tss, telemetryServer, logger) if err != nil { log.Error().Err(err).Msg("CreateSignerMap") return err @@ -282,7 +282,7 @@ func start(_ *cobra.Command, _ []string) error { dbpath := filepath.Join(userDir, ".zetaclient/chainobserver") // Creates a map of all chain observers for each chain. Each chain observer is responsible for observing events on the chain and processing them. - observerMap, err := CreateChainObserverMap(appContext, zetacoreClient, tss, dbpath, logger, telemetryServer) + observerMap, err := CreateChainObserverMap(appContext, zetacoreClient, tss, dbpath, telemetryServer, logger) if err != nil { startLogger.Err(err).Msg("CreateChainObserverMap") return err diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 521c6dc858..41d21c5a97 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -1,8 +1,11 @@ package main import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -54,13 +57,14 @@ func CreateZetacoreClient( return client, nil } +// CreateSignerMap creates a map of ChainSigners for all chains in the config func CreateSignerMap( appContext *context.AppContext, tss interfaces.TSSSigner, - logger base.Logger, ts *metrics.TelemetryServer, + logger base.Logger, ) (map[int64]interfaces.ChainSigner, error) { - coreContext := appContext.ZetacoreContext() + zetacoreContext := appContext.ZetacoreContext() signerMap := make(map[int64]interfaces.ChainSigner) // EVM signers @@ -68,7 +72,7 @@ func CreateSignerMap( if evmConfig.Chain.IsZetaChain() { continue } - evmChainParams, found := coreContext.GetEVMChainParams(evmConfig.Chain.ChainId) + evmChainParams, found := zetacoreContext.GetEVMChainParams(evmConfig.Chain.ChainId) if !found { logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue @@ -77,15 +81,15 @@ func CreateSignerMap( erc20CustodyAddress := ethcommon.HexToAddress(evmChainParams.Erc20CustodyContractAddress) signer, err := evmsigner.NewSigner( evmConfig.Chain, - evmConfig.Endpoint, + zetacoreContext, tss, + ts, + logger, + evmConfig.Endpoint, config.GetConnectorABI(), config.GetERC20CustodyABI(), mpiAddress, - erc20CustodyAddress, - coreContext, - logger, - ts) + erc20CustodyAddress) if err != nil { logger.Std.Error().Err(err).Msgf("NewEVMSigner error for chain %s", evmConfig.Chain.String()) continue @@ -95,7 +99,7 @@ func CreateSignerMap( // BTC signer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - signer, err := btcsigner.NewSigner(btcConfig, tss, logger, ts, coreContext) + signer, err := btcsigner.NewSigner(btcChain, zetacoreContext, tss, ts, logger, btcConfig) if err != nil { logger.Std.Error().Err(err).Msgf("NewBTCSigner error for chain %s", btcChain.String()) } else { @@ -112,31 +116,68 @@ func CreateChainObserverMap( zetacoreClient *zetacore.Client, tss interfaces.TSSSigner, dbpath string, - logger base.Logger, ts *metrics.TelemetryServer, + logger base.Logger, ) (map[int64]interfaces.ChainObserver, error) { + zetacoreContext := appContext.ZetacoreContext() observerMap := make(map[int64]interfaces.ChainObserver) // EVM observers for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { if evmConfig.Chain.IsZetaChain() { continue } - _, found := appContext.ZetacoreContext().GetEVMChainParams(evmConfig.Chain.ChainId) + chainParams, found := zetacoreContext.GetEVMChainParams(evmConfig.Chain.ChainId) if !found { logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } - co, err := evmobserver.NewObserver(appContext, zetacoreClient, tss, dbpath, logger, evmConfig, ts) + + // create EVM client + evmClient, err := ethclient.Dial(evmConfig.Endpoint) + if err != nil { + logger.Std.Error().Err(err).Msgf("error dailing endpoint %s", evmConfig.Endpoint) + continue + } + + // create EVM chain observer + co, err := evmobserver.NewObserver( + evmConfig, + evmClient, + *chainParams, + zetacoreContext, + zetacoreClient, + tss, + dbpath, + logger, + ts, + ) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) continue } observerMap[evmConfig.Chain.ChainId] = co } + // BTC observer + _, chainParams, found := appContext.ZetacoreContext().GetBTCChainParams() + if !found { + return nil, fmt.Errorf("bitcoin chains params not found") + } + + // create BTC chain observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - co, err := btcobserver.NewObserver(appContext, btcChain, zetacoreClient, tss, dbpath, logger, btcConfig, ts) + co, err := btcobserver.NewObserver( + *chainParams, + appContext, + btcChain, + zetacoreClient, + tss, + dbpath, + logger, + btcConfig, + ts, + ) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) diff --git a/zetaclient/chains/base/logger_test.go b/zetaclient/chains/base/logger_test.go index 07c2859b0e..7b9910759a 100644 --- a/zetaclient/chains/base/logger_test.go +++ b/zetaclient/chains/base/logger_test.go @@ -7,6 +7,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/testutils" ) func TestInitLogger(t *testing.T) { @@ -23,7 +24,7 @@ func TestInitLogger(t *testing.T) { LogFormat: "json", LogLevel: 1, // zerolog.InfoLevel, ComplianceConfig: config.ComplianceConfig{ - LogPath: createTempDir(t), + LogPath: testutils.CreateTempDir(t), }, }, fail: false, @@ -34,7 +35,7 @@ func TestInitLogger(t *testing.T) { LogFormat: "text", LogLevel: 2, // zerolog.WarnLevel, ComplianceConfig: config.ComplianceConfig{ - LogPath: createTempDir(t), + LogPath: testutils.CreateTempDir(t), }, }, fail: false, @@ -45,7 +46,7 @@ func TestInitLogger(t *testing.T) { LogFormat: "unknown", LogLevel: 3, // zerolog.ErrorLevel, ComplianceConfig: config.ComplianceConfig{ - LogPath: createTempDir(t), + LogPath: testutils.CreateTempDir(t), }, }, fail: false, @@ -57,7 +58,7 @@ func TestInitLogger(t *testing.T) { LogLevel: 4, // zerolog.DebugLevel, LogSampler: true, ComplianceConfig: config.ComplianceConfig{ - LogPath: createTempDir(t), + LogPath: testutils.CreateTempDir(t), }, }, }, diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 0f148b6a87..a77eb706c9 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -21,14 +21,20 @@ import ( ) const ( - // EnvVarLatestBlock is the environment variable to indicate latest block height + // EnvVarLatestBlock is the environment variable that forces the observer to scan from the latest block EnvVarLatestBlock = "latest" - // DefaultBlockCacheSize is the default size of the block cache + // DefaultBlockCacheSize is the default number of blocks that the observer will keep in cache for performance (without RPC calls) + // Cached blocks can be used to get block information and verify transactions DefaultBlockCacheSize = 1000 + + // DefaultHeaderCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) + // Cached headers can be used to get header information + DefaultHeaderCacheSize = 1000 ) -// Observer is the base observer. +// Observer is the base structure for chain observers, grouping the common logic for each chain observer client. +// The common logic includes: chain, chainParams, contexts, zetacore client, tss, lastBlock, db, metrics, loggers etc. type Observer struct { // chain contains static information about the observed chain chain chains.Chain @@ -54,9 +60,15 @@ type Observer struct { // blockCache is the cache for blocks blockCache *lru.Cache + // headerCache is the cache for headers + headerCache *lru.Cache + // db is the database to persist data db *gorm.DB + // ts is the telemetry server for metrics + ts *metrics.TelemetryServer + // logger contains the loggers used by observer logger ObserverLogger @@ -72,7 +84,8 @@ func NewObserver( zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, blockCacheSize int, - dbPath string, + headerCacheSize int, + ts *metrics.TelemetryServer, logger Logger, ) (*Observer, error) { ob := Observer{ @@ -83,6 +96,7 @@ func NewObserver( tss: tss, lastBlock: 0, lastBlockScanned: 0, + ts: ts, stop: make(chan struct{}), } @@ -96,10 +110,10 @@ func NewObserver( return nil, errors.Wrap(err, "error creating block cache") } - // open database - err = ob.OpenDB(dbPath) + // create header cache + ob.headerCache, err = lru.New(headerCacheSize) if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("error opening observer db for chain: %s", chain.ChainName)) + return nil, errors.Wrap(err, "error creating header cache") } return &ob, nil @@ -195,14 +209,36 @@ func (ob *Observer) WithBlockCache(cache *lru.Cache) *Observer { return ob } +// HeaderCache returns the header cache for the observer. +func (ob *Observer) HeaderCache() *lru.Cache { + return ob.headerCache +} + +// WithHeaderCache attaches a new header cache to the observer. +func (ob *Observer) WithHeaderCache(cache *lru.Cache) *Observer { + ob.headerCache = cache + return ob +} + // DB returns the database for the observer. func (ob *Observer) DB() *gorm.DB { return ob.db } +// WithTelemetryServer attaches a new telemetry server to the observer. +func (ob *Observer) WithTelemetryServer(ts *metrics.TelemetryServer) *Observer { + ob.ts = ts + return ob +} + +// TelemetryServer returns the telemetry server for the observer. +func (ob *Observer) TelemetryServer() *metrics.TelemetryServer { + return ob.ts +} + // Logger returns the logger for the observer. -func (ob *Observer) Logger() ObserverLogger { - return ob.logger +func (ob *Observer) Logger() *ObserverLogger { + return &ob.logger } // WithLogger attaches a new logger to the observer. @@ -219,45 +255,55 @@ func (ob *Observer) WithLogger(logger Logger) *Observer { return ob } -// Stop returns the stop channel for the observer. -func (ob *Observer) Stop() chan struct{} { +// StopChannel returns the stop channel for the observer. +func (ob *Observer) StopChannel() chan struct{} { return ob.stop } // OpenDB open sql database in the given path. func (ob *Observer) OpenDB(dbPath string) error { - if dbPath != "" { - // create db path if not exist - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return errors.Wrap(err, "error creating db path") - } - } - - // open db by chain name - chainName := ob.chain.ChainName.String() - path := fmt.Sprintf("%s/%s", dbPath, chainName) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + // create db path if not exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) if err != nil { - return errors.Wrap(err, "error opening db") + return errors.Wrapf(err, "error creating db path: %s", dbPath) } + } - // migrate db - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return errors.Wrap(err, "error migrating db") - } - ob.db = db + // open db by chain name + chainName := ob.chain.ChainName.String() + path := fmt.Sprintf("%s/%s", dbPath, chainName) + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { + return errors.Wrap(err, "error opening db") + } + + // migrate db + err = db.AutoMigrate(&clienttypes.LastBlockSQLType{}) + if err != nil { + return errors.Wrap(err, "error migrating db") + } + ob.db = db + + return nil +} + +// CloseDB close the database. +func (ob *Observer) CloseDB() error { + dbInst, err := ob.db.DB() + if err != nil { + return fmt.Errorf("error getting database instance: %w", err) + } + err = dbInst.Close() + if err != nil { + return fmt.Errorf("error closing database: %w", err) } return nil } // LoadLastBlockScanned loads last scanned block from environment variable or from database. // The last scanned block is the height from which the observer should continue scanning. -func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool, err error) { +func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) error { // get environment variable envvar := EnvVarLatestBlockByChain(ob.chain) scanFromBlock := os.Getenv(envvar) @@ -267,27 +313,33 @@ func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool logger.Info(). Msgf("LoadLastBlockScanned: envvar %s is set; scan from block %s", envvar, scanFromBlock) if scanFromBlock == EnvVarLatestBlock { - return true, nil + return nil } blockNumber, err := strconv.ParseUint(scanFromBlock, 10, 64) if err != nil { - return false, err + return err } ob.WithLastBlockScanned(blockNumber) - return false, nil + return nil } // load from DB otherwise. If not found, start from latest block blockNumber, err := ob.ReadLastBlockScannedFromDB() if err != nil { - logger.Info().Msgf("LoadLastBlockScanned: chain %d starts scanning from latest block", ob.chain.ChainId) - return true, nil + logger.Info().Msgf("LoadLastBlockScanned: last scanned block not found in db for chain %d", ob.chain.ChainId) + return nil } ob.WithLastBlockScanned(blockNumber) logger.Info(). Msgf("LoadLastBlockScanned: chain %d starts scanning from block %d", ob.chain.ChainId, ob.LastBlockScanned()) - return false, nil + return nil +} + +// SaveLastBlockScanned saves the last scanned block to memory and database. +func (ob *Observer) SaveLastBlockScanned(blockNumber uint64) error { + ob.WithLastBlockScanned(blockNumber) + return ob.WriteLastBlockScannedToDB(blockNumber) } // WriteLastBlockScannedToDB saves the last scanned block to the database. @@ -307,5 +359,5 @@ func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { // EnvVarLatestBlock returns the environment variable for the latest block by chain. func EnvVarLatestBlockByChain(chain chains.Chain) string { - return chain.ChainName.String() + "_SCAN_FROM" + return fmt.Sprintf("CHAIN_%d_SCAN_FROM", chain.ChainId) } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 13d724d497..c82a351b2a 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -15,25 +15,19 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -// create a temporary directory for testing -func createTempDir(t *testing.T) string { - tempPath, err := os.MkdirTemp("", "tempdir-") - require.NoError(t, err) - return tempPath -} - // createObserver creates a new observer for testing -func createObserver(t *testing.T, dbPath string) *base.Observer { +func createObserver(t *testing.T) *base.Observer { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) zetacoreContext := context.NewZetacoreContext(config.NewConfig()) zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() - blockCacheSize := base.DefaultBlockCacheSize // create observer logger := base.DefaultLogger() @@ -43,8 +37,9 @@ func createObserver(t *testing.T, dbPath string) *base.Observer { zetacoreContext, zetacoreClient, tss, - blockCacheSize, - dbPath, + base.DefaultBlockCacheSize, + base.DefaultHeaderCacheSize, + nil, logger, ) require.NoError(t, err) @@ -60,7 +55,7 @@ func TestNewObserver(t *testing.T) { zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize - dbPath := createTempDir(t) + headersCacheSize := base.DefaultHeaderCacheSize // test cases tests := []struct { @@ -71,7 +66,7 @@ func TestNewObserver(t *testing.T) { zetacoreClient interfaces.ZetacoreClient tss interfaces.TSSSigner blockCacheSize int - dbPath string + headerCacheSize int fail bool message string }{ @@ -83,7 +78,7 @@ func TestNewObserver(t *testing.T) { zetacoreClient: zetacoreClient, tss: tss, blockCacheSize: blockCacheSize, - dbPath: dbPath, + headerCacheSize: headersCacheSize, fail: false, }, { @@ -94,21 +89,21 @@ func TestNewObserver(t *testing.T) { zetacoreClient: zetacoreClient, tss: tss, blockCacheSize: 0, - dbPath: dbPath, + headerCacheSize: headersCacheSize, fail: true, message: "error creating block cache", }, { - name: "should return error on invalid db path", + name: "should return error on invalid header cache size", chain: chain, chainParams: chainParams, zetacoreContext: zetacoreContext, zetacoreClient: zetacoreClient, tss: tss, blockCacheSize: blockCacheSize, - dbPath: "/invalid/123db", + headerCacheSize: 0, fail: true, - message: "error opening observer db", + message: "error creating header cache", }, } @@ -122,7 +117,8 @@ func TestNewObserver(t *testing.T) { tt.zetacoreClient, tt.tss, tt.blockCacheSize, - tt.dbPath, + tt.headerCacheSize, + nil, base.DefaultLogger(), ) if tt.fail { @@ -138,10 +134,8 @@ func TestNewObserver(t *testing.T) { } func TestObserverGetterAndSetter(t *testing.T) { - dbPath := createTempDir(t) - t.Run("should be able to update chain", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update chain newChain := chains.BscMainnet @@ -149,7 +143,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newChain, ob.Chain()) }) t.Run("should be able to update chain params", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update chain params newChainParams := *sample.ChainParams(chains.BscMainnet.ChainId) @@ -157,7 +151,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.True(t, observertypes.ChainParamsEqual(newChainParams, ob.ChainParams())) }) t.Run("should be able to update zetacore context", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update zetacore context newZetacoreContext := context.NewZetacoreContext(config.NewConfig()) @@ -165,7 +159,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreContext, ob.ZetacoreContext()) }) t.Run("should be able to update zetacore client", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update zetacore client newZetacoreClient := mocks.NewMockZetacoreClient() @@ -173,7 +167,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreClient, ob.ZetacoreClient()) }) t.Run("should be able to update tss", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update tss newTSS := mocks.NewTSSAthens3() @@ -181,7 +175,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newTSS, ob.TSS()) }) t.Run("should be able to update last block", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update last block newLastBlock := uint64(100) @@ -189,15 +183,15 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newLastBlock, ob.LastBlock()) }) t.Run("should be able to update last block scanned", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update last block scanned newLastBlockScanned := uint64(100) ob = ob.WithLastBlockScanned(newLastBlockScanned) require.Equal(t, newLastBlockScanned, ob.LastBlockScanned()) }) - t.Run("should be able to update block cache", func(t *testing.T) { - ob := createObserver(t, dbPath) + t.Run("should be able to replace block cache", func(t *testing.T) { + ob := createObserver(t) // update block cache newBlockCache, err := lru.New(200) @@ -206,14 +200,35 @@ func TestObserverGetterAndSetter(t *testing.T) { ob = ob.WithBlockCache(newBlockCache) require.Equal(t, newBlockCache, ob.BlockCache()) }) + t.Run("should be able to replace header cache", func(t *testing.T) { + ob := createObserver(t) + + // update headers cache + newHeadersCache, err := lru.New(200) + require.NoError(t, err) + + ob = ob.WithHeaderCache(newHeadersCache) + require.Equal(t, newHeadersCache, ob.HeaderCache()) + }) t.Run("should be able to get database", func(t *testing.T) { - ob := createObserver(t, dbPath) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + ob.OpenDB(dbPath) db := ob.DB() require.NotNil(t, db) }) + t.Run("should be able to update telemetry server", func(t *testing.T) { + ob := createObserver(t) + + // update telemetry server + newServer := metrics.NewTelemetryServer() + ob = ob.WithTelemetryServer(newServer) + require.Equal(t, newServer, ob.TelemetryServer()) + }) t.Run("should be able to get logger", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) logger := ob.Logger() // should be able to print log @@ -226,13 +241,18 @@ func TestObserverGetterAndSetter(t *testing.T) { }) } -func TestOpenDB(t *testing.T) { - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) +func TestOpenCloseDB(t *testing.T) { + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) - t.Run("should be able to open db", func(t *testing.T) { + t.Run("should be able to open/close db", func(t *testing.T) { + // open db err := ob.OpenDB(dbPath) require.NoError(t, err) + + // close db + err = ob.CloseDB() + require.NoError(t, err) }) t.Run("should return error on invalid db path", func(t *testing.T) { err := ob.OpenDB("/invalid/123db") @@ -245,71 +265,116 @@ func TestLoadLastBlockScanned(t *testing.T) { envvar := base.EnvVarLatestBlockByChain(chain) t.Run("should be able to load last block scanned", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) + // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 100, ob.LastBlockScanned()) - require.False(t, fromLatest) }) - t.Run("should use latest block if last block scanned not found", func(t *testing.T) { - // create empty db - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + t.Run("latest block scanned should be 0 if not found in db", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.True(t, fromLatest) + require.EqualValues(t, 0, ob.LastBlockScanned()) }) t.Run("should overwrite last block scanned if env var is set", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) + // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // set env var os.Setenv(envvar, "101") // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 101, ob.LastBlockScanned()) - require.False(t, fromLatest) + }) + t.Run("last block scanned should remain 0 if env var is set to latest", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) + + // create db and write 100 as last block scanned + ob.WriteLastBlockScannedToDB(100) // set env var to 'latest' os.Setenv(envvar, base.EnvVarLatestBlock) - // read last block scanned - fromLatest, err = ob.LoadLastBlockScanned(log.Logger) + // last block scanned should remain 0 + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.True(t, fromLatest) + require.EqualValues(t, 0, ob.LastBlockScanned()) }) t.Run("should return error on invalid env var", func(t *testing.T) { - // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) // set invalid env var os.Setenv(envvar, "invalid") // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.Error(t, err) - require.False(t, fromLatest) + }) +} + +func TestSaveLastBlockScanned(t *testing.T) { + t.Run("should be able to save last block scanned", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) + + // save 100 as last block scanned + err = ob.SaveLastBlockScanned(100) + require.NoError(t, err) + + // check last block scanned in memory + require.EqualValues(t, 100, ob.LastBlockScanned()) + + // read last block scanned from db + lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() + require.NoError(t, err) + require.EqualValues(t, 100, lastBlockScanned) }) } func TestReadWriteLastBlockScannedToDB(t *testing.T) { t.Run("should be able to write and read last block scanned to db", func(t *testing.T) { - // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) - err := ob.WriteLastBlockScannedToDB(100) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) + + // write last block scanned + err = ob.WriteLastBlockScannedToDB(100) require.NoError(t, err) lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() @@ -318,8 +383,10 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { }) t.Run("should return error when last block scanned not found in db", func(t *testing.T) { // create empty db - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath) + require.NoError(t, err) lastScannedBlock, err := ob.ReadLastBlockScannedFromDB() require.Error(t, err) diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index 622e1a47a8..2585218767 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -4,9 +4,11 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" ) -// Signer is the base signer +// Signer is the base structure for grouping the common logic between chain signers. +// The common logic includes: chain, chainParams, contexts, tss, metrics, loggers etc. type Signer struct { // chain contains static information about the external chain chain chains.Chain @@ -17,6 +19,9 @@ type Signer struct { // tss is the TSS signer tss interfaces.TSSSigner + // ts is the telemetry server for metrics + ts *metrics.TelemetryServer + // logger contains the loggers used by signer logger Logger } @@ -26,12 +31,14 @@ func NewSigner( chain chains.Chain, zetacoreContext *context.ZetacoreContext, tss interfaces.TSSSigner, + ts *metrics.TelemetryServer, logger Logger, ) *Signer { return &Signer{ chain: chain, zetacoreContext: zetacoreContext, tss: tss, + ts: ts, logger: Logger{ Std: logger.Std.With().Int64("chain", chain.ChainId).Str("module", "signer").Logger(), Compliance: logger.Compliance, @@ -72,7 +79,18 @@ func (s *Signer) WithTSS(tss interfaces.TSSSigner) *Signer { return s } +// TelemetryServer returns the telemetry server for the signer +func (s *Signer) TelemetryServer() *metrics.TelemetryServer { + return s.ts +} + +// WithTelemetryServer attaches a new telemetry server to the signer +func (s *Signer) WithTelemetryServer(ts *metrics.TelemetryServer) *Signer { + s.ts = ts + return s +} + // Logger returns the logger for the signer -func (s *Signer) Logger() Logger { - return s.logger +func (s *Signer) Logger() *Logger { + return &s.logger } diff --git a/zetaclient/chains/base/signer_test.go b/zetaclient/chains/base/signer_test.go index 7c3628637b..960c508d6e 100644 --- a/zetaclient/chains/base/signer_test.go +++ b/zetaclient/chains/base/signer_test.go @@ -9,6 +9,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -21,7 +22,7 @@ func createSigner(_ *testing.T) *base.Signer { logger := base.DefaultLogger() // create signer - return base.NewSigner(chain, zetacoreContext, tss, logger) + return base.NewSigner(chain, zetacoreContext, tss, nil, logger) } func TestNewSigner(t *testing.T) { @@ -54,6 +55,14 @@ func TestSignerGetterAndSetter(t *testing.T) { signer = signer.WithTSS(newTSS) require.Equal(t, newTSS, signer.TSS()) }) + t.Run("should be able to update telemetry server", func(t *testing.T) { + signer := createSigner(t) + + // update telemetry server + newTs := metrics.NewTelemetryServer() + signer = signer.WithTelemetryServer(newTs) + require.Equal(t, newTs, signer.TelemetryServer()) + }) t.Run("should be able to get logger", func(t *testing.T) { ob := createSigner(t) logger := ob.Logger() diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/observer/live_test.go index dd1053f620..eaf99ee5ea 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/observer/live_test.go @@ -47,8 +47,9 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { tss := &mocks.TSS{ PrivKey: privateKey, } + params := mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 10) appContext := clientcontext.NewAppContext(&clientcontext.ZetacoreContext{}, config.Config{}) - client, err := NewObserver(appContext, chains.BitcoinRegtest, nil, tss, tempSQLiteDbPath, + client, err := NewObserver(params, appContext, chains.BitcoinRegtest, nil, tss, tempSQLiteDbPath, base.DefaultLogger(), config.BTCConfig{}, nil) suite.Require().NoError(err) suite.rpcClient, err = getRPCClient(18332) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 6b93ba12cd..f5067763ec 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -133,6 +133,7 @@ type Observer struct { // NewObserver returns a new Bitcoin chain observer func NewObserver( + chainParams observertypes.ChainParams, appcontext *context.AppContext, chain chains.Chain, zetacoreClient interfaces.ZetacoreClient, @@ -168,6 +169,7 @@ func NewObserver( Compliance: logger.Compliance, } + ob.params = chainParams ob.zetacoreClient = zetacoreClient ob.Tss = tss ob.coreContext = appcontext.ZetacoreContext() @@ -175,13 +177,6 @@ func NewObserver( ob.includedTxResults = make(map[string]*btcjson.GetTransactionResult) ob.broadcastedTx = make(map[string]string) - // set the Bitcoin chain params - _, chainParams, found := appcontext.ZetacoreContext().GetBTCChainParams() - if !found { - return nil, fmt.Errorf("btc chains params not initialized") - } - ob.params = *chainParams - // create the RPC client ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) connCfg := &rpcclient.ConnConfig{ diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 2d2494dd16..3c6478cf99 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -70,6 +70,7 @@ func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransaction func TestNewBitcoinObserver(t *testing.T) { t.Run("should return error because zetacore doesn't update zetacore context", func(t *testing.T) { cfg := config.NewConfig() + params := mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 10) coreContext := context.NewZetacoreContext(cfg) appContext := context.NewAppContext(coreContext, cfg) chain := chains.BitcoinMainnet @@ -79,8 +80,8 @@ func TestNewBitcoinObserver(t *testing.T) { btcCfg := cfg.BitcoinConfig ts := metrics.NewTelemetryServer() - client, err := NewObserver(appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, logger, btcCfg, ts) - require.ErrorContains(t, err, "btc chains params not initialized") + client, err := NewObserver(params, appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, logger, btcCfg, ts) + require.ErrorContains(t, err, "error ping the bitcoin server") require.Nil(t, client) }) } diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index f2d47fd988..d39b8a8f9d 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -15,7 +15,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/rs/zerolog" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/coin" @@ -30,7 +29,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/outboundprocessor" - "github.com/zeta-chain/zetacore/zetaclient/tss" ) const ( @@ -45,20 +43,25 @@ var _ interfaces.ChainSigner = &Signer{} // Signer deals with signing BTC transactions and implements the ChainSigner interface type Signer struct { - tssSigner interfaces.TSSSigner - rpcClient interfaces.BTCRPCClient - logger zerolog.Logger - loggerCompliance zerolog.Logger - ts *metrics.TelemetryServer - coreContext *context.ZetacoreContext + // base.Signer implements the base chain signer + base.Signer + + // client is the RPC client to interact with the Bitcoin chain + client interfaces.BTCRPCClient } +// NewSigner creates a new Bitcoin signer func NewSigner( - cfg config.BTCConfig, - tssSigner interfaces.TSSSigner, - logger base.Logger, + chain chains.Chain, + zetacoreContext *context.ZetacoreContext, + tss interfaces.TSSSigner, ts *metrics.TelemetryServer, - coreContext *context.ZetacoreContext) (*Signer, error) { + logger base.Logger, + cfg config.BTCConfig) (*Signer, error) { + // create base signer + baseSigner := base.NewSigner(chain, zetacoreContext, tss, ts, logger) + + // create the bitcoin rpc client using the provided config connCfg := &rpcclient.ConnConfig{ Host: cfg.RPCHost, User: cfg.RPCUsername, @@ -73,12 +76,8 @@ func NewSigner( } return &Signer{ - tssSigner: tssSigner, - rpcClient: client, - logger: logger.Std.With().Str("chain", "BTC").Str("module", "BTCSigner").Logger(), - loggerCompliance: logger.Compliance, - ts: ts, - coreContext: coreContext, + Signer: *baseSigner, + client: client, }, nil } @@ -130,12 +129,12 @@ func (signer *Signer) AddWithdrawTxOutputs( if remainingSats < 0 { return fmt.Errorf("remainder value is negative: %d", remainingSats) } else if remainingSats == nonceMark { - signer.logger.Info().Msgf("adjust remainder value to avoid duplicate nonce-mark: %d", remainingSats) + signer.Logger().Std.Info().Msgf("adjust remainder value to avoid duplicate nonce-mark: %d", remainingSats) remainingSats-- } // 1st output: the nonce-mark btc to TSS self - tssAddrP2WPKH := signer.tssSigner.BTCAddressWitnessPubkeyHash() + tssAddrP2WPKH := signer.TSS().BTCAddressWitnessPubkeyHash() payToSelfScript, err := bitcoin.PayToAddrScript(tssAddrP2WPKH) if err != nil { return err @@ -182,7 +181,10 @@ func (signer *Signer) SignWithdrawTx( // refresh unspent UTXOs and continue with keysign regardless of error err := observer.FetchUTXOs() if err != nil { - signer.logger.Error().Err(err).Msgf("SignWithdrawTx: FetchUTXOs error: nonce %d chain %d", nonce, chain.ChainId) + signer.Logger(). + Std.Error(). + Err(err). + Msgf("SignWithdrawTx: FetchUTXOs error: nonce %d chain %d", nonce, chain.ChainId) } // select N UTXOs to cover the total expense @@ -216,16 +218,16 @@ func (signer *Signer) SignWithdrawTx( return nil, err } if sizeLimit < bitcoin.BtcOutboundBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user - signer.logger.Info(). + signer.Logger().Std.Info(). Msgf("sizeLimit %d is less than BtcOutboundBytesWithdrawer %d for nonce %d", sizeLimit, txSize, nonce) } if txSize < bitcoin.OutboundBytesMin { // outbound shouldn't be blocked a low sizeLimit - signer.logger.Warn(). + signer.Logger().Std.Warn(). Msgf("txSize %d is less than outboundBytesMin %d; use outboundBytesMin", txSize, bitcoin.OutboundBytesMin) txSize = bitcoin.OutboundBytesMin } if txSize > bitcoin.OutboundBytesMax { // in case of accident - signer.logger.Warn(). + signer.Logger().Std.Warn(). Msgf("txSize %d is greater than outboundBytesMax %d; use outboundBytesMax", txSize, bitcoin.OutboundBytesMax) txSize = bitcoin.OutboundBytesMax } @@ -233,8 +235,10 @@ func (signer *Signer) SignWithdrawTx( // fee calculation // #nosec G701 always in range (checked above) fees := new(big.Int).Mul(big.NewInt(int64(txSize)), gasPrice) - signer.logger.Info().Msgf("bitcoin outbound nonce %d gasPrice %s size %d fees %s consolidated %d utxos of value %v", - nonce, gasPrice.String(), txSize, fees.String(), consolidatedUtxo, consolidatedValue) + signer.Logger(). + Std.Info(). + Msgf("bitcoin outbound nonce %d gasPrice %s size %d fees %s consolidated %d utxos of value %v", + nonce, gasPrice.String(), txSize, fees.String(), consolidatedUtxo, consolidatedValue) // add tx outputs err = signer.AddWithdrawTxOutputs(tx, to, total, amount, nonceMark, fees, cancelTx) @@ -260,11 +264,7 @@ func (signer *Signer) SignWithdrawTx( } } - tssSigner, ok := signer.tssSigner.(*tss.TSS) - if !ok { - return nil, fmt.Errorf("tssSigner is not a TSS") - } - sig65Bs, err := tssSigner.SignBatch(witnessHashes, height, nonce, &chain) + sig65Bs, err := signer.TSS().SignBatch(witnessHashes, height, nonce, chain.ChainId) if err != nil { return nil, fmt.Errorf("SignBatch error: %v", err) } @@ -278,7 +278,7 @@ func (signer *Signer) SignWithdrawTx( S: S, } - pkCompressed := signer.tssSigner.PubKeyCompressedBytes() + pkCompressed := signer.TSS().PubKeyCompressedBytes() hashType := txscript.SigHashAll txWitness := wire.TxWitness{append(sig.Serialize(), byte(hashType)), pkCompressed} tx.TxIn[ix].Witness = txWitness @@ -298,12 +298,12 @@ func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error { str := hex.EncodeToString(outBuff.Bytes()) fmt.Printf("BTCSigner: Transaction Data: %s\n", str) - hash, err := signer.rpcClient.SendRawTransaction(signedTx, true) + hash, err := signer.client.SendRawTransaction(signedTx, true) if err != nil { return err } - signer.logger.Info().Msgf("Broadcasting BTC tx , hash %s ", hash) + signer.Logger().Std.Info().Msgf("Broadcasting BTC tx , hash %s ", hash) return nil } @@ -318,11 +318,11 @@ func (signer *Signer) TryProcessOutbound( defer func() { outboundProcessor.EndTryProcess(outboundID) if err := recover(); err != nil { - signer.logger.Error().Msgf("BTC TryProcessOutbound: %s, caught panic error: %v", cctx.Index, err) + signer.Logger().Std.Error().Msgf("BTC TryProcessOutbound: %s, caught panic error: %v", cctx.Index, err) } }() - logger := signer.logger.With(). + logger := signer.Logger().Std.With(). Str("OutboundID", outboundID). Str("SendHash", cctx.Index). Logger() @@ -341,7 +341,7 @@ func (signer *Signer) TryProcessOutbound( logger.Error().Msgf("chain observer is not a bitcoin observer") return } - flags := signer.coreContext.GetCrossChainFlags() + flags := signer.ZetacoreContext().GetCrossChainFlags() if !flags.IsOutboundEnabled { logger.Info().Msgf("outbound is disabled") return @@ -375,7 +375,7 @@ func (signer *Signer) TryProcessOutbound( amount := float64(params.Amount.Uint64()) / 1e8 // Add 1 satoshi/byte to gasPrice to avoid minRelayTxFee issue - networkInfo, err := signer.rpcClient.GetNetworkInfo() + networkInfo, err := signer.client.GetNetworkInfo() if err != nil { logger.Error().Err(err).Msgf("cannot get bitcoin network info") return @@ -386,7 +386,7 @@ func (signer *Signer) TryProcessOutbound( // compliance check cancelTx := compliance.IsCctxRestricted(cctx) if cancelTx { - compliance.PrintComplianceLog(logger, signer.loggerCompliance, + compliance.PrintComplianceLog(logger, signer.Logger().Compliance, true, chain.ChainId, cctx.Index, cctx.InboundParams.Sender, params.Receiver, "BTC") amount = 0.0 // zero out the amount to cancel the tx } diff --git a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go index 8cc90cf3eb..2506f57059 100644 --- a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go @@ -15,7 +15,6 @@ import ( "github.com/btcsuite/btcutil" "github.com/stretchr/testify/suite" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -147,7 +146,7 @@ func getTSSTX( return "", err } - sig65B, err := tss.Sign(witnessHash, 10, 10, &chains.Chain{}, "") + sig65B, err := tss.Sign(witnessHash, 10, 10, 0, "") R := big.NewInt(0).SetBytes(sig65B[:32]) S := big.NewInt(0).SetBytes(sig65B[32:64]) sig := btcec.Signature{ diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index 74628211a4..e351d4a65c 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -21,8 +21,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -48,13 +46,14 @@ func (s *BTCSignerSuite) SetUpTest(c *C) { tss := &mocks.TSS{ PrivKey: privateKey, } - cfg := config.NewConfig() s.btcSigner, err = NewSigner( - config.BTCConfig{}, + chains.Chain{}, + nil, tss, + nil, base.DefaultLogger(), - &metrics.TelemetryServer{}, - context.NewZetacoreContext(cfg)) + config.BTCConfig{}, + ) c.Assert(err, IsNil) } @@ -231,16 +230,17 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { func TestAddWithdrawTxOutputs(t *testing.T) { // Create test signer and receiver address signer, err := NewSigner( - config.BTCConfig{}, + chains.Chain{}, + nil, mocks.NewTSSMainnet(), - base.DefaultLogger(), - &metrics.TelemetryServer{}, nil, + base.DefaultLogger(), + config.BTCConfig{}, ) require.NoError(t, err) // tss address and script - tssAddr := signer.tssSigner.BTCAddressWitnessPubkeyHash() + tssAddr := signer.TSS().BTCAddressWitnessPubkeyHash() tssScript, err := bitcoin.PayToAddrScript(tssAddr) require.NoError(t, err) fmt.Printf("tss address: %s", tssAddr.EncodeAddress()) @@ -392,13 +392,13 @@ func TestNewBTCSigner(t *testing.T) { tss := &mocks.TSS{ PrivKey: privateKey, } - cfg := config.NewConfig() btcSigner, err := NewSigner( - config.BTCConfig{}, + chains.Chain{}, + nil, tss, + nil, base.DefaultLogger(), - &metrics.TelemetryServer{}, - context.NewZetacoreContext(cfg)) + config.BTCConfig{}) require.NoError(t, err) require.NotNil(t, btcSigner) } diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 1e261c4d0a..41ada01375 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -36,33 +36,33 @@ import ( // WatchInbound watches evm chain for incoming txs and post votes to zetacore func (ob *Observer) WatchInbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInbound_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchInbound_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") + ob.Logger().Inbound.Error().Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) - sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.Logger().Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) + sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } err := ob.ObserveInbound(sampledLogger) if err != nil { - ob.logger.Inbound.Err(err).Msg("WatchInbound: observeInbound error") + ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + case <-ob.StopChannel(): + ob.Logger().Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) return } } @@ -72,29 +72,29 @@ func (ob *Observer) WatchInbound() { // If it was, it tries to broadcast the confirmation vote. If this zeta client has previously broadcast the vote, the tx would be rejected func (ob *Observer) WatchInboundTracker() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.logger.Inbound.Err(err).Msg("error creating ticker") + ob.Logger().Inbound.Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.Chain().ChainId) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { - ob.logger.Inbound.Err(err).Msg("ProcessInboundTrackers error") + ob.Logger().Inbound.Err(err).Msg("ProcessInboundTrackers error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + case <-ob.StopChannel(): + ob.Logger().Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return } } @@ -102,7 +102,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers from zetacore func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) if err != nil { return err } @@ -114,15 +114,20 @@ func (ob *Observer) ProcessInboundTrackers() error { err, "error getting transaction for inbound %s chain %d", tracker.TxHash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tracker.TxHash)) if err != nil { - return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) + return errors.Wrapf( + err, + "error getting receipt for inbound %s chain %d", + tracker.TxHash, + ob.Chain().ChainId, + ) } - ob.logger.Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.Chain().ChainId) // check and vote on inbound tx switch tracker.CoinType { @@ -137,11 +142,11 @@ func (ob *Observer) ProcessInboundTrackers() error { "unknown coin type %s for inbound %s chain %d", tracker.CoinType, tx.Hash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } if err != nil { - return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } } return nil @@ -153,17 +158,17 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if err != nil { return err } - if blockNumber < ob.GetLastBlockHeight() { + if blockNumber < ob.LastBlock() { return fmt.Errorf( "observeInbound: block number should not decrease: current %d last %d", blockNumber, - ob.GetLastBlockHeight(), + ob.LastBlock(), ) } - ob.SetLastBlockHeight(blockNumber) + ob.WithLastBlock(blockNumber) // increment prom counter - metrics.GetBlockByNumberPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetBlockByNumberPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // skip if current height is too low if blockNumber < ob.GetChainParams().ConfirmationCount { @@ -172,10 +177,10 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { confirmedBlockNum := blockNumber - ob.GetChainParams().ConfirmationCount // skip if no new block is confirmed - lastScanned := ob.GetLastBlockHeightScanned() + lastScanned := ob.LastBlockScanned() if lastScanned >= confirmedBlockNum { sampledLogger.Debug(). - Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.chain.ChainId) + Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.Chain().ChainId) return nil } @@ -205,12 +210,11 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if lastScannedLowest > lastScanned { sampledLogger.Info(). Msgf("observeInbound: lasstScanned heights for chain %d ZetaSent %d ERC20Deposited %d TssRecvd %d", - ob.chain.ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) - ob.SetLastBlockHeightScanned(lastScannedLowest) - if err := ob.db.Save(clienttypes.ToLastBlockSQLType(lastScannedLowest)).Error; err != nil { - ob.logger.Inbound.Error(). + ob.Chain().ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) + if err := ob.SaveLastBlockScanned(lastScannedLowest); err != nil { + ob.Logger().Inbound.Error(). Err(err). - Msgf("observeInbound: error writing lastScannedLowest %d to db", lastScannedLowest) + Msgf("observeInbound: error saving lastScannedLowest %d to db", lastScannedLowest) } } return nil @@ -222,7 +226,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { // filter ZetaSent logs addrConnector, connector, err := ob.GetConnectorContract() if err != nil { - ob.logger.Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") + ob.Logger().Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") return startBlock - 1 // lastScanned } iter, err := connector.FilterZetaSent(&bind.FilterOpts{ @@ -231,8 +235,8 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}, []*big.Int{}) if err != nil { - ob.logger.Chain.Warn().Err(err).Msgf( - "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) + ob.Logger().Chain.Warn().Err(err).Msgf( + "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) return startBlock - 1 // lastScanned } @@ -245,10 +249,10 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -261,7 +265,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // post to zetacore beingScanned := uint64(0) @@ -273,7 +277,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { } // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("ObserveZetaSent: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -301,7 +305,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { // filter ERC20CustodyDeposited logs addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() if err != nil { - ob.logger.Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") + ob.Logger().Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") return startBlock - 1 // lastScanned } @@ -311,8 +315,8 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}) if err != nil { - ob.logger.Inbound.Warn().Err(err).Msgf( - "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) + ob.Logger().Inbound.Warn().Err(err).Msgf( + "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) return startBlock - 1 // lastScanned } @@ -325,10 +329,10 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -341,7 +345,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // post to zeatcore guard := make(map[string]bool) @@ -353,15 +357,15 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { } tx, _, err := ob.TransactionByHash(event.Raw.TxHash.Hex()) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf( - "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf( + "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.Chain().ChainId) return beingScanned - 1 // we have to re-scan from this block next time } sender := ethcommon.HexToAddress(tx.From) // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("ObserveERC20Deposited: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -387,23 +391,23 @@ func (ob *Observer) ObserverTSSReceive(startBlock, toBlock uint64) uint64 { // post new block header (if any) to zetacore and ignore error // TODO: consider having a independent ticker(from TSS scaning) for posting block headers // https://github.com/zeta-chain/node/issues/1847 - blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) + blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) if found && blockHeaderVerification.Enabled { // post block header for supported chains // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 err := ob.postBlockHeader(toBlock) if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error posting block header") + ob.Logger().Inbound.Error().Err(err).Msg("error posting block header") } } // observe TSS received gas token in block 'bn' err := ob.ObserveTSSReceiveInBlock(bn) if err != nil { - ob.logger.Inbound.Error(). + ob.Logger().Inbound.Error(). Err(err). - Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.chain.ChainId) + Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.Chain().ChainId) return bn - 1 // we have to re-scan from this block next time } } @@ -418,7 +422,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -442,7 +446,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( if err == nil { msg = ob.BuildInboundVoteMsgForZetaSentEvent(event) } else { - ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err } break // only one event is allowed per tx @@ -450,7 +454,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( } if msg == nil { // no event, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -467,7 +471,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -492,7 +496,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( if err == nil { msg = ob.BuildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) } else { - ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err } break // only one event is allowed per tx @@ -500,7 +504,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( } if msg == nil { // no event, donation, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -517,7 +521,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -526,7 +530,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( } // checks receiver and tx status - if ethcommon.HexToAddress(tx.To) != ob.Tss.EVMAddress() { + if ethcommon.HexToAddress(tx.To) != ob.TSS().EVMAddress() { return "", fmt.Errorf("tx.To %s is not TSS address", tx.To) } if receipt.Status != ethtypes.ReceiptStatusSuccessful { @@ -538,7 +542,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( msg := ob.BuildInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) if msg == nil { // donation, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -555,16 +559,16 @@ func (ob *Observer) PostVoteInbound( retryGasLimit uint64, ) (string, error) { txHash := msg.InboundHash - chainID := ob.chain.ChainId - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) + chainID := ob.Chain().ChainId + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) if err != nil { - ob.logger.Inbound.Err(err). + ob.Logger().Inbound.Err(err). Msgf("inbound detected: error posting vote for chain %d token %s inbound %s", chainID, coinType, txHash) return "", err } else if zetaHash != "" { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) + ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) } else { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) + ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) } return ballot, err @@ -589,10 +593,10 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( } if config.ContainRestrictedAddress(sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), maybeReceiver) { compliance.PrintComplianceLog( - ob.logger.Inbound, - ob.logger.Compliance, + ob.Logger().Inbound, + ob.Logger().Compliance, false, - ob.chain.ChainId, + ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), @@ -603,21 +607,22 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( // donation check if bytes.Equal(event.Message, []byte(constant.DonationMessage)) { - ob.logger.Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.chain.ChainId) + ob.Logger().Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.Chain().ChainId) return nil } message := hex.EncodeToString(event.Message) - ob.logger.Inbound.Info(). + ob.Logger().Inbound.Info(). Msgf("ERC20CustodyDeposited inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) + ob.Chain(). + ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.chain.ChainId, + ob.Chain().ChainId, "", clienttypes.BytesToEthHex(event.Recipient), - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, sdkmath.NewUintFromBigInt(event.Amount), hex.EncodeToString(event.Message), event.Raw.TxHash.Hex(), @@ -625,7 +630,7 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( 1_500_000, coin.CoinType_ERC20, event.Asset.String(), - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -636,7 +641,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( ) *types.MsgVoteInbound { destChain := chains.GetChainFromChainID(event.DestinationChainId.Int64()) if destChain == nil { - ob.logger.Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) + ob.Logger().Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) return nil } destAddr := clienttypes.BytesToEthHex(event.DestinationAddress) @@ -644,32 +649,33 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( // compliance check sender := event.ZetaTxSenderAddress.Hex() if config.ContainRestrictedAddress(sender, destAddr, event.SourceTxOriginAddress.Hex()) { - compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") return nil } if !destChain.IsZetaChain() { - paramsDest, found := ob.coreContext.GetEVMChainParams(destChain.ChainId) + paramsDest, found := ob.ZetacoreContext().GetEVMChainParams(destChain.ChainId) if !found { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) return nil } if strings.EqualFold(destAddr, paramsDest.ZetaTokenContractAddress) { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("potential attack attempt: %s destination address is ZETA token contract address %s", destChain, destAddr) return nil } } message := base64.StdEncoding.EncodeToString(event.Message) - ob.logger.Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) + ob.Logger().Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.Chain(). + ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) return zetacore.GetInboundVoteMessage( sender, - ob.chain.ChainId, + ob.Chain().ChainId, event.SourceTxOriginAddress.Hex(), destAddr, destChain.ChainId, @@ -680,7 +686,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( event.DestinationGasLimit.Uint64(), coin.CoinType_Zeta, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -700,8 +706,8 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( maybeReceiver = parsedAddress.Hex() } if config.ContainRestrictedAddress(sender.Hex(), maybeReceiver) { - compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") return nil } @@ -709,19 +715,19 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( // #nosec G703 err is already checked data, _ := hex.DecodeString(message) if bytes.Equal(data, []byte(constant.DonationMessage)) { - ob.logger.Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.Chain().ChainId) return nil } - ob.logger.Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) + ob.Logger().Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.Chain().ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.chain.ChainId, + ob.Chain().ChainId, sender.Hex(), sender.Hex(), - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, sdkmath.NewUintFromBigInt(&tx.Value), message, tx.Hash, @@ -729,7 +735,7 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( 90_000, coin.CoinType_Gas, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, // not a smart contract call ) } @@ -738,15 +744,15 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { block, err := ob.GetBlockByNumberCached(blockNumber) if err != nil { - return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.chain.ChainId) + return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.Chain().ChainId) } for i := range block.Transactions { tx := block.Transactions[i] - if ethcommon.HexToAddress(tx.To) == ob.Tss.EVMAddress() { + if ethcommon.HexToAddress(tx.To) == ob.TSS().EVMAddress() { receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tx.Hash)) if err != nil { - return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } _, err = ob.CheckAndVoteInboundTokenGas(&tx, receipt, true) @@ -755,7 +761,7 @@ func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { err, "error checking and voting inbound gas asset for inbound %s chain %d", tx.Hash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } } diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 99db258f5a..ada5399909 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -5,27 +5,19 @@ import ( "fmt" "math" "math/big" - "os" - "strconv" "strings" "sync" - "sync/atomic" "time" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rlp" - lru "github.com/hashicorp/golang-lru" "github.com/onrik/ethrpc" "github.com/pkg/errors" - "github.com/rs/zerolog" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zeta.non-eth.sol" zetaconnectoreth "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.eth.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" - "gorm.io/driver/sqlite" - "gorm.io/gorm" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" @@ -39,225 +31,128 @@ import ( clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) -// Logger is the logger for evm chains -// TODO: Merge this logger with the one in bitcoin -// https://github.com/zeta-chain/node/issues/2022 -type Logger struct { - // Chain is the parent logger for the chain - Chain zerolog.Logger +var _ interfaces.ChainObserver = &Observer{} - // Inbound is the logger for incoming transactions - Inbound zerolog.Logger +// Observer is the observer for evm chains +type Observer struct { + // base.Observer implements the base chain observer + base.Observer - // Outbound is the logger for outgoing transactions - Outbound zerolog.Logger + // evmClient is the EVM client for the observed chain + evmClient interfaces.EVMRPCClient - // GasPrice is the logger for gas prices - GasPrice zerolog.Logger + // evmJSONRPC is the EVM JSON RPC client for the observed chain + evmJSONRPC interfaces.EVMJSONRPCClient - // Compliance is the logger for compliance checks - Compliance zerolog.Logger -} + // outboundPendingTransactions is the map to index pending transactions by hash + outboundPendingTransactions map[string]*ethtypes.Transaction -var _ interfaces.ChainObserver = &Observer{} + // outboundConfirmedReceipts is the map to index confirmed receipts by hash + outboundConfirmedReceipts map[string]*ethtypes.Receipt -// Observer is the observer for evm chains -type Observer struct { - Tss interfaces.TSSSigner + // outboundConfirmedTransactions is the map to index confirmed transactions by hash + outboundConfirmedTransactions map[string]*ethtypes.Transaction + // Mu protects fields from concurrent access Mu *sync.Mutex - - chain chains.Chain - evmClient interfaces.EVMRPCClient - evmJSONRPC interfaces.EVMJSONRPCClient - zetacoreClient interfaces.ZetacoreClient - lastBlockScanned uint64 - lastBlock uint64 - db *gorm.DB - outboundPendingTransactions map[string]*ethtypes.Transaction - outboundConfirmedReceipts map[string]*ethtypes.Receipt - outboundConfirmedTransactions map[string]*ethtypes.Transaction - stop chan struct{} - logger Logger - coreContext *clientcontext.ZetacoreContext - chainParams observertypes.ChainParams - ts *metrics.TelemetryServer - - blockCache *lru.Cache - headerCache *lru.Cache } // NewObserver returns a new EVM chain observer func NewObserver( - appContext *clientcontext.AppContext, + evmCfg config.EVMConfig, + evmClient interfaces.EVMRPCClient, + chainParams observertypes.ChainParams, + zetacoreContext *clientcontext.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, - evmCfg config.EVMConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - ob := Observer{ - ts: ts, - } - - chainLogger := logger.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() - ob.logger = Logger{ - Chain: chainLogger, - Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), - Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), - GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: logger.Compliance, - } - - ob.coreContext = appContext.ZetacoreContext() - chainParams, found := ob.coreContext.GetEVMChainParams(evmCfg.Chain.ChainId) - if !found { - return nil, fmt.Errorf("evm chains params not initialized for chain %d", evmCfg.Chain.ChainId) - } - - ob.chainParams = *chainParams - ob.stop = make(chan struct{}) - ob.chain = evmCfg.Chain - ob.Mu = &sync.Mutex{} - ob.zetacoreClient = zetacoreClient - ob.Tss = tss - ob.outboundPendingTransactions = make(map[string]*ethtypes.Transaction) - ob.outboundConfirmedReceipts = make(map[string]*ethtypes.Receipt) - ob.outboundConfirmedTransactions = make(map[string]*ethtypes.Transaction) - - ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.ChainName.String(), evmCfg.Endpoint) - client, err := ethclient.Dial(evmCfg.Endpoint) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("eth Client Dial") - return nil, err - } - - ob.evmClient = client - ob.evmJSONRPC = ethrpc.NewEthRPC(evmCfg.Endpoint) - - // create block header and block caches - ob.blockCache, err = lru.New(1000) + // create base observer + baseObserver, err := base.NewObserver( + evmCfg.Chain, + chainParams, + zetacoreContext, + zetacoreClient, + tss, + base.DefaultBlockCacheSize, + base.DefaultHeaderCacheSize, + ts, + logger, + ) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create block cache") return nil, err } - ob.headerCache, err = lru.New(1000) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create header cache") - return nil, err + // create evm observer + ob := Observer{ + Observer: *baseObserver, + evmClient: evmClient, + evmJSONRPC: ethrpc.NewEthRPC(evmCfg.Endpoint), + outboundPendingTransactions: make(map[string]*ethtypes.Transaction), + outboundConfirmedReceipts: make(map[string]*ethtypes.Receipt), + outboundConfirmedTransactions: make(map[string]*ethtypes.Transaction), + Mu: &sync.Mutex{}, } - err = ob.LoadDB(dbpath, ob.chain) + // open database and load data + err = ob.LoadDB(dbpath, evmCfg.Chain) if err != nil { return nil, err } - ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) - return &ob, nil } -// WithChain attaches a new chain to the observer -func (ob *Observer) WithChain(chain chains.Chain) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.chain = chain -} - -// WithLogger attaches a new logger to the observer -func (ob *Observer) WithLogger(logger zerolog.Logger) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.logger = Logger{ - Chain: logger, - Inbound: logger.With().Str("module", "WatchInbound").Logger(), - Outbound: logger.With().Str("module", "WatchOutbound").Logger(), - GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), - } -} - // WithEvmClient attaches a new evm client to the observer func (ob *Observer) WithEvmClient(client interfaces.EVMRPCClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() ob.evmClient = client } // WithEvmJSONRPC attaches a new evm json rpc client to the observer func (ob *Observer) WithEvmJSONRPC(client interfaces.EVMJSONRPCClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() ob.evmJSONRPC = client } -// WithZetacoreClient attaches a new client to interact with zetacore to the observer -func (ob *Observer) WithZetacoreClient(client interfaces.ZetacoreClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.zetacoreClient = client -} - -// WithBlockCache attaches a new block cache to the observer -func (ob *Observer) WithBlockCache(cache *lru.Cache) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.blockCache = cache -} - -// Chain returns the chain for the observer -func (ob *Observer) Chain() chains.Chain { - ob.Mu.Lock() - defer ob.Mu.Unlock() - return ob.chain -} - // SetChainParams sets the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.chainParams = params + ob.Observer.WithChainParams(params) } // GetChainParams returns the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.chainParams + return ob.Observer.ChainParams() } +// GetConnectorContract returns the non-Eth connector address and binder func (ob *Observer) GetConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) - contract, err := FetchConnectorContract(addr, ob.evmClient) + contract, err := zetaconnector.NewZetaConnectorNonEth(addr, ob.evmClient) return addr, contract, err } +// GetConnectorContractEth returns the Eth connector address and binder func (ob *Observer) GetConnectorContractEth() (ethcommon.Address, *zetaconnectoreth.ZetaConnectorEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) contract, err := FetchConnectorContractEth(addr, ob.evmClient) return addr, contract, err } -func (ob *Observer) GetZetaTokenNonEthContract() (ethcommon.Address, *zeta.ZetaNonEth, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().ZetaTokenContractAddress) - contract, err := FetchZetaZetaNonEthTokenContract(addr, ob.evmClient) - return addr, contract, err -} - +// GetERC20CustodyContract returns ERC20Custody contract address and binder func (ob *Observer) GetERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().Erc20CustodyContractAddress) - contract, err := FetchERC20CustodyContract(addr, ob.evmClient) + contract, err := erc20custody.NewERC20Custody(addr, ob.evmClient) return addr, contract, err } -func FetchConnectorContract( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*zetaconnector.ZetaConnectorNonEth, error) { - return zetaconnector.NewZetaConnectorNonEth(addr, client) -} - +// FetchConnectorContractEth returns the Eth connector address and binder func FetchConnectorContractEth( addr ethcommon.Address, client interfaces.EVMRPCClient, @@ -265,20 +160,14 @@ func FetchConnectorContractEth( return zetaconnectoreth.NewZetaConnectorEth(addr, client) } -func FetchZetaZetaNonEthTokenContract( +// FetchZetaTokenContract returns the non-Eth ZETA token binder +func FetchZetaTokenContract( addr ethcommon.Address, client interfaces.EVMRPCClient, ) (*zeta.ZetaNonEth, error) { return zeta.NewZetaNonEth(addr, client) } -func FetchERC20CustodyContract( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*erc20custody.ERC20Custody, error) { - return erc20custody.NewERC20Custody(addr, client) -} - // Start all observation routines for the evm chain func (ob *Observer) Start() { // watch evm chain for incoming txs and post votes to zetacore @@ -299,7 +188,7 @@ func (ob *Observer) Start() { // WatchRPCStatus watches the RPC status of the evm chain func (ob *Observer) WatchRPCStatus() { - ob.logger.Chain.Info().Msgf("Starting RPC status check for chain %s", ob.chain.String()) + ob.Logger().Chain.Info().Msgf("Starting RPC status check for chain %d", ob.Chain().ChainId) ticker := time.NewTicker(60 * time.Second) for { select { @@ -309,50 +198,47 @@ func (ob *Observer) WatchRPCStatus() { } bn, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } gasPrice, err := ob.evmClient.SuggestGasPrice(context.Background()) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(bn)) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } // #nosec G701 always in range blockTime := time.Unix(int64(header.Time), 0).UTC() elapsedSeconds := time.Since(blockTime).Seconds() if elapsedSeconds > 100 { - ob.logger.Chain.Warn(). + ob.Logger().Chain.Warn(). Msgf("RPC Status Check warning: RPC stale or chain stuck (check explorer)? Latest block %d timestamp is %.0fs ago", bn, elapsedSeconds) continue } - ob.logger.Chain.Info(). + ob.Logger().Chain.Info(). Msgf("[OK] RPC status: latest block num %d, timestamp %s ( %.0fs ago), suggested gas price %d", header.Number, blockTime.String(), elapsedSeconds, gasPrice.Uint64()) - case <-ob.stop: + case <-ob.StopChannel(): return } } } +// Stop all goroutines and closes the database func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) - close(ob.stop) // this notifies all goroutines to stop + // notifies all goroutines to stop + ob.Logger().Chain.Info().Msgf("stopping observer for chain %d", ob.Chain().ChainId) + close(ob.StopChannel()) - ob.logger.Chain.Info().Msg("closing ob.db") - dbInst, err := ob.db.DB() + // close database + err := ob.CloseDB() if err != nil { - ob.logger.Chain.Info().Msg("error getting database instance") + ob.Logger().Chain.Error().Err(err).Msg("CloseDB failed") } - err = dbInst.Close() - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("error closing database") - } - - ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) + ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) } // SetPendingTx sets the pending transaction in memory @@ -419,47 +305,25 @@ func (ob *Observer) CheckTxInclusion(tx *ethtypes.Transaction, receipt *ethtypes return nil } -// SetLastBlockHeightScanned set last block height scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) SetLastBlockHeightScanned(height uint64) { - atomic.StoreUint64(&ob.lastBlockScanned, height) - ob.ts.SetLastScannedBlockNumber(ob.chain, height) -} - -// GetLastBlockHeightScanned get last block height scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) GetLastBlockHeightScanned() uint64 { - height := atomic.LoadUint64(&ob.lastBlockScanned) - return height -} - -// SetLastBlockHeight set external last block height -func (ob *Observer) SetLastBlockHeight(height uint64) { - atomic.StoreUint64(&ob.lastBlock, height) -} - -// GetLastBlockHeight get external last block height -func (ob *Observer) GetLastBlockHeight() uint64 { - return atomic.LoadUint64(&ob.lastBlock) -} - // WatchGasPrice watches evm chain for gas prices and post to zetacore func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } // start gas price ticker ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchGasPrice_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchGasPrice_%d", ob.Chain().ChainId), ob.GetChainParams().GasPriceTicker, ) if err != nil { - ob.logger.GasPrice.Error().Err(err).Msg("NewDynamicTicker error") + ob.Logger().GasPrice.Error().Err(err).Msg("NewDynamicTicker error") return } - ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) + ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", + ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -470,11 +334,11 @@ func (ob *Observer) WatchGasPrice() { } err = ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.stop: - ob.logger.GasPrice.Info().Msg("WatchGasPrice stopped") + ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) + case <-ob.StopChannel(): + ob.Logger().GasPrice.Info().Msg("WatchGasPrice stopped") return } } @@ -485,21 +349,21 @@ func (ob *Observer) PostGasPrice() error { // GAS PRICE gasPrice, err := ob.evmClient.SuggestGasPrice(context.TODO()) if err != nil { - ob.logger.GasPrice.Err(err).Msg("Err SuggestGasPrice:") + ob.Logger().GasPrice.Err(err).Msg("Err SuggestGasPrice:") return err } blockNum, err := ob.evmClient.BlockNumber(context.TODO()) if err != nil { - ob.logger.GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") + ob.Logger().GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") return err } // SUPPLY supply := "100" // lockedAmount on ETH, totalSupply on other chains - zetaHash, err := ob.zetacoreClient.PostGasPrice(ob.chain, gasPrice.Uint64(), supply, blockNum) + zetaHash, err := ob.ZetacoreClient().PostGasPrice(ob.Chain(), gasPrice.Uint64(), supply, blockNum) if err != nil { - ob.logger.GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") + ob.Logger().GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") return err } _ = zetaHash @@ -521,21 +385,21 @@ func (ob *Observer) TransactionByHash(txHash string) (*ethrpc.Transaction, bool, } func (ob *Observer) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, error) { - if header, ok := ob.headerCache.Get(blockNumber); ok { + if header, ok := ob.HeaderCache().Get(blockNumber); ok { return header.(*ethtypes.Header), nil } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) if err != nil { return nil, err } - ob.headerCache.Add(blockNumber, header) + ob.HeaderCache().Add(blockNumber, header) return header, nil } // GetBlockByNumberCached get block by number from cache // returns block, ethrpc.Block, isFallback, isSkip, error func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, error) { - if block, ok := ob.blockCache.Get(blockNumber); ok { + if block, ok := ob.BlockCache().Get(blockNumber); ok { return block.(*ethrpc.Block), nil } if blockNumber > math.MaxInt32 { @@ -546,13 +410,13 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, e if err != nil { return nil, err } - ob.blockCache.Add(blockNumber, block) + ob.BlockCache().Add(blockNumber, block) return block, nil } // RemoveCachedBlock remove block from cache func (ob *Observer) RemoveCachedBlock(blockNumber uint64) { - ob.blockCache.Remove(blockNumber) + ob.BlockCache().Remove(blockNumber) } // BlockByNumber query block by number via JSON-RPC @@ -570,92 +434,53 @@ func (ob *Observer) BlockByNumber(blockNumber int) (*ethrpc.Block, error) { return block, nil } -// LoadLastScannedBlock loads last scanned block from specified height or from database -// The last scanned block is the height from which the observer should continue scanning for inbound transactions -func (ob *Observer) LoadLastScannedBlock() error { - // get environment variable - envvar := ob.chain.ChainName.String() + "_SCAN_FROM" - scanFromBlock := os.Getenv(envvar) - - // load from environment variable if set - if scanFromBlock != "" { - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: envvar %s is set; scan from block %s", envvar, scanFromBlock) - if scanFromBlock == base.EnvVarLatestBlock { - header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(header.Number.Uint64()) - } else { - scanFromBlockInt, err := strconv.ParseUint(scanFromBlock, 10, 64) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(scanFromBlockInt) - } - } else { - // load from DB otherwise - var lastBlock clienttypes.LastBlockSQLType - if err := ob.db.First(&lastBlock, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") - header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(header.Number.Uint64()) - if dbc := ob.db.Save(clienttypes.ToLastBlockSQLType(ob.GetLastBlockHeightScanned())); dbc.Error != nil { - ob.logger.Chain.Error().Err(dbc.Error).Msgf("LoadLastScannedBlock: error writing last scanned block %d to DB", ob.GetLastBlockHeightScanned()) - } - } else { - ob.SetLastBlockHeightScanned(lastBlock.Num) - } +// LoadDB open sql database and load data into EVM observer +func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { + if dbPath == "" { + return errors.New("empty db path") } - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) - return nil -} + // open database + err := ob.OpenDB(dbPath) + if err != nil { + return errors.Wrapf(err, "error OpenDB for chain %d", chain.ChainId) + } -// LoadDB open sql database and load data into EVM observer -func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { - if dbPath != "" { - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return err - } - } - path := fmt.Sprintf("%s/%s", dbPath, chain.ChainName.String()) //Use "file::memory:?cache=shared" for temp db - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) - if err != nil { - ob.logger.Chain.Error(). - Err(err). - Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) - return err - } + // run auto migration + // transaction and receipt tables are used nowhere but we still run migration in case they are needed in future + err = ob.DB().AutoMigrate( + &clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + ) + if err != nil { + return errors.Wrapf(err, "error AutoMigrate for chain %d", chain.ChainId) + } - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("error migrating db") - return err - } + // try reading last scanned block from env variable or DB + err = ob.LoadLastBlockScanned(ob.Logger().Chain) + if err != nil { + return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", chain.ChainId) + } - ob.db = db - err = ob.LoadLastScannedBlock() + // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: + // 1. environment variable is set explicitly to "latest" + // 2. environment variable is empty and last scanned block is not found in DB + if ob.LastBlockScanned() == 0 { + blockNumber, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - return err + return errors.Wrapf(err, "error BlockNumber for chain %d", chain.ChainId) } + ob.WithLastBlockScanned(blockNumber) } + ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", chain.ChainId, ob.LastBlockScanned()) + return nil } func (ob *Observer) postBlockHeader(tip uint64) error { bn := tip - res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) + res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { // #nosec G701 always positive bn = uint64(res.ChainState.LatestHeight) + 1 // the next header to post @@ -667,23 +492,23 @@ func (ob *Observer) postBlockHeader(tip uint64) error { header, err := ob.GetBlockHeaderCached(bn) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) return err } headerRLP, err := rlp.EncodeToBytes(header) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) return err } - _, err = ob.zetacoreClient.PostVoteBlockHeader( - ob.chain.ChainId, + _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ob.Chain().ChainId, header.Hash().Bytes(), header.Number.Int64(), proofs.NewEthereumHeader(headerRLP), ) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) return err } return nil diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 0601c083f2..b7b9ca5593 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -1,6 +1,8 @@ package observer_test import ( + "fmt" + "os" "sync" "testing" @@ -21,6 +23,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" + "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -28,17 +31,24 @@ import ( // the relative path to the testdata directory var TestDataDir = "../../../" -// getAppContext creates an app context for unit tests -func getAppContext( +// getZetacoreContext creates a zetacore context for unit tests +func getZetacoreContext( evmChain chains.Chain, + endpoint string, evmChainParams *observertypes.ChainParams, -) (*context.AppContext, config.EVMConfig) { +) (*context.ZetacoreContext, config.EVMConfig) { + // use default endpoint if not provided + if endpoint == "" { + endpoint = "http://localhost:8545" + } + // create config cfg := config.NewConfig() cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ Chain: evmChain, - Endpoint: "http://localhost:8545", + Endpoint: endpoint, } + // create zetacore context coreCtx := context.NewZetacoreContext(cfg) evmChainParamsMap := make(map[int64]*observertypes.ChainParams) @@ -57,8 +67,7 @@ func getAppContext( zerolog.Logger{}, ) // create app context - appCtx := context.NewAppContext(coreCtx, cfg) - return appCtx, cfg.EVMChainConfigs[evmChain.ChainId] + return coreCtx, cfg.EVMChainConfigs[evmChain.ChainId] } // MockEVMObserver creates a mock ChainObserver with custom chain, TSS, params etc @@ -70,7 +79,13 @@ func MockEVMObserver( zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, lastBlock uint64, - params observertypes.ChainParams) *observer.Observer { + params observertypes.ChainParams, +) *observer.Observer { + // use default mock evm client if not provided + if evmClient == nil { + evmClient = mocks.NewMockEvmClient().WithBlockNumber(1000) + } + // use default mock zetacore client if not provided if zetacoreClient == nil { zetacoreClient = mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) @@ -79,17 +94,192 @@ func MockEVMObserver( if tss == nil { tss = mocks.NewTSSMainnet() } - // create app context - appCtx, evmCfg := getAppContext(chain, ¶ms) + // create zetacore context + coreCtx, evmCfg := getZetacoreContext(chain, "", ¶ms) + + // create dbpath + dbpath := testutils.CreateTempDir(t) + + // create observer + ob, err := observer.NewObserver(evmCfg, evmClient, params, coreCtx, zetacoreClient, tss, dbpath, base.Logger{}, nil) + require.NoError(t, err) + ob.WithEvmJSONRPC(evmJSONRPC) + ob.WithLastBlock(lastBlock) + + return ob +} + +func Test_NewObserver(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) + + // test cases + tests := []struct { + name string + evmCfg config.EVMConfig + chainParams observertypes.ChainParams + mockClient interfaces.EVMRPCClient + tss interfaces.TSSSigner + dbpath string + logger base.Logger + ts *metrics.TelemetryServer + fail bool + message string + }{ + { + name: "should be able to create observer", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + mockClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: false, + }, + { + name: "should fail on invalid dbpath", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + mockClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + tss: mocks.NewTSSMainnet(), + dbpath: "/invalid/dbpath", // invalid dbpath + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error creating db path", + }, + { + name: "should fail if RPC call fails", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + mockClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error RPC", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create zetacore context, client and tss + zetacoreCtx, _ := getZetacoreContext(tt.evmCfg.Chain, tt.evmCfg.Endpoint, ¶ms) + zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) + + // create observer + ob, err := observer.NewObserver( + tt.evmCfg, + tt.mockClient, + tt.chainParams, + zetacoreCtx, + zetacoreClient, + tt.tss, + tt.dbpath, + tt.logger, + tt.ts, + ) + + // check result + if tt.fail { + require.ErrorContains(t, err, tt.message) + require.Nil(t, ob) + } else { + require.NoError(t, err) + require.NotNil(t, ob) + } + }) + } +} + +func Test_LoadDB(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) + evmCfg := config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + } - // create chain observer - client, err := observer.NewObserver(appCtx, zetacoreClient, tss, "", base.Logger{}, evmCfg, nil) + // create mock client, tss and test dbpath + mockClient := mocks.NewMockEvmClient().WithBlockNumber(1000) + tss := mocks.NewTSSMainnet() + + // create zetacore context and client + zetacoreCtx, _ := getZetacoreContext(evmCfg.Chain, evmCfg.Endpoint, ¶ms) + zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) + + // create observer + dbpath := testutils.CreateTempDir(t) + ob, err := observer.NewObserver( + evmCfg, + mockClient, + params, + zetacoreCtx, + zetacoreClient, + tss, + dbpath, + base.Logger{}, + nil, + ) require.NoError(t, err) - client.WithEvmClient(evmClient) - client.WithEvmJSONRPC(evmJSONRPC) - client.SetLastBlockHeight(lastBlock) - return client + t.Run("should load db successfully", func(t *testing.T) { + err := ob.LoadDB(dbpath, chain) + require.NoError(t, err) + require.EqualValues(t, 1000, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid dbpath", func(t *testing.T) { + // load db with invalid dbpath + err := ob.LoadDB("/invalid/dbpath", chain) + require.ErrorContains(t, err, "error OpenDB") + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load db + err := ob.LoadDB(dbpath, chain) + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer + tempClient := mocks.NewMockEvmClient() + ob, err := observer.NewObserver( + evmCfg, + tempClient, + params, + zetacoreCtx, + zetacoreClient, + tss, + dbpath, + base.Logger{}, + nil, + ) + require.NoError(t, err) + + // set RPC error + tempClient.WithError(fmt.Errorf("error RPC")) + + // load db + err = ob.LoadDB(dbpath, chain) + require.ErrorContains(t, err, "error RPC") + }) } func Test_BlockCache(t *testing.T) { diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 8dda7ac034..9c3bd1c66b 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -29,33 +29,33 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.Tss.EVMAddress().String() - return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) + tssAddr := ob.TSS().EVMAddress().String() + return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) } // WatchOutbound watches evm chain for outgoing txs status func (ob *Observer) WatchOutbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchOutbound_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchOutbound_%d", ob.Chain().ChainId), ob.GetChainParams().OutboundTicker, ) if err != nil { - ob.logger.Outbound.Error().Err(err).Msg("error creating ticker") + ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") return } - ob.logger.Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.chain.ChainId) - sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.Logger().Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.Chain().ChainId) + sampledLogger := ob.Logger().Outbound.Sample(&zerolog.BasicSampler{N: 10}) defer ticker.Stop() for { select { case <-ticker.C(): - if !clientcontext.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.Chain().ChainId) continue } - trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ob.Chain().ChainId, interfaces.Ascending) if err != nil { continue } @@ -72,23 +72,23 @@ func (ob *Observer) WatchOutbound() { txCount++ outboundReceipt = receipt outbound = tx - ob.logger.Outbound.Info(). - Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, nonceInt) + ob.Logger().Outbound.Info(). + Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.Chain().ChainId, nonceInt) if txCount > 1 { - ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.chain.ChainId, nonceInt, outboundReceipt, outbound) + ob.Logger().Outbound.Error().Msgf( + "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.Chain().ChainId, nonceInt, outboundReceipt, outbound) } } } if txCount == 1 { // should be only one txHash confirmed for each nonce. ob.SetTxNReceipt(nonceInt, outboundReceipt, outbound) } else if txCount > 1 { // should not happen. We can't tell which txHash is true. It might happen (e.g. glitchy/hacked endpoint) - ob.logger.Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, nonceInt) + ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.Chain().ChainId, nonceInt) } } - ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.stop: - ob.logger.Outbound.Info().Msg("WatchOutbound: stopped") + ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) + case <-ob.StopChannel(): + ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") return } } @@ -105,8 +105,8 @@ func (ob *Observer) PostVoteOutbound( cointype coin.CoinType, logger zerolog.Logger, ) { - chainID := ob.chain.ChainId - zetaTxHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( + chainID := ob.Chain().ChainId + zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( cctxIndex, receipt.TxHash.Hex(), receipt.BlockNumber.Uint64(), @@ -115,7 +115,7 @@ func (ob *Observer) PostVoteOutbound( transaction.Gas(), receiveValue, receiveStatus, - ob.chain, + ob.Chain(), nonce, cointype, ) @@ -137,17 +137,17 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg return false, false, nil } receipt, transaction := ob.GetTxNReceipt(nonce) - sendID := fmt.Sprintf("%d-%d", ob.chain.ChainId, nonce) + sendID := fmt.Sprintf("%d-%d", ob.Chain().ChainId, nonce) logger = logger.With().Str("sendID", sendID).Logger() // get connector and erce20Custody contracts connectorAddr, connector, err := ob.GetConnectorContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.chain.ChainId) + return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.Chain().ChainId) } custodyAddr, custody, err := ob.GetERC20CustodyContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.chain.ChainId) + return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.Chain().ChainId) } // define a few common variables @@ -181,7 +181,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg if err != nil { logger.Error(). Err(err). - Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.chain.ChainId, receipt.TxHash) + Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.Chain().ChainId, receipt.TxHash) return false, false, err } @@ -346,7 +346,7 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.chain.ChainId) + Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.Chain().ChainId) return nil, nil, false } if transaction == nil { // should not happen @@ -355,17 +355,17 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check tx sender and nonce - signer := ethtypes.NewLondonSigner(big.NewInt(ob.chain.ChainId)) + signer := ethtypes.NewLondonSigner(big.NewInt(ob.Chain().ChainId)) from, err := signer.Sender(transaction) if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.chain.ChainId) + Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.Chain().ChainId) return nil, nil, false } - if from != ob.Tss.EVMAddress() { // must be TSS address + if from != ob.TSS().EVMAddress() { // must be TSS address log.Error().Msgf("confirmTxByHash: sender %s for outbound %s chain %d is not TSS address %s", - from.Hex(), transaction.Hash().Hex(), ob.chain.ChainId, ob.Tss.EVMAddress().Hex()) + from.Hex(), transaction.Hash().Hex(), ob.Chain().ChainId, ob.TSS().EVMAddress().Hex()) return nil, nil, false } if transaction.Nonce() != nonce { // must match cctx nonce @@ -394,10 +394,10 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check confirmations - if !ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()) { + if !ob.HasEnoughConfirmations(receipt, ob.LastBlock()) { log.Debug(). Msgf("confirmTxByHash: txHash %s nonce %d included but not confirmed: receipt block %d, current block %d", - txHash, nonce, receipt.BlockNumber, ob.GetLastBlockHeight()) + txHash, nonce, receipt.BlockNumber, ob.LastBlock()) return nil, nil, false } diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index fbb4d5ef88..d7df5a33d1 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -70,7 +70,7 @@ func TestSigner_NewOutboundData(t *testing.T) { evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) t.Run("NewOutboundData success", func(t *testing.T) { diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index d49895e2a3..99f2c7fe8f 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "fmt" "math/big" - "math/rand" "strconv" "strings" "sync" @@ -48,39 +47,57 @@ var ( // Signer deals with the signing EVM transactions and implements the ChainSigner interface type Signer struct { - client interfaces.EVMRPCClient - chain *chains.Chain - tssSigner interfaces.TSSSigner - ethSigner ethtypes.Signer - logger base.Logger - ts *metrics.TelemetryServer - coreContext *clientcontext.ZetacoreContext - - // mu protects below fields from concurrent access - mu *sync.Mutex - zetaConnectorABI abi.ABI - erc20CustodyABI abi.ABI - zetaConnectorAddress ethcommon.Address - er20CustodyAddress ethcommon.Address + // base.Signer implements the base chain signer + base.Signer + + // client is the EVM RPC client to interact with the EVM chain + client interfaces.EVMRPCClient + + // ethSigner encapsulates EVM transaction signature handling + ethSigner ethtypes.Signer + + // zetaConnectorABI is the ABI of the ZetaConnector contract + zetaConnectorABI abi.ABI + + // erc20CustodyABI is the ABI of the ERC20Custody contract + erc20CustodyABI abi.ABI + + // zetaConnectorAddress is the address of the ZetaConnector contract + zetaConnectorAddress ethcommon.Address + + // er20CustodyAddress is the address of the ERC20Custody contract + er20CustodyAddress ethcommon.Address + + // outboundHashBeingReported is a map of outboundHash being reported outboundHashBeingReported map[string]bool + + // mu protects fields from concurrent access + mu *sync.Mutex } +// NewSigner creates a new EVM signer func NewSigner( chain chains.Chain, + zetacoreContext *clientcontext.ZetacoreContext, + tss interfaces.TSSSigner, + ts *metrics.TelemetryServer, + logger base.Logger, endpoint string, - tssSigner interfaces.TSSSigner, zetaConnectorABI string, erc20CustodyABI string, zetaConnectorAddress ethcommon.Address, erc20CustodyAddress ethcommon.Address, - coreContext *clientcontext.ZetacoreContext, - logger base.Logger, - ts *metrics.TelemetryServer, ) (*Signer, error) { + // create base signer + baseSigner := base.NewSigner(chain, zetacoreContext, tss, ts, logger) + + // create EVM client client, ethSigner, err := getEVMRPC(endpoint) if err != nil { return nil, err } + + // prepare ABIs connectorABI, err := abi.JSON(strings.NewReader(zetaConnectorABI)) if err != nil { return nil, err @@ -91,22 +108,15 @@ func NewSigner( } return &Signer{ - client: client, - chain: &chain, - tssSigner: tssSigner, - ethSigner: ethSigner, - zetaConnectorABI: connectorABI, - erc20CustodyABI: custodyABI, - zetaConnectorAddress: zetaConnectorAddress, - er20CustodyAddress: erc20CustodyAddress, - coreContext: coreContext, - logger: base.Logger{ - Std: logger.Std.With().Str("chain", chain.ChainName.String()).Str("module", "EVMSigner").Logger(), - Compliance: logger.Compliance, - }, - ts: ts, - mu: &sync.Mutex{}, + Signer: *baseSigner, + client: client, + ethSigner: ethSigner, + zetaConnectorABI: connectorABI, + erc20CustodyABI: custodyABI, + zetaConnectorAddress: zetaConnectorAddress, + er20CustodyAddress: erc20CustodyAddress, outboundHashBeingReported: make(map[string]bool), + mu: &sync.Mutex{}, }, nil } @@ -149,15 +159,14 @@ func (signer *Signer) Sign( nonce uint64, height uint64, ) (*ethtypes.Transaction, []byte, []byte, error) { - log.Debug().Msgf("TSS SIGNER: %s", signer.tssSigner.Pubkey()) + log.Debug().Msgf("Sign: TSS signer: %s", signer.TSS().Pubkey()) // TODO: use EIP-1559 transaction type // https://github.com/zeta-chain/node/issues/1952 tx := ethtypes.NewTransaction(nonce, to, amount, gasLimit, gasPrice, data) - hashBytes := signer.ethSigner.Hash(tx).Bytes() - sig, err := signer.tssSigner.Sign(hashBytes, height, nonce, signer.chain, "") + sig, err := signer.TSS().Sign(hashBytes, height, nonce, signer.Chain().ChainId, "") if err != nil { return nil, nil, nil, err } @@ -165,11 +174,11 @@ func (signer *Signer) Sign( log.Debug().Msgf("Sign: Signature: %s", hex.EncodeToString(sig[:])) pubk, err := crypto.SigToPub(hashBytes, sig[:]) if err != nil { - signer.logger.Std.Error().Err(err).Msgf("SigToPub error") + signer.Logger().Std.Error().Err(err).Msgf("SigToPub error") } addr := crypto.PubkeyToAddress(*pubk) - signer.logger.Std.Info().Msgf("Sign: Ecrecovery of signature: %s", addr.Hex()) + signer.Logger().Std.Info().Msgf("Sign: Ecrecovery of signature: %s", addr.Hex()) signedTX, err := tx.WithSignature(signer.ethSigner, sig[:]) if err != nil { return nil, nil, nil, err @@ -269,7 +278,7 @@ func (signer *Signer) SignRevertTx(txData *OutboundData) (*ethtypes.Transaction, func (signer *Signer) SignCancelTx(txData *OutboundData) (*ethtypes.Transaction, error) { tx, _, _, err := signer.Sign( nil, - signer.tssSigner.EVMAddress(), + signer.TSS().EVMAddress(), zeroValue, // zero out the amount to cancel the tx evm.EthTransferGasLimit, txData.gasPrice, @@ -326,7 +335,7 @@ func (signer *Signer) TryProcessOutbound( zetacoreClient interfaces.ZetacoreClient, height uint64, ) { - logger := signer.logger.Std.With(). + logger := signer.Logger().Std.With(). Str("outboundID", outboundID). Str("SendHash", cctx.Index). Logger() @@ -363,16 +372,16 @@ func (signer *Signer) TryProcessOutbound( toChain := chains.GetChainFromChainID(txData.toChainID.Int64()) // Get cross-chain flags - crossChainflags := signer.coreContext.GetCrossChainFlags() + crossChainflags := signer.ZetacoreContext().GetCrossChainFlags() // https://github.com/zeta-chain/node/issues/2050 var tx *ethtypes.Transaction // compliance check goes first if compliance.IsCctxRestricted(cctx) { compliance.PrintComplianceLog( logger, - signer.logger.Compliance, + signer.Logger().Compliance, true, - signer.chain.ChainId, + signer.Chain().ChainId, cctx.Index, cctx.InboundParams.Sender, txData.to.Hex(), @@ -529,22 +538,21 @@ func (signer *Signer) BroadcastOutbound( if tx == nil { logger.Warn().Msgf("BroadcastOutbound: no tx to broadcast %s", cctx.Index) } - // Try to broadcast transaction + + // broadcast transaction if tx != nil { outboundHash := tx.Hash().Hex() - logger.Info(). - Msgf("on chain %s nonce %d, outboundHash %s signer %s", signer.chain, cctx.GetCurrentOutboundParam().TssNonce, outboundHash, myID) - //if len(signers) == 0 || myid == signers[send.OutboundParams.Broadcaster] || myid == signers[int(send.OutboundParams.Broadcaster+1)%len(signers)] { + logger.Info().Msgf("BroadcastOutbound: broadcasting tx %s on chain %d nonce %d signer %s", + outboundHash, toChain.ChainId, tx.Nonce(), myID) + + // try broacasting tx with increasing backoff (1s, 2s, 4s, 8s, 16s) in case of RPC error backOff := 1000 * time.Millisecond - // retry loop: 1s, 2s, 4s, 8s, 16s in case of RPC error for i := 0; i < 5; i++ { - logger.Info(). - Msgf("broadcasting tx %s to chain %s: nonce %d, retry %d", outboundHash, toChain, cctx.GetCurrentOutboundParam().TssNonce, i) - // #nosec G404 randomness is not a security issue here - time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond) // FIXME: use backoff + time.Sleep(backOff) err := signer.Broadcast(tx) if err != nil { - log.Warn().Err(err).Msgf("Outbound Broadcast error") + log.Warn().Err(err).Msgf("BroadcastOutbound: error broadcasting tx %s on chain %d nonce %d retry %d", + outboundHash, toChain.ChainId, cctx.GetCurrentOutboundParam().TssNonce, i) retry, report := zetacore.HandleBroadcastError( err, strconv.FormatUint(cctx.GetCurrentOutboundParam().TssNonce, 10), @@ -560,8 +568,8 @@ func (signer *Signer) BroadcastOutbound( backOff *= 2 continue } - logger.Info(). - Msgf("Broadcast success: nonce %d to chain %s outboundHash %s", cctx.GetCurrentOutboundParam().TssNonce, toChain, outboundHash) + logger.Info().Msgf("BroadcastOutbound: broadcasted tx %s on chain %d nonce %d", + outboundHash, toChain.ChainId, cctx.GetCurrentOutboundParam().TssNonce) signer.reportToOutboundTracker(zetacoreClient, toChain.ChainId, tx.Nonce(), outboundHash, logger) break // successful broadcast; no need to retry } diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index 186582dd24..bdce93f60c 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -43,39 +43,50 @@ func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { mpiAddress := ConnectorAddress erc20CustodyAddress := ERC20CustodyAddress logger := base.Logger{} - ts := &metrics.TelemetryServer{} cfg := config.NewConfig() return NewSigner( chains.BscMainnet, - mocks.EVMRPCEnabled, + context.NewZetacoreContext(cfg), tss, + nil, + logger, + mocks.EVMRPCEnabled, config.GetConnectorABI(), config.GetERC20CustodyABI(), mpiAddress, - erc20CustodyAddress, - context.NewZetacoreContext(cfg), - logger, - ts) + erc20CustodyAddress) } // getNewEvmChainObserver creates a new EVM chain observer for testing -func getNewEvmChainObserver(tss interfaces.TSSSigner) (*observer.Observer, error) { +func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.Observer, error) { // use default mock TSS if not provided if tss == nil { tss = mocks.NewTSSMainnet() } - - logger := base.Logger{} - ts := &metrics.TelemetryServer{} cfg := config.NewConfig() + // prepare mock arguments to create observer evmcfg := config.EVMConfig{Chain: chains.BscMainnet, Endpoint: "http://localhost:8545"} + evmClient := mocks.NewMockEvmClient().WithBlockNumber(1000) + params := mocks.MockChainParams(evmcfg.Chain.ChainId, 10) cfg.EVMChainConfigs[chains.BscMainnet.ChainId] = evmcfg coreCTX := context.NewZetacoreContext(cfg) - appCTX := context.NewAppContext(coreCTX, cfg) + dbpath := testutils.CreateTempDir(t) + logger := base.Logger{} + ts := &metrics.TelemetryServer{} - return observer.NewObserver(appCTX, mocks.NewMockZetacoreClient(), tss, "", logger, evmcfg, ts) + return observer.NewObserver( + evmcfg, + evmClient, + params, + coreCTX, + mocks.NewMockZetacoreClient(), + tss, + dbpath, + logger, + ts, + ) } func getNewOutboundProcessor() *outboundprocessor.Processor { @@ -146,7 +157,7 @@ func TestSigner_TryProcessOutbound(t *testing.T) { require.NoError(t, err) cctx := getCCTX(t) processor := getNewOutboundProcessor() - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) // Test with mock client that has keys @@ -167,7 +178,7 @@ func TestSigner_SignOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -201,7 +212,7 @@ func TestSigner_SignRevertTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -239,7 +250,7 @@ func TestSigner_SignCancelTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -256,7 +267,7 @@ func TestSigner_SignCancelTx(t *testing.T) { // Verify tx body basics // Note: Cancel tx sends 0 gas token to TSS self address - verifyTxBodyBasics(t, tx, evmSigner.tssSigner.EVMAddress(), txData.nonce, big.NewInt(0)) + verifyTxBodyBasics(t, tx, evmSigner.TSS().EVMAddress(), txData.nonce, big.NewInt(0)) }) t.Run("SignCancelTx - should fail if keysign fails", func(t *testing.T) { // Pause tss to make keysign fail @@ -277,7 +288,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -313,7 +324,7 @@ func TestSigner_SignCommandTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -358,7 +369,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -396,7 +407,7 @@ func TestSigner_BroadcastOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -446,7 +457,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -489,7 +500,7 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 1d901d4f53..1ef94ec8de 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -167,7 +167,10 @@ type TSSSigner interface { // Note: it specifies optionalPubkey to use a different pubkey than the current pubkey set during keygen // TODO: check if optionalPubkey is needed // https://github.com/zeta-chain/node/issues/2085 - Sign(data []byte, height uint64, nonce uint64, chain *chains.Chain, optionalPubkey string) ([65]byte, error) + Sign(data []byte, height uint64, nonce uint64, chainID int64, optionalPubkey string) ([65]byte, error) + + // SignBatch signs the data in batch + SignBatch(digests [][]byte, height uint64, nonce uint64, chainID int64) ([][65]byte, error) EVMAddress() ethcommon.Address BTCAddress() string diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index f975c7eb88..e27d64fb12 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -123,7 +123,7 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { zetaTokenAddressString := externalEvmChainParams.ZetaTokenContractAddress zetaTokenAddress := ethcommon.HexToAddress(zetaTokenAddressString) - zetatokenNonEth, err := observer.FetchZetaZetaNonEthTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) + zetatokenNonEth, err := observer.FetchZetaTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) if err != nil { return err } diff --git a/zetaclient/testutils/mocks/evm_rpc.go b/zetaclient/testutils/mocks/evm_rpc.go index 926be6ec28..fa40357592 100644 --- a/zetaclient/testutils/mocks/evm_rpc.go +++ b/zetaclient/testutils/mocks/evm_rpc.go @@ -31,7 +31,9 @@ func (s subscription) Err() <-chan error { var _ interfaces.EVMRPCClient = &MockEvmClient{} type MockEvmClient struct { - Receipts []*ethtypes.Receipt + err error + blockNumber uint64 + Receipts []*ethtypes.Receipt } func NewMockEvmClient() *MockEvmClient { @@ -44,56 +46,92 @@ func (e *MockEvmClient) SubscribeFilterLogs( _ ethereum.FilterQuery, _ chan<- ethtypes.Log, ) (ethereum.Subscription, error) { + if e.err != nil { + return subscription{}, e.err + } return subscription{}, nil } func (e *MockEvmClient) CodeAt(_ context.Context, _ ethcommon.Address, _ *big.Int) ([]byte, error) { + if e.err != nil { + return nil, e.err + } return []byte{}, nil } func (e *MockEvmClient) CallContract(_ context.Context, _ ethereum.CallMsg, _ *big.Int) ([]byte, error) { + if e.err != nil { + return nil, e.err + } return []byte{}, nil } func (e *MockEvmClient) HeaderByNumber(_ context.Context, _ *big.Int) (*ethtypes.Header, error) { + if e.err != nil { + return nil, e.err + } return ðtypes.Header{}, nil } func (e *MockEvmClient) PendingCodeAt(_ context.Context, _ ethcommon.Address) ([]byte, error) { + if e.err != nil { + return nil, e.err + } return []byte{}, nil } func (e *MockEvmClient) PendingNonceAt(_ context.Context, _ ethcommon.Address) (uint64, error) { + if e.err != nil { + return 0, e.err + } return 0, nil } func (e *MockEvmClient) SuggestGasPrice(_ context.Context) (*big.Int, error) { + if e.err != nil { + return nil, e.err + } return big.NewInt(0), nil } func (e *MockEvmClient) SuggestGasTipCap(_ context.Context) (*big.Int, error) { + if e.err != nil { + return nil, e.err + } return big.NewInt(0), nil } func (e *MockEvmClient) EstimateGas(_ context.Context, _ ethereum.CallMsg) (gas uint64, err error) { + if e.err != nil { + return 0, e.err + } gas = 0 err = nil return } func (e *MockEvmClient) SendTransaction(_ context.Context, _ *ethtypes.Transaction) error { - return nil + return e.err } func (e *MockEvmClient) FilterLogs(_ context.Context, _ ethereum.FilterQuery) ([]ethtypes.Log, error) { + if e.err != nil { + return nil, e.err + } return []ethtypes.Log{}, nil } func (e *MockEvmClient) BlockNumber(_ context.Context) (uint64, error) { - return 0, nil + if e.err != nil { + return 0, e.err + } + return e.blockNumber, nil } func (e *MockEvmClient) BlockByNumber(_ context.Context, _ *big.Int) (*ethtypes.Block, error) { + if e.err != nil { + return nil, e.err + } return ðtypes.Block{}, nil } @@ -101,10 +139,17 @@ func (e *MockEvmClient) TransactionByHash( _ context.Context, _ ethcommon.Hash, ) (tx *ethtypes.Transaction, isPending bool, err error) { + if e.err != nil { + return nil, false, e.err + } return ðtypes.Transaction{}, false, nil } func (e *MockEvmClient) TransactionReceipt(_ context.Context, _ ethcommon.Hash) (*ethtypes.Receipt, error) { + if e.err != nil { + return nil, e.err + } + // pop a receipt from the list if len(e.Receipts) > 0 { receipt := e.Receipts[len(e.Receipts)-1] @@ -120,6 +165,9 @@ func (e *MockEvmClient) TransactionSender( _ ethcommon.Hash, _ uint, ) (ethcommon.Address, error) { + if e.err != nil { + return ethcommon.Address{}, e.err + } return ethcommon.Address{}, nil } @@ -131,6 +179,16 @@ func (e *MockEvmClient) Reset() *MockEvmClient { // ---------------------------------------------------------------------------- // Feed data to the mock evm client for testing // ---------------------------------------------------------------------------- +func (e *MockEvmClient) WithError(err error) *MockEvmClient { + e.err = err + return e +} + +func (e *MockEvmClient) WithBlockNumber(blockNumber uint64) *MockEvmClient { + e.blockNumber = blockNumber + return e +} + func (e *MockEvmClient) WithReceipt(receipt *ethtypes.Receipt) *MockEvmClient { e.Receipts = append(e.Receipts, receipt) return e diff --git a/zetaclient/testutils/mocks/tss_signer.go b/zetaclient/testutils/mocks/tss_signer.go index 87d2f2ac3b..ea439e23b6 100644 --- a/zetaclient/testutils/mocks/tss_signer.go +++ b/zetaclient/testutils/mocks/tss_signer.go @@ -67,7 +67,7 @@ func (s *TSS) WithPrivKey(privKey *ecdsa.PrivateKey) *TSS { } // Sign uses test key unrelated to any tss key in production -func (s *TSS) Sign(data []byte, _ uint64, _ uint64, _ *chains.Chain, _ string) ([65]byte, error) { +func (s *TSS) Sign(data []byte, _ uint64, _ uint64, _ int64, _ string) ([65]byte, error) { // return error if tss is paused if s.paused { return [65]byte{}, fmt.Errorf("tss is paused") @@ -83,6 +83,17 @@ func (s *TSS) Sign(data []byte, _ uint64, _ uint64, _ *chains.Chain, _ string) ( return sigbyte, nil } +// SignBatch uses test key unrelated to any tss key in production +func (s *TSS) SignBatch(_ [][]byte, _ uint64, _ uint64, _ int64) ([][65]byte, error) { + // return error if tss is paused + if s.paused { + return nil, fmt.Errorf("tss is paused") + } + + // mock not implemented yet + return nil, fmt.Errorf("not implemented") +} + func (s *TSS) Pubkey() []byte { publicKeyBytes := crypto.FromECDSAPub(&s.PrivKey.PublicKey) return publicKeyBytes diff --git a/zetaclient/testutils/os.go b/zetaclient/testutils/os.go new file mode 100644 index 0000000000..714b650bb5 --- /dev/null +++ b/zetaclient/testutils/os.go @@ -0,0 +1,15 @@ +package testutils + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// create a temporary directory for testing +func CreateTempDir(t *testing.T) string { + tempPath, err := os.MkdirTemp("", "tempdir-") + require.NoError(t, err) + return tempPath +} diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index 2915dd6d0a..8fdd0384ee 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -218,7 +218,7 @@ func (tss *TSS) Sign( digest []byte, height uint64, nonce uint64, - chain *chains.Chain, + chainID int64, optionalPubKey string, ) ([65]byte, error) { H := digest @@ -250,8 +250,8 @@ func (tss *TSS) Sign( // post blame data if enabled if IsEnvFlagEnabled(envFlagPostBlame) { digest := hex.EncodeToString(digest) - index := observertypes.GetBlameIndex(chain.ChainId, nonce, digest, height) - zetaHash, err := tss.ZetacoreClient.PostBlameData(&ksRes.Blame, chain.ChainId, index) + index := observertypes.GetBlameIndex(chainID, nonce, digest, height) + zetaHash, err := tss.ZetacoreClient.PostBlameData(&ksRes.Blame, chainID, index) if err != nil { log.Error().Err(err).Msg("error sending blame data to core") return [65]byte{}, err @@ -304,7 +304,7 @@ func (tss *TSS) Sign( // SignBatch is hash of some data // digest should be batch of hashes of some data -func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chain *chains.Chain) ([][65]byte, error) { +func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chainID int64) ([][65]byte, error) { tssPubkey := tss.CurrentPubkey digestBase64 := make([]string, len(digests)) for i, digest := range digests { @@ -326,8 +326,8 @@ func (tss *TSS) SignBatch(digests [][]byte, height uint64, nonce uint64, chain * // post blame data if enabled if IsEnvFlagEnabled(envFlagPostBlame) { digest := combineDigests(digestBase64) - index := observertypes.GetBlameIndex(chain.ChainId, nonce, hex.EncodeToString(digest), height) - zetaHash, err := tss.ZetacoreClient.PostBlameData(&ksRes.Blame, chain.ChainId, index) + index := observertypes.GetBlameIndex(chainID, nonce, hex.EncodeToString(digest), height) + zetaHash, err := tss.ZetacoreClient.PostBlameData(&ksRes.Blame, chainID, index) if err != nil { log.Error().Err(err).Msg("error sending blame data to core") return [][65]byte{}, err From b51ab8a3ec291871548ed828f10e3cc925195d43 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 20 Jun 2024 07:42:53 -0500 Subject: [PATCH 08/21] integrated base observer to evm and bitcoin chain --- cmd/zetaclientd/utils.go | 28 +- zetaclient/chains/base/observer.go | 36 +- zetaclient/chains/base/observer_test.go | 31 +- zetaclient/chains/bitcoin/observer/inbound.go | 86 +-- .../chains/bitcoin/observer/inbound_test.go | 53 +- .../chains/bitcoin/observer/live_test.go | 41 +- .../chains/bitcoin/observer/observer.go | 509 ++++++++---------- .../chains/bitcoin/observer/observer_test.go | 270 ++++++++-- .../chains/bitcoin/observer/outbound.go | 57 +- .../chains/bitcoin/observer/outbound_test.go | 85 +-- .../chains/evm/observer/inbound_test.go | 60 ++- zetaclient/chains/evm/observer/observer.go | 54 +- .../chains/evm/observer/observer_test.go | 112 ++-- .../chains/evm/observer/outbound_test.go | 56 +- zetaclient/testutils/mocks/btc_rpc.go | 23 +- 15 files changed, 871 insertions(+), 630 deletions(-) diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 41d21c5a97..5caea7af05 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" @@ -159,7 +160,7 @@ func CreateChainObserverMap( } // BTC observer - _, chainParams, found := appContext.ZetacoreContext().GetBTCChainParams() + _, chainParams, found := zetacoreContext.GetBTCChainParams() if !found { return nil, fmt.Errorf("bitcoin chains params not found") } @@ -167,15 +168,34 @@ func CreateChainObserverMap( // create BTC chain observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { + // create BTC client + connCfg := &rpcclient.ConnConfig{ + Host: btcConfig.RPCHost, + User: btcConfig.RPCUsername, + Pass: btcConfig.RPCPassword, + HTTPPostMode: true, + DisableTLS: true, + Params: btcConfig.RPCParams, + } + btcClient, err := rpcclient.New(connCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating rpc client: %s", err) + } + err = btcClient.Ping() + if err != nil { + return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + } + + // create BTC chain observer co, err := btcobserver.NewObserver( - *chainParams, - appContext, btcChain, + btcClient, + *chainParams, + zetacoreContext, zetacoreClient, tss, dbpath, logger, - btcConfig, ts, ) if err != nil { diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index a77eb706c9..b2aa983bbd 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -11,6 +11,7 @@ import ( "github.com/rs/zerolog" "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -31,6 +32,9 @@ const ( // DefaultHeaderCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) // Cached headers can be used to get header information DefaultHeaderCacheSize = 1000 + + // TempSQLiteDBPath is the temporary in-memory SQLite database used for testing + TempSQLiteDBPath = "file::memory:?cache=shared" ) // Observer is the base structure for chain observers, grouping the common logic for each chain observer client. @@ -119,6 +123,19 @@ func NewObserver( return &ob, nil } +// Stop notifies all goroutines to stop and closes the database. +func (ob *Observer) Stop() { + ob.logger.Chain.Info().Msgf("observer is stopping for chain %d", ob.Chain().ChainId) + close(ob.stop) + + // close database + err := ob.CloseDB() + if err != nil { + ob.Logger().Chain.Error().Err(err).Msgf("CloseDB failed for chain %d", ob.Chain().ChainId) + } + ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) +} + // Chain returns the chain for the observer. func (ob *Observer) Chain() chains.Chain { return ob.chain @@ -261,7 +278,7 @@ func (ob *Observer) StopChannel() chan struct{} { } // OpenDB open sql database in the given path. -func (ob *Observer) OpenDB(dbPath string) error { +func (ob *Observer) OpenDB(dbPath string, dbName string) error { // create db path if not exist if _, err := os.Stat(dbPath); os.IsNotExist(err) { err := os.MkdirAll(dbPath, os.ModePerm) @@ -270,10 +287,19 @@ func (ob *Observer) OpenDB(dbPath string) error { } } - // open db by chain name - chainName := ob.chain.ChainName.String() - path := fmt.Sprintf("%s/%s", dbPath, chainName) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + // use custom dbName or chain name if not provided + if dbName == "" { + dbName = ob.chain.ChainName.String() + } + path := fmt.Sprintf("%s/%s", dbPath, dbName) + + // use memory db if specified + if dbPath == TempSQLiteDBPath { + path = TempSQLiteDBPath + } + + // open db + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) if err != nil { return errors.Wrap(err, "error opening db") } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index c82a351b2a..870f88715e 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -214,7 +214,7 @@ func TestObserverGetterAndSetter(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - ob.OpenDB(dbPath) + ob.OpenDB(dbPath, "") db := ob.DB() require.NotNil(t, db) @@ -247,7 +247,16 @@ func TestOpenCloseDB(t *testing.T) { t.Run("should be able to open/close db", func(t *testing.T) { // open db - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // close db + err = ob.CloseDB() + require.NoError(t, err) + }) + t.Run("should use memory db if specified", func(t *testing.T) { + // open db with memory + err := ob.OpenDB(base.TempSQLiteDBPath, "") require.NoError(t, err) // close db @@ -255,7 +264,7 @@ func TestOpenCloseDB(t *testing.T) { require.NoError(t, err) }) t.Run("should return error on invalid db path", func(t *testing.T) { - err := ob.OpenDB("/invalid/123db") + err := ob.OpenDB("/invalid/123db", "") require.ErrorContains(t, err, "error creating db path") }) } @@ -268,7 +277,7 @@ func TestLoadLastBlockScanned(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) // create db and write 100 as last block scanned @@ -283,7 +292,7 @@ func TestLoadLastBlockScanned(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) // read last block scanned @@ -295,7 +304,7 @@ func TestLoadLastBlockScanned(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) // create db and write 100 as last block scanned @@ -313,7 +322,7 @@ func TestLoadLastBlockScanned(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) // create db and write 100 as last block scanned @@ -331,7 +340,7 @@ func TestLoadLastBlockScanned(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) // set invalid env var @@ -348,7 +357,7 @@ func TestSaveLastBlockScanned(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) // save 100 as last block scanned @@ -370,7 +379,7 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { // create observer and open db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) // write last block scanned @@ -385,7 +394,7 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { // create empty db dbPath := testutils.CreateTempDir(t) ob := createObserver(t) - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") require.NoError(t, err) lastScannedBlock, err := ob.ReadLastBlockScannedFromDB() diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 54c5294745..2438411b5b 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -25,7 +25,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// WatchInbound watches Bitcoin chain for incoming txs and post votes to zetacore +// WatchInbound watches Bitcoin chain for inbounds on a ticker func (ob *Observer) WatchInbound() { ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.GetChainParams().InboundTicker) if err != nil { @@ -34,15 +34,15 @@ func (ob *Observer) WatchInbound() { } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) + ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } err := ob.ObserveInbound() @@ -50,47 +50,49 @@ func (ob *Observer) WatchInbound() { ob.logger.Inbound.Error().Err(err).Msg("WatchInbound error observing in tx") } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) return } } } +// ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore func (ob *Observer) ObserveInbound() error { // get and update latest block height - cnt, err := ob.rpcClient.GetBlockCount() + cnt, err := ob.btcClient.GetBlockCount() if err != nil { return fmt.Errorf("observeInboundBTC: error getting block number: %s", err) } if cnt < 0 { return fmt.Errorf("observeInboundBTC: block number is negative: %d", cnt) } - if cnt < ob.GetLastBlockHeight() { + // #nosec G701 checked positive + lastBlock := uint64(cnt) + if lastBlock < ob.LastBlock() { return fmt.Errorf( "observeInboundBTC: block number should not decrease: current %d last %d", cnt, - ob.GetLastBlockHeight(), + ob.LastBlock(), ) } - ob.SetLastBlockHeight(cnt) + ob.WithLastBlock(lastBlock) // skip if current height is too low - // #nosec G701 always in range - confirmedBlockNum := cnt - int64(ob.GetChainParams().ConfirmationCount) - if confirmedBlockNum < 0 { + if lastBlock < ob.GetChainParams().ConfirmationCount { return fmt.Errorf("observeInboundBTC: skipping observer, current block number %d is too low", cnt) } // skip if no new block is confirmed - lastScanned := ob.GetLastBlockHeightScanned() - if lastScanned >= confirmedBlockNum { + lastScanned := ob.LastBlockScanned() + if lastScanned >= lastBlock-ob.GetChainParams().ConfirmationCount { return nil } // query incoming gas asset to TSS address blockNumber := lastScanned + 1 - res, err := ob.GetBlockByNumberCached(blockNumber) + // #nosec G701 always in range + res, err := ob.GetBlockByNumberCached(int64(blockNumber)) if err != nil { ob.logger.Inbound.Error().Err(err).Msgf("observeInboundBTC: error getting bitcoin block %d", blockNumber) return err @@ -103,9 +105,10 @@ func (ob *Observer) ObserveInbound() error { // https://github.com/zeta-chain/node/issues/1847 // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 - blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) + blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) if found && blockHeaderVerification.Enabled { - err = ob.postBlockHeader(blockNumber) + // #nosec G701 always in range + err = ob.postBlockHeader(int64(blockNumber)) if err != nil { ob.logger.Inbound.Warn().Err(err).Msgf("observeInboundBTC: error posting block header %d", blockNumber) } @@ -113,14 +116,14 @@ func (ob *Observer) ObserveInbound() error { if len(res.Block.Tx) > 1 { // get depositor fee - depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) + depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) // filter incoming txs to TSS address - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() // #nosec G701 always positive inbounds, err := FilterAndParseIncomingTx( - ob.rpcClient, + ob.btcClient, res.Block.Tx, uint64(res.Block.Height), tssAddress, @@ -139,7 +142,7 @@ func (ob *Observer) ObserveInbound() error { for _, inbound := range inbounds { msg := ob.GetInboundVoteMessageFromBtcEvent(inbound) if msg != nil { - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -157,11 +160,8 @@ func (ob *Observer) ObserveInbound() error { } } - // Save LastBlockHeight - ob.SetLastBlockHeightScanned(blockNumber) - - // #nosec G701 always positive - if err := ob.db.Save(types.ToLastBlockSQLType(uint64(blockNumber))).Error; err != nil { + // save last scanned block to both memory and db + if err := ob.SaveLastBlockScanned(blockNumber); err != nil { ob.logger.Inbound.Error(). Err(err). Msgf("observeInboundBTC: error writing last scanned block %d to db", blockNumber) @@ -182,18 +182,18 @@ func (ob *Observer) WatchInboundTracker() { for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { ob.logger.Inbound.Error(). Err(err). - Msgf("error observing inbound tracker for chain %d", ob.chain.ChainId) + Msgf("error observing inbound tracker for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return } } @@ -201,7 +201,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) if err != nil { return err } @@ -214,7 +214,7 @@ func (ob *Observer) ProcessInboundTrackers() error { return err } ob.logger.Inbound.Info(). - Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.chain.ChainName, ballotIdentifier, coin.CoinType_Gas.String()) + Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.Chain().ChainName, ballotIdentifier, coin.CoinType_Gas.String()) } return nil @@ -227,7 +227,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - tx, err := ob.rpcClient.GetRawTransactionVerbose(hash) + tx, err := ob.btcClient.GetRawTransactionVerbose(hash) if err != nil { return "", err } @@ -237,7 +237,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - blockVb, err := ob.rpcClient.GetBlockVerboseTx(blockHash) + blockVb, err := ob.btcClient.GetBlockVerboseTx(blockHash) if err != nil { return "", err } @@ -246,15 +246,15 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", fmt.Errorf("block %d has no transactions", blockVb.Height) } - depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) - tss, err := ob.zetacoreClient.GetBtcTssAddress(ob.chain.ChainId) + depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) + tss, err := ob.ZetacoreClient().GetBtcTssAddress(ob.Chain().ChainId) if err != nil { return "", err } // #nosec G701 always positive event, err := GetBtcEvent( - ob.rpcClient, + ob.btcClient, *tx, tss, uint64(blockVb.Height), @@ -279,7 +279,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return msg.Digest(), nil } - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -343,10 +343,10 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) return zetacore.GetInboundVoteMessage( inbound.FromAddress, - ob.chain.ChainId, + ob.Chain().ChainId, inbound.FromAddress, inbound.FromAddress, - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, cosmosmath.NewUintFromBigInt(amountInt), message, inbound.TxHash, @@ -354,7 +354,7 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) 0, coin.CoinType_Gas, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, ) } @@ -368,7 +368,7 @@ func (ob *Observer) DoesInboundContainsRestrictedAddress(inTx *BTCInboundEvent) } if config.ContainRestrictedAddress(inTx.FromAddress, receiver) { compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") + false, ob.Chain().ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") return true } return false diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index b67b7d7b50..63042e8b85 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -1,4 +1,4 @@ -package observer +package observer_test import ( "bytes" @@ -18,6 +18,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -198,7 +199,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", sender) }) @@ -210,7 +211,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", sender) }) @@ -222,7 +223,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) }) @@ -234,7 +235,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", sender) }) @@ -246,7 +247,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", sender) }) @@ -272,7 +273,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Empty(t, sender) }) @@ -288,7 +289,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // use invalid tx hash txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.Error(t, err) require.Empty(t, sender) }) @@ -296,7 +297,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { // create mock rpc client without preloaded tx rpcClient := mocks.NewMockBTCRPCClient() txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "error getting raw transaction") require.Empty(t, sender) }) @@ -305,7 +306,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // invalid output index txVin := btcjson.Vin{Txid: txHash, Vout: 3} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "out of range") require.Empty(t, sender) }) @@ -328,7 +329,7 @@ func TestGetBtcEvent(t *testing.T) { // expected result memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) require.NoError(t, err) - eventExpected := &BTCInboundEvent{ + eventExpected := &observer.BTCInboundEvent{ FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", ToAddress: tssAddress, Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis @@ -347,7 +348,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -362,7 +363,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -377,7 +378,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -392,7 +393,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -407,7 +408,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -418,7 +419,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -429,13 +430,13 @@ func TestGetBtcEvent(t *testing.T) { // modify the tx to have Vout[0] a P2SH output tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) // append 1 byte to script to make it longer than 22 bytes tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" - event, err = GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err = observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -446,7 +447,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -457,7 +458,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -468,7 +469,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -479,7 +480,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -502,7 +503,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -513,7 +514,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -523,7 +524,7 @@ func TestGetBtcEventErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/observer/live_test.go index eaf99ee5ea..c5512a1fae 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/observer/live_test.go @@ -1,4 +1,4 @@ -package observer +package observer_test import ( "context" @@ -23,8 +23,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" - "github.com/zeta-chain/zetacore/zetaclient/config" - clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -47,35 +46,39 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { tss := &mocks.TSS{ PrivKey: privateKey, } - params := mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 10) - appContext := clientcontext.NewAppContext(&clientcontext.ZetacoreContext{}, config.Config{}) - client, err := NewObserver(params, appContext, chains.BitcoinRegtest, nil, tss, tempSQLiteDbPath, - base.DefaultLogger(), config.BTCConfig{}, nil) + + // create mock arguments for constructor + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + btcClient := mocks.NewMockBTCRPCClient() + + // create observer + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, + base.DefaultLogger(), nil) suite.Require().NoError(err) + suite.Require().NotNil(ob) suite.rpcClient, err = getRPCClient(18332) suite.Require().NoError(err) skBytes, err := hex.DecodeString(skHex) suite.Require().NoError(err) suite.T().Logf("skBytes: %d", len(skBytes)) - btc := client.rpcClient - - _, err = btc.CreateWallet("e2e") + _, err = btcClient.CreateWallet("e2e") suite.Require().NoError(err) - addr, err := btc.GetNewAddress("test") + addr, err := btcClient.GetNewAddress("test") suite.Require().NoError(err) suite.T().Logf("deployer address: %s", addr) //err = btc.ImportPrivKey(privkeyWIF) //suite.Require().NoError(err) - btc.GenerateToAddress(101, addr, nil) + btcClient.GenerateToAddress(101, addr, nil) suite.Require().NoError(err) - bal, err := btc.GetBalance("*") + bal, err := btcClient.GetBalance("*") suite.Require().NoError(err) suite.T().Logf("balance: %f", bal.ToBTC()) - utxo, err := btc.ListUnspent() + utxo, err := btcClient.ListUnspent() suite.Require().NoError(err) suite.T().Logf("utxo: %d", len(utxo)) for _, u := range utxo { @@ -154,7 +157,7 @@ func (suite *BitcoinObserverTestSuite) Test1() { suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := FilterAndParseIncomingTx( + inbounds, err := observer.FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -191,7 +194,7 @@ func (suite *BitcoinObserverTestSuite) Test2() { suite.T().Logf("block height %d", block.Height) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := FilterAndParseIncomingTx( + inbounds, err := observer.FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -243,11 +246,11 @@ func LiveTestGetBlockHeightByHash(t *testing.T) { invalidHash := "invalidhash" // get block by invalid hash - _, err = GetBlockHeightByHash(client, invalidHash) + _, err = observer.GetBlockHeightByHash(client, invalidHash) require.ErrorContains(t, err, "error decoding block hash") // get block height by block hash - height, err := GetBlockHeightByHash(client, hash) + height, err := observer.GetBlockHeightByHash(client, hash) require.NoError(t, err) require.Equal(t, expectedHeight, height) } @@ -461,7 +464,7 @@ BLOCKLOOP: Txid: mpvin.TxID, Vout: mpvin.Vout, } - senderAddr, err := GetSenderAddressByVin(client, vin, net) + senderAddr, err := observer.GetSenderAddressByVin(client, vin, net) if err != nil { fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) time.Sleep(3 * time.Second) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index f5067763ec..0cf1766895 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -6,24 +6,17 @@ import ( "fmt" "math" "math/big" - "os" "sort" "sync" - "sync/atomic" "time" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" "github.com/rs/zerolog" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" @@ -31,47 +24,34 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" - "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) const ( // btcBlocksPerDay represents Bitcoin blocks per days for LRU block cache size btcBlocksPerDay = 144 - // bigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC - bigValueSats = 200000000 + // RegnetStartBlock is the hardcoded start block for regnet + RegnetStartBlock = 100 - // bigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations - bigValueConfirmationCount = 6 + // BigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC + BigValueSats = 200000000 + + // BigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations + BigValueConfirmationCount = 6 ) var _ interfaces.ChainObserver = &Observer{} // Logger contains list of loggers used by Bitcoin chain observer -// TODO: Merge this logger with the one in evm -// https://github.com/zeta-chain/node/issues/2022 type Logger struct { - // Chain is the parent logger for the chain - Chain zerolog.Logger - - // Inbound is the logger for incoming transactions - Inbound zerolog.Logger // The logger for incoming transactions - - // Outbound is the logger for outgoing transactions - Outbound zerolog.Logger // The logger for outgoing transactions + // base.Logger contains a list of base observer loggers + base.ObserverLogger // UTXOs is the logger for UTXOs management - UTXOs zerolog.Logger // The logger for UTXOs management - - // GasPrice is the logger for gas price - GasPrice zerolog.Logger // The logger for gas price - - // Compliance is the logger for compliance checks - Compliance zerolog.Logger // The logger for compliance checks + UTXOs zerolog.Logger } // BTCInboundEvent represents an incoming transaction event @@ -98,23 +78,20 @@ type BTCBlockNHeader struct { // Observer is the Bitcoin chain observer type Observer struct { - BlockCache *lru.Cache + // base.Observer implements the base chain observer + base.Observer - // Mu is lock for all the maps, utxos and core params - Mu *sync.Mutex + // netParams contains the Bitcoin network parameters + netParams *chaincfg.Params + + // btcClient is the Bitcoin RPC client that interacts with the Bitcoin node + btcClient interfaces.BTCRPCClient - Tss interfaces.TSSSigner + // pendingNonce is the outbound artificial pending nonce + pendingNonce uint64 - chain chains.Chain - netParams *chaincfg.Params - rpcClient interfaces.BTCRPCClient - zetacoreClient interfaces.ZetacoreClient - lastBlock int64 - lastBlockScanned int64 - pendingNonce uint64 - utxos []btcjson.ListUnspentResult - params observertypes.ChainParams - coreContext *context.ZetacoreContext + // utxos contains the UTXOs owned by the TSS address + utxos []btcjson.ListUnspentResult // includedTxHashes indexes included tx with tx hash includedTxHashes map[string]bool @@ -125,152 +102,120 @@ type Observer struct { // broadcastedTx indexes the outbound hash with the outbound tx identifier broadcastedTx map[string]string - db *gorm.DB - stop chan struct{} + // logger contains the loggers used by the bitcoin observer logger Logger - ts *metrics.TelemetryServer + + // Mu protects the maps, utxos and chain params from concurrent access + Mu *sync.Mutex } // NewObserver returns a new Bitcoin chain observer func NewObserver( - chainParams observertypes.ChainParams, - appcontext *context.AppContext, chain chains.Chain, + btcClient interfaces.BTCRPCClient, + chainParams observertypes.ChainParams, + zetacoreContext *context.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, - btcCfg config.BTCConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - // initialize the observer - ob := Observer{ - ts: ts, - } - ob.stop = make(chan struct{}) - ob.chain = chain - - // get the bitcoin network params - netParams, err := chains.BitcoinNetParamsFromChainID(ob.chain.ChainId) - if err != nil { - return nil, fmt.Errorf("error getting net params for chain %d: %s", ob.chain.ChainId, err) - } - ob.netParams = netParams - - ob.Mu = &sync.Mutex{} - - chainLogger := logger.Std.With().Str("chain", chain.ChainName.String()).Logger() - ob.logger = Logger{ - Chain: chainLogger, - Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), - Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), - UTXOs: chainLogger.With().Str("module", "WatchUTXOs").Logger(), - GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: logger.Compliance, - } - - ob.params = chainParams - ob.zetacoreClient = zetacoreClient - ob.Tss = tss - ob.coreContext = appcontext.ZetacoreContext() - ob.includedTxHashes = make(map[string]bool) - ob.includedTxResults = make(map[string]*btcjson.GetTransactionResult) - ob.broadcastedTx = make(map[string]string) - - // create the RPC client - ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) - connCfg := &rpcclient.ConnConfig{ - Host: btcCfg.RPCHost, - User: btcCfg.RPCUsername, - Pass: btcCfg.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcCfg.RPCParams, - } - rpcClient, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - - // try connection - ob.rpcClient = rpcClient - err = rpcClient.Ping() + // create base observer + baseObserver, err := base.NewObserver( + chain, + chainParams, + zetacoreContext, + zetacoreClient, + tss, + btcBlocksPerDay, + base.DefaultHeaderCacheSize, + ts, + logger, + ) if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + return nil, err } - ob.BlockCache, err = lru.New(btcBlocksPerDay) + // get the bitcoin network params + netParams, err := chains.BitcoinNetParamsFromChainID(chain.ChainId) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create bitcoin block cache") - return nil, err + return nil, fmt.Errorf("error getting net params for chain %d: %s", chain.ChainId, err) + } + + // create bitcoin observer + ob := &Observer{ + Observer: *baseObserver, + netParams: netParams, + btcClient: btcClient, + pendingNonce: 0, + utxos: []btcjson.ListUnspentResult{}, + includedTxHashes: make(map[string]bool), + includedTxResults: make(map[string]*btcjson.GetTransactionResult), + broadcastedTx: make(map[string]string), + logger: Logger{ + ObserverLogger: *baseObserver.Logger(), + UTXOs: baseObserver.Logger().Chain.With().Str("module", "utxos").Logger(), + }, + Mu: &sync.Mutex{}, } // load btc chain observer DB - err = ob.loadDB(dbpath) + err = ob.LoadDB(dbpath) if err != nil { return nil, err } - return &ob, nil + return ob, nil } -func (ob *Observer) WithZetacoreClient(client *zetacore.Client) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.zetacoreClient = client +// BtcClient returns the btc client +func (ob *Observer) BtcClient() interfaces.BTCRPCClient { + return ob.btcClient } -func (ob *Observer) WithLogger(logger zerolog.Logger) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.logger = Logger{ - Chain: logger, - Inbound: logger.With().Str("module", "WatchInbound").Logger(), - Outbound: logger.With().Str("module", "WatchOutbound").Logger(), - UTXOs: logger.With().Str("module", "WatchUTXOs").Logger(), - GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), - } -} - -func (ob *Observer) WithBtcClient(client *rpcclient.Client) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.rpcClient = client -} - -func (ob *Observer) WithChain(chain chains.Chain) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.chain = chain -} - -func (ob *Observer) Chain() chains.Chain { - ob.Mu.Lock() - defer ob.Mu.Unlock() - return ob.chain +// WithBtcClient attaches a new btc client to the observer +func (ob *Observer) WithBtcClient(client interfaces.BTCRPCClient) { + ob.btcClient = client } +// SetChainParams sets the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.params = params + ob.WithChainParams(params) } +// GetChainParams returns the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.params + return ob.ChainParams() } // Start starts the Go routine to observe the Bitcoin chain func (ob *Observer) Start() { - ob.logger.Chain.Info().Msgf("Bitcoin client is starting") - go ob.WatchInbound() // watch bitcoin chain for incoming txs and post votes to zetacore - go ob.WatchOutbound() // watch bitcoin chain for outgoing txs status - go ob.WatchUTXOs() // watch bitcoin chain for UTXOs owned by the TSS address - go ob.WatchGasPrice() // watch bitcoin chain for gas rate and post to zetacore - go ob.WatchInboundTracker() // watch zetacore for bitcoin inbound trackers - go ob.WatchRPCStatus() // watch the RPC status of the bitcoin chain + ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + + // watch bitcoin chain for incoming txs and post votes to zetacore + go ob.WatchInbound() + + // watch bitcoin chain for outgoing txs status + go ob.WatchOutbound() + + // watch bitcoin chain for UTXOs owned by the TSS address + go ob.WatchUTXOs() + + // watch bitcoin chain for gas rate and post to zetacore + go ob.WatchGasPrice() + + // watch zetacore for bitcoin inbound trackers + go ob.WatchInboundTracker() + + // watch the RPC status of the bitcoin chain + go ob.WatchRPCStatus() } // WatchRPCStatus watches the RPC status of the Bitcoin chain @@ -285,19 +230,19 @@ func (ob *Observer) WatchRPCStatus() { continue } - bn, err := ob.rpcClient.GetBlockCount() + bn, err := ob.btcClient.GetBlockCount() if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - hash, err := ob.rpcClient.GetBlockHash(bn) + hash, err := ob.btcClient.GetBlockHash(bn) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - header, err := ob.rpcClient.GetBlockHeader(hash) + header, err := ob.btcClient.GetBlockHeader(hash) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue @@ -310,8 +255,8 @@ func (ob *Observer) WatchRPCStatus() { continue } - tssAddr := ob.Tss.BTCAddressWitnessPubkeyHash() - res, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) + tssAddr := ob.TSS().BTCAddressWitnessPubkeyHash() + res, err := ob.btcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) if err != nil { ob.logger.Chain.Error(). Err(err). @@ -329,56 +274,27 @@ func (ob *Observer) WatchRPCStatus() { ob.logger.Chain.Info(). Msgf("[OK] RPC status check: latest block number %d, timestamp %s (%.fs ago), tss addr %s, #utxos: %d", bn, blockTime, elapsedSeconds, tssAddr, len(res)) - case <-ob.stop: + case <-ob.StopChannel(): return } } } -func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) - close(ob.stop) // this notifies all goroutines to stop - ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) -} - -func (ob *Observer) SetLastBlockHeight(height int64) { - atomic.StoreInt64(&ob.lastBlock, height) -} - -func (ob *Observer) GetLastBlockHeight() int64 { - return atomic.LoadInt64(&ob.lastBlock) -} - -func (ob *Observer) SetLastBlockHeightScanned(height int64) { - atomic.StoreInt64(&ob.lastBlockScanned, height) - // #nosec G701 checked as positive - ob.ts.SetLastScannedBlockNumber(ob.chain, uint64(height)) -} - -func (ob *Observer) GetLastBlockHeightScanned() int64 { - return atomic.LoadInt64(&ob.lastBlockScanned) -} - +// GetPendingNonce returns the artificial pending nonce +// Note: pending nonce is accessed concurrently func (ob *Observer) GetPendingNonce() uint64 { ob.Mu.Lock() defer ob.Mu.Unlock() return ob.pendingNonce } -// GetBaseGasPrice ... -// TODO: implement -// https://github.com/zeta-chain/node/issues/868 -func (ob *Observer) GetBaseGasPrice() *big.Int { - return big.NewInt(0) -} - // ConfirmationsThreshold returns number of required Bitcoin confirmations depending on sent BTC amount. func (ob *Observer) ConfirmationsThreshold(amount *big.Int) int64 { - if amount.Cmp(big.NewInt(bigValueSats)) >= 0 { - return bigValueConfirmationCount + if amount.Cmp(big.NewInt(BigValueSats)) >= 0 { + return BigValueConfirmationCount } - if bigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { - return bigValueConfirmationCount + if BigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { + return BigValueConfirmationCount } // #nosec G701 always in range @@ -390,7 +306,7 @@ func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } // start gas price ticker @@ -400,7 +316,7 @@ func (ob *Observer) WatchGasPrice() { return } ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) + ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -411,25 +327,27 @@ func (ob *Observer) WatchGasPrice() { } err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.stop: - ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) return } } } +// PostGasPrice posts gas price to zetacore func (ob *Observer) PostGasPrice() error { - if ob.chain.ChainId == 18444 { //bitcoin regtest; hardcode here since this RPC is not available on regtest - blockNumber, err := ob.rpcClient.GetBlockCount() + // hardcode gas price here since this RPC is not available on regtest + if chains.IsBitcoinRegnet(ob.Chain().ChainId) { + blockNumber, err := ob.btcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always in range - _, err = ob.zetacoreClient.PostGasPrice(ob.chain, 1, "100", uint64(blockNumber)) + _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), 1, "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -438,7 +356,7 @@ func (ob *Observer) PostGasPrice() error { } // EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation - feeResult, err := ob.rpcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) + feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) if err != nil { return err } @@ -450,13 +368,13 @@ func (ob *Observer) PostGasPrice() error { } feeRatePerByte := bitcoin.FeeRateToSatPerByte(*feeResult.FeeRate) - blockNumber, err := ob.rpcClient.GetBlockCount() + blockNumber, err := ob.btcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always positive - _, err = ob.zetacoreClient.PostGasPrice(ob.chain, feeRatePerByte.Uint64(), "100", uint64(blockNumber)) + _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRatePerByte.Uint64(), "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -527,13 +445,14 @@ func (ob *Observer) WatchUTXOs() { ob.logger.UTXOs.Error().Err(err).Msg("error fetching btc utxos") } ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.UTXOs) - case <-ob.stop: - ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.Chain().ChainId) return } } } +// FetchUTXOs fetches TSS-owned UTXOs from the Bitcoin node func (ob *Observer) FetchUTXOs() error { defer func() { if err := recover(); err != nil { @@ -545,19 +464,19 @@ func (ob *Observer) FetchUTXOs() error { ob.refreshPendingNonce() // get the current block height. - bh, err := ob.rpcClient.GetBlockCount() + bh, err := ob.btcClient.GetBlockCount() if err != nil { return fmt.Errorf("btc: error getting block height : %v", err) } maxConfirmations := int(bh) // List all unspent UTXOs (160ms) - tssAddr := ob.Tss.BTCAddress() - address, err := chains.DecodeBtcAddress(tssAddr, ob.chain.ChainId) + tssAddr := ob.TSS().BTCAddress() + address, err := chains.DecodeBtcAddress(tssAddr, ob.Chain().ChainId) if err != nil { return fmt.Errorf("btc: error decoding wallet address (%s) : %s", tssAddr, err.Error()) } - utxos, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) + utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) if err != nil { return err } @@ -590,7 +509,7 @@ func (ob *Observer) FetchUTXOs() error { } ob.Mu.Lock() - ob.ts.SetNumberOfUTXOs(len(utxosFiltered)) + ob.TelemetryServer().SetNumberOfUTXOs(len(utxosFiltered)) ob.utxos = utxosFiltered ob.Mu.Unlock() return nil @@ -604,7 +523,7 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { ob.Mu.Unlock() broadcastEntry := clienttypes.ToOutboundHashSQLType(txHash, outboundID) - if err := ob.db.Save(&broadcastEntry).Error; err != nil { + if err := ob.DB().Save(&broadcastEntry).Error; err != nil { ob.logger.Outbound.Error(). Err(err). Msgf("SaveBroadcastedTx: error saving broadcasted txHash %s for outbound %s", txHash, outboundID) @@ -685,67 +604,23 @@ func GetRawTxResult( return btcjson.TxRawResult{}, fmt.Errorf("getRawTxResult: tx %s not included yet", hash) } -func (ob *Observer) BuildBroadcastedTxMap() error { - var broadcastedTransactions []clienttypes.OutboundHashSQLType - if err := ob.db.Find(&broadcastedTransactions).Error; err != nil { - ob.logger.Chain.Error().Err(err).Msg("error iterating over db") - return err - } - for _, entry := range broadcastedTransactions { - ob.broadcastedTx[entry.Key] = entry.Hash - } - return nil -} - -// LoadLastScannedBlock loads last scanned block from database -// The last scanned block is the height from which the observer should continue scanning for inbound transactions -func (ob *Observer) LoadLastScannedBlock() error { - // Get the latest block number from node - bn, err := ob.rpcClient.GetBlockCount() - if err != nil { - return err - } - if bn < 0 { - return fmt.Errorf("LoadLastScannedBlock: negative block number %d", bn) - } - - //Load persisted block number - var lastBlockNum clienttypes.LastBlockSQLType - if err := ob.db.First(&lastBlockNum, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") - ob.SetLastBlockHeightScanned(bn) - } else { - // #nosec G701 always in range - lastBN := int64(lastBlockNum.Num) - ob.SetLastBlockHeightScanned(lastBN) - } - - // bitcoin regtest starts from block 100 - if chains.IsBitcoinRegnet(ob.chain.ChainId) { - ob.SetLastBlockHeightScanned(100) - } - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) - - return nil -} - +// GetBlockByNumberCached gets cached block (and header) by block number func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { - if result, ok := ob.BlockCache.Get(blockNumber); ok { + if result, ok := ob.BlockCache().Get(blockNumber); ok { return result.(*BTCBlockNHeader), nil } // Get the block hash - hash, err := ob.rpcClient.GetBlockHash(blockNumber) + hash, err := ob.btcClient.GetBlockHash(blockNumber) if err != nil { return nil, err } // Get the block header - header, err := ob.rpcClient.GetBlockHeader(hash) + header, err := ob.btcClient.GetBlockHeader(hash) if err != nil { return nil, err } // Get the block with verbose transactions - block, err := ob.rpcClient.GetBlockVerboseTx(hash) + block, err := ob.btcClient.GetBlockVerboseTx(hash) if err != nil { return nil, err } @@ -753,11 +628,85 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, Header: header, Block: block, } - ob.BlockCache.Add(blockNumber, blockNheader) - ob.BlockCache.Add(hash, blockNheader) + ob.BlockCache().Add(blockNumber, blockNheader) + ob.BlockCache().Add(hash, blockNheader) return blockNheader, nil } +// LoadDB open sql database and load data into Bitcoin observer +func (ob *Observer) LoadDB(dbPath string) error { + if dbPath == "" { + return errors.New("empty db path") + } + + // open database, the custom dbName is used here for backward compatibility + err := ob.OpenDB(dbPath, "btc_chain_client") + if err != nil { + return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) + } + + // run auto migration + // transaction result table is used nowhere but we still run migration in case they are needed in future + err = ob.DB().AutoMigrate( + &clienttypes.TransactionResultSQLType{}, + &clienttypes.OutboundHashSQLType{}, + ) + if err != nil { + return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) + } + + // load last scanned block + err = ob.LoadLastBlockScanned() + if err != nil { + return err + } + + // load broadcasted transactions + err = ob.LoadBroadcastedTxMap() + return err +} + +// LoadLastBlockScanned loads the last scanned block from the database +func (ob *Observer) LoadLastBlockScanned() error { + err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) + if err != nil { + return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) + } + + // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: + // 1. environment variable is set explicitly to "latest" + // 2. environment variable is empty and last scanned block is not found in DB + if ob.LastBlockScanned() == 0 { + blockNumber, err := ob.btcClient.GetBlockCount() + if err != nil { + return errors.Wrapf(err, "error GetBlockCount for chain %d", ob.Chain().ChainId) + } + // #nosec G701 always positive + ob.WithLastBlockScanned(uint64(blockNumber)) + } + + // bitcoin regtest starts from hardcoded block 100 + if chains.IsBitcoinRegnet(ob.Chain().ChainId) { + ob.WithLastBlockScanned(RegnetStartBlock) + } + ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) + + return nil +} + +// LoadBroadcastedTxMap loads broadcasted transactions from the database +func (ob *Observer) LoadBroadcastedTxMap() error { + var broadcastedTransactions []clienttypes.OutboundHashSQLType + if err := ob.DB().Find(&broadcastedTransactions).Error; err != nil { + ob.logger.Chain.Error().Err(err).Msgf("error iterating over db for chain %d", ob.Chain().ChainId) + return err + } + for _, entry := range broadcastedTransactions { + ob.broadcastedTx[entry.Key] = entry.Hash + } + return nil +} + // isTssTransaction checks if a given transaction was sent by TSS itself. // An unconfirmed transaction is safe to spend only if it was sent by TSS and verified by ourselves. func (ob *Observer) isTssTransaction(txid string) bool { @@ -769,7 +718,7 @@ func (ob *Observer) isTssTransaction(txid string) bool { func (ob *Observer) postBlockHeader(tip int64) error { ob.logger.Inbound.Info().Msgf("postBlockHeader: tip %d", tip) bn := tip - res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) + res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { bn = res.ChainState.LatestHeight + 1 } @@ -788,8 +737,8 @@ func (ob *Observer) postBlockHeader(tip int64) error { return err } blockHash := res2.Header.BlockHash() - _, err = ob.zetacoreClient.PostVoteBlockHeader( - ob.chain.ChainId, + _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ob.Chain().ChainId, blockHash[:], res2.Block.Height, proofs.NewBitcoinHeader(headerBuf.Bytes()), @@ -800,37 +749,3 @@ func (ob *Observer) postBlockHeader(tip int64) error { } return err } - -func (ob *Observer) loadDB(dbpath string) error { - if _, err := os.Stat(dbpath); os.IsNotExist(err) { - err := os.MkdirAll(dbpath, os.ModePerm) - if err != nil { - return err - } - } - path := fmt.Sprintf("%s/btc_chain_client", dbpath) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - if err != nil { - ob.logger.Chain.Error().Err(err).Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) - return err - } - ob.db = db - - err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}, - &clienttypes.OutboundHashSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return err - } - - // Load last scanned block - err = ob.LoadLastScannedBlock() - if err != nil { - return err - } - - //Load broadcasted transactions - err = ob.BuildBroadcastedTxMap() - - return err -} diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 3c6478cf99..eeead6d8ae 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -1,9 +1,10 @@ -package observer +package observer_test import ( + "fmt" "math/big" + "os" "strconv" - "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -12,21 +13,17 @@ import ( "gorm.io/gorm" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" - "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) -const ( - // tempSQLiteDbPath is the temporary SQLite database used for testing - tempSQLiteDbPath = "file::memory:?cache=shared" -) - var ( // the relative path to the testdata directory TestDataDir = "../../../" @@ -36,7 +33,7 @@ var ( func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransactionResult) { submittedTx := map[string]btcjson.GetTransactionResult{} - db, err := gorm.Open(sqlite.Open(tempSQLiteDbPath), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(base.TempSQLiteDBPath), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}) @@ -67,27 +64,236 @@ func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransaction return db, submittedTx } -func TestNewBitcoinObserver(t *testing.T) { - t.Run("should return error because zetacore doesn't update zetacore context", func(t *testing.T) { - cfg := config.NewConfig() - params := mocks.MockChainParams(chains.BitcoinMainnet.ChainId, 10) - coreContext := context.NewZetacoreContext(cfg) - appContext := context.NewAppContext(coreContext, cfg) - chain := chains.BitcoinMainnet - zetacoreClient := mocks.NewMockZetacoreClient() - tss := mocks.NewMockTSS(chains.BitcoinTestnet, sample.EthAddress().String(), "") - logger := base.Logger{} - btcCfg := cfg.BitcoinConfig - ts := metrics.NewTelemetryServer() - - client, err := NewObserver(params, appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, logger, btcCfg, ts) - require.ErrorContains(t, err, "error ping the bitcoin server") - require.Nil(t, client) +// MockBTCObserver creates a mock Bitcoin observer for testing +func MockBTCObserver( + t *testing.T, + chain chains.Chain, + params observertypes.ChainParams, + btcClient interfaces.BTCRPCClient, + dbpath string, +) *observer.Observer { + // use default mock btc client if not provided + if btcClient == nil { + btcClient = mocks.NewMockBTCRPCClient().WithBlockCount(100) + } + + // use memory db if dbpath is empty + if dbpath == "" { + dbpath = "file::memory:?cache=shared" + } + + // create observer + ob, err := observer.NewObserver( + chain, + btcClient, + params, + nil, + nil, + nil, + dbpath, + base.Logger{}, + nil, + ) + require.NoError(t, err) + + return ob +} + +func Test_NewObserver(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // test cases + tests := []struct { + name string + chain chains.Chain + btcClient interfaces.BTCRPCClient + chainParams observertypes.ChainParams + coreContext *context.ZetacoreContext + coreClient interfaces.ZetacoreClient + tss interfaces.TSSSigner + dbpath string + logger base.Logger + ts *metrics.TelemetryServer + fail bool + message string + }{ + { + name: "should be able to create observer", + chain: chain, + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + chainParams: params, + coreContext: nil, + coreClient: nil, + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: false, + }, + { + name: "should fail if net params is not found", + chain: chains.Chain{ChainId: 111}, // invalid chain id + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + chainParams: params, + coreContext: nil, + coreClient: nil, + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error getting net params", + }, + { + name: "should fail on invalid dbpath", + chain: chain, + chainParams: params, + coreContext: nil, + coreClient: nil, + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + tss: mocks.NewTSSMainnet(), + dbpath: "/invalid/dbpath", // invalid dbpath + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error creating db path", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create observer + ob, err := observer.NewObserver( + tt.chain, + tt.btcClient, + tt.chainParams, + tt.coreContext, + tt.coreClient, + tt.tss, + tt.dbpath, + tt.logger, + tt.ts, + ) + + // check result + if tt.fail { + require.ErrorContains(t, err, tt.message) + require.Nil(t, ob) + } else { + require.NoError(t, err) + require.NotNil(t, ob) + } + }) + } +} + +func Test_LoadDB(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create mock btc client, tss and test dbpath + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) + tss := mocks.NewTSSMainnet() + + // create observer + dbpath := testutils.CreateTempDir(t) + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, dbpath, base.Logger{}, nil) + require.NoError(t, err) + + t.Run("should load db successfully", func(t *testing.T) { + err := ob.LoadDB(dbpath) + require.NoError(t, err) + require.EqualValues(t, 100, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid dbpath", func(t *testing.T) { + // load db with empty dbpath + err := ob.LoadDB("") + require.ErrorContains(t, err, "empty db path") + + // load db with invalid dbpath + err = ob.LoadDB("/invalid/dbpath") + require.ErrorContains(t, err, "error OpenDB") + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load db + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) +} + +func Test_LoadLastBlockScanned(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create observer using mock btc client + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(200) + dbpath := testutils.CreateTempDir(t) + + t.Run("should load last block scanned", func(t *testing.T) { + // create observer and write 199 as last block scanned + ob := MockBTCObserver(t, chain, params, btcClient, dbpath) + ob.WriteLastBlockScannedToDB(199) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, 199, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // create observer + ob := MockBTCObserver(t, chain, params, btcClient, dbpath) + + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer on separate path, as we need to reset last block scanned + otherPath := testutils.CreateTempDir(t) + obOther := MockBTCObserver(t, chain, params, btcClient, otherPath) + + // reset last block scanned to 0 so that it will be loaded from RPC + obOther.WithLastBlockScanned(0) + + // set RPC error + btcClient.WithError(fmt.Errorf("error RPC")) + + // load last block scanned + err := obOther.LoadLastBlockScanned() + require.ErrorContains(t, err, "error RPC") + }) + t.Run("should use hardcode block 100 for regtest", func(t *testing.T) { + // use regtest chain + regtest := chains.BitcoinRegtest + obRegnet := MockBTCObserver(t, regtest, params, btcClient, dbpath) + + // load last block scanned + err := obRegnet.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, observer.RegnetStartBlock, obRegnet.LastBlockScanned()) }) } func TestConfirmationThreshold(t *testing.T) { - ob := &Observer{Mu: &sync.Mutex{}} + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + ob := MockBTCObserver(t, chain, params, nil, "") + t.Run("should return confirmations in chain param", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) require.Equal(t, int64(3), ob.ConfirmationsThreshold(big.NewInt(1000))) @@ -95,12 +301,16 @@ func TestConfirmationThreshold(t *testing.T) { t.Run("should return big value confirmations", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) - require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(bigValueSats))) + require.Equal( + t, + int64(observer.BigValueConfirmationCount), + ob.ConfirmationsThreshold(big.NewInt(observer.BigValueSats)), + ) }) t.Run("big value confirmations is the upper cap", func(t *testing.T) { - ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: bigValueConfirmationCount + 1}) - require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) + ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: observer.BigValueConfirmationCount + 1}) + require.Equal(t, int64(observer.BigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) }) } diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index a17d46883f..6e80e1ea5c 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -21,8 +21,8 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.Tss.BTCAddress() - return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) + tssAddr := ob.TSS().BTCAddress() + return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) } // WatchOutbound watches Bitcoin chain for outgoing txs status @@ -34,32 +34,33 @@ func (ob *Observer) WatchOutbound() { } defer ticker.Stop() - ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) + chainID := ob.Chain().ChainId + ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", chainID) sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) continue } - trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(chainID, interfaces.Ascending) if err != nil { ob.logger.Outbound.Error(). Err(err). - Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", chainID) continue } for _, tracker := range trackers { // get original cctx parameters outboundID := ob.GetTxID(tracker.Nonce) - cctx, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, tracker.Nonce) + cctx, err := ob.ZetacoreClient().GetCctxByNonce(chainID, tracker.Nonce) if err != nil { ob.logger.Outbound.Info(). Err(err). - Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", ob.chain.ChainId, tracker.Nonce) + Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", chainID, tracker.Nonce) break } @@ -85,10 +86,10 @@ func (ob *Observer) WatchOutbound() { txCount++ txResult = result ob.logger.Outbound.Info(). - Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, tracker.Nonce) + Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, chainID, tracker.Nonce) if txCount > 1 { ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, ob.chain.ChainId, tracker.Nonce, result) + "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, chainID, tracker.Nonce, result) } } } @@ -97,12 +98,12 @@ func (ob *Observer) WatchOutbound() { ob.setIncludedTx(tracker.Nonce, txResult) } else if txCount > 1 { ob.removeIncludedTx(tracker.Nonce) // we can't tell which txHash is true, so we remove all (if any) to be safe - ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, tracker.Nonce) + ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, chainID, tracker.Nonce) } } ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.stop: - ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", chainID) return } } @@ -162,7 +163,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // Get outbound block height - blockHeight, err := GetBlockHeightByHash(ob.rpcClient, res.BlockHash) + blockHeight, err := GetBlockHeightByHash(ob.btcClient, res.BlockHash) if err != nil { return true, false, errors.Wrapf( err, @@ -172,7 +173,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } logger.Debug().Msgf("Bitcoin outbound confirmed: txid %s, amount %s\n", res.TxID, amountInSat.String()) - zetaHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( sendHash, res.TxID, // #nosec G701 always positive @@ -182,7 +183,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg 0, // gas limit not used with Bitcoin amountInSat, chains.ReceiveStatus_success, - ob.chain, + ob.Chain(), nonce, coin.CoinType_Gas, ) @@ -299,7 +300,7 @@ func (ob *Observer) SelectUTXOs( // 2. The tracker is missing in zetacore. func (ob *Observer) refreshPendingNonce() { // get pending nonces from zetacore - p, err := ob.zetacoreClient.GetPendingNoncesByChain(ob.chain.ChainId) + p, err := ob.ZetacoreClient().GetPendingNoncesByChain(ob.Chain().ChainId) if err != nil { ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") } @@ -336,7 +337,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return res.TxID, nil } if !test { // if not unit test, get cctx from zetacore - send, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, nonce) + send, err := ob.ZetacoreClient().GetCctxByNonce(ob.Chain().ChainId, nonce) if err != nil { return "", errors.Wrapf(err, "getOutboundIDByNonce: error getting cctx for nonce %d", nonce) } @@ -345,7 +346,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return "", fmt.Errorf("getOutboundIDByNonce: cannot find outbound txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) + _, getTxResult, err := GetTxResultByHash(ob.btcClient, txid) if err != nil { return "", errors.Wrapf( err, @@ -363,7 +364,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error } func (ob *Observer) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { - tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() amount := chains.NonceMarkAmount(nonce) for i, utxo := range ob.utxos { sats, err := bitcoin.GetSatoshis(utxo.Amount) @@ -386,7 +387,7 @@ func (ob *Observer) checkIncludedTx( txHash string, ) (*btcjson.GetTransactionResult, bool) { outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) - hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) + hash, getTxResult, err := GetTxResultByHash(ob.btcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -470,7 +471,7 @@ func (ob *Observer) checkTssOutboundResult( ) error { params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce - rawResult, err := GetRawTxResult(ob.rpcClient, hash, res) + rawResult, err := GetRawTxResult(ob.btcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } @@ -507,7 +508,7 @@ func (ob *Observer) checkTSSVin(vins []btcjson.Vin, nonce uint64) error { if nonce > 0 && len(vins) <= 1 { return fmt.Errorf("checkTSSVin: len(vins) <= 1") } - pubKeyTss := hex.EncodeToString(ob.Tss.PubKeyCompressedBytes()) + pubKeyTss := hex.EncodeToString(ob.TSS().PubKeyCompressedBytes()) for i, vin := range vins { // The length of the Witness should be always 2 for SegWit inputs. if len(vin.Witness) != 2 { @@ -547,7 +548,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ } nonce := params.TssNonce - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout receiverExpected := tssAddress @@ -555,7 +556,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ // the 2nd output is the payment to recipient receiverExpected = params.Receiver } - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.chain) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.Chain()) if err != nil { return err } @@ -606,10 +607,10 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams } nonce := params.TssNonce - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.chain) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain()) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index bad0997c0c..910384ef9a 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -3,7 +3,6 @@ package observer import ( "math" "sort" - "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -11,22 +10,27 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -func MockBTCObserverMainnet() *Observer { - cfg := config.NewConfig() - coreContext := context.NewZetacoreContext(cfg) +// the relative path to the testdata directory +var TestDataDir = "../../../" - return &Observer{ - chain: chains.BitcoinMainnet, - zetacoreClient: mocks.NewMockZetacoreClient(), - Tss: mocks.NewTSSMainnet(), - coreContext: coreContext, - } +// MockBTCObserverMainnet creates a mock Bitcoin mainnet observer for testing +func MockBTCObserverMainnet(t *testing.T) *Observer { + // setup mock arguments + chain := chains.BitcoinMainnet + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) + params := mocks.MockChainParams(chain.ChainId, 10) + tss := mocks.NewTSSMainnet() + + // create Bitcoin observer + ob, err := NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, base.Logger{}, nil) + require.NoError(t, err) + + return ob } // helper function to create a test Bitcoin observer @@ -37,26 +41,27 @@ func createObserverWithPrivateKey(t *testing.T) *Observer { tss := &mocks.TSS{ PrivKey: privateKey, } - return &Observer{ - Tss: tss, - Mu: &sync.Mutex{}, - includedTxResults: make(map[string]*btcjson.GetTransactionResult), - } + + // create Bitcoin observer with mock tss + ob := MockBTCObserverMainnet(t) + ob.WithTSS(tss) + + return ob } // helper function to create a test Bitcoin observer with UTXOs func createObserverWithUTXOs(t *testing.T) *Observer { // Create Bitcoin observer - client := createObserverWithPrivateKey(t) - tssAddress := client.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + ob := createObserverWithPrivateKey(t) + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() // Create 10 dummy UTXOs (22.44 BTC in total) - client.utxos = make([]btcjson.ListUnspentResult, 0, 10) + ob.utxos = make([]btcjson.ListUnspentResult, 0, 10) amounts := []float64{0.01, 0.12, 0.18, 0.24, 0.5, 1.26, 2.97, 3.28, 5.16, 8.72} for _, amount := range amounts { - client.utxos = append(client.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) + ob.utxos = append(ob.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) } - return client + return ob } func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex int) { @@ -65,7 +70,7 @@ func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex i ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark - tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() nonceMark := btcjson.ListUnspentResult{ TxID: txid, Address: tssAddress, @@ -90,22 +95,22 @@ func TestCheckTSSVout(t *testing.T) { nonce := uint64(148) // create mainnet mock client - btcClient := MockBTCObserverMainnet() + ob := MockBTCObserverMainnet(t) t.Run("valid TSS vout should pass", func(t *testing.T) { rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 2 or > 3", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}) + err := ob.checkTSSVout(params, []btcjson.Vout{{}}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) + err = ob.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail on invalid TSS vout", func(t *testing.T) { @@ -114,7 +119,7 @@ func TestCheckTSSVout(t *testing.T) { // invalid TSS vout rawResult.Vout[0].ScriptPubKey.Hex = "invalid script" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.Error(t, err) }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -123,7 +128,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -132,7 +137,7 @@ func TestCheckTSSVout(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the receiver address", func(t *testing.T) { @@ -141,7 +146,7 @@ func TestCheckTSSVout(t *testing.T) { // not receiver address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params receiver") }) t.Run("should fail if vout 1 not match payment amount", func(t *testing.T) { @@ -150,7 +155,7 @@ func TestCheckTSSVout(t *testing.T) { // not match payment amount rawResult.Vout[1].Value = 0.00011000 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params amount") }) t.Run("should fail if vout 2 is not to the TSS address", func(t *testing.T) { @@ -159,7 +164,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[2].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } @@ -172,7 +177,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { nonce := uint64(148) // create mainnet mock client - btcClient := MockBTCObserverMainnet() + ob := MockBTCObserverMainnet(t) t.Run("valid TSS vout should pass", func(t *testing.T) { // remove change vout to simulate cancelled tx @@ -181,17 +186,17 @@ func TestCheckTSSVoutCancelled(t *testing.T) { rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 1 or > 2", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}) + err := ob.checkTSSVoutCancelled(params, []btcjson.Vout{}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) + err = ob.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -203,7 +208,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -215,7 +220,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the TSS address", func(t *testing.T) { @@ -228,7 +233,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index 906634e1ad..bb8930f4ca 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -40,7 +40,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -56,7 +56,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -72,7 +72,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -89,7 +89,17 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain connector - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) + ob := MockEVMObserver( + t, + chain, + nil, + nil, + nil, + nil, + memDBPath, + lastBlock, + mocks.MockChainParams(chainID, confirmation), + ) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -115,7 +125,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -131,7 +141,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -147,7 +157,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -164,7 +174,17 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain ERC20 custody - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) + ob := MockEVMObserver( + t, + chain, + nil, + nil, + nil, + nil, + memDBPath, + lastBlock, + mocks.MockChainParams(chainID, confirmation), + ) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -190,7 +210,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -200,7 +220,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -210,7 +230,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) @@ -221,7 +241,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) @@ -232,7 +252,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) @@ -249,7 +269,7 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_Zeta, inboundHash) // parse ZetaSent event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) connector := mocks.MockConnectorNonEth(t, chainID) event := testutils.ParseReceiptZetaSent(receipt, connector) @@ -296,7 +316,7 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_ERC20, inboundHash) // parse Deposited event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) custody := mocks.MockERC20Custody(t, chainID) event := testutils.ParseReceiptERC20Deposited(receipt, custody) sender := ethcommon.HexToAddress(tx.From) @@ -354,7 +374,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(txDonation)) // create test compliance config - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) cfg := config.Config{ ComplianceConfig: config.ComplianceConfig{}, } @@ -424,7 +444,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation t.Run("should observe TSS receive in block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) // feed archived block and receipt evmJSONRPC.WithBlock(block) @@ -433,20 +453,20 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { require.NoError(t, err) }) t.Run("should not observe on error getting block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock JSONRPC contains no block require.ErrorContains(t, err, "error getting block") }) t.Run("should not observe on error getting receipt", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) evmJSONRPC.WithBlock(block) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock evmClient contains no receipt require.ErrorContains(t, err, "error getting receipt") }) t.Run("should not observe on error posting vote", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) // feed archived block and pause zetacore client evmJSONRPC.WithBlock(block) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index ada5399909..158b3e326c 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -19,7 +19,6 @@ import ( zetaconnectoreth "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.eth.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -53,7 +52,7 @@ type Observer struct { // outboundConfirmedTransactions is the map to index confirmed transactions by hash outboundConfirmedTransactions map[string]*ethtypes.Transaction - // Mu protects fields from concurrent access + // Mu protects the maps and chain params from concurrent access Mu *sync.Mutex } @@ -86,7 +85,7 @@ func NewObserver( } // create evm observer - ob := Observer{ + ob := &Observer{ Observer: *baseObserver, evmClient: evmClient, evmJSONRPC: ethrpc.NewEthRPC(evmCfg.Endpoint), @@ -97,12 +96,12 @@ func NewObserver( } // open database and load data - err = ob.LoadDB(dbpath, evmCfg.Chain) + err = ob.LoadDB(dbpath) if err != nil { return nil, err } - return &ob, nil + return ob, nil } // WithEvmClient attaches a new evm client to the observer @@ -120,7 +119,7 @@ func (ob *Observer) WithEvmJSONRPC(client interfaces.EVMJSONRPCClient) { func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.Observer.WithChainParams(params) + ob.WithChainParams(params) } // GetChainParams returns the chain params for the observer @@ -128,7 +127,7 @@ func (ob *Observer) SetChainParams(params observertypes.ChainParams) { func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.Observer.ChainParams() + return ob.ChainParams() } // GetConnectorContract returns the non-Eth connector address and binder @@ -170,6 +169,8 @@ func FetchZetaTokenContract( // Start all observation routines for the evm chain func (ob *Observer) Start() { + ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + // watch evm chain for incoming txs and post votes to zetacore go ob.WatchInbound() @@ -227,20 +228,6 @@ func (ob *Observer) WatchRPCStatus() { } } -// Stop all goroutines and closes the database -func (ob *Observer) Stop() { - // notifies all goroutines to stop - ob.Logger().Chain.Info().Msgf("stopping observer for chain %d", ob.Chain().ChainId) - close(ob.StopChannel()) - - // close database - err := ob.CloseDB() - if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("CloseDB failed") - } - ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) -} - // SetPendingTx sets the pending transaction in memory func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { ob.Mu.Lock() @@ -435,15 +422,15 @@ func (ob *Observer) BlockByNumber(blockNumber int) (*ethrpc.Block, error) { } // LoadDB open sql database and load data into EVM observer -func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { +func (ob *Observer) LoadDB(dbPath string) error { if dbPath == "" { return errors.New("empty db path") } // open database - err := ob.OpenDB(dbPath) + err := ob.OpenDB(dbPath, "") if err != nil { - return errors.Wrapf(err, "error OpenDB for chain %d", chain.ChainId) + return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) } // run auto migration @@ -453,13 +440,20 @@ func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { &clienttypes.TransactionSQLType{}, ) if err != nil { - return errors.Wrapf(err, "error AutoMigrate for chain %d", chain.ChainId) + return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) } - // try reading last scanned block from env variable or DB - err = ob.LoadLastBlockScanned(ob.Logger().Chain) + // load last block scanned + err = ob.LoadLastBlockScanned() + + return err +} + +// LoadLastBlockScanned loads the last scanned block from the database +func (ob *Observer) LoadLastBlockScanned() error { + err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) if err != nil { - return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", chain.ChainId) + return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) } // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: @@ -468,11 +462,11 @@ func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { if ob.LastBlockScanned() == 0 { blockNumber, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - return errors.Wrapf(err, "error BlockNumber for chain %d", chain.ChainId) + return errors.Wrapf(err, "error BlockNumber for chain %d", ob.Chain().ChainId) } ob.WithLastBlockScanned(blockNumber) } - ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", chain.ChainId, ob.LastBlockScanned()) + ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) return nil } diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index b7b9ca5593..9ef7e47e1b 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -78,6 +78,7 @@ func MockEVMObserver( evmJSONRPC interfaces.EVMJSONRPCClient, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, + dbpath string, lastBlock uint64, params observertypes.ChainParams, ) *observer.Observer { @@ -97,9 +98,6 @@ func MockEVMObserver( // create zetacore context coreCtx, evmCfg := getZetacoreContext(chain, "", ¶ms) - // create dbpath - dbpath := testutils.CreateTempDir(t) - // create observer ob, err := observer.NewObserver(evmCfg, evmClient, params, coreCtx, zetacoreClient, tss, dbpath, base.Logger{}, nil) require.NoError(t, err) @@ -119,7 +117,7 @@ func Test_NewObserver(t *testing.T) { name string evmCfg config.EVMConfig chainParams observertypes.ChainParams - mockClient interfaces.EVMRPCClient + evmClient interfaces.EVMRPCClient tss interfaces.TSSSigner dbpath string logger base.Logger @@ -134,7 +132,7 @@ func Test_NewObserver(t *testing.T) { Endpoint: "http://localhost:8545", }, chainParams: params, - mockClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), tss: mocks.NewTSSMainnet(), dbpath: testutils.CreateTempDir(t), logger: base.Logger{}, @@ -148,7 +146,7 @@ func Test_NewObserver(t *testing.T) { Endpoint: "http://localhost:8545", }, chainParams: params, - mockClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), tss: mocks.NewTSSMainnet(), dbpath: "/invalid/dbpath", // invalid dbpath logger: base.Logger{}, @@ -163,7 +161,7 @@ func Test_NewObserver(t *testing.T) { Endpoint: "http://localhost:8545", }, chainParams: params, - mockClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), + evmClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), tss: mocks.NewTSSMainnet(), dbpath: testutils.CreateTempDir(t), logger: base.Logger{}, @@ -183,7 +181,7 @@ func Test_NewObserver(t *testing.T) { // create observer ob, err := observer.NewObserver( tt.evmCfg, - tt.mockClient, + tt.evmClient, tt.chainParams, zetacoreCtx, zetacoreClient, @@ -209,42 +207,21 @@ func Test_LoadDB(t *testing.T) { // use Ethereum chain for testing chain := chains.Ethereum params := mocks.MockChainParams(chain.ChainId, 10) - evmCfg := config.EVMConfig{ - Chain: chain, - Endpoint: "http://localhost:8545", - } - - // create mock client, tss and test dbpath - mockClient := mocks.NewMockEvmClient().WithBlockNumber(1000) - tss := mocks.NewTSSMainnet() - - // create zetacore context and client - zetacoreCtx, _ := getZetacoreContext(evmCfg.Chain, evmCfg.Endpoint, ¶ms) - zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) - - // create observer dbpath := testutils.CreateTempDir(t) - ob, err := observer.NewObserver( - evmCfg, - mockClient, - params, - zetacoreCtx, - zetacoreClient, - tss, - dbpath, - base.Logger{}, - nil, - ) - require.NoError(t, err) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, dbpath, 1, params) t.Run("should load db successfully", func(t *testing.T) { - err := ob.LoadDB(dbpath, chain) + err := ob.LoadDB(dbpath) require.NoError(t, err) require.EqualValues(t, 1000, ob.LastBlockScanned()) }) t.Run("should fail on invalid dbpath", func(t *testing.T) { + // load db with empty dbpath + err := ob.LoadDB("") + require.ErrorContains(t, err, "empty db path") + // load db with invalid dbpath - err := ob.LoadDB("/invalid/dbpath", chain) + err = ob.LoadDB("/invalid/dbpath") require.ErrorContains(t, err, "error OpenDB") }) t.Run("should fail on invalid env var", func(t *testing.T) { @@ -254,30 +231,65 @@ func Test_LoadDB(t *testing.T) { defer os.Unsetenv(envvar) // load db - err := ob.LoadDB(dbpath, chain) + err := ob.LoadDB(dbpath) require.ErrorContains(t, err, "error LoadLastBlockScanned") }) t.Run("should fail on RPC error", func(t *testing.T) { // create observer tempClient := mocks.NewMockEvmClient() - ob, err := observer.NewObserver( - evmCfg, - tempClient, - params, - zetacoreCtx, - zetacoreClient, - tss, - dbpath, - base.Logger{}, - nil, - ) - require.NoError(t, err) + ob := MockEVMObserver(t, chain, tempClient, nil, nil, nil, dbpath, 1, params) // set RPC error tempClient.WithError(fmt.Errorf("error RPC")) // load db - err = ob.LoadDB(dbpath, chain) + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error RPC") + }) +} + +func Test_LoadLastBlockScanned(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) + + // create observer using mock evm client + evmClient := mocks.NewMockEvmClient().WithBlockNumber(100) + dbpath := testutils.CreateTempDir(t) + ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, dbpath, 1, params) + + t.Run("should load last block scanned", func(t *testing.T) { + // create db and write 123 as last block scanned + ob.WriteLastBlockScannedToDB(123) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, 123, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer on separate path, as we need to reset last block scanned + otherPath := testutils.CreateTempDir(t) + obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, otherPath, 1, params) + + // reset last block scanned to 0 so that it will be loaded from RPC + obOther.WithLastBlockScanned(0) + + // set RPC error + evmClient.WithError(fmt.Errorf("error RPC")) + + // load last block scanned + err := obOther.LoadLastBlockScanned() require.ErrorContains(t, err, "error RPC") }) } diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index e0806d6086..df9a642a8e 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -13,12 +13,17 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) +const ( + memDBPath = base.TempSQLiteDBPath +) + // getContractsByChainID is a helper func to get contracts and addresses by chainID func getContractsByChainID( t *testing.T, @@ -57,11 +62,12 @@ func Test_IsOutboundProcessed(t *testing.T) { ) t.Run("should post vote and return true if outbound is processed", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) + // post outbound vote - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) @@ -73,9 +79,9 @@ func Test_IsOutboundProcessed(t *testing.T) { cctx := testutils.LoadCctxByNonce(t, chainID, nonce) cctx.InboundParams.Sender = sample.EthAddress().Hex() - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) // modify compliance config to restrict sender address cfg := config.Config{ @@ -85,29 +91,29 @@ func Test_IsOutboundProcessed(t *testing.T) { config.LoadComplianceConfig(cfg) // post outbound vote - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) }) t.Run("should return false if outbound is not confirmed", func(t *testing.T) { - // create evm client and DO NOT set outbound as confirmed - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + // create evm observer and DO NOT set outbound as confirmed + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) }) t.Run("should fail if unable to parse ZetaReceived event", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail - chainParamsNew := client.GetChainParams() + chainParamsNew := ob.GetChainParams() chainParamsNew.ConnectorContractAddress = sample.EthAddress().Hex() - client.SetChainParams(chainParamsNew) - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + ob.SetChainParams(chainParamsNew) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.Error(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) @@ -147,15 +153,15 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { ) t.Run("should fail if unable to get connector/custody contract", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) abiConnector := zetaconnector.ZetaConnectorNonEthMetaData.ABI abiCustody := erc20custody.ERC20CustodyMetaData.ABI // set invalid connector ABI zetaconnector.ZetaConnectorNonEthMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) zetaconnector.ZetaConnectorNonEthMetaData.ABI = abiConnector // reset connector ABI require.ErrorContains(t, err, "error getting zeta connector") require.False(t, isIncluded) @@ -163,7 +169,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { // set invalid custody ABI erc20custody.ERC20CustodyMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err = client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err = ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.ErrorContains(t, err, "error getting erc20 custody") require.False(t, isIncluded) require.False(t, isConfirmed) @@ -193,8 +199,8 @@ func Test_PostVoteOutbound(t *testing.T) { // create evm client using mock zetacore client and post outbound vote zetacoreClient := mocks.NewMockZetacoreClient() - client := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, 1, observertypes.ChainParams{}) - client.PostVoteOutbound( + ob := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, memDBPath, 1, observertypes.ChainParams{}) + ob.PostVoteOutbound( cctx.Index, receipt, outbound, @@ -207,7 +213,7 @@ func Test_PostVoteOutbound(t *testing.T) { // pause the mock zetacore client to simulate error posting vote zetacoreClient.Pause() - client.PostVoteOutbound( + ob.PostVoteOutbound( cctx.Index, receipt, outbound, diff --git a/zetaclient/testutils/mocks/btc_rpc.go b/zetaclient/testutils/mocks/btc_rpc.go index cfd63ef87b..01d286d31a 100644 --- a/zetaclient/testutils/mocks/btc_rpc.go +++ b/zetaclient/testutils/mocks/btc_rpc.go @@ -17,7 +17,9 @@ var _ interfaces.BTCRPCClient = &MockBTCRPCClient{} // MockBTCRPCClient is a mock implementation of the BTCRPCClient interface type MockBTCRPCClient struct { - Txs []*btcutil.Tx + err error + blockCount int64 + Txs []*btcutil.Tx } // NewMockBTCRPCClient creates a new mock BTC RPC client @@ -28,6 +30,10 @@ func NewMockBTCRPCClient() *MockBTCRPCClient { // Reset clears the mock data func (c *MockBTCRPCClient) Reset() *MockBTCRPCClient { + if c.err != nil { + return nil + } + c.Txs = []*btcutil.Tx{} return c } @@ -95,7 +101,10 @@ func (c *MockBTCRPCClient) GetRawTransactionVerbose(_ *chainhash.Hash) (*btcjson } func (c *MockBTCRPCClient) GetBlockCount() (int64, error) { - return 0, errors.New("not implemented") + if c.err != nil { + return 0, c.err + } + return c.blockCount, nil } func (c *MockBTCRPCClient) GetBlockHash(_ int64) (*chainhash.Hash, error) { @@ -118,6 +127,16 @@ func (c *MockBTCRPCClient) GetBlockHeader(_ *chainhash.Hash) (*wire.BlockHeader, // Feed data to the mock BTC RPC client for testing // ---------------------------------------------------------------------------- +func (c *MockBTCRPCClient) WithError(err error) *MockBTCRPCClient { + c.err = err + return c +} + +func (c *MockBTCRPCClient) WithBlockCount(blkCnt int64) *MockBTCRPCClient { + c.blockCount = blkCnt + return c +} + func (c *MockBTCRPCClient) WithRawTransaction(tx *btcutil.Tx) *MockBTCRPCClient { c.Txs = append(c.Txs, tx) return c From 7d1650b452df8bef47c7f273b28693fe3f7d5c16 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 20 Jun 2024 08:11:09 -0500 Subject: [PATCH 09/21] added changelog entry --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index d7bd6e96d7..6ffc7eef7c 100644 --- a/changelog.md +++ b/changelog.md @@ -48,6 +48,7 @@ * [2296](https://github.com/zeta-chain/node/pull/2296) - move `testdata` package to `testutil` to organize test-related utilities * [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs * [2317](https://github.com/zeta-chain/node/pull/2317) - add ValidateOutbound method for cctx orchestrator +* [2355](https://github.com/zeta-chain/node/pull/2355) - integrated base Signer/Observer structures into EVM/Bitcoin Signer and Observer ### Tests From 0cb55e1d3c9e7b9f076873c8a76532167ec9ee2c Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 20 Jun 2024 11:12:47 -0500 Subject: [PATCH 10/21] cherry pick base Signer structure integration --- changelog.md | 2 +- cmd/zetaclientd/utils.go | 66 +-- zetaclient/chains/base/observer.go | 136 ++--- zetaclient/chains/base/observer_test.go | 281 ++++------ zetaclient/chains/bitcoin/observer/inbound.go | 86 +-- .../chains/bitcoin/observer/inbound_test.go | 53 +- .../chains/bitcoin/observer/live_test.go | 40 +- .../chains/bitcoin/observer/observer.go | 514 ++++++++++-------- .../chains/bitcoin/observer/observer_test.go | 269 +-------- .../chains/bitcoin/observer/outbound.go | 57 +- .../chains/bitcoin/observer/outbound_test.go | 85 ++- zetaclient/chains/evm/observer/inbound.go | 210 ++++--- .../chains/evm/observer/inbound_test.go | 60 +- zetaclient/chains/evm/observer/observer.go | 437 ++++++++++----- .../chains/evm/observer/observer_test.go | 232 +------- zetaclient/chains/evm/observer/outbound.go | 62 +-- .../chains/evm/observer/outbound_test.go | 56 +- .../chains/evm/signer/outbound_data_test.go | 2 +- zetaclient/chains/evm/signer/signer_test.go | 44 +- .../supplychecker/zeta_supply_checker.go | 2 +- 20 files changed, 1173 insertions(+), 1521 deletions(-) diff --git a/changelog.md b/changelog.md index 6ffc7eef7c..1e02187c5d 100644 --- a/changelog.md +++ b/changelog.md @@ -48,7 +48,7 @@ * [2296](https://github.com/zeta-chain/node/pull/2296) - move `testdata` package to `testutil` to organize test-related utilities * [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs * [2317](https://github.com/zeta-chain/node/pull/2317) - add ValidateOutbound method for cctx orchestrator -* [2355](https://github.com/zeta-chain/node/pull/2355) - integrated base Signer/Observer structures into EVM/Bitcoin Signer and Observer +* [2355](https://github.com/zeta-chain/node/pull/2355) - integrated base Signer structure into EVM/Bitcoin Signer ### Tests diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 31444cf5e7..64db1c3efa 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -1,12 +1,8 @@ package main import ( - "fmt" - - "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -120,84 +116,28 @@ func CreateChainObserverMap( logger base.Logger, ts *metrics.TelemetryServer, ) (map[int64]interfaces.ChainObserver, error) { - zetacoreContext := appContext.ZetacoreContext() observerMap := make(map[int64]interfaces.ChainObserver) // EVM observers for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { if evmConfig.Chain.IsZetaChain() { continue } - chainParams, found := zetacoreContext.GetEVMChainParams(evmConfig.Chain.ChainId) + _, found := appContext.ZetacoreContext().GetEVMChainParams(evmConfig.Chain.ChainId) if !found { logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } - - // create EVM client - evmClient, err := ethclient.Dial(evmConfig.Endpoint) - if err != nil { - logger.Std.Error().Err(err).Msgf("error dailing endpoint %s", evmConfig.Endpoint) - continue - } - - // create EVM chain observer - co, err := evmobserver.NewObserver( - evmConfig, - evmClient, - *chainParams, - zetacoreContext, - zetacoreClient, - tss, - dbpath, - logger, - ts, - ) + co, err := evmobserver.NewObserver(appContext, zetacoreClient, tss, dbpath, logger, evmConfig, ts) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) continue } observerMap[evmConfig.Chain.ChainId] = co } - // BTC observer - _, chainParams, found := zetacoreContext.GetBTCChainParams() - if !found { - return nil, fmt.Errorf("bitcoin chains params not found") - } - - // create BTC chain observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - // create BTC client - connCfg := &rpcclient.ConnConfig{ - Host: btcConfig.RPCHost, - User: btcConfig.RPCUsername, - Pass: btcConfig.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcConfig.RPCParams, - } - btcClient, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - err = btcClient.Ping() - if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) - } - - // create BTC chain observer - co, err := btcobserver.NewObserver( - btcChain, - btcClient, - *chainParams, - zetacoreContext, - zetacoreClient, - tss, - dbpath, - logger, - ts, - ) + co, err := btcobserver.NewObserver(appContext, btcChain, zetacoreClient, tss, dbpath, logger, btcConfig, ts) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index b2aa983bbd..905775b2b2 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -11,7 +11,6 @@ import ( "github.com/rs/zerolog" "gorm.io/driver/sqlite" "gorm.io/gorm" - "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -29,12 +28,9 @@ const ( // Cached blocks can be used to get block information and verify transactions DefaultBlockCacheSize = 1000 - // DefaultHeaderCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) + // DefaultHeadersCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) // Cached headers can be used to get header information - DefaultHeaderCacheSize = 1000 - - // TempSQLiteDBPath is the temporary in-memory SQLite database used for testing - TempSQLiteDBPath = "file::memory:?cache=shared" + DefaultHeadersCacheSize = 1000 ) // Observer is the base structure for chain observers, grouping the common logic for each chain observer client. @@ -88,7 +84,8 @@ func NewObserver( zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, blockCacheSize int, - headerCacheSize int, + headersCacheSize int, + dbPath string, ts *metrics.TelemetryServer, logger Logger, ) (*Observer, error) { @@ -115,25 +112,18 @@ func NewObserver( } // create header cache - ob.headerCache, err = lru.New(headerCacheSize) + ob.headerCache, err = lru.New(headersCacheSize) if err != nil { return nil, errors.Wrap(err, "error creating header cache") } - return &ob, nil -} - -// Stop notifies all goroutines to stop and closes the database. -func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("observer is stopping for chain %d", ob.Chain().ChainId) - close(ob.stop) - - // close database - err := ob.CloseDB() + // open database + err = ob.OpenDB(dbPath) if err != nil { - ob.Logger().Chain.Error().Err(err).Msgf("CloseDB failed for chain %d", ob.Chain().ChainId) + return nil, errors.Wrap(err, fmt.Sprintf("error opening observer db for chain: %s", chain.ChainName)) } - ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) + + return &ob, nil } // Chain returns the chain for the observer. @@ -242,20 +232,9 @@ func (ob *Observer) DB() *gorm.DB { return ob.db } -// WithTelemetryServer attaches a new telemetry server to the observer. -func (ob *Observer) WithTelemetryServer(ts *metrics.TelemetryServer) *Observer { - ob.ts = ts - return ob -} - -// TelemetryServer returns the telemetry server for the observer. -func (ob *Observer) TelemetryServer() *metrics.TelemetryServer { - return ob.ts -} - // Logger returns the logger for the observer. -func (ob *Observer) Logger() *ObserverLogger { - return &ob.logger +func (ob *Observer) Logger() ObserverLogger { + return ob.logger } // WithLogger attaches a new logger to the observer. @@ -272,64 +251,45 @@ func (ob *Observer) WithLogger(logger Logger) *Observer { return ob } -// StopChannel returns the stop channel for the observer. -func (ob *Observer) StopChannel() chan struct{} { +// Stop returns the stop channel for the observer. +func (ob *Observer) Stop() chan struct{} { return ob.stop } // OpenDB open sql database in the given path. -func (ob *Observer) OpenDB(dbPath string, dbName string) error { - // create db path if not exist - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return errors.Wrapf(err, "error creating db path: %s", dbPath) +func (ob *Observer) OpenDB(dbPath string) error { + if dbPath != "" { + // create db path if not exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) + if err != nil { + return errors.Wrap(err, "error creating db path") + } } - } - // use custom dbName or chain name if not provided - if dbName == "" { - dbName = ob.chain.ChainName.String() - } - path := fmt.Sprintf("%s/%s", dbPath, dbName) - - // use memory db if specified - if dbPath == TempSQLiteDBPath { - path = TempSQLiteDBPath - } - - // open db - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - if err != nil { - return errors.Wrap(err, "error opening db") - } - - // migrate db - err = db.AutoMigrate(&clienttypes.LastBlockSQLType{}) - if err != nil { - return errors.Wrap(err, "error migrating db") - } - ob.db = db - - return nil -} + // open db by chain name + chainName := ob.chain.ChainName.String() + path := fmt.Sprintf("%s/%s", dbPath, chainName) + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { + return errors.Wrap(err, "error opening db") + } -// CloseDB close the database. -func (ob *Observer) CloseDB() error { - dbInst, err := ob.db.DB() - if err != nil { - return fmt.Errorf("error getting database instance: %w", err) - } - err = dbInst.Close() - if err != nil { - return fmt.Errorf("error closing database: %w", err) + // migrate db + err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + &clienttypes.LastBlockSQLType{}) + if err != nil { + return errors.Wrap(err, "error migrating db") + } + ob.db = db } return nil } // LoadLastBlockScanned loads last scanned block from environment variable or from database. // The last scanned block is the height from which the observer should continue scanning. -func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) error { +func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool, err error) { // get environment variable envvar := EnvVarLatestBlockByChain(ob.chain) scanFromBlock := os.Getenv(envvar) @@ -339,33 +299,27 @@ func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) error { logger.Info(). Msgf("LoadLastBlockScanned: envvar %s is set; scan from block %s", envvar, scanFromBlock) if scanFromBlock == EnvVarLatestBlock { - return nil + return true, nil } blockNumber, err := strconv.ParseUint(scanFromBlock, 10, 64) if err != nil { - return err + return false, err } ob.WithLastBlockScanned(blockNumber) - return nil + return false, nil } // load from DB otherwise. If not found, start from latest block blockNumber, err := ob.ReadLastBlockScannedFromDB() if err != nil { - logger.Info().Msgf("LoadLastBlockScanned: last scanned block not found in db for chain %d", ob.chain.ChainId) - return nil + logger.Info().Msgf("LoadLastBlockScanned: chain %d starts scanning from latest block", ob.chain.ChainId) + return true, nil } ob.WithLastBlockScanned(blockNumber) logger.Info(). Msgf("LoadLastBlockScanned: chain %d starts scanning from block %d", ob.chain.ChainId, ob.LastBlockScanned()) - return nil -} - -// SaveLastBlockScanned saves the last scanned block to memory and database. -func (ob *Observer) SaveLastBlockScanned(blockNumber uint64) error { - ob.WithLastBlockScanned(blockNumber) - return ob.WriteLastBlockScannedToDB(blockNumber) + return false, nil } // WriteLastBlockScannedToDB saves the last scanned block to the database. @@ -385,5 +339,5 @@ func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { // EnvVarLatestBlock returns the environment variable for the latest block by chain. func EnvVarLatestBlockByChain(chain chains.Chain) string { - return fmt.Sprintf("CHAIN_%d_SCAN_FROM", chain.ChainId) + return chain.ChainName.String() + "_SCAN_FROM" } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 870f88715e..7dd2f18081 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -15,13 +15,18 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" - "github.com/zeta-chain/zetacore/zetaclient/metrics" - "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) +// create a temporary directory for testing +func createTempDir(t *testing.T) string { + tempPath, err := os.MkdirTemp("", "tempdir-") + require.NoError(t, err) + return tempPath +} + // createObserver creates a new observer for testing -func createObserver(t *testing.T) *base.Observer { +func createObserver(t *testing.T, dbPath string) *base.Observer { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) @@ -38,7 +43,8 @@ func createObserver(t *testing.T) *base.Observer { zetacoreClient, tss, base.DefaultBlockCacheSize, - base.DefaultHeaderCacheSize, + base.DefaultHeadersCacheSize, + dbPath, nil, logger, ) @@ -55,55 +61,73 @@ func TestNewObserver(t *testing.T) { zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize - headersCacheSize := base.DefaultHeaderCacheSize + headersCacheSize := base.DefaultHeadersCacheSize + dbPath := createTempDir(t) // test cases tests := []struct { - name string - chain chains.Chain - chainParams observertypes.ChainParams - zetacoreContext *context.ZetacoreContext - zetacoreClient interfaces.ZetacoreClient - tss interfaces.TSSSigner - blockCacheSize int - headerCacheSize int - fail bool - message string + name string + chain chains.Chain + chainParams observertypes.ChainParams + zetacoreContext *context.ZetacoreContext + zetacoreClient interfaces.ZetacoreClient + tss interfaces.TSSSigner + blockCacheSize int + headersCacheSize int + dbPath string + fail bool + message string }{ { - name: "should be able to create new observer", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headerCacheSize: headersCacheSize, - fail: false, + name: "should be able to create new observer", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + headersCacheSize: headersCacheSize, + dbPath: dbPath, + fail: false, }, { - name: "should return error on invalid block cache size", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: 0, - headerCacheSize: headersCacheSize, - fail: true, - message: "error creating block cache", + name: "should return error on invalid block cache size", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: 0, + headersCacheSize: headersCacheSize, + dbPath: dbPath, + fail: true, + message: "error creating block cache", }, { - name: "should return error on invalid header cache size", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headerCacheSize: 0, - fail: true, - message: "error creating header cache", + name: "should return error on invalid header cache size", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + headersCacheSize: 0, + dbPath: dbPath, + fail: true, + message: "error creating header cache", + }, + { + name: "should return error on invalid db path", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + headersCacheSize: headersCacheSize, + dbPath: "/invalid/123db", + fail: true, + message: "error opening observer db", }, } @@ -117,7 +141,8 @@ func TestNewObserver(t *testing.T) { tt.zetacoreClient, tt.tss, tt.blockCacheSize, - tt.headerCacheSize, + tt.headersCacheSize, + tt.dbPath, nil, base.DefaultLogger(), ) @@ -134,8 +159,10 @@ func TestNewObserver(t *testing.T) { } func TestObserverGetterAndSetter(t *testing.T) { + dbPath := createTempDir(t) + t.Run("should be able to update chain", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update chain newChain := chains.BscMainnet @@ -143,7 +170,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newChain, ob.Chain()) }) t.Run("should be able to update chain params", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update chain params newChainParams := *sample.ChainParams(chains.BscMainnet.ChainId) @@ -151,7 +178,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.True(t, observertypes.ChainParamsEqual(newChainParams, ob.ChainParams())) }) t.Run("should be able to update zetacore context", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update zetacore context newZetacoreContext := context.NewZetacoreContext(config.NewConfig()) @@ -159,7 +186,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreContext, ob.ZetacoreContext()) }) t.Run("should be able to update zetacore client", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update zetacore client newZetacoreClient := mocks.NewMockZetacoreClient() @@ -167,7 +194,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreClient, ob.ZetacoreClient()) }) t.Run("should be able to update tss", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update tss newTSS := mocks.NewTSSAthens3() @@ -175,7 +202,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newTSS, ob.TSS()) }) t.Run("should be able to update last block", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update last block newLastBlock := uint64(100) @@ -183,7 +210,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newLastBlock, ob.LastBlock()) }) t.Run("should be able to update last block scanned", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update last block scanned newLastBlockScanned := uint64(100) @@ -191,7 +218,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newLastBlockScanned, ob.LastBlockScanned()) }) t.Run("should be able to replace block cache", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) // update block cache newBlockCache, err := lru.New(200) @@ -200,8 +227,8 @@ func TestObserverGetterAndSetter(t *testing.T) { ob = ob.WithBlockCache(newBlockCache) require.Equal(t, newBlockCache, ob.BlockCache()) }) - t.Run("should be able to replace header cache", func(t *testing.T) { - ob := createObserver(t) + t.Run("should be able to replace headers cache", func(t *testing.T) { + ob := createObserver(t, dbPath) // update headers cache newHeadersCache, err := lru.New(200) @@ -211,24 +238,13 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newHeadersCache, ob.HeaderCache()) }) t.Run("should be able to get database", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - ob.OpenDB(dbPath, "") + ob := createObserver(t, dbPath) db := ob.DB() require.NotNil(t, db) }) - t.Run("should be able to update telemetry server", func(t *testing.T) { - ob := createObserver(t) - - // update telemetry server - newServer := metrics.NewTelemetryServer() - ob = ob.WithTelemetryServer(newServer) - require.Equal(t, newServer, ob.TelemetryServer()) - }) t.Run("should be able to get logger", func(t *testing.T) { - ob := createObserver(t) + ob := createObserver(t, dbPath) logger := ob.Logger() // should be able to print log @@ -241,30 +257,16 @@ func TestObserverGetterAndSetter(t *testing.T) { }) } -func TestOpenCloseDB(t *testing.T) { - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - - t.Run("should be able to open/close db", func(t *testing.T) { - // open db - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) - - // close db - err = ob.CloseDB() - require.NoError(t, err) - }) - t.Run("should use memory db if specified", func(t *testing.T) { - // open db with memory - err := ob.OpenDB(base.TempSQLiteDBPath, "") - require.NoError(t, err) +func TestOpenDB(t *testing.T) { + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) - // close db - err = ob.CloseDB() + t.Run("should be able to open db", func(t *testing.T) { + err := ob.OpenDB(dbPath) require.NoError(t, err) }) t.Run("should return error on invalid db path", func(t *testing.T) { - err := ob.OpenDB("/invalid/123db", "") + err := ob.OpenDB("/invalid/123db") require.ErrorContains(t, err, "error creating db path") }) } @@ -274,116 +276,71 @@ func TestLoadLastBlockScanned(t *testing.T) { envvar := base.EnvVarLatestBlockByChain(chain) t.Run("should be able to load last block scanned", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) - // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // read last block scanned - err = ob.LoadLastBlockScanned(log.Logger) + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 100, ob.LastBlockScanned()) + require.False(t, fromLatest) }) - t.Run("latest block scanned should be 0 if not found in db", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) + t.Run("should use latest block if last block scanned not found", func(t *testing.T) { + // create empty db + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) // read last block scanned - err = ob.LoadLastBlockScanned(log.Logger) + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.EqualValues(t, 0, ob.LastBlockScanned()) + require.True(t, fromLatest) }) t.Run("should overwrite last block scanned if env var is set", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) - // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // set env var os.Setenv(envvar, "101") // read last block scanned - err = ob.LoadLastBlockScanned(log.Logger) + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 101, ob.LastBlockScanned()) - }) - t.Run("last block scanned should remain 0 if env var is set to latest", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) - - // create db and write 100 as last block scanned - ob.WriteLastBlockScannedToDB(100) + require.False(t, fromLatest) // set env var to 'latest' os.Setenv(envvar, base.EnvVarLatestBlock) - // last block scanned should remain 0 - err = ob.LoadLastBlockScanned(log.Logger) + // read last block scanned + fromLatest, err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.EqualValues(t, 0, ob.LastBlockScanned()) + require.True(t, fromLatest) }) t.Run("should return error on invalid env var", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) + // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) // set invalid env var os.Setenv(envvar, "invalid") // read last block scanned - err = ob.LoadLastBlockScanned(log.Logger) + fromLatest, err := ob.LoadLastBlockScanned(log.Logger) require.Error(t, err) - }) -} - -func TestSaveLastBlockScanned(t *testing.T) { - t.Run("should be able to save last block scanned", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) - - // save 100 as last block scanned - err = ob.SaveLastBlockScanned(100) - require.NoError(t, err) - - // check last block scanned in memory - require.EqualValues(t, 100, ob.LastBlockScanned()) - - // read last block scanned from db - lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() - require.NoError(t, err) - require.EqualValues(t, 100, lastBlockScanned) + require.False(t, fromLatest) }) } func TestReadWriteLastBlockScannedToDB(t *testing.T) { t.Run("should be able to write and read last block scanned to db", func(t *testing.T) { - // create observer and open db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) - - // write last block scanned - err = ob.WriteLastBlockScannedToDB(100) + // create db and write 100 as last block scanned + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) + err := ob.WriteLastBlockScannedToDB(100) require.NoError(t, err) lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() @@ -392,10 +349,8 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { }) t.Run("should return error when last block scanned not found in db", func(t *testing.T) { // create empty db - dbPath := testutils.CreateTempDir(t) - ob := createObserver(t) - err := ob.OpenDB(dbPath, "") - require.NoError(t, err) + dbPath := createTempDir(t) + ob := createObserver(t, dbPath) lastScannedBlock, err := ob.ReadLastBlockScannedFromDB() require.Error(t, err) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 2438411b5b..54c5294745 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -25,7 +25,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// WatchInbound watches Bitcoin chain for inbounds on a ticker +// WatchInbound watches Bitcoin chain for incoming txs and post votes to zetacore func (ob *Observer) WatchInbound() { ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.GetChainParams().InboundTicker) if err != nil { @@ -34,15 +34,15 @@ func (ob *Observer) WatchInbound() { } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) + ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) continue } err := ob.ObserveInbound() @@ -50,49 +50,47 @@ func (ob *Observer) WatchInbound() { ob.logger.Inbound.Error().Err(err).Msg("WatchInbound error observing in tx") } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.StopChannel(): - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) + case <-ob.stop: + ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) return } } } -// ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore func (ob *Observer) ObserveInbound() error { // get and update latest block height - cnt, err := ob.btcClient.GetBlockCount() + cnt, err := ob.rpcClient.GetBlockCount() if err != nil { return fmt.Errorf("observeInboundBTC: error getting block number: %s", err) } if cnt < 0 { return fmt.Errorf("observeInboundBTC: block number is negative: %d", cnt) } - // #nosec G701 checked positive - lastBlock := uint64(cnt) - if lastBlock < ob.LastBlock() { + if cnt < ob.GetLastBlockHeight() { return fmt.Errorf( "observeInboundBTC: block number should not decrease: current %d last %d", cnt, - ob.LastBlock(), + ob.GetLastBlockHeight(), ) } - ob.WithLastBlock(lastBlock) + ob.SetLastBlockHeight(cnt) // skip if current height is too low - if lastBlock < ob.GetChainParams().ConfirmationCount { + // #nosec G701 always in range + confirmedBlockNum := cnt - int64(ob.GetChainParams().ConfirmationCount) + if confirmedBlockNum < 0 { return fmt.Errorf("observeInboundBTC: skipping observer, current block number %d is too low", cnt) } // skip if no new block is confirmed - lastScanned := ob.LastBlockScanned() - if lastScanned >= lastBlock-ob.GetChainParams().ConfirmationCount { + lastScanned := ob.GetLastBlockHeightScanned() + if lastScanned >= confirmedBlockNum { return nil } // query incoming gas asset to TSS address blockNumber := lastScanned + 1 - // #nosec G701 always in range - res, err := ob.GetBlockByNumberCached(int64(blockNumber)) + res, err := ob.GetBlockByNumberCached(blockNumber) if err != nil { ob.logger.Inbound.Error().Err(err).Msgf("observeInboundBTC: error getting bitcoin block %d", blockNumber) return err @@ -105,10 +103,9 @@ func (ob *Observer) ObserveInbound() error { // https://github.com/zeta-chain/node/issues/1847 // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 - blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) + blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) if found && blockHeaderVerification.Enabled { - // #nosec G701 always in range - err = ob.postBlockHeader(int64(blockNumber)) + err = ob.postBlockHeader(blockNumber) if err != nil { ob.logger.Inbound.Warn().Err(err).Msgf("observeInboundBTC: error posting block header %d", blockNumber) } @@ -116,14 +113,14 @@ func (ob *Observer) ObserveInbound() error { if len(res.Block.Tx) > 1 { // get depositor fee - depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) + depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) // filter incoming txs to TSS address - tssAddress := ob.TSS().BTCAddress() + tssAddress := ob.Tss.BTCAddress() // #nosec G701 always positive inbounds, err := FilterAndParseIncomingTx( - ob.btcClient, + ob.rpcClient, res.Block.Tx, uint64(res.Block.Height), tssAddress, @@ -142,7 +139,7 @@ func (ob *Observer) ObserveInbound() error { for _, inbound := range inbounds { msg := ob.GetInboundVoteMessageFromBtcEvent(inbound) if msg != nil { - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( + zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -160,8 +157,11 @@ func (ob *Observer) ObserveInbound() error { } } - // save last scanned block to both memory and db - if err := ob.SaveLastBlockScanned(blockNumber); err != nil { + // Save LastBlockHeight + ob.SetLastBlockHeightScanned(blockNumber) + + // #nosec G701 always positive + if err := ob.db.Save(types.ToLastBlockSQLType(uint64(blockNumber))).Error; err != nil { ob.logger.Inbound.Error(). Err(err). Msgf("observeInboundBTC: error writing last scanned block %d to db", blockNumber) @@ -182,18 +182,18 @@ func (ob *Observer) WatchInboundTracker() { for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { ob.logger.Inbound.Error(). Err(err). - Msgf("error observing inbound tracker for chain %d", ob.Chain().ChainId) + Msgf("error observing inbound tracker for chain %d", ob.chain.ChainId) } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.StopChannel(): - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) + case <-ob.stop: + ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) return } } @@ -201,7 +201,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) + trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) if err != nil { return err } @@ -214,7 +214,7 @@ func (ob *Observer) ProcessInboundTrackers() error { return err } ob.logger.Inbound.Info(). - Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.Chain().ChainName, ballotIdentifier, coin.CoinType_Gas.String()) + Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.chain.ChainName, ballotIdentifier, coin.CoinType_Gas.String()) } return nil @@ -227,7 +227,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - tx, err := ob.btcClient.GetRawTransactionVerbose(hash) + tx, err := ob.rpcClient.GetRawTransactionVerbose(hash) if err != nil { return "", err } @@ -237,7 +237,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - blockVb, err := ob.btcClient.GetBlockVerboseTx(blockHash) + blockVb, err := ob.rpcClient.GetBlockVerboseTx(blockHash) if err != nil { return "", err } @@ -246,15 +246,15 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", fmt.Errorf("block %d has no transactions", blockVb.Height) } - depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) - tss, err := ob.ZetacoreClient().GetBtcTssAddress(ob.Chain().ChainId) + depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) + tss, err := ob.zetacoreClient.GetBtcTssAddress(ob.chain.ChainId) if err != nil { return "", err } // #nosec G701 always positive event, err := GetBtcEvent( - ob.btcClient, + ob.rpcClient, *tx, tss, uint64(blockVb.Height), @@ -279,7 +279,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return msg.Digest(), nil } - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( + zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -343,10 +343,10 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) return zetacore.GetInboundVoteMessage( inbound.FromAddress, - ob.Chain().ChainId, + ob.chain.ChainId, inbound.FromAddress, inbound.FromAddress, - ob.ZetacoreClient().Chain().ChainId, + ob.zetacoreClient.Chain().ChainId, cosmosmath.NewUintFromBigInt(amountInt), message, inbound.TxHash, @@ -354,7 +354,7 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) 0, coin.CoinType_Gas, "", - ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), + ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), 0, ) } @@ -368,7 +368,7 @@ func (ob *Observer) DoesInboundContainsRestrictedAddress(inTx *BTCInboundEvent) } if config.ContainRestrictedAddress(inTx.FromAddress, receiver) { compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.Chain().ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") + false, ob.chain.ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") return true } return false diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index 63042e8b85..b67b7d7b50 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -1,4 +1,4 @@ -package observer_test +package observer import ( "bytes" @@ -18,7 +18,6 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" - "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -199,7 +198,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", sender) }) @@ -211,7 +210,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", sender) }) @@ -223,7 +222,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) }) @@ -235,7 +234,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", sender) }) @@ -247,7 +246,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", sender) }) @@ -273,7 +272,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Empty(t, sender) }) @@ -289,7 +288,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // use invalid tx hash txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.Error(t, err) require.Empty(t, sender) }) @@ -297,7 +296,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { // create mock rpc client without preloaded tx rpcClient := mocks.NewMockBTCRPCClient() txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "error getting raw transaction") require.Empty(t, sender) }) @@ -306,7 +305,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // invalid output index txVin := btcjson.Vin{Txid: txHash, Vout: 3} - sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "out of range") require.Empty(t, sender) }) @@ -329,7 +328,7 @@ func TestGetBtcEvent(t *testing.T) { // expected result memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) require.NoError(t, err) - eventExpected := &observer.BTCInboundEvent{ + eventExpected := &BTCInboundEvent{ FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", ToAddress: tssAddress, Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis @@ -348,7 +347,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -363,7 +362,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -378,7 +377,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -393,7 +392,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -408,7 +407,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -419,7 +418,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -430,13 +429,13 @@ func TestGetBtcEvent(t *testing.T) { // modify the tx to have Vout[0] a P2SH output tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) // append 1 byte to script to make it longer than 22 bytes tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" - event, err = observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err = GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -447,7 +446,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -458,7 +457,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -469,7 +468,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -480,7 +479,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -503,7 +502,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -514,7 +513,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -524,7 +523,7 @@ func TestGetBtcEventErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // get BTC event - event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/observer/live_test.go index c5512a1fae..dd1053f620 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/observer/live_test.go @@ -1,4 +1,4 @@ -package observer_test +package observer import ( "context" @@ -23,7 +23,8 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" - "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/zetacore/zetaclient/config" + clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -46,39 +47,34 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { tss := &mocks.TSS{ PrivKey: privateKey, } - - // create mock arguments for constructor - chain := chains.BitcoinMainnet - params := mocks.MockChainParams(chain.ChainId, 10) - btcClient := mocks.NewMockBTCRPCClient() - - // create observer - ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, - base.DefaultLogger(), nil) + appContext := clientcontext.NewAppContext(&clientcontext.ZetacoreContext{}, config.Config{}) + client, err := NewObserver(appContext, chains.BitcoinRegtest, nil, tss, tempSQLiteDbPath, + base.DefaultLogger(), config.BTCConfig{}, nil) suite.Require().NoError(err) - suite.Require().NotNil(ob) suite.rpcClient, err = getRPCClient(18332) suite.Require().NoError(err) skBytes, err := hex.DecodeString(skHex) suite.Require().NoError(err) suite.T().Logf("skBytes: %d", len(skBytes)) - _, err = btcClient.CreateWallet("e2e") + btc := client.rpcClient + + _, err = btc.CreateWallet("e2e") suite.Require().NoError(err) - addr, err := btcClient.GetNewAddress("test") + addr, err := btc.GetNewAddress("test") suite.Require().NoError(err) suite.T().Logf("deployer address: %s", addr) //err = btc.ImportPrivKey(privkeyWIF) //suite.Require().NoError(err) - btcClient.GenerateToAddress(101, addr, nil) + btc.GenerateToAddress(101, addr, nil) suite.Require().NoError(err) - bal, err := btcClient.GetBalance("*") + bal, err := btc.GetBalance("*") suite.Require().NoError(err) suite.T().Logf("balance: %f", bal.ToBTC()) - utxo, err := btcClient.ListUnspent() + utxo, err := btc.ListUnspent() suite.Require().NoError(err) suite.T().Logf("utxo: %d", len(utxo)) for _, u := range utxo { @@ -157,7 +153,7 @@ func (suite *BitcoinObserverTestSuite) Test1() { suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := observer.FilterAndParseIncomingTx( + inbounds, err := FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -194,7 +190,7 @@ func (suite *BitcoinObserverTestSuite) Test2() { suite.T().Logf("block height %d", block.Height) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := observer.FilterAndParseIncomingTx( + inbounds, err := FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -246,11 +242,11 @@ func LiveTestGetBlockHeightByHash(t *testing.T) { invalidHash := "invalidhash" // get block by invalid hash - _, err = observer.GetBlockHeightByHash(client, invalidHash) + _, err = GetBlockHeightByHash(client, invalidHash) require.ErrorContains(t, err, "error decoding block hash") // get block height by block hash - height, err := observer.GetBlockHeightByHash(client, hash) + height, err := GetBlockHeightByHash(client, hash) require.NoError(t, err) require.Equal(t, expectedHeight, height) } @@ -464,7 +460,7 @@ BLOCKLOOP: Txid: mpvin.TxID, Vout: mpvin.Vout, } - senderAddr, err := observer.GetSenderAddressByVin(client, vin, net) + senderAddr, err := GetSenderAddressByVin(client, vin, net) if err != nil { fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) time.Sleep(3 * time.Second) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 0cf1766895..6b93ba12cd 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -6,17 +6,24 @@ import ( "fmt" "math" "math/big" + "os" "sort" "sync" + "sync/atomic" "time" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" "github.com/rs/zerolog" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" @@ -24,34 +31,47 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" + "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) const ( // btcBlocksPerDay represents Bitcoin blocks per days for LRU block cache size btcBlocksPerDay = 144 - // RegnetStartBlock is the hardcoded start block for regnet - RegnetStartBlock = 100 + // bigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC + bigValueSats = 200000000 - // BigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC - BigValueSats = 200000000 - - // BigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations - BigValueConfirmationCount = 6 + // bigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations + bigValueConfirmationCount = 6 ) var _ interfaces.ChainObserver = &Observer{} // Logger contains list of loggers used by Bitcoin chain observer +// TODO: Merge this logger with the one in evm +// https://github.com/zeta-chain/node/issues/2022 type Logger struct { - // base.Logger contains a list of base observer loggers - base.ObserverLogger + // Chain is the parent logger for the chain + Chain zerolog.Logger + + // Inbound is the logger for incoming transactions + Inbound zerolog.Logger // The logger for incoming transactions + + // Outbound is the logger for outgoing transactions + Outbound zerolog.Logger // The logger for outgoing transactions // UTXOs is the logger for UTXOs management - UTXOs zerolog.Logger + UTXOs zerolog.Logger // The logger for UTXOs management + + // GasPrice is the logger for gas price + GasPrice zerolog.Logger // The logger for gas price + + // Compliance is the logger for compliance checks + Compliance zerolog.Logger // The logger for compliance checks } // BTCInboundEvent represents an incoming transaction event @@ -78,20 +98,23 @@ type BTCBlockNHeader struct { // Observer is the Bitcoin chain observer type Observer struct { - // base.Observer implements the base chain observer - base.Observer + BlockCache *lru.Cache - // netParams contains the Bitcoin network parameters - netParams *chaincfg.Params - - // btcClient is the Bitcoin RPC client that interacts with the Bitcoin node - btcClient interfaces.BTCRPCClient + // Mu is lock for all the maps, utxos and core params + Mu *sync.Mutex - // pendingNonce is the outbound artificial pending nonce - pendingNonce uint64 + Tss interfaces.TSSSigner - // utxos contains the UTXOs owned by the TSS address - utxos []btcjson.ListUnspentResult + chain chains.Chain + netParams *chaincfg.Params + rpcClient interfaces.BTCRPCClient + zetacoreClient interfaces.ZetacoreClient + lastBlock int64 + lastBlockScanned int64 + pendingNonce uint64 + utxos []btcjson.ListUnspentResult + params observertypes.ChainParams + coreContext *context.ZetacoreContext // includedTxHashes indexes included tx with tx hash includedTxHashes map[string]bool @@ -102,120 +125,157 @@ type Observer struct { // broadcastedTx indexes the outbound hash with the outbound tx identifier broadcastedTx map[string]string - // logger contains the loggers used by the bitcoin observer + db *gorm.DB + stop chan struct{} logger Logger - - // Mu protects the maps, utxos and chain params from concurrent access - Mu *sync.Mutex + ts *metrics.TelemetryServer } // NewObserver returns a new Bitcoin chain observer func NewObserver( + appcontext *context.AppContext, chain chains.Chain, - btcClient interfaces.BTCRPCClient, - chainParams observertypes.ChainParams, - zetacoreContext *context.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, + btcCfg config.BTCConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - // create base observer - baseObserver, err := base.NewObserver( - chain, - chainParams, - zetacoreContext, - zetacoreClient, - tss, - btcBlocksPerDay, - base.DefaultHeaderCacheSize, - ts, - logger, - ) - if err != nil { - return nil, err + // initialize the observer + ob := Observer{ + ts: ts, } + ob.stop = make(chan struct{}) + ob.chain = chain // get the bitcoin network params - netParams, err := chains.BitcoinNetParamsFromChainID(chain.ChainId) + netParams, err := chains.BitcoinNetParamsFromChainID(ob.chain.ChainId) + if err != nil { + return nil, fmt.Errorf("error getting net params for chain %d: %s", ob.chain.ChainId, err) + } + ob.netParams = netParams + + ob.Mu = &sync.Mutex{} + + chainLogger := logger.Std.With().Str("chain", chain.ChainName.String()).Logger() + ob.logger = Logger{ + Chain: chainLogger, + Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), + Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), + UTXOs: chainLogger.With().Str("module", "WatchUTXOs").Logger(), + GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), + Compliance: logger.Compliance, + } + + ob.zetacoreClient = zetacoreClient + ob.Tss = tss + ob.coreContext = appcontext.ZetacoreContext() + ob.includedTxHashes = make(map[string]bool) + ob.includedTxResults = make(map[string]*btcjson.GetTransactionResult) + ob.broadcastedTx = make(map[string]string) + + // set the Bitcoin chain params + _, chainParams, found := appcontext.ZetacoreContext().GetBTCChainParams() + if !found { + return nil, fmt.Errorf("btc chains params not initialized") + } + ob.params = *chainParams + + // create the RPC client + ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) + connCfg := &rpcclient.ConnConfig{ + Host: btcCfg.RPCHost, + User: btcCfg.RPCUsername, + Pass: btcCfg.RPCPassword, + HTTPPostMode: true, + DisableTLS: true, + Params: btcCfg.RPCParams, + } + rpcClient, err := rpcclient.New(connCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating rpc client: %s", err) + } + + // try connection + ob.rpcClient = rpcClient + err = rpcClient.Ping() + if err != nil { + return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + } + + ob.BlockCache, err = lru.New(btcBlocksPerDay) if err != nil { - return nil, fmt.Errorf("error getting net params for chain %d: %s", chain.ChainId, err) - } - - // create bitcoin observer - ob := &Observer{ - Observer: *baseObserver, - netParams: netParams, - btcClient: btcClient, - pendingNonce: 0, - utxos: []btcjson.ListUnspentResult{}, - includedTxHashes: make(map[string]bool), - includedTxResults: make(map[string]*btcjson.GetTransactionResult), - broadcastedTx: make(map[string]string), - logger: Logger{ - ObserverLogger: *baseObserver.Logger(), - UTXOs: baseObserver.Logger().Chain.With().Str("module", "utxos").Logger(), - }, - Mu: &sync.Mutex{}, + ob.logger.Chain.Error().Err(err).Msg("failed to create bitcoin block cache") + return nil, err } // load btc chain observer DB - err = ob.LoadDB(dbpath) + err = ob.loadDB(dbpath) if err != nil { return nil, err } - return ob, nil + return &ob, nil } -// BtcClient returns the btc client -func (ob *Observer) BtcClient() interfaces.BTCRPCClient { - return ob.btcClient +func (ob *Observer) WithZetacoreClient(client *zetacore.Client) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.zetacoreClient = client +} + +func (ob *Observer) WithLogger(logger zerolog.Logger) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.logger = Logger{ + Chain: logger, + Inbound: logger.With().Str("module", "WatchInbound").Logger(), + Outbound: logger.With().Str("module", "WatchOutbound").Logger(), + UTXOs: logger.With().Str("module", "WatchUTXOs").Logger(), + GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), + } +} + +func (ob *Observer) WithBtcClient(client *rpcclient.Client) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.rpcClient = client +} + +func (ob *Observer) WithChain(chain chains.Chain) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.chain = chain } -// WithBtcClient attaches a new btc client to the observer -func (ob *Observer) WithBtcClient(client interfaces.BTCRPCClient) { - ob.btcClient = client +func (ob *Observer) Chain() chains.Chain { + ob.Mu.Lock() + defer ob.Mu.Unlock() + return ob.chain } -// SetChainParams sets the chain params for the observer -// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.WithChainParams(params) + ob.params = params } -// GetChainParams returns the chain params for the observer -// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.ChainParams() + return ob.params } // Start starts the Go routine to observe the Bitcoin chain func (ob *Observer) Start() { - ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) - - // watch bitcoin chain for incoming txs and post votes to zetacore - go ob.WatchInbound() - - // watch bitcoin chain for outgoing txs status - go ob.WatchOutbound() - - // watch bitcoin chain for UTXOs owned by the TSS address - go ob.WatchUTXOs() - - // watch bitcoin chain for gas rate and post to zetacore - go ob.WatchGasPrice() - - // watch zetacore for bitcoin inbound trackers - go ob.WatchInboundTracker() - - // watch the RPC status of the bitcoin chain - go ob.WatchRPCStatus() + ob.logger.Chain.Info().Msgf("Bitcoin client is starting") + go ob.WatchInbound() // watch bitcoin chain for incoming txs and post votes to zetacore + go ob.WatchOutbound() // watch bitcoin chain for outgoing txs status + go ob.WatchUTXOs() // watch bitcoin chain for UTXOs owned by the TSS address + go ob.WatchGasPrice() // watch bitcoin chain for gas rate and post to zetacore + go ob.WatchInboundTracker() // watch zetacore for bitcoin inbound trackers + go ob.WatchRPCStatus() // watch the RPC status of the bitcoin chain } // WatchRPCStatus watches the RPC status of the Bitcoin chain @@ -230,19 +290,19 @@ func (ob *Observer) WatchRPCStatus() { continue } - bn, err := ob.btcClient.GetBlockCount() + bn, err := ob.rpcClient.GetBlockCount() if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - hash, err := ob.btcClient.GetBlockHash(bn) + hash, err := ob.rpcClient.GetBlockHash(bn) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - header, err := ob.btcClient.GetBlockHeader(hash) + header, err := ob.rpcClient.GetBlockHeader(hash) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue @@ -255,8 +315,8 @@ func (ob *Observer) WatchRPCStatus() { continue } - tssAddr := ob.TSS().BTCAddressWitnessPubkeyHash() - res, err := ob.btcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) + tssAddr := ob.Tss.BTCAddressWitnessPubkeyHash() + res, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) if err != nil { ob.logger.Chain.Error(). Err(err). @@ -274,27 +334,56 @@ func (ob *Observer) WatchRPCStatus() { ob.logger.Chain.Info(). Msgf("[OK] RPC status check: latest block number %d, timestamp %s (%.fs ago), tss addr %s, #utxos: %d", bn, blockTime, elapsedSeconds, tssAddr, len(res)) - case <-ob.StopChannel(): + case <-ob.stop: return } } } -// GetPendingNonce returns the artificial pending nonce -// Note: pending nonce is accessed concurrently +func (ob *Observer) Stop() { + ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) + close(ob.stop) // this notifies all goroutines to stop + ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) +} + +func (ob *Observer) SetLastBlockHeight(height int64) { + atomic.StoreInt64(&ob.lastBlock, height) +} + +func (ob *Observer) GetLastBlockHeight() int64 { + return atomic.LoadInt64(&ob.lastBlock) +} + +func (ob *Observer) SetLastBlockHeightScanned(height int64) { + atomic.StoreInt64(&ob.lastBlockScanned, height) + // #nosec G701 checked as positive + ob.ts.SetLastScannedBlockNumber(ob.chain, uint64(height)) +} + +func (ob *Observer) GetLastBlockHeightScanned() int64 { + return atomic.LoadInt64(&ob.lastBlockScanned) +} + func (ob *Observer) GetPendingNonce() uint64 { ob.Mu.Lock() defer ob.Mu.Unlock() return ob.pendingNonce } +// GetBaseGasPrice ... +// TODO: implement +// https://github.com/zeta-chain/node/issues/868 +func (ob *Observer) GetBaseGasPrice() *big.Int { + return big.NewInt(0) +} + // ConfirmationsThreshold returns number of required Bitcoin confirmations depending on sent BTC amount. func (ob *Observer) ConfirmationsThreshold(amount *big.Int) int64 { - if amount.Cmp(big.NewInt(BigValueSats)) >= 0 { - return BigValueConfirmationCount + if amount.Cmp(big.NewInt(bigValueSats)) >= 0 { + return bigValueConfirmationCount } - if BigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { - return BigValueConfirmationCount + if bigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { + return bigValueConfirmationCount } // #nosec G701 always in range @@ -306,7 +395,7 @@ func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) } // start gas price ticker @@ -316,7 +405,7 @@ func (ob *Observer) WatchGasPrice() { return } ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) + ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -327,27 +416,25 @@ func (ob *Observer) WatchGasPrice() { } err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) } ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.StopChannel(): - ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) + case <-ob.stop: + ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.chain.ChainId) return } } } -// PostGasPrice posts gas price to zetacore func (ob *Observer) PostGasPrice() error { - // hardcode gas price here since this RPC is not available on regtest - if chains.IsBitcoinRegnet(ob.Chain().ChainId) { - blockNumber, err := ob.btcClient.GetBlockCount() + if ob.chain.ChainId == 18444 { //bitcoin regtest; hardcode here since this RPC is not available on regtest + blockNumber, err := ob.rpcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always in range - _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), 1, "100", uint64(blockNumber)) + _, err = ob.zetacoreClient.PostGasPrice(ob.chain, 1, "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -356,7 +443,7 @@ func (ob *Observer) PostGasPrice() error { } // EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation - feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) + feeResult, err := ob.rpcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) if err != nil { return err } @@ -368,13 +455,13 @@ func (ob *Observer) PostGasPrice() error { } feeRatePerByte := bitcoin.FeeRateToSatPerByte(*feeResult.FeeRate) - blockNumber, err := ob.btcClient.GetBlockCount() + blockNumber, err := ob.rpcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always positive - _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRatePerByte.Uint64(), "100", uint64(blockNumber)) + _, err = ob.zetacoreClient.PostGasPrice(ob.chain, feeRatePerByte.Uint64(), "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -445,14 +532,13 @@ func (ob *Observer) WatchUTXOs() { ob.logger.UTXOs.Error().Err(err).Msg("error fetching btc utxos") } ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.UTXOs) - case <-ob.StopChannel(): - ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.Chain().ChainId) + case <-ob.stop: + ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.chain.ChainId) return } } } -// FetchUTXOs fetches TSS-owned UTXOs from the Bitcoin node func (ob *Observer) FetchUTXOs() error { defer func() { if err := recover(); err != nil { @@ -464,19 +550,19 @@ func (ob *Observer) FetchUTXOs() error { ob.refreshPendingNonce() // get the current block height. - bh, err := ob.btcClient.GetBlockCount() + bh, err := ob.rpcClient.GetBlockCount() if err != nil { return fmt.Errorf("btc: error getting block height : %v", err) } maxConfirmations := int(bh) // List all unspent UTXOs (160ms) - tssAddr := ob.TSS().BTCAddress() - address, err := chains.DecodeBtcAddress(tssAddr, ob.Chain().ChainId) + tssAddr := ob.Tss.BTCAddress() + address, err := chains.DecodeBtcAddress(tssAddr, ob.chain.ChainId) if err != nil { return fmt.Errorf("btc: error decoding wallet address (%s) : %s", tssAddr, err.Error()) } - utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) + utxos, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) if err != nil { return err } @@ -509,7 +595,7 @@ func (ob *Observer) FetchUTXOs() error { } ob.Mu.Lock() - ob.TelemetryServer().SetNumberOfUTXOs(len(utxosFiltered)) + ob.ts.SetNumberOfUTXOs(len(utxosFiltered)) ob.utxos = utxosFiltered ob.Mu.Unlock() return nil @@ -523,7 +609,7 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { ob.Mu.Unlock() broadcastEntry := clienttypes.ToOutboundHashSQLType(txHash, outboundID) - if err := ob.DB().Save(&broadcastEntry).Error; err != nil { + if err := ob.db.Save(&broadcastEntry).Error; err != nil { ob.logger.Outbound.Error(). Err(err). Msgf("SaveBroadcastedTx: error saving broadcasted txHash %s for outbound %s", txHash, outboundID) @@ -604,23 +690,67 @@ func GetRawTxResult( return btcjson.TxRawResult{}, fmt.Errorf("getRawTxResult: tx %s not included yet", hash) } -// GetBlockByNumberCached gets cached block (and header) by block number +func (ob *Observer) BuildBroadcastedTxMap() error { + var broadcastedTransactions []clienttypes.OutboundHashSQLType + if err := ob.db.Find(&broadcastedTransactions).Error; err != nil { + ob.logger.Chain.Error().Err(err).Msg("error iterating over db") + return err + } + for _, entry := range broadcastedTransactions { + ob.broadcastedTx[entry.Key] = entry.Hash + } + return nil +} + +// LoadLastScannedBlock loads last scanned block from database +// The last scanned block is the height from which the observer should continue scanning for inbound transactions +func (ob *Observer) LoadLastScannedBlock() error { + // Get the latest block number from node + bn, err := ob.rpcClient.GetBlockCount() + if err != nil { + return err + } + if bn < 0 { + return fmt.Errorf("LoadLastScannedBlock: negative block number %d", bn) + } + + //Load persisted block number + var lastBlockNum clienttypes.LastBlockSQLType + if err := ob.db.First(&lastBlockNum, clienttypes.LastBlockNumID).Error; err != nil { + ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") + ob.SetLastBlockHeightScanned(bn) + } else { + // #nosec G701 always in range + lastBN := int64(lastBlockNum.Num) + ob.SetLastBlockHeightScanned(lastBN) + } + + // bitcoin regtest starts from block 100 + if chains.IsBitcoinRegnet(ob.chain.ChainId) { + ob.SetLastBlockHeightScanned(100) + } + ob.logger.Chain.Info(). + Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) + + return nil +} + func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { - if result, ok := ob.BlockCache().Get(blockNumber); ok { + if result, ok := ob.BlockCache.Get(blockNumber); ok { return result.(*BTCBlockNHeader), nil } // Get the block hash - hash, err := ob.btcClient.GetBlockHash(blockNumber) + hash, err := ob.rpcClient.GetBlockHash(blockNumber) if err != nil { return nil, err } // Get the block header - header, err := ob.btcClient.GetBlockHeader(hash) + header, err := ob.rpcClient.GetBlockHeader(hash) if err != nil { return nil, err } // Get the block with verbose transactions - block, err := ob.btcClient.GetBlockVerboseTx(hash) + block, err := ob.rpcClient.GetBlockVerboseTx(hash) if err != nil { return nil, err } @@ -628,85 +758,11 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, Header: header, Block: block, } - ob.BlockCache().Add(blockNumber, blockNheader) - ob.BlockCache().Add(hash, blockNheader) + ob.BlockCache.Add(blockNumber, blockNheader) + ob.BlockCache.Add(hash, blockNheader) return blockNheader, nil } -// LoadDB open sql database and load data into Bitcoin observer -func (ob *Observer) LoadDB(dbPath string) error { - if dbPath == "" { - return errors.New("empty db path") - } - - // open database, the custom dbName is used here for backward compatibility - err := ob.OpenDB(dbPath, "btc_chain_client") - if err != nil { - return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) - } - - // run auto migration - // transaction result table is used nowhere but we still run migration in case they are needed in future - err = ob.DB().AutoMigrate( - &clienttypes.TransactionResultSQLType{}, - &clienttypes.OutboundHashSQLType{}, - ) - if err != nil { - return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) - } - - // load last scanned block - err = ob.LoadLastBlockScanned() - if err != nil { - return err - } - - // load broadcasted transactions - err = ob.LoadBroadcastedTxMap() - return err -} - -// LoadLastBlockScanned loads the last scanned block from the database -func (ob *Observer) LoadLastBlockScanned() error { - err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) - if err != nil { - return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) - } - - // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: - // 1. environment variable is set explicitly to "latest" - // 2. environment variable is empty and last scanned block is not found in DB - if ob.LastBlockScanned() == 0 { - blockNumber, err := ob.btcClient.GetBlockCount() - if err != nil { - return errors.Wrapf(err, "error GetBlockCount for chain %d", ob.Chain().ChainId) - } - // #nosec G701 always positive - ob.WithLastBlockScanned(uint64(blockNumber)) - } - - // bitcoin regtest starts from hardcoded block 100 - if chains.IsBitcoinRegnet(ob.Chain().ChainId) { - ob.WithLastBlockScanned(RegnetStartBlock) - } - ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) - - return nil -} - -// LoadBroadcastedTxMap loads broadcasted transactions from the database -func (ob *Observer) LoadBroadcastedTxMap() error { - var broadcastedTransactions []clienttypes.OutboundHashSQLType - if err := ob.DB().Find(&broadcastedTransactions).Error; err != nil { - ob.logger.Chain.Error().Err(err).Msgf("error iterating over db for chain %d", ob.Chain().ChainId) - return err - } - for _, entry := range broadcastedTransactions { - ob.broadcastedTx[entry.Key] = entry.Hash - } - return nil -} - // isTssTransaction checks if a given transaction was sent by TSS itself. // An unconfirmed transaction is safe to spend only if it was sent by TSS and verified by ourselves. func (ob *Observer) isTssTransaction(txid string) bool { @@ -718,7 +774,7 @@ func (ob *Observer) isTssTransaction(txid string) bool { func (ob *Observer) postBlockHeader(tip int64) error { ob.logger.Inbound.Info().Msgf("postBlockHeader: tip %d", tip) bn := tip - res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) + res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { bn = res.ChainState.LatestHeight + 1 } @@ -737,8 +793,8 @@ func (ob *Observer) postBlockHeader(tip int64) error { return err } blockHash := res2.Header.BlockHash() - _, err = ob.ZetacoreClient().PostVoteBlockHeader( - ob.Chain().ChainId, + _, err = ob.zetacoreClient.PostVoteBlockHeader( + ob.chain.ChainId, blockHash[:], res2.Block.Height, proofs.NewBitcoinHeader(headerBuf.Bytes()), @@ -749,3 +805,37 @@ func (ob *Observer) postBlockHeader(tip int64) error { } return err } + +func (ob *Observer) loadDB(dbpath string) error { + if _, err := os.Stat(dbpath); os.IsNotExist(err) { + err := os.MkdirAll(dbpath, os.ModePerm) + if err != nil { + return err + } + } + path := fmt.Sprintf("%s/btc_chain_client", dbpath) + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + ob.logger.Chain.Error().Err(err).Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) + return err + } + ob.db = db + + err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}, + &clienttypes.OutboundHashSQLType{}, + &clienttypes.LastBlockSQLType{}) + if err != nil { + return err + } + + // Load last scanned block + err = ob.LoadLastScannedBlock() + if err != nil { + return err + } + + //Load broadcasted transactions + err = ob.BuildBroadcastedTxMap() + + return err +} diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index eeead6d8ae..2d2494dd16 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -1,10 +1,9 @@ -package observer_test +package observer import ( - "fmt" "math/big" - "os" "strconv" + "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -13,17 +12,21 @@ import ( "gorm.io/gorm" "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" - "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" - "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" - "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) +const ( + // tempSQLiteDbPath is the temporary SQLite database used for testing + tempSQLiteDbPath = "file::memory:?cache=shared" +) + var ( // the relative path to the testdata directory TestDataDir = "../../../" @@ -33,7 +36,7 @@ var ( func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransactionResult) { submittedTx := map[string]btcjson.GetTransactionResult{} - db, err := gorm.Open(sqlite.Open(base.TempSQLiteDBPath), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(tempSQLiteDbPath), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}) @@ -64,236 +67,26 @@ func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransaction return db, submittedTx } -// MockBTCObserver creates a mock Bitcoin observer for testing -func MockBTCObserver( - t *testing.T, - chain chains.Chain, - params observertypes.ChainParams, - btcClient interfaces.BTCRPCClient, - dbpath string, -) *observer.Observer { - // use default mock btc client if not provided - if btcClient == nil { - btcClient = mocks.NewMockBTCRPCClient().WithBlockCount(100) - } - - // use memory db if dbpath is empty - if dbpath == "" { - dbpath = "file::memory:?cache=shared" - } - - // create observer - ob, err := observer.NewObserver( - chain, - btcClient, - params, - nil, - nil, - nil, - dbpath, - base.Logger{}, - nil, - ) - require.NoError(t, err) - - return ob -} - -func Test_NewObserver(t *testing.T) { - // use Bitcoin mainnet chain for testing - chain := chains.BitcoinMainnet - params := mocks.MockChainParams(chain.ChainId, 10) - - // test cases - tests := []struct { - name string - chain chains.Chain - btcClient interfaces.BTCRPCClient - chainParams observertypes.ChainParams - coreContext *context.ZetacoreContext - coreClient interfaces.ZetacoreClient - tss interfaces.TSSSigner - dbpath string - logger base.Logger - ts *metrics.TelemetryServer - fail bool - message string - }{ - { - name: "should be able to create observer", - chain: chain, - btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), - chainParams: params, - coreContext: nil, - coreClient: nil, - tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), - logger: base.Logger{}, - ts: nil, - fail: false, - }, - { - name: "should fail if net params is not found", - chain: chains.Chain{ChainId: 111}, // invalid chain id - btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), - chainParams: params, - coreContext: nil, - coreClient: nil, - tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), - logger: base.Logger{}, - ts: nil, - fail: true, - message: "error getting net params", - }, - { - name: "should fail on invalid dbpath", - chain: chain, - chainParams: params, - coreContext: nil, - coreClient: nil, - btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), - tss: mocks.NewTSSMainnet(), - dbpath: "/invalid/dbpath", // invalid dbpath - logger: base.Logger{}, - ts: nil, - fail: true, - message: "error creating db path", - }, - } - - // run tests - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // create observer - ob, err := observer.NewObserver( - tt.chain, - tt.btcClient, - tt.chainParams, - tt.coreContext, - tt.coreClient, - tt.tss, - tt.dbpath, - tt.logger, - tt.ts, - ) - - // check result - if tt.fail { - require.ErrorContains(t, err, tt.message) - require.Nil(t, ob) - } else { - require.NoError(t, err) - require.NotNil(t, ob) - } - }) - } -} - -func Test_LoadDB(t *testing.T) { - // use Bitcoin mainnet chain for testing - chain := chains.BitcoinMainnet - params := mocks.MockChainParams(chain.ChainId, 10) - - // create mock btc client, tss and test dbpath - btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) - tss := mocks.NewTSSMainnet() - - // create observer - dbpath := testutils.CreateTempDir(t) - ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, dbpath, base.Logger{}, nil) - require.NoError(t, err) - - t.Run("should load db successfully", func(t *testing.T) { - err := ob.LoadDB(dbpath) - require.NoError(t, err) - require.EqualValues(t, 100, ob.LastBlockScanned()) - }) - t.Run("should fail on invalid dbpath", func(t *testing.T) { - // load db with empty dbpath - err := ob.LoadDB("") - require.ErrorContains(t, err, "empty db path") - - // load db with invalid dbpath - err = ob.LoadDB("/invalid/dbpath") - require.ErrorContains(t, err, "error OpenDB") - }) - t.Run("should fail on invalid env var", func(t *testing.T) { - // set invalid environment variable - envvar := base.EnvVarLatestBlockByChain(chain) - os.Setenv(envvar, "invalid") - defer os.Unsetenv(envvar) - - // load db - err := ob.LoadDB(dbpath) - require.ErrorContains(t, err, "error LoadLastBlockScanned") - }) -} - -func Test_LoadLastBlockScanned(t *testing.T) { - // use Bitcoin mainnet chain for testing - chain := chains.BitcoinMainnet - params := mocks.MockChainParams(chain.ChainId, 10) - - // create observer using mock btc client - btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(200) - dbpath := testutils.CreateTempDir(t) - - t.Run("should load last block scanned", func(t *testing.T) { - // create observer and write 199 as last block scanned - ob := MockBTCObserver(t, chain, params, btcClient, dbpath) - ob.WriteLastBlockScannedToDB(199) - - // load last block scanned - err := ob.LoadLastBlockScanned() - require.NoError(t, err) - require.EqualValues(t, 199, ob.LastBlockScanned()) - }) - t.Run("should fail on invalid env var", func(t *testing.T) { - // create observer - ob := MockBTCObserver(t, chain, params, btcClient, dbpath) - - // set invalid environment variable - envvar := base.EnvVarLatestBlockByChain(chain) - os.Setenv(envvar, "invalid") - defer os.Unsetenv(envvar) - - // load last block scanned - err := ob.LoadLastBlockScanned() - require.ErrorContains(t, err, "error LoadLastBlockScanned") - }) - t.Run("should fail on RPC error", func(t *testing.T) { - // create observer on separate path, as we need to reset last block scanned - otherPath := testutils.CreateTempDir(t) - obOther := MockBTCObserver(t, chain, params, btcClient, otherPath) - - // reset last block scanned to 0 so that it will be loaded from RPC - obOther.WithLastBlockScanned(0) - - // set RPC error - btcClient.WithError(fmt.Errorf("error RPC")) - - // load last block scanned - err := obOther.LoadLastBlockScanned() - require.ErrorContains(t, err, "error RPC") - }) - t.Run("should use hardcode block 100 for regtest", func(t *testing.T) { - // use regtest chain - regtest := chains.BitcoinRegtest - obRegnet := MockBTCObserver(t, regtest, params, btcClient, dbpath) - - // load last block scanned - err := obRegnet.LoadLastBlockScanned() - require.NoError(t, err) - require.EqualValues(t, observer.RegnetStartBlock, obRegnet.LastBlockScanned()) +func TestNewBitcoinObserver(t *testing.T) { + t.Run("should return error because zetacore doesn't update zetacore context", func(t *testing.T) { + cfg := config.NewConfig() + coreContext := context.NewZetacoreContext(cfg) + appContext := context.NewAppContext(coreContext, cfg) + chain := chains.BitcoinMainnet + zetacoreClient := mocks.NewMockZetacoreClient() + tss := mocks.NewMockTSS(chains.BitcoinTestnet, sample.EthAddress().String(), "") + logger := base.Logger{} + btcCfg := cfg.BitcoinConfig + ts := metrics.NewTelemetryServer() + + client, err := NewObserver(appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, logger, btcCfg, ts) + require.ErrorContains(t, err, "btc chains params not initialized") + require.Nil(t, client) }) } func TestConfirmationThreshold(t *testing.T) { - chain := chains.BitcoinMainnet - params := mocks.MockChainParams(chain.ChainId, 10) - ob := MockBTCObserver(t, chain, params, nil, "") - + ob := &Observer{Mu: &sync.Mutex{}} t.Run("should return confirmations in chain param", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) require.Equal(t, int64(3), ob.ConfirmationsThreshold(big.NewInt(1000))) @@ -301,16 +94,12 @@ func TestConfirmationThreshold(t *testing.T) { t.Run("should return big value confirmations", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) - require.Equal( - t, - int64(observer.BigValueConfirmationCount), - ob.ConfirmationsThreshold(big.NewInt(observer.BigValueSats)), - ) + require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(bigValueSats))) }) t.Run("big value confirmations is the upper cap", func(t *testing.T) { - ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: observer.BigValueConfirmationCount + 1}) - require.Equal(t, int64(observer.BigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) + ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: bigValueConfirmationCount + 1}) + require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) }) } diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 6e80e1ea5c..a17d46883f 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -21,8 +21,8 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.TSS().BTCAddress() - return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) + tssAddr := ob.Tss.BTCAddress() + return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) } // WatchOutbound watches Bitcoin chain for outgoing txs status @@ -34,33 +34,32 @@ func (ob *Observer) WatchOutbound() { } defer ticker.Stop() - chainID := ob.Chain().ChainId - ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", chainID) + ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { + if !context.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) continue } - trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(chainID, interfaces.Ascending) + trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) if err != nil { ob.logger.Outbound.Error(). Err(err). - Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", chainID) + Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", ob.chain.ChainId) continue } for _, tracker := range trackers { // get original cctx parameters outboundID := ob.GetTxID(tracker.Nonce) - cctx, err := ob.ZetacoreClient().GetCctxByNonce(chainID, tracker.Nonce) + cctx, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, tracker.Nonce) if err != nil { ob.logger.Outbound.Info(). Err(err). - Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", chainID, tracker.Nonce) + Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", ob.chain.ChainId, tracker.Nonce) break } @@ -86,10 +85,10 @@ func (ob *Observer) WatchOutbound() { txCount++ txResult = result ob.logger.Outbound.Info(). - Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, chainID, tracker.Nonce) + Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, tracker.Nonce) if txCount > 1 { ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, chainID, tracker.Nonce, result) + "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, ob.chain.ChainId, tracker.Nonce, result) } } } @@ -98,12 +97,12 @@ func (ob *Observer) WatchOutbound() { ob.setIncludedTx(tracker.Nonce, txResult) } else if txCount > 1 { ob.removeIncludedTx(tracker.Nonce) // we can't tell which txHash is true, so we remove all (if any) to be safe - ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, chainID, tracker.Nonce) + ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, tracker.Nonce) } } ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.StopChannel(): - ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", chainID) + case <-ob.stop: + ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", ob.chain.ChainId) return } } @@ -163,7 +162,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // Get outbound block height - blockHeight, err := GetBlockHeightByHash(ob.btcClient, res.BlockHash) + blockHeight, err := GetBlockHeightByHash(ob.rpcClient, res.BlockHash) if err != nil { return true, false, errors.Wrapf( err, @@ -173,7 +172,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } logger.Debug().Msgf("Bitcoin outbound confirmed: txid %s, amount %s\n", res.TxID, amountInSat.String()) - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( + zetaHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( sendHash, res.TxID, // #nosec G701 always positive @@ -183,7 +182,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg 0, // gas limit not used with Bitcoin amountInSat, chains.ReceiveStatus_success, - ob.Chain(), + ob.chain, nonce, coin.CoinType_Gas, ) @@ -300,7 +299,7 @@ func (ob *Observer) SelectUTXOs( // 2. The tracker is missing in zetacore. func (ob *Observer) refreshPendingNonce() { // get pending nonces from zetacore - p, err := ob.ZetacoreClient().GetPendingNoncesByChain(ob.Chain().ChainId) + p, err := ob.zetacoreClient.GetPendingNoncesByChain(ob.chain.ChainId) if err != nil { ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") } @@ -337,7 +336,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return res.TxID, nil } if !test { // if not unit test, get cctx from zetacore - send, err := ob.ZetacoreClient().GetCctxByNonce(ob.Chain().ChainId, nonce) + send, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, nonce) if err != nil { return "", errors.Wrapf(err, "getOutboundIDByNonce: error getting cctx for nonce %d", nonce) } @@ -346,7 +345,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return "", fmt.Errorf("getOutboundIDByNonce: cannot find outbound txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := GetTxResultByHash(ob.btcClient, txid) + _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) if err != nil { return "", errors.Wrapf( err, @@ -364,7 +363,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error } func (ob *Observer) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { - tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() amount := chains.NonceMarkAmount(nonce) for i, utxo := range ob.utxos { sats, err := bitcoin.GetSatoshis(utxo.Amount) @@ -387,7 +386,7 @@ func (ob *Observer) checkIncludedTx( txHash string, ) (*btcjson.GetTransactionResult, bool) { outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) - hash, getTxResult, err := GetTxResultByHash(ob.btcClient, txHash) + hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -471,7 +470,7 @@ func (ob *Observer) checkTssOutboundResult( ) error { params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce - rawResult, err := GetRawTxResult(ob.btcClient, hash, res) + rawResult, err := GetRawTxResult(ob.rpcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } @@ -508,7 +507,7 @@ func (ob *Observer) checkTSSVin(vins []btcjson.Vin, nonce uint64) error { if nonce > 0 && len(vins) <= 1 { return fmt.Errorf("checkTSSVin: len(vins) <= 1") } - pubKeyTss := hex.EncodeToString(ob.TSS().PubKeyCompressedBytes()) + pubKeyTss := hex.EncodeToString(ob.Tss.PubKeyCompressedBytes()) for i, vin := range vins { // The length of the Witness should be always 2 for SegWit inputs. if len(vin.Witness) != 2 { @@ -548,7 +547,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ } nonce := params.TssNonce - tssAddress := ob.TSS().BTCAddress() + tssAddress := ob.Tss.BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout receiverExpected := tssAddress @@ -556,7 +555,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ // the 2nd output is the payment to recipient receiverExpected = params.Receiver } - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.Chain()) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.chain) if err != nil { return err } @@ -607,10 +606,10 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams } nonce := params.TssNonce - tssAddress := ob.TSS().BTCAddress() + tssAddress := ob.Tss.BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain()) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.chain) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index 910384ef9a..bad0997c0c 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -3,6 +3,7 @@ package observer import ( "math" "sort" + "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -10,27 +11,22 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/zetaclient/chains/base" + "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -// the relative path to the testdata directory -var TestDataDir = "../../../" +func MockBTCObserverMainnet() *Observer { + cfg := config.NewConfig() + coreContext := context.NewZetacoreContext(cfg) -// MockBTCObserverMainnet creates a mock Bitcoin mainnet observer for testing -func MockBTCObserverMainnet(t *testing.T) *Observer { - // setup mock arguments - chain := chains.BitcoinMainnet - btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) - params := mocks.MockChainParams(chain.ChainId, 10) - tss := mocks.NewTSSMainnet() - - // create Bitcoin observer - ob, err := NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, base.Logger{}, nil) - require.NoError(t, err) - - return ob + return &Observer{ + chain: chains.BitcoinMainnet, + zetacoreClient: mocks.NewMockZetacoreClient(), + Tss: mocks.NewTSSMainnet(), + coreContext: coreContext, + } } // helper function to create a test Bitcoin observer @@ -41,27 +37,26 @@ func createObserverWithPrivateKey(t *testing.T) *Observer { tss := &mocks.TSS{ PrivKey: privateKey, } - - // create Bitcoin observer with mock tss - ob := MockBTCObserverMainnet(t) - ob.WithTSS(tss) - - return ob + return &Observer{ + Tss: tss, + Mu: &sync.Mutex{}, + includedTxResults: make(map[string]*btcjson.GetTransactionResult), + } } // helper function to create a test Bitcoin observer with UTXOs func createObserverWithUTXOs(t *testing.T) *Observer { // Create Bitcoin observer - ob := createObserverWithPrivateKey(t) - tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() + client := createObserverWithPrivateKey(t) + tssAddress := client.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() // Create 10 dummy UTXOs (22.44 BTC in total) - ob.utxos = make([]btcjson.ListUnspentResult, 0, 10) + client.utxos = make([]btcjson.ListUnspentResult, 0, 10) amounts := []float64{0.01, 0.12, 0.18, 0.24, 0.5, 1.26, 2.97, 3.28, 5.16, 8.72} for _, amount := range amounts { - ob.utxos = append(ob.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) + client.utxos = append(client.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) } - return ob + return client } func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex int) { @@ -70,7 +65,7 @@ func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex i ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark - tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() nonceMark := btcjson.ListUnspentResult{ TxID: txid, Address: tssAddress, @@ -95,22 +90,22 @@ func TestCheckTSSVout(t *testing.T) { nonce := uint64(148) // create mainnet mock client - ob := MockBTCObserverMainnet(t) + btcClient := MockBTCObserverMainnet() t.Run("valid TSS vout should pass", func(t *testing.T) { rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := ob.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 2 or > 3", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := ob.checkTSSVout(params, []btcjson.Vout{{}}) + err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}) require.ErrorContains(t, err, "invalid number of vouts") - err = ob.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) + err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail on invalid TSS vout", func(t *testing.T) { @@ -119,7 +114,7 @@ func TestCheckTSSVout(t *testing.T) { // invalid TSS vout rawResult.Vout[0].ScriptPubKey.Hex = "invalid script" - err := ob.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.Error(t, err) }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -128,7 +123,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := ob.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -137,7 +132,7 @@ func TestCheckTSSVout(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := ob.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the receiver address", func(t *testing.T) { @@ -146,7 +141,7 @@ func TestCheckTSSVout(t *testing.T) { // not receiver address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := ob.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params receiver") }) t.Run("should fail if vout 1 not match payment amount", func(t *testing.T) { @@ -155,7 +150,7 @@ func TestCheckTSSVout(t *testing.T) { // not match payment amount rawResult.Vout[1].Value = 0.00011000 - err := ob.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params amount") }) t.Run("should fail if vout 2 is not to the TSS address", func(t *testing.T) { @@ -164,7 +159,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[2].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := ob.checkTSSVout(params, rawResult.Vout) + err := btcClient.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } @@ -177,7 +172,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { nonce := uint64(148) // create mainnet mock client - ob := MockBTCObserverMainnet(t) + btcClient := MockBTCObserverMainnet() t.Run("valid TSS vout should pass", func(t *testing.T) { // remove change vout to simulate cancelled tx @@ -186,17 +181,17 @@ func TestCheckTSSVoutCancelled(t *testing.T) { rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutboundParam() - err := ob.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 1 or > 2", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := ob.checkTSSVoutCancelled(params, []btcjson.Vout{}) + err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}) require.ErrorContains(t, err, "invalid number of vouts") - err = ob.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) + err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -208,7 +203,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := ob.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -220,7 +215,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := ob.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the TSS address", func(t *testing.T) { @@ -233,7 +228,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := ob.checkTSSVoutCancelled(params, rawResult.Vout) + err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 41ada01375..1e261c4d0a 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -36,33 +36,33 @@ import ( // WatchInbound watches evm chain for incoming txs and post votes to zetacore func (ob *Observer) WatchInbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInbound_%d", ob.Chain().ChainId), + fmt.Sprintf("EVM_WatchInbound_%d", ob.chain.ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msg("error creating ticker") + ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.Logger().Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) - sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) + sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) continue } err := ob.ObserveInbound(sampledLogger) if err != nil { - ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") + ob.logger.Inbound.Err(err).Msg("WatchInbound: observeInbound error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) - case <-ob.StopChannel(): - ob.Logger().Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) + case <-ob.stop: + ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) return } } @@ -72,29 +72,29 @@ func (ob *Observer) WatchInbound() { // If it was, it tries to broadcast the confirmation vote. If this zeta client has previously broadcast the vote, the tx would be rejected func (ob *Observer) WatchInboundTracker() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.Chain().ChainId), + fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.chain.ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.Logger().Inbound.Err(err).Msg("error creating ticker") + ob.logger.Inbound.Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.Logger().Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.Chain().ChainId) + ob.logger.Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.chain.ChainId) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { - ob.Logger().Inbound.Err(err).Msg("ProcessInboundTrackers error") + ob.logger.Inbound.Err(err).Msg("ProcessInboundTrackers error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) - case <-ob.StopChannel(): - ob.Logger().Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) + case <-ob.stop: + ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) return } } @@ -102,7 +102,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers from zetacore func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) + trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) if err != nil { return err } @@ -114,20 +114,15 @@ func (ob *Observer) ProcessInboundTrackers() error { err, "error getting transaction for inbound %s chain %d", tracker.TxHash, - ob.Chain().ChainId, + ob.chain.ChainId, ) } receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tracker.TxHash)) if err != nil { - return errors.Wrapf( - err, - "error getting receipt for inbound %s chain %d", - tracker.TxHash, - ob.Chain().ChainId, - ) + return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) } - ob.Logger().Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.Chain().ChainId) + ob.logger.Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) // check and vote on inbound tx switch tracker.CoinType { @@ -142,11 +137,11 @@ func (ob *Observer) ProcessInboundTrackers() error { "unknown coin type %s for inbound %s chain %d", tracker.CoinType, tx.Hash, - ob.Chain().ChainId, + ob.chain.ChainId, ) } if err != nil { - return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) + return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.chain.ChainId) } } return nil @@ -158,17 +153,17 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if err != nil { return err } - if blockNumber < ob.LastBlock() { + if blockNumber < ob.GetLastBlockHeight() { return fmt.Errorf( "observeInbound: block number should not decrease: current %d last %d", blockNumber, - ob.LastBlock(), + ob.GetLastBlockHeight(), ) } - ob.WithLastBlock(blockNumber) + ob.SetLastBlockHeight(blockNumber) // increment prom counter - metrics.GetBlockByNumberPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() + metrics.GetBlockByNumberPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() // skip if current height is too low if blockNumber < ob.GetChainParams().ConfirmationCount { @@ -177,10 +172,10 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { confirmedBlockNum := blockNumber - ob.GetChainParams().ConfirmationCount // skip if no new block is confirmed - lastScanned := ob.LastBlockScanned() + lastScanned := ob.GetLastBlockHeightScanned() if lastScanned >= confirmedBlockNum { sampledLogger.Debug(). - Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.Chain().ChainId) + Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.chain.ChainId) return nil } @@ -210,11 +205,12 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if lastScannedLowest > lastScanned { sampledLogger.Info(). Msgf("observeInbound: lasstScanned heights for chain %d ZetaSent %d ERC20Deposited %d TssRecvd %d", - ob.Chain().ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) - if err := ob.SaveLastBlockScanned(lastScannedLowest); err != nil { - ob.Logger().Inbound.Error(). + ob.chain.ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) + ob.SetLastBlockHeightScanned(lastScannedLowest) + if err := ob.db.Save(clienttypes.ToLastBlockSQLType(lastScannedLowest)).Error; err != nil { + ob.logger.Inbound.Error(). Err(err). - Msgf("observeInbound: error saving lastScannedLowest %d to db", lastScannedLowest) + Msgf("observeInbound: error writing lastScannedLowest %d to db", lastScannedLowest) } } return nil @@ -226,7 +222,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { // filter ZetaSent logs addrConnector, connector, err := ob.GetConnectorContract() if err != nil { - ob.Logger().Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") + ob.logger.Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") return startBlock - 1 // lastScanned } iter, err := connector.FilterZetaSent(&bind.FilterOpts{ @@ -235,8 +231,8 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}, []*big.Int{}) if err != nil { - ob.Logger().Chain.Warn().Err(err).Msgf( - "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) + ob.logger.Chain.Warn().Err(err).Msgf( + "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) return startBlock - 1 // lastScanned } @@ -249,10 +245,10 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.Logger().Inbound.Warn(). + ob.logger.Inbound.Warn(). Err(err). Msgf("ObserveZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -265,7 +261,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() // post to zetacore beingScanned := uint64(0) @@ -277,7 +273,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { } // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.Logger().Inbound.Warn(). + ob.logger.Inbound.Warn(). Msgf("ObserveZetaSent: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -305,7 +301,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { // filter ERC20CustodyDeposited logs addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() if err != nil { - ob.Logger().Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") + ob.logger.Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") return startBlock - 1 // lastScanned } @@ -315,8 +311,8 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}) if err != nil { - ob.Logger().Inbound.Warn().Err(err).Msgf( - "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) + ob.logger.Inbound.Warn().Err(err).Msgf( + "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) return startBlock - 1 // lastScanned } @@ -329,10 +325,10 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.Logger().Inbound.Warn(). + ob.logger.Inbound.Warn(). Err(err). Msgf("ObserveERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -345,7 +341,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() // post to zeatcore guard := make(map[string]bool) @@ -357,15 +353,15 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { } tx, _, err := ob.TransactionByHash(event.Raw.TxHash.Hex()) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msgf( - "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.Chain().ChainId) + ob.logger.Inbound.Error().Err(err).Msgf( + "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.chain.ChainId) return beingScanned - 1 // we have to re-scan from this block next time } sender := ethcommon.HexToAddress(tx.From) // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.Logger().Inbound.Warn(). + ob.logger.Inbound.Warn(). Msgf("ObserveERC20Deposited: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -391,23 +387,23 @@ func (ob *Observer) ObserverTSSReceive(startBlock, toBlock uint64) uint64 { // post new block header (if any) to zetacore and ignore error // TODO: consider having a independent ticker(from TSS scaning) for posting block headers // https://github.com/zeta-chain/node/issues/1847 - blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) + blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) if found && blockHeaderVerification.Enabled { // post block header for supported chains // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 err := ob.postBlockHeader(toBlock) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msg("error posting block header") + ob.logger.Inbound.Error().Err(err).Msg("error posting block header") } } // observe TSS received gas token in block 'bn' err := ob.ObserveTSSReceiveInBlock(bn) if err != nil { - ob.Logger().Inbound.Error(). + ob.logger.Inbound.Error(). Err(err). - Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.Chain().ChainId) + Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.chain.ChainId) return bn - 1 // we have to re-scan from this block next time } } @@ -422,7 +418,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -446,7 +442,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( if err == nil { msg = ob.BuildInboundVoteMsgForZetaSentEvent(event) } else { - ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) + ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) return "", err } break // only one event is allowed per tx @@ -454,7 +450,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( } if msg == nil { // no event, restricted tx, etc. - ob.Logger().Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) + ob.logger.Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) return "", nil } if vote { @@ -471,7 +467,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -496,7 +492,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( if err == nil { msg = ob.BuildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) } else { - ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) + ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) return "", err } break // only one event is allowed per tx @@ -504,7 +500,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( } if msg == nil { // no event, donation, restricted tx, etc. - ob.Logger().Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) + ob.logger.Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) return "", nil } if vote { @@ -521,7 +517,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -530,7 +526,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( } // checks receiver and tx status - if ethcommon.HexToAddress(tx.To) != ob.TSS().EVMAddress() { + if ethcommon.HexToAddress(tx.To) != ob.Tss.EVMAddress() { return "", fmt.Errorf("tx.To %s is not TSS address", tx.To) } if receipt.Status != ethtypes.ReceiptStatusSuccessful { @@ -542,7 +538,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( msg := ob.BuildInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) if msg == nil { // donation, restricted tx, etc. - ob.Logger().Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) + ob.logger.Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.chain.ChainId) return "", nil } if vote { @@ -559,16 +555,16 @@ func (ob *Observer) PostVoteInbound( retryGasLimit uint64, ) (string, error) { txHash := msg.InboundHash - chainID := ob.Chain().ChainId - zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) + chainID := ob.chain.ChainId + zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) if err != nil { - ob.Logger().Inbound.Err(err). + ob.logger.Inbound.Err(err). Msgf("inbound detected: error posting vote for chain %d token %s inbound %s", chainID, coinType, txHash) return "", err } else if zetaHash != "" { - ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) + ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) } else { - ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) + ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) } return ballot, err @@ -593,10 +589,10 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( } if config.ContainRestrictedAddress(sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), maybeReceiver) { compliance.PrintComplianceLog( - ob.Logger().Inbound, - ob.Logger().Compliance, + ob.logger.Inbound, + ob.logger.Compliance, false, - ob.Chain().ChainId, + ob.chain.ChainId, event.Raw.TxHash.Hex(), sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), @@ -607,22 +603,21 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( // donation check if bytes.Equal(event.Message, []byte(constant.DonationMessage)) { - ob.Logger().Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.Chain().ChainId) + ob.logger.Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.chain.ChainId) return nil } message := hex.EncodeToString(event.Message) - ob.Logger().Inbound.Info(). + ob.logger.Inbound.Info(). Msgf("ERC20CustodyDeposited inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.Chain(). - ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) + ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.Chain().ChainId, + ob.chain.ChainId, "", clienttypes.BytesToEthHex(event.Recipient), - ob.ZetacoreClient().Chain().ChainId, + ob.zetacoreClient.Chain().ChainId, sdkmath.NewUintFromBigInt(event.Amount), hex.EncodeToString(event.Message), event.Raw.TxHash.Hex(), @@ -630,7 +625,7 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( 1_500_000, coin.CoinType_ERC20, event.Asset.String(), - ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), + ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -641,7 +636,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( ) *types.MsgVoteInbound { destChain := chains.GetChainFromChainID(event.DestinationChainId.Int64()) if destChain == nil { - ob.Logger().Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) + ob.logger.Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) return nil } destAddr := clienttypes.BytesToEthHex(event.DestinationAddress) @@ -649,33 +644,32 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( // compliance check sender := event.ZetaTxSenderAddress.Hex() if config.ContainRestrictedAddress(sender, destAddr, event.SourceTxOriginAddress.Hex()) { - compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, - false, ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") + compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, + false, ob.chain.ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") return nil } if !destChain.IsZetaChain() { - paramsDest, found := ob.ZetacoreContext().GetEVMChainParams(destChain.ChainId) + paramsDest, found := ob.coreContext.GetEVMChainParams(destChain.ChainId) if !found { - ob.Logger().Inbound.Warn(). + ob.logger.Inbound.Warn(). Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) return nil } if strings.EqualFold(destAddr, paramsDest.ZetaTokenContractAddress) { - ob.Logger().Inbound.Warn(). + ob.logger.Inbound.Warn(). Msgf("potential attack attempt: %s destination address is ZETA token contract address %s", destChain, destAddr) return nil } } message := base64.StdEncoding.EncodeToString(event.Message) - ob.Logger().Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.Chain(). - ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) + ob.logger.Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) return zetacore.GetInboundVoteMessage( sender, - ob.Chain().ChainId, + ob.chain.ChainId, event.SourceTxOriginAddress.Hex(), destAddr, destChain.ChainId, @@ -686,7 +680,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( event.DestinationGasLimit.Uint64(), coin.CoinType_Zeta, "", - ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), + ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -706,8 +700,8 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( maybeReceiver = parsedAddress.Hex() } if config.ContainRestrictedAddress(sender.Hex(), maybeReceiver) { - compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, - false, ob.Chain().ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") + compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, + false, ob.chain.ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") return nil } @@ -715,19 +709,19 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( // #nosec G703 err is already checked data, _ := hex.DecodeString(message) if bytes.Equal(data, []byte(constant.DonationMessage)) { - ob.Logger().Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.Chain().ChainId) + ob.logger.Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.chain.ChainId) return nil } - ob.Logger().Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.Chain().ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) + ob.logger.Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.chain.ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.Chain().ChainId, + ob.chain.ChainId, sender.Hex(), sender.Hex(), - ob.ZetacoreClient().Chain().ChainId, + ob.zetacoreClient.Chain().ChainId, sdkmath.NewUintFromBigInt(&tx.Value), message, tx.Hash, @@ -735,7 +729,7 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( 90_000, coin.CoinType_Gas, "", - ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), + ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), 0, // not a smart contract call ) } @@ -744,15 +738,15 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { block, err := ob.GetBlockByNumberCached(blockNumber) if err != nil { - return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.Chain().ChainId) + return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.chain.ChainId) } for i := range block.Transactions { tx := block.Transactions[i] - if ethcommon.HexToAddress(tx.To) == ob.TSS().EVMAddress() { + if ethcommon.HexToAddress(tx.To) == ob.Tss.EVMAddress() { receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tx.Hash)) if err != nil { - return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) + return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.chain.ChainId) } _, err = ob.CheckAndVoteInboundTokenGas(&tx, receipt, true) @@ -761,7 +755,7 @@ func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { err, "error checking and voting inbound gas asset for inbound %s chain %d", tx.Hash, - ob.Chain().ChainId, + ob.chain.ChainId, ) } } diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index bb8930f4ca..906634e1ad 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -40,7 +40,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -56,7 +56,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -72,7 +72,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -89,17 +89,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain connector - ob := MockEVMObserver( - t, - chain, - nil, - nil, - nil, - nil, - memDBPath, - lastBlock, - mocks.MockChainParams(chainID, confirmation), - ) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -125,7 +115,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -141,7 +131,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -157,7 +147,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -174,17 +164,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain ERC20 custody - ob := MockEVMObserver( - t, - chain, - nil, - nil, - nil, - nil, - memDBPath, - lastBlock, - mocks.MockChainParams(chainID, confirmation), - ) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -210,7 +190,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -220,7 +200,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -230,7 +210,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) @@ -241,7 +221,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) @@ -252,7 +232,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) @@ -269,7 +249,7 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_Zeta, inboundHash) // parse ZetaSent event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) connector := mocks.MockConnectorNonEth(t, chainID) event := testutils.ParseReceiptZetaSent(receipt, connector) @@ -316,7 +296,7 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_ERC20, inboundHash) // parse Deposited event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) custody := mocks.MockERC20Custody(t, chainID) event := testutils.ParseReceiptERC20Deposited(receipt, custody) sender := ethcommon.HexToAddress(tx.From) @@ -374,7 +354,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(txDonation)) // create test compliance config - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) cfg := config.Config{ ComplianceConfig: config.ComplianceConfig{}, } @@ -444,7 +424,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation t.Run("should observe TSS receive in block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) // feed archived block and receipt evmJSONRPC.WithBlock(block) @@ -453,20 +433,20 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { require.NoError(t, err) }) t.Run("should not observe on error getting block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock JSONRPC contains no block require.ErrorContains(t, err, "error getting block") }) t.Run("should not observe on error getting receipt", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) evmJSONRPC.WithBlock(block) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock evmClient contains no receipt require.ErrorContains(t, err, "error getting receipt") }) t.Run("should not observe on error posting vote", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) // feed archived block and pause zetacore client evmJSONRPC.WithBlock(block) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 158b3e326c..99db258f5a 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -5,20 +5,29 @@ import ( "fmt" "math" "math/big" + "os" + "strconv" "strings" "sync" + "sync/atomic" "time" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rlp" + lru "github.com/hashicorp/golang-lru" "github.com/onrik/ethrpc" "github.com/pkg/errors" + "github.com/rs/zerolog" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zeta.non-eth.sol" zetaconnectoreth "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.eth.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -30,128 +39,225 @@ import ( clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) -var _ interfaces.ChainObserver = &Observer{} +// Logger is the logger for evm chains +// TODO: Merge this logger with the one in bitcoin +// https://github.com/zeta-chain/node/issues/2022 +type Logger struct { + // Chain is the parent logger for the chain + Chain zerolog.Logger -// Observer is the observer for evm chains -type Observer struct { - // base.Observer implements the base chain observer - base.Observer + // Inbound is the logger for incoming transactions + Inbound zerolog.Logger - // evmClient is the EVM client for the observed chain - evmClient interfaces.EVMRPCClient + // Outbound is the logger for outgoing transactions + Outbound zerolog.Logger - // evmJSONRPC is the EVM JSON RPC client for the observed chain - evmJSONRPC interfaces.EVMJSONRPCClient + // GasPrice is the logger for gas prices + GasPrice zerolog.Logger - // outboundPendingTransactions is the map to index pending transactions by hash - outboundPendingTransactions map[string]*ethtypes.Transaction + // Compliance is the logger for compliance checks + Compliance zerolog.Logger +} - // outboundConfirmedReceipts is the map to index confirmed receipts by hash - outboundConfirmedReceipts map[string]*ethtypes.Receipt +var _ interfaces.ChainObserver = &Observer{} - // outboundConfirmedTransactions is the map to index confirmed transactions by hash - outboundConfirmedTransactions map[string]*ethtypes.Transaction +// Observer is the observer for evm chains +type Observer struct { + Tss interfaces.TSSSigner - // Mu protects the maps and chain params from concurrent access Mu *sync.Mutex + + chain chains.Chain + evmClient interfaces.EVMRPCClient + evmJSONRPC interfaces.EVMJSONRPCClient + zetacoreClient interfaces.ZetacoreClient + lastBlockScanned uint64 + lastBlock uint64 + db *gorm.DB + outboundPendingTransactions map[string]*ethtypes.Transaction + outboundConfirmedReceipts map[string]*ethtypes.Receipt + outboundConfirmedTransactions map[string]*ethtypes.Transaction + stop chan struct{} + logger Logger + coreContext *clientcontext.ZetacoreContext + chainParams observertypes.ChainParams + ts *metrics.TelemetryServer + + blockCache *lru.Cache + headerCache *lru.Cache } // NewObserver returns a new EVM chain observer func NewObserver( - evmCfg config.EVMConfig, - evmClient interfaces.EVMRPCClient, - chainParams observertypes.ChainParams, - zetacoreContext *clientcontext.ZetacoreContext, + appContext *clientcontext.AppContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, + evmCfg config.EVMConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - // create base observer - baseObserver, err := base.NewObserver( - evmCfg.Chain, - chainParams, - zetacoreContext, - zetacoreClient, - tss, - base.DefaultBlockCacheSize, - base.DefaultHeaderCacheSize, - ts, - logger, - ) + ob := Observer{ + ts: ts, + } + + chainLogger := logger.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() + ob.logger = Logger{ + Chain: chainLogger, + Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), + Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), + GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), + Compliance: logger.Compliance, + } + + ob.coreContext = appContext.ZetacoreContext() + chainParams, found := ob.coreContext.GetEVMChainParams(evmCfg.Chain.ChainId) + if !found { + return nil, fmt.Errorf("evm chains params not initialized for chain %d", evmCfg.Chain.ChainId) + } + + ob.chainParams = *chainParams + ob.stop = make(chan struct{}) + ob.chain = evmCfg.Chain + ob.Mu = &sync.Mutex{} + ob.zetacoreClient = zetacoreClient + ob.Tss = tss + ob.outboundPendingTransactions = make(map[string]*ethtypes.Transaction) + ob.outboundConfirmedReceipts = make(map[string]*ethtypes.Receipt) + ob.outboundConfirmedTransactions = make(map[string]*ethtypes.Transaction) + + ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.ChainName.String(), evmCfg.Endpoint) + client, err := ethclient.Dial(evmCfg.Endpoint) + if err != nil { + ob.logger.Chain.Error().Err(err).Msg("eth Client Dial") + return nil, err + } + + ob.evmClient = client + ob.evmJSONRPC = ethrpc.NewEthRPC(evmCfg.Endpoint) + + // create block header and block caches + ob.blockCache, err = lru.New(1000) if err != nil { + ob.logger.Chain.Error().Err(err).Msg("failed to create block cache") return nil, err } - // create evm observer - ob := &Observer{ - Observer: *baseObserver, - evmClient: evmClient, - evmJSONRPC: ethrpc.NewEthRPC(evmCfg.Endpoint), - outboundPendingTransactions: make(map[string]*ethtypes.Transaction), - outboundConfirmedReceipts: make(map[string]*ethtypes.Receipt), - outboundConfirmedTransactions: make(map[string]*ethtypes.Transaction), - Mu: &sync.Mutex{}, + ob.headerCache, err = lru.New(1000) + if err != nil { + ob.logger.Chain.Error().Err(err).Msg("failed to create header cache") + return nil, err } - // open database and load data - err = ob.LoadDB(dbpath) + err = ob.LoadDB(dbpath, ob.chain) if err != nil { return nil, err } - return ob, nil + ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) + + return &ob, nil +} + +// WithChain attaches a new chain to the observer +func (ob *Observer) WithChain(chain chains.Chain) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.chain = chain +} + +// WithLogger attaches a new logger to the observer +func (ob *Observer) WithLogger(logger zerolog.Logger) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.logger = Logger{ + Chain: logger, + Inbound: logger.With().Str("module", "WatchInbound").Logger(), + Outbound: logger.With().Str("module", "WatchOutbound").Logger(), + GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), + } } // WithEvmClient attaches a new evm client to the observer func (ob *Observer) WithEvmClient(client interfaces.EVMRPCClient) { + ob.Mu.Lock() + defer ob.Mu.Unlock() ob.evmClient = client } // WithEvmJSONRPC attaches a new evm json rpc client to the observer func (ob *Observer) WithEvmJSONRPC(client interfaces.EVMJSONRPCClient) { + ob.Mu.Lock() + defer ob.Mu.Unlock() ob.evmJSONRPC = client } +// WithZetacoreClient attaches a new client to interact with zetacore to the observer +func (ob *Observer) WithZetacoreClient(client interfaces.ZetacoreClient) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.zetacoreClient = client +} + +// WithBlockCache attaches a new block cache to the observer +func (ob *Observer) WithBlockCache(cache *lru.Cache) { + ob.Mu.Lock() + defer ob.Mu.Unlock() + ob.blockCache = cache +} + +// Chain returns the chain for the observer +func (ob *Observer) Chain() chains.Chain { + ob.Mu.Lock() + defer ob.Mu.Unlock() + return ob.chain +} + // SetChainParams sets the chain params for the observer -// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.WithChainParams(params) + ob.chainParams = params } // GetChainParams returns the chain params for the observer -// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.ChainParams() + return ob.chainParams } -// GetConnectorContract returns the non-Eth connector address and binder func (ob *Observer) GetConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) - contract, err := zetaconnector.NewZetaConnectorNonEth(addr, ob.evmClient) + contract, err := FetchConnectorContract(addr, ob.evmClient) return addr, contract, err } -// GetConnectorContractEth returns the Eth connector address and binder func (ob *Observer) GetConnectorContractEth() (ethcommon.Address, *zetaconnectoreth.ZetaConnectorEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) contract, err := FetchConnectorContractEth(addr, ob.evmClient) return addr, contract, err } -// GetERC20CustodyContract returns ERC20Custody contract address and binder +func (ob *Observer) GetZetaTokenNonEthContract() (ethcommon.Address, *zeta.ZetaNonEth, error) { + addr := ethcommon.HexToAddress(ob.GetChainParams().ZetaTokenContractAddress) + contract, err := FetchZetaZetaNonEthTokenContract(addr, ob.evmClient) + return addr, contract, err +} + func (ob *Observer) GetERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().Erc20CustodyContractAddress) - contract, err := erc20custody.NewERC20Custody(addr, ob.evmClient) + contract, err := FetchERC20CustodyContract(addr, ob.evmClient) return addr, contract, err } -// FetchConnectorContractEth returns the Eth connector address and binder +func FetchConnectorContract( + addr ethcommon.Address, + client interfaces.EVMRPCClient, +) (*zetaconnector.ZetaConnectorNonEth, error) { + return zetaconnector.NewZetaConnectorNonEth(addr, client) +} + func FetchConnectorContractEth( addr ethcommon.Address, client interfaces.EVMRPCClient, @@ -159,18 +265,22 @@ func FetchConnectorContractEth( return zetaconnectoreth.NewZetaConnectorEth(addr, client) } -// FetchZetaTokenContract returns the non-Eth ZETA token binder -func FetchZetaTokenContract( +func FetchZetaZetaNonEthTokenContract( addr ethcommon.Address, client interfaces.EVMRPCClient, ) (*zeta.ZetaNonEth, error) { return zeta.NewZetaNonEth(addr, client) } +func FetchERC20CustodyContract( + addr ethcommon.Address, + client interfaces.EVMRPCClient, +) (*erc20custody.ERC20Custody, error) { + return erc20custody.NewERC20Custody(addr, client) +} + // Start all observation routines for the evm chain func (ob *Observer) Start() { - ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) - // watch evm chain for incoming txs and post votes to zetacore go ob.WatchInbound() @@ -189,7 +299,7 @@ func (ob *Observer) Start() { // WatchRPCStatus watches the RPC status of the evm chain func (ob *Observer) WatchRPCStatus() { - ob.Logger().Chain.Info().Msgf("Starting RPC status check for chain %d", ob.Chain().ChainId) + ob.logger.Chain.Info().Msgf("Starting RPC status check for chain %s", ob.chain.String()) ticker := time.NewTicker(60 * time.Second) for { select { @@ -199,35 +309,52 @@ func (ob *Observer) WatchRPCStatus() { } bn, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } gasPrice, err := ob.evmClient.SuggestGasPrice(context.Background()) if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(bn)) if err != nil { - ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } // #nosec G701 always in range blockTime := time.Unix(int64(header.Time), 0).UTC() elapsedSeconds := time.Since(blockTime).Seconds() if elapsedSeconds > 100 { - ob.Logger().Chain.Warn(). + ob.logger.Chain.Warn(). Msgf("RPC Status Check warning: RPC stale or chain stuck (check explorer)? Latest block %d timestamp is %.0fs ago", bn, elapsedSeconds) continue } - ob.Logger().Chain.Info(). + ob.logger.Chain.Info(). Msgf("[OK] RPC status: latest block num %d, timestamp %s ( %.0fs ago), suggested gas price %d", header.Number, blockTime.String(), elapsedSeconds, gasPrice.Uint64()) - case <-ob.StopChannel(): + case <-ob.stop: return } } } +func (ob *Observer) Stop() { + ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) + close(ob.stop) // this notifies all goroutines to stop + + ob.logger.Chain.Info().Msg("closing ob.db") + dbInst, err := ob.db.DB() + if err != nil { + ob.logger.Chain.Info().Msg("error getting database instance") + } + err = dbInst.Close() + if err != nil { + ob.logger.Chain.Error().Err(err).Msg("error closing database") + } + + ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) +} + // SetPendingTx sets the pending transaction in memory func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { ob.Mu.Lock() @@ -292,25 +419,47 @@ func (ob *Observer) CheckTxInclusion(tx *ethtypes.Transaction, receipt *ethtypes return nil } +// SetLastBlockHeightScanned set last block height scanned (not necessarily caught up with external block; could be slow/paused) +func (ob *Observer) SetLastBlockHeightScanned(height uint64) { + atomic.StoreUint64(&ob.lastBlockScanned, height) + ob.ts.SetLastScannedBlockNumber(ob.chain, height) +} + +// GetLastBlockHeightScanned get last block height scanned (not necessarily caught up with external block; could be slow/paused) +func (ob *Observer) GetLastBlockHeightScanned() uint64 { + height := atomic.LoadUint64(&ob.lastBlockScanned) + return height +} + +// SetLastBlockHeight set external last block height +func (ob *Observer) SetLastBlockHeight(height uint64) { + atomic.StoreUint64(&ob.lastBlock, height) +} + +// GetLastBlockHeight get external last block height +func (ob *Observer) GetLastBlockHeight() uint64 { + return atomic.LoadUint64(&ob.lastBlock) +} + // WatchGasPrice watches evm chain for gas prices and post to zetacore func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) } // start gas price ticker ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchGasPrice_%d", ob.Chain().ChainId), + fmt.Sprintf("EVM_WatchGasPrice_%d", ob.chain.ChainId), ob.GetChainParams().GasPriceTicker, ) if err != nil { - ob.Logger().GasPrice.Error().Err(err).Msg("NewDynamicTicker error") + ob.logger.GasPrice.Error().Err(err).Msg("NewDynamicTicker error") return } - ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) + ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", + ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -321,11 +470,11 @@ func (ob *Observer) WatchGasPrice() { } err = ob.PostGasPrice() if err != nil { - ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) - case <-ob.StopChannel(): - ob.Logger().GasPrice.Info().Msg("WatchGasPrice stopped") + ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) + case <-ob.stop: + ob.logger.GasPrice.Info().Msg("WatchGasPrice stopped") return } } @@ -336,21 +485,21 @@ func (ob *Observer) PostGasPrice() error { // GAS PRICE gasPrice, err := ob.evmClient.SuggestGasPrice(context.TODO()) if err != nil { - ob.Logger().GasPrice.Err(err).Msg("Err SuggestGasPrice:") + ob.logger.GasPrice.Err(err).Msg("Err SuggestGasPrice:") return err } blockNum, err := ob.evmClient.BlockNumber(context.TODO()) if err != nil { - ob.Logger().GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") + ob.logger.GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") return err } // SUPPLY supply := "100" // lockedAmount on ETH, totalSupply on other chains - zetaHash, err := ob.ZetacoreClient().PostGasPrice(ob.Chain(), gasPrice.Uint64(), supply, blockNum) + zetaHash, err := ob.zetacoreClient.PostGasPrice(ob.chain, gasPrice.Uint64(), supply, blockNum) if err != nil { - ob.Logger().GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") + ob.logger.GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") return err } _ = zetaHash @@ -372,21 +521,21 @@ func (ob *Observer) TransactionByHash(txHash string) (*ethrpc.Transaction, bool, } func (ob *Observer) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, error) { - if header, ok := ob.HeaderCache().Get(blockNumber); ok { + if header, ok := ob.headerCache.Get(blockNumber); ok { return header.(*ethtypes.Header), nil } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) if err != nil { return nil, err } - ob.HeaderCache().Add(blockNumber, header) + ob.headerCache.Add(blockNumber, header) return header, nil } // GetBlockByNumberCached get block by number from cache // returns block, ethrpc.Block, isFallback, isSkip, error func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, error) { - if block, ok := ob.BlockCache().Get(blockNumber); ok { + if block, ok := ob.blockCache.Get(blockNumber); ok { return block.(*ethrpc.Block), nil } if blockNumber > math.MaxInt32 { @@ -397,13 +546,13 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, e if err != nil { return nil, err } - ob.BlockCache().Add(blockNumber, block) + ob.blockCache.Add(blockNumber, block) return block, nil } // RemoveCachedBlock remove block from cache func (ob *Observer) RemoveCachedBlock(blockNumber uint64) { - ob.BlockCache().Remove(blockNumber) + ob.blockCache.Remove(blockNumber) } // BlockByNumber query block by number via JSON-RPC @@ -421,60 +570,92 @@ func (ob *Observer) BlockByNumber(blockNumber int) (*ethrpc.Block, error) { return block, nil } -// LoadDB open sql database and load data into EVM observer -func (ob *Observer) LoadDB(dbPath string) error { - if dbPath == "" { - return errors.New("empty db path") - } - - // open database - err := ob.OpenDB(dbPath, "") - if err != nil { - return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) - } - - // run auto migration - // transaction and receipt tables are used nowhere but we still run migration in case they are needed in future - err = ob.DB().AutoMigrate( - &clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - ) - if err != nil { - return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) +// LoadLastScannedBlock loads last scanned block from specified height or from database +// The last scanned block is the height from which the observer should continue scanning for inbound transactions +func (ob *Observer) LoadLastScannedBlock() error { + // get environment variable + envvar := ob.chain.ChainName.String() + "_SCAN_FROM" + scanFromBlock := os.Getenv(envvar) + + // load from environment variable if set + if scanFromBlock != "" { + ob.logger.Chain.Info(). + Msgf("LoadLastScannedBlock: envvar %s is set; scan from block %s", envvar, scanFromBlock) + if scanFromBlock == base.EnvVarLatestBlock { + header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) + if err != nil { + return err + } + ob.SetLastBlockHeightScanned(header.Number.Uint64()) + } else { + scanFromBlockInt, err := strconv.ParseUint(scanFromBlock, 10, 64) + if err != nil { + return err + } + ob.SetLastBlockHeightScanned(scanFromBlockInt) + } + } else { + // load from DB otherwise + var lastBlock clienttypes.LastBlockSQLType + if err := ob.db.First(&lastBlock, clienttypes.LastBlockNumID).Error; err != nil { + ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") + header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) + if err != nil { + return err + } + ob.SetLastBlockHeightScanned(header.Number.Uint64()) + if dbc := ob.db.Save(clienttypes.ToLastBlockSQLType(ob.GetLastBlockHeightScanned())); dbc.Error != nil { + ob.logger.Chain.Error().Err(dbc.Error).Msgf("LoadLastScannedBlock: error writing last scanned block %d to DB", ob.GetLastBlockHeightScanned()) + } + } else { + ob.SetLastBlockHeightScanned(lastBlock.Num) + } } + ob.logger.Chain.Info(). + Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) - // load last block scanned - err = ob.LoadLastBlockScanned() - - return err + return nil } -// LoadLastBlockScanned loads the last scanned block from the database -func (ob *Observer) LoadLastBlockScanned() error { - err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) - if err != nil { - return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) - } +// LoadDB open sql database and load data into EVM observer +func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { + if dbPath != "" { + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) + if err != nil { + return err + } + } + path := fmt.Sprintf("%s/%s", dbPath, chain.ChainName.String()) //Use "file::memory:?cache=shared" for temp db + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { + ob.logger.Chain.Error(). + Err(err). + Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) + return err + } - // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: - // 1. environment variable is set explicitly to "latest" - // 2. environment variable is empty and last scanned block is not found in DB - if ob.LastBlockScanned() == 0 { - blockNumber, err := ob.evmClient.BlockNumber(context.Background()) + err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + &clienttypes.LastBlockSQLType{}) if err != nil { - return errors.Wrapf(err, "error BlockNumber for chain %d", ob.Chain().ChainId) + ob.logger.Chain.Error().Err(err).Msg("error migrating db") + return err } - ob.WithLastBlockScanned(blockNumber) - } - ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) + ob.db = db + err = ob.LoadLastScannedBlock() + if err != nil { + return err + } + } return nil } func (ob *Observer) postBlockHeader(tip uint64) error { bn := tip - res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) + res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { // #nosec G701 always positive bn = uint64(res.ChainState.LatestHeight) + 1 // the next header to post @@ -486,23 +667,23 @@ func (ob *Observer) postBlockHeader(tip uint64) error { header, err := ob.GetBlockHeaderCached(bn) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) + ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) return err } headerRLP, err := rlp.EncodeToBytes(header) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) + ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) return err } - _, err = ob.ZetacoreClient().PostVoteBlockHeader( - ob.Chain().ChainId, + _, err = ob.zetacoreClient.PostVoteBlockHeader( + ob.chain.ChainId, header.Hash().Bytes(), header.Number.Int64(), proofs.NewEthereumHeader(headerRLP), ) if err != nil { - ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) + ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) return err } return nil diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 9ef7e47e1b..0601c083f2 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -1,8 +1,6 @@ package observer_test import ( - "fmt" - "os" "sync" "testing" @@ -23,7 +21,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" - "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -31,24 +28,17 @@ import ( // the relative path to the testdata directory var TestDataDir = "../../../" -// getZetacoreContext creates a zetacore context for unit tests -func getZetacoreContext( +// getAppContext creates an app context for unit tests +func getAppContext( evmChain chains.Chain, - endpoint string, evmChainParams *observertypes.ChainParams, -) (*context.ZetacoreContext, config.EVMConfig) { - // use default endpoint if not provided - if endpoint == "" { - endpoint = "http://localhost:8545" - } - +) (*context.AppContext, config.EVMConfig) { // create config cfg := config.NewConfig() cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ Chain: evmChain, - Endpoint: endpoint, + Endpoint: "http://localhost:8545", } - // create zetacore context coreCtx := context.NewZetacoreContext(cfg) evmChainParamsMap := make(map[int64]*observertypes.ChainParams) @@ -67,7 +57,8 @@ func getZetacoreContext( zerolog.Logger{}, ) // create app context - return coreCtx, cfg.EVMChainConfigs[evmChain.ChainId] + appCtx := context.NewAppContext(coreCtx, cfg) + return appCtx, cfg.EVMChainConfigs[evmChain.ChainId] } // MockEVMObserver creates a mock ChainObserver with custom chain, TSS, params etc @@ -78,15 +69,8 @@ func MockEVMObserver( evmJSONRPC interfaces.EVMJSONRPCClient, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, - dbpath string, lastBlock uint64, - params observertypes.ChainParams, -) *observer.Observer { - // use default mock evm client if not provided - if evmClient == nil { - evmClient = mocks.NewMockEvmClient().WithBlockNumber(1000) - } - + params observertypes.ChainParams) *observer.Observer { // use default mock zetacore client if not provided if zetacoreClient == nil { zetacoreClient = mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) @@ -95,203 +79,17 @@ func MockEVMObserver( if tss == nil { tss = mocks.NewTSSMainnet() } - // create zetacore context - coreCtx, evmCfg := getZetacoreContext(chain, "", ¶ms) + // create app context + appCtx, evmCfg := getAppContext(chain, ¶ms) - // create observer - ob, err := observer.NewObserver(evmCfg, evmClient, params, coreCtx, zetacoreClient, tss, dbpath, base.Logger{}, nil) + // create chain observer + client, err := observer.NewObserver(appCtx, zetacoreClient, tss, "", base.Logger{}, evmCfg, nil) require.NoError(t, err) - ob.WithEvmJSONRPC(evmJSONRPC) - ob.WithLastBlock(lastBlock) - - return ob -} - -func Test_NewObserver(t *testing.T) { - // use Ethereum chain for testing - chain := chains.Ethereum - params := mocks.MockChainParams(chain.ChainId, 10) - - // test cases - tests := []struct { - name string - evmCfg config.EVMConfig - chainParams observertypes.ChainParams - evmClient interfaces.EVMRPCClient - tss interfaces.TSSSigner - dbpath string - logger base.Logger - ts *metrics.TelemetryServer - fail bool - message string - }{ - { - name: "should be able to create observer", - evmCfg: config.EVMConfig{ - Chain: chain, - Endpoint: "http://localhost:8545", - }, - chainParams: params, - evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), - tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), - logger: base.Logger{}, - ts: nil, - fail: false, - }, - { - name: "should fail on invalid dbpath", - evmCfg: config.EVMConfig{ - Chain: chain, - Endpoint: "http://localhost:8545", - }, - chainParams: params, - evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), - tss: mocks.NewTSSMainnet(), - dbpath: "/invalid/dbpath", // invalid dbpath - logger: base.Logger{}, - ts: nil, - fail: true, - message: "error creating db path", - }, - { - name: "should fail if RPC call fails", - evmCfg: config.EVMConfig{ - Chain: chain, - Endpoint: "http://localhost:8545", - }, - chainParams: params, - evmClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), - tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), - logger: base.Logger{}, - ts: nil, - fail: true, - message: "error RPC", - }, - } - - // run tests - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // create zetacore context, client and tss - zetacoreCtx, _ := getZetacoreContext(tt.evmCfg.Chain, tt.evmCfg.Endpoint, ¶ms) - zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) - - // create observer - ob, err := observer.NewObserver( - tt.evmCfg, - tt.evmClient, - tt.chainParams, - zetacoreCtx, - zetacoreClient, - tt.tss, - tt.dbpath, - tt.logger, - tt.ts, - ) - - // check result - if tt.fail { - require.ErrorContains(t, err, tt.message) - require.Nil(t, ob) - } else { - require.NoError(t, err) - require.NotNil(t, ob) - } - }) - } -} - -func Test_LoadDB(t *testing.T) { - // use Ethereum chain for testing - chain := chains.Ethereum - params := mocks.MockChainParams(chain.ChainId, 10) - dbpath := testutils.CreateTempDir(t) - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, dbpath, 1, params) - - t.Run("should load db successfully", func(t *testing.T) { - err := ob.LoadDB(dbpath) - require.NoError(t, err) - require.EqualValues(t, 1000, ob.LastBlockScanned()) - }) - t.Run("should fail on invalid dbpath", func(t *testing.T) { - // load db with empty dbpath - err := ob.LoadDB("") - require.ErrorContains(t, err, "empty db path") - - // load db with invalid dbpath - err = ob.LoadDB("/invalid/dbpath") - require.ErrorContains(t, err, "error OpenDB") - }) - t.Run("should fail on invalid env var", func(t *testing.T) { - // set invalid environment variable - envvar := base.EnvVarLatestBlockByChain(chain) - os.Setenv(envvar, "invalid") - defer os.Unsetenv(envvar) - - // load db - err := ob.LoadDB(dbpath) - require.ErrorContains(t, err, "error LoadLastBlockScanned") - }) - t.Run("should fail on RPC error", func(t *testing.T) { - // create observer - tempClient := mocks.NewMockEvmClient() - ob := MockEVMObserver(t, chain, tempClient, nil, nil, nil, dbpath, 1, params) - - // set RPC error - tempClient.WithError(fmt.Errorf("error RPC")) - - // load db - err := ob.LoadDB(dbpath) - require.ErrorContains(t, err, "error RPC") - }) -} - -func Test_LoadLastBlockScanned(t *testing.T) { - // use Ethereum chain for testing - chain := chains.Ethereum - params := mocks.MockChainParams(chain.ChainId, 10) + client.WithEvmClient(evmClient) + client.WithEvmJSONRPC(evmJSONRPC) + client.SetLastBlockHeight(lastBlock) - // create observer using mock evm client - evmClient := mocks.NewMockEvmClient().WithBlockNumber(100) - dbpath := testutils.CreateTempDir(t) - ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, dbpath, 1, params) - - t.Run("should load last block scanned", func(t *testing.T) { - // create db and write 123 as last block scanned - ob.WriteLastBlockScannedToDB(123) - - // load last block scanned - err := ob.LoadLastBlockScanned() - require.NoError(t, err) - require.EqualValues(t, 123, ob.LastBlockScanned()) - }) - t.Run("should fail on invalid env var", func(t *testing.T) { - // set invalid environment variable - envvar := base.EnvVarLatestBlockByChain(chain) - os.Setenv(envvar, "invalid") - defer os.Unsetenv(envvar) - - // load last block scanned - err := ob.LoadLastBlockScanned() - require.ErrorContains(t, err, "error LoadLastBlockScanned") - }) - t.Run("should fail on RPC error", func(t *testing.T) { - // create observer on separate path, as we need to reset last block scanned - otherPath := testutils.CreateTempDir(t) - obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, otherPath, 1, params) - - // reset last block scanned to 0 so that it will be loaded from RPC - obOther.WithLastBlockScanned(0) - - // set RPC error - evmClient.WithError(fmt.Errorf("error RPC")) - - // load last block scanned - err := obOther.LoadLastBlockScanned() - require.ErrorContains(t, err, "error RPC") - }) + return client } func Test_BlockCache(t *testing.T) { diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 9c3bd1c66b..8dda7ac034 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -29,33 +29,33 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.TSS().EVMAddress().String() - return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) + tssAddr := ob.Tss.EVMAddress().String() + return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) } // WatchOutbound watches evm chain for outgoing txs status func (ob *Observer) WatchOutbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchOutbound_%d", ob.Chain().ChainId), + fmt.Sprintf("EVM_WatchOutbound_%d", ob.chain.ChainId), ob.GetChainParams().OutboundTicker, ) if err != nil { - ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") + ob.logger.Outbound.Error().Err(err).Msg("error creating ticker") return } - ob.Logger().Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.Chain().ChainId) - sampledLogger := ob.Logger().Outbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.logger.Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.chain.ChainId) + sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) defer ticker.Stop() for { select { case <-ticker.C(): - if !clientcontext.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { + if !clientcontext.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.Chain().ChainId) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) continue } - trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ob.Chain().ChainId, interfaces.Ascending) + trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) if err != nil { continue } @@ -72,23 +72,23 @@ func (ob *Observer) WatchOutbound() { txCount++ outboundReceipt = receipt outbound = tx - ob.Logger().Outbound.Info(). - Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.Chain().ChainId, nonceInt) + ob.logger.Outbound.Info(). + Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, nonceInt) if txCount > 1 { - ob.Logger().Outbound.Error().Msgf( - "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.Chain().ChainId, nonceInt, outboundReceipt, outbound) + ob.logger.Outbound.Error().Msgf( + "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.chain.ChainId, nonceInt, outboundReceipt, outbound) } } } if txCount == 1 { // should be only one txHash confirmed for each nonce. ob.SetTxNReceipt(nonceInt, outboundReceipt, outbound) } else if txCount > 1 { // should not happen. We can't tell which txHash is true. It might happen (e.g. glitchy/hacked endpoint) - ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.Chain().ChainId, nonceInt) + ob.logger.Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, nonceInt) } } - ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) - case <-ob.StopChannel(): - ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") + ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) + case <-ob.stop: + ob.logger.Outbound.Info().Msg("WatchOutbound: stopped") return } } @@ -105,8 +105,8 @@ func (ob *Observer) PostVoteOutbound( cointype coin.CoinType, logger zerolog.Logger, ) { - chainID := ob.Chain().ChainId - zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( + chainID := ob.chain.ChainId + zetaTxHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( cctxIndex, receipt.TxHash.Hex(), receipt.BlockNumber.Uint64(), @@ -115,7 +115,7 @@ func (ob *Observer) PostVoteOutbound( transaction.Gas(), receiveValue, receiveStatus, - ob.Chain(), + ob.chain, nonce, cointype, ) @@ -137,17 +137,17 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg return false, false, nil } receipt, transaction := ob.GetTxNReceipt(nonce) - sendID := fmt.Sprintf("%d-%d", ob.Chain().ChainId, nonce) + sendID := fmt.Sprintf("%d-%d", ob.chain.ChainId, nonce) logger = logger.With().Str("sendID", sendID).Logger() // get connector and erce20Custody contracts connectorAddr, connector, err := ob.GetConnectorContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.Chain().ChainId) + return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.chain.ChainId) } custodyAddr, custody, err := ob.GetERC20CustodyContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.Chain().ChainId) + return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.chain.ChainId) } // define a few common variables @@ -181,7 +181,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg if err != nil { logger.Error(). Err(err). - Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.Chain().ChainId, receipt.TxHash) + Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.chain.ChainId, receipt.TxHash) return false, false, err } @@ -346,7 +346,7 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.Chain().ChainId) + Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.chain.ChainId) return nil, nil, false } if transaction == nil { // should not happen @@ -355,17 +355,17 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check tx sender and nonce - signer := ethtypes.NewLondonSigner(big.NewInt(ob.Chain().ChainId)) + signer := ethtypes.NewLondonSigner(big.NewInt(ob.chain.ChainId)) from, err := signer.Sender(transaction) if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.Chain().ChainId) + Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.chain.ChainId) return nil, nil, false } - if from != ob.TSS().EVMAddress() { // must be TSS address + if from != ob.Tss.EVMAddress() { // must be TSS address log.Error().Msgf("confirmTxByHash: sender %s for outbound %s chain %d is not TSS address %s", - from.Hex(), transaction.Hash().Hex(), ob.Chain().ChainId, ob.TSS().EVMAddress().Hex()) + from.Hex(), transaction.Hash().Hex(), ob.chain.ChainId, ob.Tss.EVMAddress().Hex()) return nil, nil, false } if transaction.Nonce() != nonce { // must match cctx nonce @@ -394,10 +394,10 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check confirmations - if !ob.HasEnoughConfirmations(receipt, ob.LastBlock()) { + if !ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()) { log.Debug(). Msgf("confirmTxByHash: txHash %s nonce %d included but not confirmed: receipt block %d, current block %d", - txHash, nonce, receipt.BlockNumber, ob.LastBlock()) + txHash, nonce, receipt.BlockNumber, ob.GetLastBlockHeight()) return nil, nil, false } diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index df9a642a8e..e0806d6086 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -13,17 +13,12 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -const ( - memDBPath = base.TempSQLiteDBPath -) - // getContractsByChainID is a helper func to get contracts and addresses by chainID func getContractsByChainID( t *testing.T, @@ -62,12 +57,11 @@ func Test_IsOutboundProcessed(t *testing.T) { ) t.Run("should post vote and return true if outbound is processed", func(t *testing.T) { - // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) - ob.SetTxNReceipt(nonce, receipt, outbound) - + // create evm client and set outbound and receipt + client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + client.SetTxNReceipt(nonce, receipt, outbound) // post outbound vote - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) @@ -79,9 +73,9 @@ func Test_IsOutboundProcessed(t *testing.T) { cctx := testutils.LoadCctxByNonce(t, chainID, nonce) cctx.InboundParams.Sender = sample.EthAddress().Hex() - // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) - ob.SetTxNReceipt(nonce, receipt, outbound) + // create evm client and set outbound and receipt + client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + client.SetTxNReceipt(nonce, receipt, outbound) // modify compliance config to restrict sender address cfg := config.Config{ @@ -91,29 +85,29 @@ func Test_IsOutboundProcessed(t *testing.T) { config.LoadComplianceConfig(cfg) // post outbound vote - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) }) t.Run("should return false if outbound is not confirmed", func(t *testing.T) { - // create evm observer and DO NOT set outbound as confirmed - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + // create evm client and DO NOT set outbound as confirmed + client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) }) t.Run("should fail if unable to parse ZetaReceived event", func(t *testing.T) { - // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) - ob.SetTxNReceipt(nonce, receipt, outbound) + // create evm client and set outbound and receipt + client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + client.SetTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail - chainParamsNew := ob.GetChainParams() + chainParamsNew := client.GetChainParams() chainParamsNew.ConnectorContractAddress = sample.EthAddress().Hex() - ob.SetChainParams(chainParamsNew) - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + client.SetChainParams(chainParamsNew) + isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) require.Error(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) @@ -153,15 +147,15 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { ) t.Run("should fail if unable to get connector/custody contract", func(t *testing.T) { - // create evm observer and set outbound and receipt - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) - ob.SetTxNReceipt(nonce, receipt, outbound) + // create evm client and set outbound and receipt + client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) + client.SetTxNReceipt(nonce, receipt, outbound) abiConnector := zetaconnector.ZetaConnectorNonEthMetaData.ABI abiCustody := erc20custody.ERC20CustodyMetaData.ABI // set invalid connector ABI zetaconnector.ZetaConnectorNonEthMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) zetaconnector.ZetaConnectorNonEthMetaData.ABI = abiConnector // reset connector ABI require.ErrorContains(t, err, "error getting zeta connector") require.False(t, isIncluded) @@ -169,7 +163,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { // set invalid custody ABI erc20custody.ERC20CustodyMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err = ob.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err = client.IsOutboundProcessed(cctx, zerolog.Logger{}) require.ErrorContains(t, err, "error getting erc20 custody") require.False(t, isIncluded) require.False(t, isConfirmed) @@ -199,8 +193,8 @@ func Test_PostVoteOutbound(t *testing.T) { // create evm client using mock zetacore client and post outbound vote zetacoreClient := mocks.NewMockZetacoreClient() - ob := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, memDBPath, 1, observertypes.ChainParams{}) - ob.PostVoteOutbound( + client := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, 1, observertypes.ChainParams{}) + client.PostVoteOutbound( cctx.Index, receipt, outbound, @@ -213,7 +207,7 @@ func Test_PostVoteOutbound(t *testing.T) { // pause the mock zetacore client to simulate error posting vote zetacoreClient.Pause() - ob.PostVoteOutbound( + client.PostVoteOutbound( cctx.Index, receipt, outbound, diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index d7df5a33d1..fbb4d5ef88 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -70,7 +70,7 @@ func TestSigner_NewOutboundData(t *testing.T) { evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) - mockObserver, err := getNewEvmChainObserver(t, nil) + mockObserver, err := getNewEvmChainObserver(nil) require.NoError(t, err) t.Run("NewOutboundData success", func(t *testing.T) { diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index bdce93f60c..410ea5adf3 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -59,34 +59,22 @@ func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { } // getNewEvmChainObserver creates a new EVM chain observer for testing -func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.Observer, error) { +func getNewEvmChainObserver(tss interfaces.TSSSigner) (*observer.Observer, error) { // use default mock TSS if not provided if tss == nil { tss = mocks.NewTSSMainnet() } + + logger := base.Logger{} + ts := &metrics.TelemetryServer{} cfg := config.NewConfig() - // prepare mock arguments to create observer evmcfg := config.EVMConfig{Chain: chains.BscMainnet, Endpoint: "http://localhost:8545"} - evmClient := mocks.NewMockEvmClient().WithBlockNumber(1000) - params := mocks.MockChainParams(evmcfg.Chain.ChainId, 10) cfg.EVMChainConfigs[chains.BscMainnet.ChainId] = evmcfg coreCTX := context.NewZetacoreContext(cfg) - dbpath := testutils.CreateTempDir(t) - logger := base.Logger{} - ts := &metrics.TelemetryServer{} + appCTX := context.NewAppContext(coreCTX, cfg) - return observer.NewObserver( - evmcfg, - evmClient, - params, - coreCTX, - mocks.NewMockZetacoreClient(), - tss, - dbpath, - logger, - ts, - ) + return observer.NewObserver(appCTX, mocks.NewMockZetacoreClient(), tss, "", logger, evmcfg, ts) } func getNewOutboundProcessor() *outboundprocessor.Processor { @@ -157,7 +145,7 @@ func TestSigner_TryProcessOutbound(t *testing.T) { require.NoError(t, err) cctx := getCCTX(t) processor := getNewOutboundProcessor() - mockObserver, err := getNewEvmChainObserver(t, nil) + mockObserver, err := getNewEvmChainObserver(nil) require.NoError(t, err) // Test with mock client that has keys @@ -178,7 +166,7 @@ func TestSigner_SignOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + mockObserver, err := getNewEvmChainObserver(tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -212,7 +200,7 @@ func TestSigner_SignRevertTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + mockObserver, err := getNewEvmChainObserver(tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -250,7 +238,7 @@ func TestSigner_SignCancelTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + mockObserver, err := getNewEvmChainObserver(tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -288,7 +276,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + mockObserver, err := getNewEvmChainObserver(tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -324,7 +312,7 @@ func TestSigner_SignCommandTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, nil) + mockObserver, err := getNewEvmChainObserver(nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -369,7 +357,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + mockObserver, err := getNewEvmChainObserver(tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -407,7 +395,7 @@ func TestSigner_BroadcastOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, nil) + mockObserver, err := getNewEvmChainObserver(nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -457,7 +445,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + mockObserver, err := getNewEvmChainObserver(tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -500,7 +488,7 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(t, tss) + mockObserver, err := getNewEvmChainObserver(tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index e27d64fb12..f975c7eb88 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -123,7 +123,7 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { zetaTokenAddressString := externalEvmChainParams.ZetaTokenContractAddress zetaTokenAddress := ethcommon.HexToAddress(zetaTokenAddressString) - zetatokenNonEth, err := observer.FetchZetaTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) + zetatokenNonEth, err := observer.FetchZetaZetaNonEthTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) if err != nil { return err } From 2b4b01c828eeb89cbf0325cef664cacd6d023201 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 20 Jun 2024 11:15:33 -0500 Subject: [PATCH 11/21] updated PR number in changelog --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 1e02187c5d..e7b0aafb11 100644 --- a/changelog.md +++ b/changelog.md @@ -48,7 +48,7 @@ * [2296](https://github.com/zeta-chain/node/pull/2296) - move `testdata` package to `testutil` to organize test-related utilities * [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs * [2317](https://github.com/zeta-chain/node/pull/2317) - add ValidateOutbound method for cctx orchestrator -* [2355](https://github.com/zeta-chain/node/pull/2355) - integrated base Signer structure into EVM/Bitcoin Signer +* [2356](https://github.com/zeta-chain/node/pull/2357) - integrated base Signer structure into EVM/Bitcoin Signer ### Tests From d882b058a3a02897e3ea51901671bf8e698693c9 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 20 Jun 2024 11:15:56 -0500 Subject: [PATCH 12/21] updated PR number in changelog --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index e7b0aafb11..f2b66e3bf0 100644 --- a/changelog.md +++ b/changelog.md @@ -48,7 +48,7 @@ * [2296](https://github.com/zeta-chain/node/pull/2296) - move `testdata` package to `testutil` to organize test-related utilities * [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs * [2317](https://github.com/zeta-chain/node/pull/2317) - add ValidateOutbound method for cctx orchestrator -* [2356](https://github.com/zeta-chain/node/pull/2357) - integrated base Signer structure into EVM/Bitcoin Signer +* [2357](https://github.com/zeta-chain/node/pull/2357) - integrated base Signer structure into EVM/Bitcoin Signer ### Tests From cdbf1495838c06edc9aa61c9ba2c7b4d34371a5f Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 20 Jun 2024 12:30:54 -0500 Subject: [PATCH 13/21] cherry picked the integration of base Observer --- changelog.md | 1 + cmd/zetaclientd/utils.go | 66 ++- zetaclient/chains/base/observer.go | 136 +++-- zetaclient/chains/base/observer_test.go | 281 ++++++---- zetaclient/chains/bitcoin/observer/inbound.go | 86 +-- .../chains/bitcoin/observer/inbound_test.go | 53 +- .../chains/bitcoin/observer/live_test.go | 40 +- .../chains/bitcoin/observer/observer.go | 514 ++++++++---------- .../chains/bitcoin/observer/observer_test.go | 269 ++++++++- .../chains/bitcoin/observer/outbound.go | 57 +- .../chains/bitcoin/observer/outbound_test.go | 85 +-- zetaclient/chains/evm/observer/inbound.go | 210 +++---- .../chains/evm/observer/inbound_test.go | 60 +- zetaclient/chains/evm/observer/observer.go | 437 +++++---------- .../chains/evm/observer/observer_test.go | 232 +++++++- zetaclient/chains/evm/observer/outbound.go | 62 +-- .../chains/evm/observer/outbound_test.go | 56 +- .../chains/evm/signer/outbound_data_test.go | 2 +- zetaclient/chains/evm/signer/signer_test.go | 44 +- .../supplychecker/zeta_supply_checker.go | 2 +- 20 files changed, 1521 insertions(+), 1172 deletions(-) diff --git a/changelog.md b/changelog.md index 72110564a5..37eeac2ff7 100644 --- a/changelog.md +++ b/changelog.md @@ -49,6 +49,7 @@ * [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs * [2317](https://github.com/zeta-chain/node/pull/2317) - add ValidateOutbound method for cctx orchestrator * [2357](https://github.com/zeta-chain/node/pull/2357) - integrated base Signer structure into EVM/Bitcoin Signer +* [2359](https://github.com/zeta-chain/node/pull/2357) - integrated base Observer structure into EVM/Bitcoin Observer ### Tests diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 64db1c3efa..31444cf5e7 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -1,8 +1,12 @@ package main import ( + "fmt" + + "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -116,28 +120,84 @@ func CreateChainObserverMap( logger base.Logger, ts *metrics.TelemetryServer, ) (map[int64]interfaces.ChainObserver, error) { + zetacoreContext := appContext.ZetacoreContext() observerMap := make(map[int64]interfaces.ChainObserver) // EVM observers for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { if evmConfig.Chain.IsZetaChain() { continue } - _, found := appContext.ZetacoreContext().GetEVMChainParams(evmConfig.Chain.ChainId) + chainParams, found := zetacoreContext.GetEVMChainParams(evmConfig.Chain.ChainId) if !found { logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } - co, err := evmobserver.NewObserver(appContext, zetacoreClient, tss, dbpath, logger, evmConfig, ts) + + // create EVM client + evmClient, err := ethclient.Dial(evmConfig.Endpoint) + if err != nil { + logger.Std.Error().Err(err).Msgf("error dailing endpoint %s", evmConfig.Endpoint) + continue + } + + // create EVM chain observer + co, err := evmobserver.NewObserver( + evmConfig, + evmClient, + *chainParams, + zetacoreContext, + zetacoreClient, + tss, + dbpath, + logger, + ts, + ) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) continue } observerMap[evmConfig.Chain.ChainId] = co } + // BTC observer + _, chainParams, found := zetacoreContext.GetBTCChainParams() + if !found { + return nil, fmt.Errorf("bitcoin chains params not found") + } + + // create BTC chain observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - co, err := btcobserver.NewObserver(appContext, btcChain, zetacoreClient, tss, dbpath, logger, btcConfig, ts) + // create BTC client + connCfg := &rpcclient.ConnConfig{ + Host: btcConfig.RPCHost, + User: btcConfig.RPCUsername, + Pass: btcConfig.RPCPassword, + HTTPPostMode: true, + DisableTLS: true, + Params: btcConfig.RPCParams, + } + btcClient, err := rpcclient.New(connCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating rpc client: %s", err) + } + err = btcClient.Ping() + if err != nil { + return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + } + + // create BTC chain observer + co, err := btcobserver.NewObserver( + btcChain, + btcClient, + *chainParams, + zetacoreContext, + zetacoreClient, + tss, + dbpath, + logger, + ts, + ) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 905775b2b2..b2aa983bbd 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -11,6 +11,7 @@ import ( "github.com/rs/zerolog" "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -28,9 +29,12 @@ const ( // Cached blocks can be used to get block information and verify transactions DefaultBlockCacheSize = 1000 - // DefaultHeadersCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) + // DefaultHeaderCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) // Cached headers can be used to get header information - DefaultHeadersCacheSize = 1000 + DefaultHeaderCacheSize = 1000 + + // TempSQLiteDBPath is the temporary in-memory SQLite database used for testing + TempSQLiteDBPath = "file::memory:?cache=shared" ) // Observer is the base structure for chain observers, grouping the common logic for each chain observer client. @@ -84,8 +88,7 @@ func NewObserver( zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, blockCacheSize int, - headersCacheSize int, - dbPath string, + headerCacheSize int, ts *metrics.TelemetryServer, logger Logger, ) (*Observer, error) { @@ -112,18 +115,25 @@ func NewObserver( } // create header cache - ob.headerCache, err = lru.New(headersCacheSize) + ob.headerCache, err = lru.New(headerCacheSize) if err != nil { return nil, errors.Wrap(err, "error creating header cache") } - // open database - err = ob.OpenDB(dbPath) + return &ob, nil +} + +// Stop notifies all goroutines to stop and closes the database. +func (ob *Observer) Stop() { + ob.logger.Chain.Info().Msgf("observer is stopping for chain %d", ob.Chain().ChainId) + close(ob.stop) + + // close database + err := ob.CloseDB() if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("error opening observer db for chain: %s", chain.ChainName)) + ob.Logger().Chain.Error().Err(err).Msgf("CloseDB failed for chain %d", ob.Chain().ChainId) } - - return &ob, nil + ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) } // Chain returns the chain for the observer. @@ -232,9 +242,20 @@ func (ob *Observer) DB() *gorm.DB { return ob.db } +// WithTelemetryServer attaches a new telemetry server to the observer. +func (ob *Observer) WithTelemetryServer(ts *metrics.TelemetryServer) *Observer { + ob.ts = ts + return ob +} + +// TelemetryServer returns the telemetry server for the observer. +func (ob *Observer) TelemetryServer() *metrics.TelemetryServer { + return ob.ts +} + // Logger returns the logger for the observer. -func (ob *Observer) Logger() ObserverLogger { - return ob.logger +func (ob *Observer) Logger() *ObserverLogger { + return &ob.logger } // WithLogger attaches a new logger to the observer. @@ -251,45 +272,64 @@ func (ob *Observer) WithLogger(logger Logger) *Observer { return ob } -// Stop returns the stop channel for the observer. -func (ob *Observer) Stop() chan struct{} { +// StopChannel returns the stop channel for the observer. +func (ob *Observer) StopChannel() chan struct{} { return ob.stop } // OpenDB open sql database in the given path. -func (ob *Observer) OpenDB(dbPath string) error { - if dbPath != "" { - // create db path if not exist - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return errors.Wrap(err, "error creating db path") - } - } - - // open db by chain name - chainName := ob.chain.ChainName.String() - path := fmt.Sprintf("%s/%s", dbPath, chainName) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) +func (ob *Observer) OpenDB(dbPath string, dbName string) error { + // create db path if not exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) if err != nil { - return errors.Wrap(err, "error opening db") + return errors.Wrapf(err, "error creating db path: %s", dbPath) } + } - // migrate db - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return errors.Wrap(err, "error migrating db") - } - ob.db = db + // use custom dbName or chain name if not provided + if dbName == "" { + dbName = ob.chain.ChainName.String() + } + path := fmt.Sprintf("%s/%s", dbPath, dbName) + + // use memory db if specified + if dbPath == TempSQLiteDBPath { + path = TempSQLiteDBPath + } + + // open db + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + return errors.Wrap(err, "error opening db") + } + + // migrate db + err = db.AutoMigrate(&clienttypes.LastBlockSQLType{}) + if err != nil { + return errors.Wrap(err, "error migrating db") + } + ob.db = db + + return nil +} + +// CloseDB close the database. +func (ob *Observer) CloseDB() error { + dbInst, err := ob.db.DB() + if err != nil { + return fmt.Errorf("error getting database instance: %w", err) + } + err = dbInst.Close() + if err != nil { + return fmt.Errorf("error closing database: %w", err) } return nil } // LoadLastBlockScanned loads last scanned block from environment variable or from database. // The last scanned block is the height from which the observer should continue scanning. -func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool, err error) { +func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) error { // get environment variable envvar := EnvVarLatestBlockByChain(ob.chain) scanFromBlock := os.Getenv(envvar) @@ -299,27 +339,33 @@ func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool logger.Info(). Msgf("LoadLastBlockScanned: envvar %s is set; scan from block %s", envvar, scanFromBlock) if scanFromBlock == EnvVarLatestBlock { - return true, nil + return nil } blockNumber, err := strconv.ParseUint(scanFromBlock, 10, 64) if err != nil { - return false, err + return err } ob.WithLastBlockScanned(blockNumber) - return false, nil + return nil } // load from DB otherwise. If not found, start from latest block blockNumber, err := ob.ReadLastBlockScannedFromDB() if err != nil { - logger.Info().Msgf("LoadLastBlockScanned: chain %d starts scanning from latest block", ob.chain.ChainId) - return true, nil + logger.Info().Msgf("LoadLastBlockScanned: last scanned block not found in db for chain %d", ob.chain.ChainId) + return nil } ob.WithLastBlockScanned(blockNumber) logger.Info(). Msgf("LoadLastBlockScanned: chain %d starts scanning from block %d", ob.chain.ChainId, ob.LastBlockScanned()) - return false, nil + return nil +} + +// SaveLastBlockScanned saves the last scanned block to memory and database. +func (ob *Observer) SaveLastBlockScanned(blockNumber uint64) error { + ob.WithLastBlockScanned(blockNumber) + return ob.WriteLastBlockScannedToDB(blockNumber) } // WriteLastBlockScannedToDB saves the last scanned block to the database. @@ -339,5 +385,5 @@ func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { // EnvVarLatestBlock returns the environment variable for the latest block by chain. func EnvVarLatestBlockByChain(chain chains.Chain) string { - return chain.ChainName.String() + "_SCAN_FROM" + return fmt.Sprintf("CHAIN_%d_SCAN_FROM", chain.ChainId) } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 7dd2f18081..870f88715e 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -15,18 +15,13 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -// create a temporary directory for testing -func createTempDir(t *testing.T) string { - tempPath, err := os.MkdirTemp("", "tempdir-") - require.NoError(t, err) - return tempPath -} - // createObserver creates a new observer for testing -func createObserver(t *testing.T, dbPath string) *base.Observer { +func createObserver(t *testing.T) *base.Observer { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) @@ -43,8 +38,7 @@ func createObserver(t *testing.T, dbPath string) *base.Observer { zetacoreClient, tss, base.DefaultBlockCacheSize, - base.DefaultHeadersCacheSize, - dbPath, + base.DefaultHeaderCacheSize, nil, logger, ) @@ -61,73 +55,55 @@ func TestNewObserver(t *testing.T) { zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize - headersCacheSize := base.DefaultHeadersCacheSize - dbPath := createTempDir(t) + headersCacheSize := base.DefaultHeaderCacheSize // test cases tests := []struct { - name string - chain chains.Chain - chainParams observertypes.ChainParams - zetacoreContext *context.ZetacoreContext - zetacoreClient interfaces.ZetacoreClient - tss interfaces.TSSSigner - blockCacheSize int - headersCacheSize int - dbPath string - fail bool - message string + name string + chain chains.Chain + chainParams observertypes.ChainParams + zetacoreContext *context.ZetacoreContext + zetacoreClient interfaces.ZetacoreClient + tss interfaces.TSSSigner + blockCacheSize int + headerCacheSize int + fail bool + message string }{ { - name: "should be able to create new observer", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headersCacheSize: headersCacheSize, - dbPath: dbPath, - fail: false, + name: "should be able to create new observer", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + headerCacheSize: headersCacheSize, + fail: false, }, { - name: "should return error on invalid block cache size", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: 0, - headersCacheSize: headersCacheSize, - dbPath: dbPath, - fail: true, - message: "error creating block cache", + name: "should return error on invalid block cache size", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: 0, + headerCacheSize: headersCacheSize, + fail: true, + message: "error creating block cache", }, { - name: "should return error on invalid header cache size", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headersCacheSize: 0, - dbPath: dbPath, - fail: true, - message: "error creating header cache", - }, - { - name: "should return error on invalid db path", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headersCacheSize: headersCacheSize, - dbPath: "/invalid/123db", - fail: true, - message: "error opening observer db", + name: "should return error on invalid header cache size", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + headerCacheSize: 0, + fail: true, + message: "error creating header cache", }, } @@ -141,8 +117,7 @@ func TestNewObserver(t *testing.T) { tt.zetacoreClient, tt.tss, tt.blockCacheSize, - tt.headersCacheSize, - tt.dbPath, + tt.headerCacheSize, nil, base.DefaultLogger(), ) @@ -159,10 +134,8 @@ func TestNewObserver(t *testing.T) { } func TestObserverGetterAndSetter(t *testing.T) { - dbPath := createTempDir(t) - t.Run("should be able to update chain", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update chain newChain := chains.BscMainnet @@ -170,7 +143,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newChain, ob.Chain()) }) t.Run("should be able to update chain params", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update chain params newChainParams := *sample.ChainParams(chains.BscMainnet.ChainId) @@ -178,7 +151,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.True(t, observertypes.ChainParamsEqual(newChainParams, ob.ChainParams())) }) t.Run("should be able to update zetacore context", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update zetacore context newZetacoreContext := context.NewZetacoreContext(config.NewConfig()) @@ -186,7 +159,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreContext, ob.ZetacoreContext()) }) t.Run("should be able to update zetacore client", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update zetacore client newZetacoreClient := mocks.NewMockZetacoreClient() @@ -194,7 +167,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreClient, ob.ZetacoreClient()) }) t.Run("should be able to update tss", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update tss newTSS := mocks.NewTSSAthens3() @@ -202,7 +175,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newTSS, ob.TSS()) }) t.Run("should be able to update last block", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update last block newLastBlock := uint64(100) @@ -210,7 +183,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newLastBlock, ob.LastBlock()) }) t.Run("should be able to update last block scanned", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update last block scanned newLastBlockScanned := uint64(100) @@ -218,7 +191,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newLastBlockScanned, ob.LastBlockScanned()) }) t.Run("should be able to replace block cache", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update block cache newBlockCache, err := lru.New(200) @@ -227,8 +200,8 @@ func TestObserverGetterAndSetter(t *testing.T) { ob = ob.WithBlockCache(newBlockCache) require.Equal(t, newBlockCache, ob.BlockCache()) }) - t.Run("should be able to replace headers cache", func(t *testing.T) { - ob := createObserver(t, dbPath) + t.Run("should be able to replace header cache", func(t *testing.T) { + ob := createObserver(t) // update headers cache newHeadersCache, err := lru.New(200) @@ -238,13 +211,24 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newHeadersCache, ob.HeaderCache()) }) t.Run("should be able to get database", func(t *testing.T) { - ob := createObserver(t, dbPath) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + ob.OpenDB(dbPath, "") db := ob.DB() require.NotNil(t, db) }) + t.Run("should be able to update telemetry server", func(t *testing.T) { + ob := createObserver(t) + + // update telemetry server + newServer := metrics.NewTelemetryServer() + ob = ob.WithTelemetryServer(newServer) + require.Equal(t, newServer, ob.TelemetryServer()) + }) t.Run("should be able to get logger", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) logger := ob.Logger() // should be able to print log @@ -257,16 +241,30 @@ func TestObserverGetterAndSetter(t *testing.T) { }) } -func TestOpenDB(t *testing.T) { - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) +func TestOpenCloseDB(t *testing.T) { + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + + t.Run("should be able to open/close db", func(t *testing.T) { + // open db + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // close db + err = ob.CloseDB() + require.NoError(t, err) + }) + t.Run("should use memory db if specified", func(t *testing.T) { + // open db with memory + err := ob.OpenDB(base.TempSQLiteDBPath, "") + require.NoError(t, err) - t.Run("should be able to open db", func(t *testing.T) { - err := ob.OpenDB(dbPath) + // close db + err = ob.CloseDB() require.NoError(t, err) }) t.Run("should return error on invalid db path", func(t *testing.T) { - err := ob.OpenDB("/invalid/123db") + err := ob.OpenDB("/invalid/123db", "") require.ErrorContains(t, err, "error creating db path") }) } @@ -276,71 +274,116 @@ func TestLoadLastBlockScanned(t *testing.T) { envvar := base.EnvVarLatestBlockByChain(chain) t.Run("should be able to load last block scanned", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 100, ob.LastBlockScanned()) - require.False(t, fromLatest) }) - t.Run("should use latest block if last block scanned not found", func(t *testing.T) { - // create empty db - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + t.Run("latest block scanned should be 0 if not found in db", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.True(t, fromLatest) + require.EqualValues(t, 0, ob.LastBlockScanned()) }) t.Run("should overwrite last block scanned if env var is set", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // set env var os.Setenv(envvar, "101") // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 101, ob.LastBlockScanned()) - require.False(t, fromLatest) + }) + t.Run("last block scanned should remain 0 if env var is set to latest", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // create db and write 100 as last block scanned + ob.WriteLastBlockScannedToDB(100) // set env var to 'latest' os.Setenv(envvar, base.EnvVarLatestBlock) - // read last block scanned - fromLatest, err = ob.LoadLastBlockScanned(log.Logger) + // last block scanned should remain 0 + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.True(t, fromLatest) + require.EqualValues(t, 0, ob.LastBlockScanned()) }) t.Run("should return error on invalid env var", func(t *testing.T) { - // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) // set invalid env var os.Setenv(envvar, "invalid") // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.Error(t, err) - require.False(t, fromLatest) + }) +} + +func TestSaveLastBlockScanned(t *testing.T) { + t.Run("should be able to save last block scanned", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // save 100 as last block scanned + err = ob.SaveLastBlockScanned(100) + require.NoError(t, err) + + // check last block scanned in memory + require.EqualValues(t, 100, ob.LastBlockScanned()) + + // read last block scanned from db + lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() + require.NoError(t, err) + require.EqualValues(t, 100, lastBlockScanned) }) } func TestReadWriteLastBlockScannedToDB(t *testing.T) { t.Run("should be able to write and read last block scanned to db", func(t *testing.T) { - // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) - err := ob.WriteLastBlockScannedToDB(100) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // write last block scanned + err = ob.WriteLastBlockScannedToDB(100) require.NoError(t, err) lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() @@ -349,8 +392,10 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { }) t.Run("should return error when last block scanned not found in db", func(t *testing.T) { // create empty db - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) lastScannedBlock, err := ob.ReadLastBlockScannedFromDB() require.Error(t, err) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 54c5294745..2438411b5b 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -25,7 +25,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// WatchInbound watches Bitcoin chain for incoming txs and post votes to zetacore +// WatchInbound watches Bitcoin chain for inbounds on a ticker func (ob *Observer) WatchInbound() { ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.GetChainParams().InboundTicker) if err != nil { @@ -34,15 +34,15 @@ func (ob *Observer) WatchInbound() { } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) + ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } err := ob.ObserveInbound() @@ -50,47 +50,49 @@ func (ob *Observer) WatchInbound() { ob.logger.Inbound.Error().Err(err).Msg("WatchInbound error observing in tx") } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) return } } } +// ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore func (ob *Observer) ObserveInbound() error { // get and update latest block height - cnt, err := ob.rpcClient.GetBlockCount() + cnt, err := ob.btcClient.GetBlockCount() if err != nil { return fmt.Errorf("observeInboundBTC: error getting block number: %s", err) } if cnt < 0 { return fmt.Errorf("observeInboundBTC: block number is negative: %d", cnt) } - if cnt < ob.GetLastBlockHeight() { + // #nosec G701 checked positive + lastBlock := uint64(cnt) + if lastBlock < ob.LastBlock() { return fmt.Errorf( "observeInboundBTC: block number should not decrease: current %d last %d", cnt, - ob.GetLastBlockHeight(), + ob.LastBlock(), ) } - ob.SetLastBlockHeight(cnt) + ob.WithLastBlock(lastBlock) // skip if current height is too low - // #nosec G701 always in range - confirmedBlockNum := cnt - int64(ob.GetChainParams().ConfirmationCount) - if confirmedBlockNum < 0 { + if lastBlock < ob.GetChainParams().ConfirmationCount { return fmt.Errorf("observeInboundBTC: skipping observer, current block number %d is too low", cnt) } // skip if no new block is confirmed - lastScanned := ob.GetLastBlockHeightScanned() - if lastScanned >= confirmedBlockNum { + lastScanned := ob.LastBlockScanned() + if lastScanned >= lastBlock-ob.GetChainParams().ConfirmationCount { return nil } // query incoming gas asset to TSS address blockNumber := lastScanned + 1 - res, err := ob.GetBlockByNumberCached(blockNumber) + // #nosec G701 always in range + res, err := ob.GetBlockByNumberCached(int64(blockNumber)) if err != nil { ob.logger.Inbound.Error().Err(err).Msgf("observeInboundBTC: error getting bitcoin block %d", blockNumber) return err @@ -103,9 +105,10 @@ func (ob *Observer) ObserveInbound() error { // https://github.com/zeta-chain/node/issues/1847 // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 - blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) + blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) if found && blockHeaderVerification.Enabled { - err = ob.postBlockHeader(blockNumber) + // #nosec G701 always in range + err = ob.postBlockHeader(int64(blockNumber)) if err != nil { ob.logger.Inbound.Warn().Err(err).Msgf("observeInboundBTC: error posting block header %d", blockNumber) } @@ -113,14 +116,14 @@ func (ob *Observer) ObserveInbound() error { if len(res.Block.Tx) > 1 { // get depositor fee - depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) + depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) // filter incoming txs to TSS address - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() // #nosec G701 always positive inbounds, err := FilterAndParseIncomingTx( - ob.rpcClient, + ob.btcClient, res.Block.Tx, uint64(res.Block.Height), tssAddress, @@ -139,7 +142,7 @@ func (ob *Observer) ObserveInbound() error { for _, inbound := range inbounds { msg := ob.GetInboundVoteMessageFromBtcEvent(inbound) if msg != nil { - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -157,11 +160,8 @@ func (ob *Observer) ObserveInbound() error { } } - // Save LastBlockHeight - ob.SetLastBlockHeightScanned(blockNumber) - - // #nosec G701 always positive - if err := ob.db.Save(types.ToLastBlockSQLType(uint64(blockNumber))).Error; err != nil { + // save last scanned block to both memory and db + if err := ob.SaveLastBlockScanned(blockNumber); err != nil { ob.logger.Inbound.Error(). Err(err). Msgf("observeInboundBTC: error writing last scanned block %d to db", blockNumber) @@ -182,18 +182,18 @@ func (ob *Observer) WatchInboundTracker() { for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { ob.logger.Inbound.Error(). Err(err). - Msgf("error observing inbound tracker for chain %d", ob.chain.ChainId) + Msgf("error observing inbound tracker for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return } } @@ -201,7 +201,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) if err != nil { return err } @@ -214,7 +214,7 @@ func (ob *Observer) ProcessInboundTrackers() error { return err } ob.logger.Inbound.Info(). - Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.chain.ChainName, ballotIdentifier, coin.CoinType_Gas.String()) + Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.Chain().ChainName, ballotIdentifier, coin.CoinType_Gas.String()) } return nil @@ -227,7 +227,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - tx, err := ob.rpcClient.GetRawTransactionVerbose(hash) + tx, err := ob.btcClient.GetRawTransactionVerbose(hash) if err != nil { return "", err } @@ -237,7 +237,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - blockVb, err := ob.rpcClient.GetBlockVerboseTx(blockHash) + blockVb, err := ob.btcClient.GetBlockVerboseTx(blockHash) if err != nil { return "", err } @@ -246,15 +246,15 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", fmt.Errorf("block %d has no transactions", blockVb.Height) } - depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) - tss, err := ob.zetacoreClient.GetBtcTssAddress(ob.chain.ChainId) + depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) + tss, err := ob.ZetacoreClient().GetBtcTssAddress(ob.Chain().ChainId) if err != nil { return "", err } // #nosec G701 always positive event, err := GetBtcEvent( - ob.rpcClient, + ob.btcClient, *tx, tss, uint64(blockVb.Height), @@ -279,7 +279,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return msg.Digest(), nil } - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -343,10 +343,10 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) return zetacore.GetInboundVoteMessage( inbound.FromAddress, - ob.chain.ChainId, + ob.Chain().ChainId, inbound.FromAddress, inbound.FromAddress, - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, cosmosmath.NewUintFromBigInt(amountInt), message, inbound.TxHash, @@ -354,7 +354,7 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) 0, coin.CoinType_Gas, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, ) } @@ -368,7 +368,7 @@ func (ob *Observer) DoesInboundContainsRestrictedAddress(inTx *BTCInboundEvent) } if config.ContainRestrictedAddress(inTx.FromAddress, receiver) { compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") + false, ob.Chain().ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") return true } return false diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index b67b7d7b50..63042e8b85 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -1,4 +1,4 @@ -package observer +package observer_test import ( "bytes" @@ -18,6 +18,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -198,7 +199,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", sender) }) @@ -210,7 +211,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", sender) }) @@ -222,7 +223,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) }) @@ -234,7 +235,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", sender) }) @@ -246,7 +247,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", sender) }) @@ -272,7 +273,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Empty(t, sender) }) @@ -288,7 +289,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // use invalid tx hash txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.Error(t, err) require.Empty(t, sender) }) @@ -296,7 +297,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { // create mock rpc client without preloaded tx rpcClient := mocks.NewMockBTCRPCClient() txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "error getting raw transaction") require.Empty(t, sender) }) @@ -305,7 +306,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // invalid output index txVin := btcjson.Vin{Txid: txHash, Vout: 3} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "out of range") require.Empty(t, sender) }) @@ -328,7 +329,7 @@ func TestGetBtcEvent(t *testing.T) { // expected result memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) require.NoError(t, err) - eventExpected := &BTCInboundEvent{ + eventExpected := &observer.BTCInboundEvent{ FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", ToAddress: tssAddress, Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis @@ -347,7 +348,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -362,7 +363,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -377,7 +378,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -392,7 +393,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -407,7 +408,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -418,7 +419,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -429,13 +430,13 @@ func TestGetBtcEvent(t *testing.T) { // modify the tx to have Vout[0] a P2SH output tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) // append 1 byte to script to make it longer than 22 bytes tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" - event, err = GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err = observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -446,7 +447,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -457,7 +458,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -468,7 +469,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -479,7 +480,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -502,7 +503,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -513,7 +514,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -523,7 +524,7 @@ func TestGetBtcEventErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/observer/live_test.go index dd1053f620..c5512a1fae 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/observer/live_test.go @@ -1,4 +1,4 @@ -package observer +package observer_test import ( "context" @@ -23,8 +23,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" - "github.com/zeta-chain/zetacore/zetaclient/config" - clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -47,34 +46,39 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { tss := &mocks.TSS{ PrivKey: privateKey, } - appContext := clientcontext.NewAppContext(&clientcontext.ZetacoreContext{}, config.Config{}) - client, err := NewObserver(appContext, chains.BitcoinRegtest, nil, tss, tempSQLiteDbPath, - base.DefaultLogger(), config.BTCConfig{}, nil) + + // create mock arguments for constructor + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + btcClient := mocks.NewMockBTCRPCClient() + + // create observer + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, + base.DefaultLogger(), nil) suite.Require().NoError(err) + suite.Require().NotNil(ob) suite.rpcClient, err = getRPCClient(18332) suite.Require().NoError(err) skBytes, err := hex.DecodeString(skHex) suite.Require().NoError(err) suite.T().Logf("skBytes: %d", len(skBytes)) - btc := client.rpcClient - - _, err = btc.CreateWallet("e2e") + _, err = btcClient.CreateWallet("e2e") suite.Require().NoError(err) - addr, err := btc.GetNewAddress("test") + addr, err := btcClient.GetNewAddress("test") suite.Require().NoError(err) suite.T().Logf("deployer address: %s", addr) //err = btc.ImportPrivKey(privkeyWIF) //suite.Require().NoError(err) - btc.GenerateToAddress(101, addr, nil) + btcClient.GenerateToAddress(101, addr, nil) suite.Require().NoError(err) - bal, err := btc.GetBalance("*") + bal, err := btcClient.GetBalance("*") suite.Require().NoError(err) suite.T().Logf("balance: %f", bal.ToBTC()) - utxo, err := btc.ListUnspent() + utxo, err := btcClient.ListUnspent() suite.Require().NoError(err) suite.T().Logf("utxo: %d", len(utxo)) for _, u := range utxo { @@ -153,7 +157,7 @@ func (suite *BitcoinObserverTestSuite) Test1() { suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := FilterAndParseIncomingTx( + inbounds, err := observer.FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -190,7 +194,7 @@ func (suite *BitcoinObserverTestSuite) Test2() { suite.T().Logf("block height %d", block.Height) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := FilterAndParseIncomingTx( + inbounds, err := observer.FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -242,11 +246,11 @@ func LiveTestGetBlockHeightByHash(t *testing.T) { invalidHash := "invalidhash" // get block by invalid hash - _, err = GetBlockHeightByHash(client, invalidHash) + _, err = observer.GetBlockHeightByHash(client, invalidHash) require.ErrorContains(t, err, "error decoding block hash") // get block height by block hash - height, err := GetBlockHeightByHash(client, hash) + height, err := observer.GetBlockHeightByHash(client, hash) require.NoError(t, err) require.Equal(t, expectedHeight, height) } @@ -460,7 +464,7 @@ BLOCKLOOP: Txid: mpvin.TxID, Vout: mpvin.Vout, } - senderAddr, err := GetSenderAddressByVin(client, vin, net) + senderAddr, err := observer.GetSenderAddressByVin(client, vin, net) if err != nil { fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) time.Sleep(3 * time.Second) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 6b93ba12cd..0cf1766895 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -6,24 +6,17 @@ import ( "fmt" "math" "math/big" - "os" "sort" "sync" - "sync/atomic" "time" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" "github.com/rs/zerolog" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" @@ -31,47 +24,34 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" - "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) const ( // btcBlocksPerDay represents Bitcoin blocks per days for LRU block cache size btcBlocksPerDay = 144 - // bigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC - bigValueSats = 200000000 + // RegnetStartBlock is the hardcoded start block for regnet + RegnetStartBlock = 100 - // bigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations - bigValueConfirmationCount = 6 + // BigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC + BigValueSats = 200000000 + + // BigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations + BigValueConfirmationCount = 6 ) var _ interfaces.ChainObserver = &Observer{} // Logger contains list of loggers used by Bitcoin chain observer -// TODO: Merge this logger with the one in evm -// https://github.com/zeta-chain/node/issues/2022 type Logger struct { - // Chain is the parent logger for the chain - Chain zerolog.Logger - - // Inbound is the logger for incoming transactions - Inbound zerolog.Logger // The logger for incoming transactions - - // Outbound is the logger for outgoing transactions - Outbound zerolog.Logger // The logger for outgoing transactions + // base.Logger contains a list of base observer loggers + base.ObserverLogger // UTXOs is the logger for UTXOs management - UTXOs zerolog.Logger // The logger for UTXOs management - - // GasPrice is the logger for gas price - GasPrice zerolog.Logger // The logger for gas price - - // Compliance is the logger for compliance checks - Compliance zerolog.Logger // The logger for compliance checks + UTXOs zerolog.Logger } // BTCInboundEvent represents an incoming transaction event @@ -98,23 +78,20 @@ type BTCBlockNHeader struct { // Observer is the Bitcoin chain observer type Observer struct { - BlockCache *lru.Cache + // base.Observer implements the base chain observer + base.Observer - // Mu is lock for all the maps, utxos and core params - Mu *sync.Mutex + // netParams contains the Bitcoin network parameters + netParams *chaincfg.Params + + // btcClient is the Bitcoin RPC client that interacts with the Bitcoin node + btcClient interfaces.BTCRPCClient - Tss interfaces.TSSSigner + // pendingNonce is the outbound artificial pending nonce + pendingNonce uint64 - chain chains.Chain - netParams *chaincfg.Params - rpcClient interfaces.BTCRPCClient - zetacoreClient interfaces.ZetacoreClient - lastBlock int64 - lastBlockScanned int64 - pendingNonce uint64 - utxos []btcjson.ListUnspentResult - params observertypes.ChainParams - coreContext *context.ZetacoreContext + // utxos contains the UTXOs owned by the TSS address + utxos []btcjson.ListUnspentResult // includedTxHashes indexes included tx with tx hash includedTxHashes map[string]bool @@ -125,157 +102,120 @@ type Observer struct { // broadcastedTx indexes the outbound hash with the outbound tx identifier broadcastedTx map[string]string - db *gorm.DB - stop chan struct{} + // logger contains the loggers used by the bitcoin observer logger Logger - ts *metrics.TelemetryServer + + // Mu protects the maps, utxos and chain params from concurrent access + Mu *sync.Mutex } // NewObserver returns a new Bitcoin chain observer func NewObserver( - appcontext *context.AppContext, chain chains.Chain, + btcClient interfaces.BTCRPCClient, + chainParams observertypes.ChainParams, + zetacoreContext *context.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, - btcCfg config.BTCConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - // initialize the observer - ob := Observer{ - ts: ts, - } - ob.stop = make(chan struct{}) - ob.chain = chain - - // get the bitcoin network params - netParams, err := chains.BitcoinNetParamsFromChainID(ob.chain.ChainId) - if err != nil { - return nil, fmt.Errorf("error getting net params for chain %d: %s", ob.chain.ChainId, err) - } - ob.netParams = netParams - - ob.Mu = &sync.Mutex{} - - chainLogger := logger.Std.With().Str("chain", chain.ChainName.String()).Logger() - ob.logger = Logger{ - Chain: chainLogger, - Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), - Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), - UTXOs: chainLogger.With().Str("module", "WatchUTXOs").Logger(), - GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: logger.Compliance, - } - - ob.zetacoreClient = zetacoreClient - ob.Tss = tss - ob.coreContext = appcontext.ZetacoreContext() - ob.includedTxHashes = make(map[string]bool) - ob.includedTxResults = make(map[string]*btcjson.GetTransactionResult) - ob.broadcastedTx = make(map[string]string) - - // set the Bitcoin chain params - _, chainParams, found := appcontext.ZetacoreContext().GetBTCChainParams() - if !found { - return nil, fmt.Errorf("btc chains params not initialized") - } - ob.params = *chainParams - - // create the RPC client - ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) - connCfg := &rpcclient.ConnConfig{ - Host: btcCfg.RPCHost, - User: btcCfg.RPCUsername, - Pass: btcCfg.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcCfg.RPCParams, - } - rpcClient, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - - // try connection - ob.rpcClient = rpcClient - err = rpcClient.Ping() + // create base observer + baseObserver, err := base.NewObserver( + chain, + chainParams, + zetacoreContext, + zetacoreClient, + tss, + btcBlocksPerDay, + base.DefaultHeaderCacheSize, + ts, + logger, + ) if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + return nil, err } - ob.BlockCache, err = lru.New(btcBlocksPerDay) + // get the bitcoin network params + netParams, err := chains.BitcoinNetParamsFromChainID(chain.ChainId) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create bitcoin block cache") - return nil, err + return nil, fmt.Errorf("error getting net params for chain %d: %s", chain.ChainId, err) + } + + // create bitcoin observer + ob := &Observer{ + Observer: *baseObserver, + netParams: netParams, + btcClient: btcClient, + pendingNonce: 0, + utxos: []btcjson.ListUnspentResult{}, + includedTxHashes: make(map[string]bool), + includedTxResults: make(map[string]*btcjson.GetTransactionResult), + broadcastedTx: make(map[string]string), + logger: Logger{ + ObserverLogger: *baseObserver.Logger(), + UTXOs: baseObserver.Logger().Chain.With().Str("module", "utxos").Logger(), + }, + Mu: &sync.Mutex{}, } // load btc chain observer DB - err = ob.loadDB(dbpath) + err = ob.LoadDB(dbpath) if err != nil { return nil, err } - return &ob, nil + return ob, nil } -func (ob *Observer) WithZetacoreClient(client *zetacore.Client) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.zetacoreClient = client -} - -func (ob *Observer) WithLogger(logger zerolog.Logger) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.logger = Logger{ - Chain: logger, - Inbound: logger.With().Str("module", "WatchInbound").Logger(), - Outbound: logger.With().Str("module", "WatchOutbound").Logger(), - UTXOs: logger.With().Str("module", "WatchUTXOs").Logger(), - GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), - } -} - -func (ob *Observer) WithBtcClient(client *rpcclient.Client) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.rpcClient = client -} - -func (ob *Observer) WithChain(chain chains.Chain) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.chain = chain +// BtcClient returns the btc client +func (ob *Observer) BtcClient() interfaces.BTCRPCClient { + return ob.btcClient } -func (ob *Observer) Chain() chains.Chain { - ob.Mu.Lock() - defer ob.Mu.Unlock() - return ob.chain +// WithBtcClient attaches a new btc client to the observer +func (ob *Observer) WithBtcClient(client interfaces.BTCRPCClient) { + ob.btcClient = client } +// SetChainParams sets the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.params = params + ob.WithChainParams(params) } +// GetChainParams returns the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.params + return ob.ChainParams() } // Start starts the Go routine to observe the Bitcoin chain func (ob *Observer) Start() { - ob.logger.Chain.Info().Msgf("Bitcoin client is starting") - go ob.WatchInbound() // watch bitcoin chain for incoming txs and post votes to zetacore - go ob.WatchOutbound() // watch bitcoin chain for outgoing txs status - go ob.WatchUTXOs() // watch bitcoin chain for UTXOs owned by the TSS address - go ob.WatchGasPrice() // watch bitcoin chain for gas rate and post to zetacore - go ob.WatchInboundTracker() // watch zetacore for bitcoin inbound trackers - go ob.WatchRPCStatus() // watch the RPC status of the bitcoin chain + ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + + // watch bitcoin chain for incoming txs and post votes to zetacore + go ob.WatchInbound() + + // watch bitcoin chain for outgoing txs status + go ob.WatchOutbound() + + // watch bitcoin chain for UTXOs owned by the TSS address + go ob.WatchUTXOs() + + // watch bitcoin chain for gas rate and post to zetacore + go ob.WatchGasPrice() + + // watch zetacore for bitcoin inbound trackers + go ob.WatchInboundTracker() + + // watch the RPC status of the bitcoin chain + go ob.WatchRPCStatus() } // WatchRPCStatus watches the RPC status of the Bitcoin chain @@ -290,19 +230,19 @@ func (ob *Observer) WatchRPCStatus() { continue } - bn, err := ob.rpcClient.GetBlockCount() + bn, err := ob.btcClient.GetBlockCount() if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - hash, err := ob.rpcClient.GetBlockHash(bn) + hash, err := ob.btcClient.GetBlockHash(bn) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - header, err := ob.rpcClient.GetBlockHeader(hash) + header, err := ob.btcClient.GetBlockHeader(hash) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue @@ -315,8 +255,8 @@ func (ob *Observer) WatchRPCStatus() { continue } - tssAddr := ob.Tss.BTCAddressWitnessPubkeyHash() - res, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) + tssAddr := ob.TSS().BTCAddressWitnessPubkeyHash() + res, err := ob.btcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) if err != nil { ob.logger.Chain.Error(). Err(err). @@ -334,56 +274,27 @@ func (ob *Observer) WatchRPCStatus() { ob.logger.Chain.Info(). Msgf("[OK] RPC status check: latest block number %d, timestamp %s (%.fs ago), tss addr %s, #utxos: %d", bn, blockTime, elapsedSeconds, tssAddr, len(res)) - case <-ob.stop: + case <-ob.StopChannel(): return } } } -func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) - close(ob.stop) // this notifies all goroutines to stop - ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) -} - -func (ob *Observer) SetLastBlockHeight(height int64) { - atomic.StoreInt64(&ob.lastBlock, height) -} - -func (ob *Observer) GetLastBlockHeight() int64 { - return atomic.LoadInt64(&ob.lastBlock) -} - -func (ob *Observer) SetLastBlockHeightScanned(height int64) { - atomic.StoreInt64(&ob.lastBlockScanned, height) - // #nosec G701 checked as positive - ob.ts.SetLastScannedBlockNumber(ob.chain, uint64(height)) -} - -func (ob *Observer) GetLastBlockHeightScanned() int64 { - return atomic.LoadInt64(&ob.lastBlockScanned) -} - +// GetPendingNonce returns the artificial pending nonce +// Note: pending nonce is accessed concurrently func (ob *Observer) GetPendingNonce() uint64 { ob.Mu.Lock() defer ob.Mu.Unlock() return ob.pendingNonce } -// GetBaseGasPrice ... -// TODO: implement -// https://github.com/zeta-chain/node/issues/868 -func (ob *Observer) GetBaseGasPrice() *big.Int { - return big.NewInt(0) -} - // ConfirmationsThreshold returns number of required Bitcoin confirmations depending on sent BTC amount. func (ob *Observer) ConfirmationsThreshold(amount *big.Int) int64 { - if amount.Cmp(big.NewInt(bigValueSats)) >= 0 { - return bigValueConfirmationCount + if amount.Cmp(big.NewInt(BigValueSats)) >= 0 { + return BigValueConfirmationCount } - if bigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { - return bigValueConfirmationCount + if BigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { + return BigValueConfirmationCount } // #nosec G701 always in range @@ -395,7 +306,7 @@ func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } // start gas price ticker @@ -405,7 +316,7 @@ func (ob *Observer) WatchGasPrice() { return } ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) + ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -416,25 +327,27 @@ func (ob *Observer) WatchGasPrice() { } err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.stop: - ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) return } } } +// PostGasPrice posts gas price to zetacore func (ob *Observer) PostGasPrice() error { - if ob.chain.ChainId == 18444 { //bitcoin regtest; hardcode here since this RPC is not available on regtest - blockNumber, err := ob.rpcClient.GetBlockCount() + // hardcode gas price here since this RPC is not available on regtest + if chains.IsBitcoinRegnet(ob.Chain().ChainId) { + blockNumber, err := ob.btcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always in range - _, err = ob.zetacoreClient.PostGasPrice(ob.chain, 1, "100", uint64(blockNumber)) + _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), 1, "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -443,7 +356,7 @@ func (ob *Observer) PostGasPrice() error { } // EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation - feeResult, err := ob.rpcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) + feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) if err != nil { return err } @@ -455,13 +368,13 @@ func (ob *Observer) PostGasPrice() error { } feeRatePerByte := bitcoin.FeeRateToSatPerByte(*feeResult.FeeRate) - blockNumber, err := ob.rpcClient.GetBlockCount() + blockNumber, err := ob.btcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always positive - _, err = ob.zetacoreClient.PostGasPrice(ob.chain, feeRatePerByte.Uint64(), "100", uint64(blockNumber)) + _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRatePerByte.Uint64(), "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -532,13 +445,14 @@ func (ob *Observer) WatchUTXOs() { ob.logger.UTXOs.Error().Err(err).Msg("error fetching btc utxos") } ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.UTXOs) - case <-ob.stop: - ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.Chain().ChainId) return } } } +// FetchUTXOs fetches TSS-owned UTXOs from the Bitcoin node func (ob *Observer) FetchUTXOs() error { defer func() { if err := recover(); err != nil { @@ -550,19 +464,19 @@ func (ob *Observer) FetchUTXOs() error { ob.refreshPendingNonce() // get the current block height. - bh, err := ob.rpcClient.GetBlockCount() + bh, err := ob.btcClient.GetBlockCount() if err != nil { return fmt.Errorf("btc: error getting block height : %v", err) } maxConfirmations := int(bh) // List all unspent UTXOs (160ms) - tssAddr := ob.Tss.BTCAddress() - address, err := chains.DecodeBtcAddress(tssAddr, ob.chain.ChainId) + tssAddr := ob.TSS().BTCAddress() + address, err := chains.DecodeBtcAddress(tssAddr, ob.Chain().ChainId) if err != nil { return fmt.Errorf("btc: error decoding wallet address (%s) : %s", tssAddr, err.Error()) } - utxos, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) + utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) if err != nil { return err } @@ -595,7 +509,7 @@ func (ob *Observer) FetchUTXOs() error { } ob.Mu.Lock() - ob.ts.SetNumberOfUTXOs(len(utxosFiltered)) + ob.TelemetryServer().SetNumberOfUTXOs(len(utxosFiltered)) ob.utxos = utxosFiltered ob.Mu.Unlock() return nil @@ -609,7 +523,7 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { ob.Mu.Unlock() broadcastEntry := clienttypes.ToOutboundHashSQLType(txHash, outboundID) - if err := ob.db.Save(&broadcastEntry).Error; err != nil { + if err := ob.DB().Save(&broadcastEntry).Error; err != nil { ob.logger.Outbound.Error(). Err(err). Msgf("SaveBroadcastedTx: error saving broadcasted txHash %s for outbound %s", txHash, outboundID) @@ -690,67 +604,23 @@ func GetRawTxResult( return btcjson.TxRawResult{}, fmt.Errorf("getRawTxResult: tx %s not included yet", hash) } -func (ob *Observer) BuildBroadcastedTxMap() error { - var broadcastedTransactions []clienttypes.OutboundHashSQLType - if err := ob.db.Find(&broadcastedTransactions).Error; err != nil { - ob.logger.Chain.Error().Err(err).Msg("error iterating over db") - return err - } - for _, entry := range broadcastedTransactions { - ob.broadcastedTx[entry.Key] = entry.Hash - } - return nil -} - -// LoadLastScannedBlock loads last scanned block from database -// The last scanned block is the height from which the observer should continue scanning for inbound transactions -func (ob *Observer) LoadLastScannedBlock() error { - // Get the latest block number from node - bn, err := ob.rpcClient.GetBlockCount() - if err != nil { - return err - } - if bn < 0 { - return fmt.Errorf("LoadLastScannedBlock: negative block number %d", bn) - } - - //Load persisted block number - var lastBlockNum clienttypes.LastBlockSQLType - if err := ob.db.First(&lastBlockNum, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") - ob.SetLastBlockHeightScanned(bn) - } else { - // #nosec G701 always in range - lastBN := int64(lastBlockNum.Num) - ob.SetLastBlockHeightScanned(lastBN) - } - - // bitcoin regtest starts from block 100 - if chains.IsBitcoinRegnet(ob.chain.ChainId) { - ob.SetLastBlockHeightScanned(100) - } - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) - - return nil -} - +// GetBlockByNumberCached gets cached block (and header) by block number func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { - if result, ok := ob.BlockCache.Get(blockNumber); ok { + if result, ok := ob.BlockCache().Get(blockNumber); ok { return result.(*BTCBlockNHeader), nil } // Get the block hash - hash, err := ob.rpcClient.GetBlockHash(blockNumber) + hash, err := ob.btcClient.GetBlockHash(blockNumber) if err != nil { return nil, err } // Get the block header - header, err := ob.rpcClient.GetBlockHeader(hash) + header, err := ob.btcClient.GetBlockHeader(hash) if err != nil { return nil, err } // Get the block with verbose transactions - block, err := ob.rpcClient.GetBlockVerboseTx(hash) + block, err := ob.btcClient.GetBlockVerboseTx(hash) if err != nil { return nil, err } @@ -758,11 +628,85 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, Header: header, Block: block, } - ob.BlockCache.Add(blockNumber, blockNheader) - ob.BlockCache.Add(hash, blockNheader) + ob.BlockCache().Add(blockNumber, blockNheader) + ob.BlockCache().Add(hash, blockNheader) return blockNheader, nil } +// LoadDB open sql database and load data into Bitcoin observer +func (ob *Observer) LoadDB(dbPath string) error { + if dbPath == "" { + return errors.New("empty db path") + } + + // open database, the custom dbName is used here for backward compatibility + err := ob.OpenDB(dbPath, "btc_chain_client") + if err != nil { + return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) + } + + // run auto migration + // transaction result table is used nowhere but we still run migration in case they are needed in future + err = ob.DB().AutoMigrate( + &clienttypes.TransactionResultSQLType{}, + &clienttypes.OutboundHashSQLType{}, + ) + if err != nil { + return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) + } + + // load last scanned block + err = ob.LoadLastBlockScanned() + if err != nil { + return err + } + + // load broadcasted transactions + err = ob.LoadBroadcastedTxMap() + return err +} + +// LoadLastBlockScanned loads the last scanned block from the database +func (ob *Observer) LoadLastBlockScanned() error { + err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) + if err != nil { + return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) + } + + // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: + // 1. environment variable is set explicitly to "latest" + // 2. environment variable is empty and last scanned block is not found in DB + if ob.LastBlockScanned() == 0 { + blockNumber, err := ob.btcClient.GetBlockCount() + if err != nil { + return errors.Wrapf(err, "error GetBlockCount for chain %d", ob.Chain().ChainId) + } + // #nosec G701 always positive + ob.WithLastBlockScanned(uint64(blockNumber)) + } + + // bitcoin regtest starts from hardcoded block 100 + if chains.IsBitcoinRegnet(ob.Chain().ChainId) { + ob.WithLastBlockScanned(RegnetStartBlock) + } + ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) + + return nil +} + +// LoadBroadcastedTxMap loads broadcasted transactions from the database +func (ob *Observer) LoadBroadcastedTxMap() error { + var broadcastedTransactions []clienttypes.OutboundHashSQLType + if err := ob.DB().Find(&broadcastedTransactions).Error; err != nil { + ob.logger.Chain.Error().Err(err).Msgf("error iterating over db for chain %d", ob.Chain().ChainId) + return err + } + for _, entry := range broadcastedTransactions { + ob.broadcastedTx[entry.Key] = entry.Hash + } + return nil +} + // isTssTransaction checks if a given transaction was sent by TSS itself. // An unconfirmed transaction is safe to spend only if it was sent by TSS and verified by ourselves. func (ob *Observer) isTssTransaction(txid string) bool { @@ -774,7 +718,7 @@ func (ob *Observer) isTssTransaction(txid string) bool { func (ob *Observer) postBlockHeader(tip int64) error { ob.logger.Inbound.Info().Msgf("postBlockHeader: tip %d", tip) bn := tip - res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) + res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { bn = res.ChainState.LatestHeight + 1 } @@ -793,8 +737,8 @@ func (ob *Observer) postBlockHeader(tip int64) error { return err } blockHash := res2.Header.BlockHash() - _, err = ob.zetacoreClient.PostVoteBlockHeader( - ob.chain.ChainId, + _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ob.Chain().ChainId, blockHash[:], res2.Block.Height, proofs.NewBitcoinHeader(headerBuf.Bytes()), @@ -805,37 +749,3 @@ func (ob *Observer) postBlockHeader(tip int64) error { } return err } - -func (ob *Observer) loadDB(dbpath string) error { - if _, err := os.Stat(dbpath); os.IsNotExist(err) { - err := os.MkdirAll(dbpath, os.ModePerm) - if err != nil { - return err - } - } - path := fmt.Sprintf("%s/btc_chain_client", dbpath) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - if err != nil { - ob.logger.Chain.Error().Err(err).Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) - return err - } - ob.db = db - - err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}, - &clienttypes.OutboundHashSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return err - } - - // Load last scanned block - err = ob.LoadLastScannedBlock() - if err != nil { - return err - } - - //Load broadcasted transactions - err = ob.BuildBroadcastedTxMap() - - return err -} diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 2d2494dd16..eeead6d8ae 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -1,9 +1,10 @@ -package observer +package observer_test import ( + "fmt" "math/big" + "os" "strconv" - "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -12,21 +13,17 @@ import ( "gorm.io/gorm" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" - "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) -const ( - // tempSQLiteDbPath is the temporary SQLite database used for testing - tempSQLiteDbPath = "file::memory:?cache=shared" -) - var ( // the relative path to the testdata directory TestDataDir = "../../../" @@ -36,7 +33,7 @@ var ( func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransactionResult) { submittedTx := map[string]btcjson.GetTransactionResult{} - db, err := gorm.Open(sqlite.Open(tempSQLiteDbPath), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(base.TempSQLiteDBPath), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}) @@ -67,26 +64,236 @@ func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransaction return db, submittedTx } -func TestNewBitcoinObserver(t *testing.T) { - t.Run("should return error because zetacore doesn't update zetacore context", func(t *testing.T) { - cfg := config.NewConfig() - coreContext := context.NewZetacoreContext(cfg) - appContext := context.NewAppContext(coreContext, cfg) - chain := chains.BitcoinMainnet - zetacoreClient := mocks.NewMockZetacoreClient() - tss := mocks.NewMockTSS(chains.BitcoinTestnet, sample.EthAddress().String(), "") - logger := base.Logger{} - btcCfg := cfg.BitcoinConfig - ts := metrics.NewTelemetryServer() - - client, err := NewObserver(appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, logger, btcCfg, ts) - require.ErrorContains(t, err, "btc chains params not initialized") - require.Nil(t, client) +// MockBTCObserver creates a mock Bitcoin observer for testing +func MockBTCObserver( + t *testing.T, + chain chains.Chain, + params observertypes.ChainParams, + btcClient interfaces.BTCRPCClient, + dbpath string, +) *observer.Observer { + // use default mock btc client if not provided + if btcClient == nil { + btcClient = mocks.NewMockBTCRPCClient().WithBlockCount(100) + } + + // use memory db if dbpath is empty + if dbpath == "" { + dbpath = "file::memory:?cache=shared" + } + + // create observer + ob, err := observer.NewObserver( + chain, + btcClient, + params, + nil, + nil, + nil, + dbpath, + base.Logger{}, + nil, + ) + require.NoError(t, err) + + return ob +} + +func Test_NewObserver(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // test cases + tests := []struct { + name string + chain chains.Chain + btcClient interfaces.BTCRPCClient + chainParams observertypes.ChainParams + coreContext *context.ZetacoreContext + coreClient interfaces.ZetacoreClient + tss interfaces.TSSSigner + dbpath string + logger base.Logger + ts *metrics.TelemetryServer + fail bool + message string + }{ + { + name: "should be able to create observer", + chain: chain, + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + chainParams: params, + coreContext: nil, + coreClient: nil, + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: false, + }, + { + name: "should fail if net params is not found", + chain: chains.Chain{ChainId: 111}, // invalid chain id + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + chainParams: params, + coreContext: nil, + coreClient: nil, + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error getting net params", + }, + { + name: "should fail on invalid dbpath", + chain: chain, + chainParams: params, + coreContext: nil, + coreClient: nil, + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + tss: mocks.NewTSSMainnet(), + dbpath: "/invalid/dbpath", // invalid dbpath + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error creating db path", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create observer + ob, err := observer.NewObserver( + tt.chain, + tt.btcClient, + tt.chainParams, + tt.coreContext, + tt.coreClient, + tt.tss, + tt.dbpath, + tt.logger, + tt.ts, + ) + + // check result + if tt.fail { + require.ErrorContains(t, err, tt.message) + require.Nil(t, ob) + } else { + require.NoError(t, err) + require.NotNil(t, ob) + } + }) + } +} + +func Test_LoadDB(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create mock btc client, tss and test dbpath + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) + tss := mocks.NewTSSMainnet() + + // create observer + dbpath := testutils.CreateTempDir(t) + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, dbpath, base.Logger{}, nil) + require.NoError(t, err) + + t.Run("should load db successfully", func(t *testing.T) { + err := ob.LoadDB(dbpath) + require.NoError(t, err) + require.EqualValues(t, 100, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid dbpath", func(t *testing.T) { + // load db with empty dbpath + err := ob.LoadDB("") + require.ErrorContains(t, err, "empty db path") + + // load db with invalid dbpath + err = ob.LoadDB("/invalid/dbpath") + require.ErrorContains(t, err, "error OpenDB") + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load db + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) +} + +func Test_LoadLastBlockScanned(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create observer using mock btc client + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(200) + dbpath := testutils.CreateTempDir(t) + + t.Run("should load last block scanned", func(t *testing.T) { + // create observer and write 199 as last block scanned + ob := MockBTCObserver(t, chain, params, btcClient, dbpath) + ob.WriteLastBlockScannedToDB(199) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, 199, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // create observer + ob := MockBTCObserver(t, chain, params, btcClient, dbpath) + + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer on separate path, as we need to reset last block scanned + otherPath := testutils.CreateTempDir(t) + obOther := MockBTCObserver(t, chain, params, btcClient, otherPath) + + // reset last block scanned to 0 so that it will be loaded from RPC + obOther.WithLastBlockScanned(0) + + // set RPC error + btcClient.WithError(fmt.Errorf("error RPC")) + + // load last block scanned + err := obOther.LoadLastBlockScanned() + require.ErrorContains(t, err, "error RPC") + }) + t.Run("should use hardcode block 100 for regtest", func(t *testing.T) { + // use regtest chain + regtest := chains.BitcoinRegtest + obRegnet := MockBTCObserver(t, regtest, params, btcClient, dbpath) + + // load last block scanned + err := obRegnet.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, observer.RegnetStartBlock, obRegnet.LastBlockScanned()) }) } func TestConfirmationThreshold(t *testing.T) { - ob := &Observer{Mu: &sync.Mutex{}} + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + ob := MockBTCObserver(t, chain, params, nil, "") + t.Run("should return confirmations in chain param", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) require.Equal(t, int64(3), ob.ConfirmationsThreshold(big.NewInt(1000))) @@ -94,12 +301,16 @@ func TestConfirmationThreshold(t *testing.T) { t.Run("should return big value confirmations", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) - require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(bigValueSats))) + require.Equal( + t, + int64(observer.BigValueConfirmationCount), + ob.ConfirmationsThreshold(big.NewInt(observer.BigValueSats)), + ) }) t.Run("big value confirmations is the upper cap", func(t *testing.T) { - ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: bigValueConfirmationCount + 1}) - require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) + ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: observer.BigValueConfirmationCount + 1}) + require.Equal(t, int64(observer.BigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) }) } diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index a17d46883f..6e80e1ea5c 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -21,8 +21,8 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.Tss.BTCAddress() - return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) + tssAddr := ob.TSS().BTCAddress() + return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) } // WatchOutbound watches Bitcoin chain for outgoing txs status @@ -34,32 +34,33 @@ func (ob *Observer) WatchOutbound() { } defer ticker.Stop() - ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) + chainID := ob.Chain().ChainId + ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", chainID) sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) continue } - trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(chainID, interfaces.Ascending) if err != nil { ob.logger.Outbound.Error(). Err(err). - Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", chainID) continue } for _, tracker := range trackers { // get original cctx parameters outboundID := ob.GetTxID(tracker.Nonce) - cctx, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, tracker.Nonce) + cctx, err := ob.ZetacoreClient().GetCctxByNonce(chainID, tracker.Nonce) if err != nil { ob.logger.Outbound.Info(). Err(err). - Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", ob.chain.ChainId, tracker.Nonce) + Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", chainID, tracker.Nonce) break } @@ -85,10 +86,10 @@ func (ob *Observer) WatchOutbound() { txCount++ txResult = result ob.logger.Outbound.Info(). - Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, tracker.Nonce) + Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, chainID, tracker.Nonce) if txCount > 1 { ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, ob.chain.ChainId, tracker.Nonce, result) + "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, chainID, tracker.Nonce, result) } } } @@ -97,12 +98,12 @@ func (ob *Observer) WatchOutbound() { ob.setIncludedTx(tracker.Nonce, txResult) } else if txCount > 1 { ob.removeIncludedTx(tracker.Nonce) // we can't tell which txHash is true, so we remove all (if any) to be safe - ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, tracker.Nonce) + ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, chainID, tracker.Nonce) } } ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.stop: - ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", chainID) return } } @@ -162,7 +163,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // Get outbound block height - blockHeight, err := GetBlockHeightByHash(ob.rpcClient, res.BlockHash) + blockHeight, err := GetBlockHeightByHash(ob.btcClient, res.BlockHash) if err != nil { return true, false, errors.Wrapf( err, @@ -172,7 +173,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } logger.Debug().Msgf("Bitcoin outbound confirmed: txid %s, amount %s\n", res.TxID, amountInSat.String()) - zetaHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( sendHash, res.TxID, // #nosec G701 always positive @@ -182,7 +183,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg 0, // gas limit not used with Bitcoin amountInSat, chains.ReceiveStatus_success, - ob.chain, + ob.Chain(), nonce, coin.CoinType_Gas, ) @@ -299,7 +300,7 @@ func (ob *Observer) SelectUTXOs( // 2. The tracker is missing in zetacore. func (ob *Observer) refreshPendingNonce() { // get pending nonces from zetacore - p, err := ob.zetacoreClient.GetPendingNoncesByChain(ob.chain.ChainId) + p, err := ob.ZetacoreClient().GetPendingNoncesByChain(ob.Chain().ChainId) if err != nil { ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") } @@ -336,7 +337,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return res.TxID, nil } if !test { // if not unit test, get cctx from zetacore - send, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, nonce) + send, err := ob.ZetacoreClient().GetCctxByNonce(ob.Chain().ChainId, nonce) if err != nil { return "", errors.Wrapf(err, "getOutboundIDByNonce: error getting cctx for nonce %d", nonce) } @@ -345,7 +346,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return "", fmt.Errorf("getOutboundIDByNonce: cannot find outbound txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) + _, getTxResult, err := GetTxResultByHash(ob.btcClient, txid) if err != nil { return "", errors.Wrapf( err, @@ -363,7 +364,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error } func (ob *Observer) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { - tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() amount := chains.NonceMarkAmount(nonce) for i, utxo := range ob.utxos { sats, err := bitcoin.GetSatoshis(utxo.Amount) @@ -386,7 +387,7 @@ func (ob *Observer) checkIncludedTx( txHash string, ) (*btcjson.GetTransactionResult, bool) { outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) - hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) + hash, getTxResult, err := GetTxResultByHash(ob.btcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -470,7 +471,7 @@ func (ob *Observer) checkTssOutboundResult( ) error { params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce - rawResult, err := GetRawTxResult(ob.rpcClient, hash, res) + rawResult, err := GetRawTxResult(ob.btcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } @@ -507,7 +508,7 @@ func (ob *Observer) checkTSSVin(vins []btcjson.Vin, nonce uint64) error { if nonce > 0 && len(vins) <= 1 { return fmt.Errorf("checkTSSVin: len(vins) <= 1") } - pubKeyTss := hex.EncodeToString(ob.Tss.PubKeyCompressedBytes()) + pubKeyTss := hex.EncodeToString(ob.TSS().PubKeyCompressedBytes()) for i, vin := range vins { // The length of the Witness should be always 2 for SegWit inputs. if len(vin.Witness) != 2 { @@ -547,7 +548,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ } nonce := params.TssNonce - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout receiverExpected := tssAddress @@ -555,7 +556,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ // the 2nd output is the payment to recipient receiverExpected = params.Receiver } - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.chain) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.Chain()) if err != nil { return err } @@ -606,10 +607,10 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams } nonce := params.TssNonce - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.chain) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain()) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index bad0997c0c..910384ef9a 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -3,7 +3,6 @@ package observer import ( "math" "sort" - "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -11,22 +10,27 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -func MockBTCObserverMainnet() *Observer { - cfg := config.NewConfig() - coreContext := context.NewZetacoreContext(cfg) +// the relative path to the testdata directory +var TestDataDir = "../../../" - return &Observer{ - chain: chains.BitcoinMainnet, - zetacoreClient: mocks.NewMockZetacoreClient(), - Tss: mocks.NewTSSMainnet(), - coreContext: coreContext, - } +// MockBTCObserverMainnet creates a mock Bitcoin mainnet observer for testing +func MockBTCObserverMainnet(t *testing.T) *Observer { + // setup mock arguments + chain := chains.BitcoinMainnet + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) + params := mocks.MockChainParams(chain.ChainId, 10) + tss := mocks.NewTSSMainnet() + + // create Bitcoin observer + ob, err := NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, base.Logger{}, nil) + require.NoError(t, err) + + return ob } // helper function to create a test Bitcoin observer @@ -37,26 +41,27 @@ func createObserverWithPrivateKey(t *testing.T) *Observer { tss := &mocks.TSS{ PrivKey: privateKey, } - return &Observer{ - Tss: tss, - Mu: &sync.Mutex{}, - includedTxResults: make(map[string]*btcjson.GetTransactionResult), - } + + // create Bitcoin observer with mock tss + ob := MockBTCObserverMainnet(t) + ob.WithTSS(tss) + + return ob } // helper function to create a test Bitcoin observer with UTXOs func createObserverWithUTXOs(t *testing.T) *Observer { // Create Bitcoin observer - client := createObserverWithPrivateKey(t) - tssAddress := client.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + ob := createObserverWithPrivateKey(t) + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() // Create 10 dummy UTXOs (22.44 BTC in total) - client.utxos = make([]btcjson.ListUnspentResult, 0, 10) + ob.utxos = make([]btcjson.ListUnspentResult, 0, 10) amounts := []float64{0.01, 0.12, 0.18, 0.24, 0.5, 1.26, 2.97, 3.28, 5.16, 8.72} for _, amount := range amounts { - client.utxos = append(client.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) + ob.utxos = append(ob.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) } - return client + return ob } func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex int) { @@ -65,7 +70,7 @@ func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex i ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark - tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() nonceMark := btcjson.ListUnspentResult{ TxID: txid, Address: tssAddress, @@ -90,22 +95,22 @@ func TestCheckTSSVout(t *testing.T) { nonce := uint64(148) // create mainnet mock client - btcClient := MockBTCObserverMainnet() + ob := MockBTCObserverMainnet(t) t.Run("valid TSS vout should pass", func(t *testing.T) { rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 2 or > 3", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}) + err := ob.checkTSSVout(params, []btcjson.Vout{{}}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) + err = ob.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail on invalid TSS vout", func(t *testing.T) { @@ -114,7 +119,7 @@ func TestCheckTSSVout(t *testing.T) { // invalid TSS vout rawResult.Vout[0].ScriptPubKey.Hex = "invalid script" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.Error(t, err) }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -123,7 +128,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -132,7 +137,7 @@ func TestCheckTSSVout(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the receiver address", func(t *testing.T) { @@ -141,7 +146,7 @@ func TestCheckTSSVout(t *testing.T) { // not receiver address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params receiver") }) t.Run("should fail if vout 1 not match payment amount", func(t *testing.T) { @@ -150,7 +155,7 @@ func TestCheckTSSVout(t *testing.T) { // not match payment amount rawResult.Vout[1].Value = 0.00011000 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params amount") }) t.Run("should fail if vout 2 is not to the TSS address", func(t *testing.T) { @@ -159,7 +164,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[2].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } @@ -172,7 +177,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { nonce := uint64(148) // create mainnet mock client - btcClient := MockBTCObserverMainnet() + ob := MockBTCObserverMainnet(t) t.Run("valid TSS vout should pass", func(t *testing.T) { // remove change vout to simulate cancelled tx @@ -181,17 +186,17 @@ func TestCheckTSSVoutCancelled(t *testing.T) { rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 1 or > 2", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}) + err := ob.checkTSSVoutCancelled(params, []btcjson.Vout{}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) + err = ob.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -203,7 +208,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -215,7 +220,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the TSS address", func(t *testing.T) { @@ -228,7 +233,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 1e261c4d0a..41ada01375 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -36,33 +36,33 @@ import ( // WatchInbound watches evm chain for incoming txs and post votes to zetacore func (ob *Observer) WatchInbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInbound_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchInbound_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") + ob.Logger().Inbound.Error().Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) - sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.Logger().Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) + sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } err := ob.ObserveInbound(sampledLogger) if err != nil { - ob.logger.Inbound.Err(err).Msg("WatchInbound: observeInbound error") + ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + case <-ob.StopChannel(): + ob.Logger().Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) return } } @@ -72,29 +72,29 @@ func (ob *Observer) WatchInbound() { // If it was, it tries to broadcast the confirmation vote. If this zeta client has previously broadcast the vote, the tx would be rejected func (ob *Observer) WatchInboundTracker() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.logger.Inbound.Err(err).Msg("error creating ticker") + ob.Logger().Inbound.Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.Chain().ChainId) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { - ob.logger.Inbound.Err(err).Msg("ProcessInboundTrackers error") + ob.Logger().Inbound.Err(err).Msg("ProcessInboundTrackers error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + case <-ob.StopChannel(): + ob.Logger().Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return } } @@ -102,7 +102,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers from zetacore func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) if err != nil { return err } @@ -114,15 +114,20 @@ func (ob *Observer) ProcessInboundTrackers() error { err, "error getting transaction for inbound %s chain %d", tracker.TxHash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tracker.TxHash)) if err != nil { - return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) + return errors.Wrapf( + err, + "error getting receipt for inbound %s chain %d", + tracker.TxHash, + ob.Chain().ChainId, + ) } - ob.logger.Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.Chain().ChainId) // check and vote on inbound tx switch tracker.CoinType { @@ -137,11 +142,11 @@ func (ob *Observer) ProcessInboundTrackers() error { "unknown coin type %s for inbound %s chain %d", tracker.CoinType, tx.Hash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } if err != nil { - return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } } return nil @@ -153,17 +158,17 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if err != nil { return err } - if blockNumber < ob.GetLastBlockHeight() { + if blockNumber < ob.LastBlock() { return fmt.Errorf( "observeInbound: block number should not decrease: current %d last %d", blockNumber, - ob.GetLastBlockHeight(), + ob.LastBlock(), ) } - ob.SetLastBlockHeight(blockNumber) + ob.WithLastBlock(blockNumber) // increment prom counter - metrics.GetBlockByNumberPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetBlockByNumberPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // skip if current height is too low if blockNumber < ob.GetChainParams().ConfirmationCount { @@ -172,10 +177,10 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { confirmedBlockNum := blockNumber - ob.GetChainParams().ConfirmationCount // skip if no new block is confirmed - lastScanned := ob.GetLastBlockHeightScanned() + lastScanned := ob.LastBlockScanned() if lastScanned >= confirmedBlockNum { sampledLogger.Debug(). - Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.chain.ChainId) + Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.Chain().ChainId) return nil } @@ -205,12 +210,11 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if lastScannedLowest > lastScanned { sampledLogger.Info(). Msgf("observeInbound: lasstScanned heights for chain %d ZetaSent %d ERC20Deposited %d TssRecvd %d", - ob.chain.ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) - ob.SetLastBlockHeightScanned(lastScannedLowest) - if err := ob.db.Save(clienttypes.ToLastBlockSQLType(lastScannedLowest)).Error; err != nil { - ob.logger.Inbound.Error(). + ob.Chain().ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) + if err := ob.SaveLastBlockScanned(lastScannedLowest); err != nil { + ob.Logger().Inbound.Error(). Err(err). - Msgf("observeInbound: error writing lastScannedLowest %d to db", lastScannedLowest) + Msgf("observeInbound: error saving lastScannedLowest %d to db", lastScannedLowest) } } return nil @@ -222,7 +226,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { // filter ZetaSent logs addrConnector, connector, err := ob.GetConnectorContract() if err != nil { - ob.logger.Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") + ob.Logger().Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") return startBlock - 1 // lastScanned } iter, err := connector.FilterZetaSent(&bind.FilterOpts{ @@ -231,8 +235,8 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}, []*big.Int{}) if err != nil { - ob.logger.Chain.Warn().Err(err).Msgf( - "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) + ob.Logger().Chain.Warn().Err(err).Msgf( + "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) return startBlock - 1 // lastScanned } @@ -245,10 +249,10 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -261,7 +265,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // post to zetacore beingScanned := uint64(0) @@ -273,7 +277,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { } // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("ObserveZetaSent: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -301,7 +305,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { // filter ERC20CustodyDeposited logs addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() if err != nil { - ob.logger.Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") + ob.Logger().Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") return startBlock - 1 // lastScanned } @@ -311,8 +315,8 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}) if err != nil { - ob.logger.Inbound.Warn().Err(err).Msgf( - "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) + ob.Logger().Inbound.Warn().Err(err).Msgf( + "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) return startBlock - 1 // lastScanned } @@ -325,10 +329,10 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -341,7 +345,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // post to zeatcore guard := make(map[string]bool) @@ -353,15 +357,15 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { } tx, _, err := ob.TransactionByHash(event.Raw.TxHash.Hex()) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf( - "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf( + "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.Chain().ChainId) return beingScanned - 1 // we have to re-scan from this block next time } sender := ethcommon.HexToAddress(tx.From) // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("ObserveERC20Deposited: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -387,23 +391,23 @@ func (ob *Observer) ObserverTSSReceive(startBlock, toBlock uint64) uint64 { // post new block header (if any) to zetacore and ignore error // TODO: consider having a independent ticker(from TSS scaning) for posting block headers // https://github.com/zeta-chain/node/issues/1847 - blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) + blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) if found && blockHeaderVerification.Enabled { // post block header for supported chains // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 err := ob.postBlockHeader(toBlock) if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error posting block header") + ob.Logger().Inbound.Error().Err(err).Msg("error posting block header") } } // observe TSS received gas token in block 'bn' err := ob.ObserveTSSReceiveInBlock(bn) if err != nil { - ob.logger.Inbound.Error(). + ob.Logger().Inbound.Error(). Err(err). - Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.chain.ChainId) + Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.Chain().ChainId) return bn - 1 // we have to re-scan from this block next time } } @@ -418,7 +422,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -442,7 +446,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( if err == nil { msg = ob.BuildInboundVoteMsgForZetaSentEvent(event) } else { - ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err } break // only one event is allowed per tx @@ -450,7 +454,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( } if msg == nil { // no event, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -467,7 +471,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -492,7 +496,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( if err == nil { msg = ob.BuildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) } else { - ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err } break // only one event is allowed per tx @@ -500,7 +504,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( } if msg == nil { // no event, donation, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -517,7 +521,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -526,7 +530,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( } // checks receiver and tx status - if ethcommon.HexToAddress(tx.To) != ob.Tss.EVMAddress() { + if ethcommon.HexToAddress(tx.To) != ob.TSS().EVMAddress() { return "", fmt.Errorf("tx.To %s is not TSS address", tx.To) } if receipt.Status != ethtypes.ReceiptStatusSuccessful { @@ -538,7 +542,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( msg := ob.BuildInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) if msg == nil { // donation, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -555,16 +559,16 @@ func (ob *Observer) PostVoteInbound( retryGasLimit uint64, ) (string, error) { txHash := msg.InboundHash - chainID := ob.chain.ChainId - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) + chainID := ob.Chain().ChainId + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) if err != nil { - ob.logger.Inbound.Err(err). + ob.Logger().Inbound.Err(err). Msgf("inbound detected: error posting vote for chain %d token %s inbound %s", chainID, coinType, txHash) return "", err } else if zetaHash != "" { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) + ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) } else { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) + ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) } return ballot, err @@ -589,10 +593,10 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( } if config.ContainRestrictedAddress(sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), maybeReceiver) { compliance.PrintComplianceLog( - ob.logger.Inbound, - ob.logger.Compliance, + ob.Logger().Inbound, + ob.Logger().Compliance, false, - ob.chain.ChainId, + ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), @@ -603,21 +607,22 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( // donation check if bytes.Equal(event.Message, []byte(constant.DonationMessage)) { - ob.logger.Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.chain.ChainId) + ob.Logger().Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.Chain().ChainId) return nil } message := hex.EncodeToString(event.Message) - ob.logger.Inbound.Info(). + ob.Logger().Inbound.Info(). Msgf("ERC20CustodyDeposited inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) + ob.Chain(). + ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.chain.ChainId, + ob.Chain().ChainId, "", clienttypes.BytesToEthHex(event.Recipient), - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, sdkmath.NewUintFromBigInt(event.Amount), hex.EncodeToString(event.Message), event.Raw.TxHash.Hex(), @@ -625,7 +630,7 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( 1_500_000, coin.CoinType_ERC20, event.Asset.String(), - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -636,7 +641,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( ) *types.MsgVoteInbound { destChain := chains.GetChainFromChainID(event.DestinationChainId.Int64()) if destChain == nil { - ob.logger.Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) + ob.Logger().Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) return nil } destAddr := clienttypes.BytesToEthHex(event.DestinationAddress) @@ -644,32 +649,33 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( // compliance check sender := event.ZetaTxSenderAddress.Hex() if config.ContainRestrictedAddress(sender, destAddr, event.SourceTxOriginAddress.Hex()) { - compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") return nil } if !destChain.IsZetaChain() { - paramsDest, found := ob.coreContext.GetEVMChainParams(destChain.ChainId) + paramsDest, found := ob.ZetacoreContext().GetEVMChainParams(destChain.ChainId) if !found { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) return nil } if strings.EqualFold(destAddr, paramsDest.ZetaTokenContractAddress) { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("potential attack attempt: %s destination address is ZETA token contract address %s", destChain, destAddr) return nil } } message := base64.StdEncoding.EncodeToString(event.Message) - ob.logger.Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) + ob.Logger().Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.Chain(). + ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) return zetacore.GetInboundVoteMessage( sender, - ob.chain.ChainId, + ob.Chain().ChainId, event.SourceTxOriginAddress.Hex(), destAddr, destChain.ChainId, @@ -680,7 +686,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( event.DestinationGasLimit.Uint64(), coin.CoinType_Zeta, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -700,8 +706,8 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( maybeReceiver = parsedAddress.Hex() } if config.ContainRestrictedAddress(sender.Hex(), maybeReceiver) { - compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") return nil } @@ -709,19 +715,19 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( // #nosec G703 err is already checked data, _ := hex.DecodeString(message) if bytes.Equal(data, []byte(constant.DonationMessage)) { - ob.logger.Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.Chain().ChainId) return nil } - ob.logger.Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) + ob.Logger().Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.Chain().ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.chain.ChainId, + ob.Chain().ChainId, sender.Hex(), sender.Hex(), - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, sdkmath.NewUintFromBigInt(&tx.Value), message, tx.Hash, @@ -729,7 +735,7 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( 90_000, coin.CoinType_Gas, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, // not a smart contract call ) } @@ -738,15 +744,15 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { block, err := ob.GetBlockByNumberCached(blockNumber) if err != nil { - return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.chain.ChainId) + return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.Chain().ChainId) } for i := range block.Transactions { tx := block.Transactions[i] - if ethcommon.HexToAddress(tx.To) == ob.Tss.EVMAddress() { + if ethcommon.HexToAddress(tx.To) == ob.TSS().EVMAddress() { receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tx.Hash)) if err != nil { - return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } _, err = ob.CheckAndVoteInboundTokenGas(&tx, receipt, true) @@ -755,7 +761,7 @@ func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { err, "error checking and voting inbound gas asset for inbound %s chain %d", tx.Hash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } } diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index 906634e1ad..bb8930f4ca 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -40,7 +40,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -56,7 +56,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -72,7 +72,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -89,7 +89,17 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain connector - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) + ob := MockEVMObserver( + t, + chain, + nil, + nil, + nil, + nil, + memDBPath, + lastBlock, + mocks.MockChainParams(chainID, confirmation), + ) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -115,7 +125,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -131,7 +141,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -147,7 +157,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -164,7 +174,17 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain ERC20 custody - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) + ob := MockEVMObserver( + t, + chain, + nil, + nil, + nil, + nil, + memDBPath, + lastBlock, + mocks.MockChainParams(chainID, confirmation), + ) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -190,7 +210,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -200,7 +220,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -210,7 +230,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) @@ -221,7 +241,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) @@ -232,7 +252,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) @@ -249,7 +269,7 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_Zeta, inboundHash) // parse ZetaSent event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) connector := mocks.MockConnectorNonEth(t, chainID) event := testutils.ParseReceiptZetaSent(receipt, connector) @@ -296,7 +316,7 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_ERC20, inboundHash) // parse Deposited event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) custody := mocks.MockERC20Custody(t, chainID) event := testutils.ParseReceiptERC20Deposited(receipt, custody) sender := ethcommon.HexToAddress(tx.From) @@ -354,7 +374,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(txDonation)) // create test compliance config - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) cfg := config.Config{ ComplianceConfig: config.ComplianceConfig{}, } @@ -424,7 +444,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation t.Run("should observe TSS receive in block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) // feed archived block and receipt evmJSONRPC.WithBlock(block) @@ -433,20 +453,20 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { require.NoError(t, err) }) t.Run("should not observe on error getting block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock JSONRPC contains no block require.ErrorContains(t, err, "error getting block") }) t.Run("should not observe on error getting receipt", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) evmJSONRPC.WithBlock(block) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock evmClient contains no receipt require.ErrorContains(t, err, "error getting receipt") }) t.Run("should not observe on error posting vote", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) // feed archived block and pause zetacore client evmJSONRPC.WithBlock(block) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 99db258f5a..158b3e326c 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -5,29 +5,20 @@ import ( "fmt" "math" "math/big" - "os" - "strconv" "strings" "sync" - "sync/atomic" "time" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rlp" - lru "github.com/hashicorp/golang-lru" "github.com/onrik/ethrpc" "github.com/pkg/errors" - "github.com/rs/zerolog" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zeta.non-eth.sol" zetaconnectoreth "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.eth.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -39,225 +30,128 @@ import ( clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) -// Logger is the logger for evm chains -// TODO: Merge this logger with the one in bitcoin -// https://github.com/zeta-chain/node/issues/2022 -type Logger struct { - // Chain is the parent logger for the chain - Chain zerolog.Logger +var _ interfaces.ChainObserver = &Observer{} - // Inbound is the logger for incoming transactions - Inbound zerolog.Logger +// Observer is the observer for evm chains +type Observer struct { + // base.Observer implements the base chain observer + base.Observer - // Outbound is the logger for outgoing transactions - Outbound zerolog.Logger + // evmClient is the EVM client for the observed chain + evmClient interfaces.EVMRPCClient - // GasPrice is the logger for gas prices - GasPrice zerolog.Logger + // evmJSONRPC is the EVM JSON RPC client for the observed chain + evmJSONRPC interfaces.EVMJSONRPCClient - // Compliance is the logger for compliance checks - Compliance zerolog.Logger -} + // outboundPendingTransactions is the map to index pending transactions by hash + outboundPendingTransactions map[string]*ethtypes.Transaction -var _ interfaces.ChainObserver = &Observer{} + // outboundConfirmedReceipts is the map to index confirmed receipts by hash + outboundConfirmedReceipts map[string]*ethtypes.Receipt -// Observer is the observer for evm chains -type Observer struct { - Tss interfaces.TSSSigner + // outboundConfirmedTransactions is the map to index confirmed transactions by hash + outboundConfirmedTransactions map[string]*ethtypes.Transaction + // Mu protects the maps and chain params from concurrent access Mu *sync.Mutex - - chain chains.Chain - evmClient interfaces.EVMRPCClient - evmJSONRPC interfaces.EVMJSONRPCClient - zetacoreClient interfaces.ZetacoreClient - lastBlockScanned uint64 - lastBlock uint64 - db *gorm.DB - outboundPendingTransactions map[string]*ethtypes.Transaction - outboundConfirmedReceipts map[string]*ethtypes.Receipt - outboundConfirmedTransactions map[string]*ethtypes.Transaction - stop chan struct{} - logger Logger - coreContext *clientcontext.ZetacoreContext - chainParams observertypes.ChainParams - ts *metrics.TelemetryServer - - blockCache *lru.Cache - headerCache *lru.Cache } // NewObserver returns a new EVM chain observer func NewObserver( - appContext *clientcontext.AppContext, + evmCfg config.EVMConfig, + evmClient interfaces.EVMRPCClient, + chainParams observertypes.ChainParams, + zetacoreContext *clientcontext.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, - evmCfg config.EVMConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - ob := Observer{ - ts: ts, - } - - chainLogger := logger.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() - ob.logger = Logger{ - Chain: chainLogger, - Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), - Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), - GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: logger.Compliance, - } - - ob.coreContext = appContext.ZetacoreContext() - chainParams, found := ob.coreContext.GetEVMChainParams(evmCfg.Chain.ChainId) - if !found { - return nil, fmt.Errorf("evm chains params not initialized for chain %d", evmCfg.Chain.ChainId) - } - - ob.chainParams = *chainParams - ob.stop = make(chan struct{}) - ob.chain = evmCfg.Chain - ob.Mu = &sync.Mutex{} - ob.zetacoreClient = zetacoreClient - ob.Tss = tss - ob.outboundPendingTransactions = make(map[string]*ethtypes.Transaction) - ob.outboundConfirmedReceipts = make(map[string]*ethtypes.Receipt) - ob.outboundConfirmedTransactions = make(map[string]*ethtypes.Transaction) - - ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.ChainName.String(), evmCfg.Endpoint) - client, err := ethclient.Dial(evmCfg.Endpoint) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("eth Client Dial") - return nil, err - } - - ob.evmClient = client - ob.evmJSONRPC = ethrpc.NewEthRPC(evmCfg.Endpoint) - - // create block header and block caches - ob.blockCache, err = lru.New(1000) + // create base observer + baseObserver, err := base.NewObserver( + evmCfg.Chain, + chainParams, + zetacoreContext, + zetacoreClient, + tss, + base.DefaultBlockCacheSize, + base.DefaultHeaderCacheSize, + ts, + logger, + ) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create block cache") return nil, err } - ob.headerCache, err = lru.New(1000) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create header cache") - return nil, err + // create evm observer + ob := &Observer{ + Observer: *baseObserver, + evmClient: evmClient, + evmJSONRPC: ethrpc.NewEthRPC(evmCfg.Endpoint), + outboundPendingTransactions: make(map[string]*ethtypes.Transaction), + outboundConfirmedReceipts: make(map[string]*ethtypes.Receipt), + outboundConfirmedTransactions: make(map[string]*ethtypes.Transaction), + Mu: &sync.Mutex{}, } - err = ob.LoadDB(dbpath, ob.chain) + // open database and load data + err = ob.LoadDB(dbpath) if err != nil { return nil, err } - ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) - - return &ob, nil -} - -// WithChain attaches a new chain to the observer -func (ob *Observer) WithChain(chain chains.Chain) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.chain = chain -} - -// WithLogger attaches a new logger to the observer -func (ob *Observer) WithLogger(logger zerolog.Logger) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.logger = Logger{ - Chain: logger, - Inbound: logger.With().Str("module", "WatchInbound").Logger(), - Outbound: logger.With().Str("module", "WatchOutbound").Logger(), - GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), - } + return ob, nil } // WithEvmClient attaches a new evm client to the observer func (ob *Observer) WithEvmClient(client interfaces.EVMRPCClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() ob.evmClient = client } // WithEvmJSONRPC attaches a new evm json rpc client to the observer func (ob *Observer) WithEvmJSONRPC(client interfaces.EVMJSONRPCClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() ob.evmJSONRPC = client } -// WithZetacoreClient attaches a new client to interact with zetacore to the observer -func (ob *Observer) WithZetacoreClient(client interfaces.ZetacoreClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.zetacoreClient = client -} - -// WithBlockCache attaches a new block cache to the observer -func (ob *Observer) WithBlockCache(cache *lru.Cache) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.blockCache = cache -} - -// Chain returns the chain for the observer -func (ob *Observer) Chain() chains.Chain { - ob.Mu.Lock() - defer ob.Mu.Unlock() - return ob.chain -} - // SetChainParams sets the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.chainParams = params + ob.WithChainParams(params) } // GetChainParams returns the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.chainParams + return ob.ChainParams() } +// GetConnectorContract returns the non-Eth connector address and binder func (ob *Observer) GetConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) - contract, err := FetchConnectorContract(addr, ob.evmClient) + contract, err := zetaconnector.NewZetaConnectorNonEth(addr, ob.evmClient) return addr, contract, err } +// GetConnectorContractEth returns the Eth connector address and binder func (ob *Observer) GetConnectorContractEth() (ethcommon.Address, *zetaconnectoreth.ZetaConnectorEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) contract, err := FetchConnectorContractEth(addr, ob.evmClient) return addr, contract, err } -func (ob *Observer) GetZetaTokenNonEthContract() (ethcommon.Address, *zeta.ZetaNonEth, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().ZetaTokenContractAddress) - contract, err := FetchZetaZetaNonEthTokenContract(addr, ob.evmClient) - return addr, contract, err -} - +// GetERC20CustodyContract returns ERC20Custody contract address and binder func (ob *Observer) GetERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().Erc20CustodyContractAddress) - contract, err := FetchERC20CustodyContract(addr, ob.evmClient) + contract, err := erc20custody.NewERC20Custody(addr, ob.evmClient) return addr, contract, err } -func FetchConnectorContract( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*zetaconnector.ZetaConnectorNonEth, error) { - return zetaconnector.NewZetaConnectorNonEth(addr, client) -} - +// FetchConnectorContractEth returns the Eth connector address and binder func FetchConnectorContractEth( addr ethcommon.Address, client interfaces.EVMRPCClient, @@ -265,22 +159,18 @@ func FetchConnectorContractEth( return zetaconnectoreth.NewZetaConnectorEth(addr, client) } -func FetchZetaZetaNonEthTokenContract( +// FetchZetaTokenContract returns the non-Eth ZETA token binder +func FetchZetaTokenContract( addr ethcommon.Address, client interfaces.EVMRPCClient, ) (*zeta.ZetaNonEth, error) { return zeta.NewZetaNonEth(addr, client) } -func FetchERC20CustodyContract( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*erc20custody.ERC20Custody, error) { - return erc20custody.NewERC20Custody(addr, client) -} - // Start all observation routines for the evm chain func (ob *Observer) Start() { + ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + // watch evm chain for incoming txs and post votes to zetacore go ob.WatchInbound() @@ -299,7 +189,7 @@ func (ob *Observer) Start() { // WatchRPCStatus watches the RPC status of the evm chain func (ob *Observer) WatchRPCStatus() { - ob.logger.Chain.Info().Msgf("Starting RPC status check for chain %s", ob.chain.String()) + ob.Logger().Chain.Info().Msgf("Starting RPC status check for chain %d", ob.Chain().ChainId) ticker := time.NewTicker(60 * time.Second) for { select { @@ -309,52 +199,35 @@ func (ob *Observer) WatchRPCStatus() { } bn, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } gasPrice, err := ob.evmClient.SuggestGasPrice(context.Background()) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(bn)) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } // #nosec G701 always in range blockTime := time.Unix(int64(header.Time), 0).UTC() elapsedSeconds := time.Since(blockTime).Seconds() if elapsedSeconds > 100 { - ob.logger.Chain.Warn(). + ob.Logger().Chain.Warn(). Msgf("RPC Status Check warning: RPC stale or chain stuck (check explorer)? Latest block %d timestamp is %.0fs ago", bn, elapsedSeconds) continue } - ob.logger.Chain.Info(). + ob.Logger().Chain.Info(). Msgf("[OK] RPC status: latest block num %d, timestamp %s ( %.0fs ago), suggested gas price %d", header.Number, blockTime.String(), elapsedSeconds, gasPrice.Uint64()) - case <-ob.stop: + case <-ob.StopChannel(): return } } } -func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) - close(ob.stop) // this notifies all goroutines to stop - - ob.logger.Chain.Info().Msg("closing ob.db") - dbInst, err := ob.db.DB() - if err != nil { - ob.logger.Chain.Info().Msg("error getting database instance") - } - err = dbInst.Close() - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("error closing database") - } - - ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) -} - // SetPendingTx sets the pending transaction in memory func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { ob.Mu.Lock() @@ -419,47 +292,25 @@ func (ob *Observer) CheckTxInclusion(tx *ethtypes.Transaction, receipt *ethtypes return nil } -// SetLastBlockHeightScanned set last block height scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) SetLastBlockHeightScanned(height uint64) { - atomic.StoreUint64(&ob.lastBlockScanned, height) - ob.ts.SetLastScannedBlockNumber(ob.chain, height) -} - -// GetLastBlockHeightScanned get last block height scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) GetLastBlockHeightScanned() uint64 { - height := atomic.LoadUint64(&ob.lastBlockScanned) - return height -} - -// SetLastBlockHeight set external last block height -func (ob *Observer) SetLastBlockHeight(height uint64) { - atomic.StoreUint64(&ob.lastBlock, height) -} - -// GetLastBlockHeight get external last block height -func (ob *Observer) GetLastBlockHeight() uint64 { - return atomic.LoadUint64(&ob.lastBlock) -} - // WatchGasPrice watches evm chain for gas prices and post to zetacore func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } // start gas price ticker ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchGasPrice_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchGasPrice_%d", ob.Chain().ChainId), ob.GetChainParams().GasPriceTicker, ) if err != nil { - ob.logger.GasPrice.Error().Err(err).Msg("NewDynamicTicker error") + ob.Logger().GasPrice.Error().Err(err).Msg("NewDynamicTicker error") return } - ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) + ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", + ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -470,11 +321,11 @@ func (ob *Observer) WatchGasPrice() { } err = ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.stop: - ob.logger.GasPrice.Info().Msg("WatchGasPrice stopped") + ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) + case <-ob.StopChannel(): + ob.Logger().GasPrice.Info().Msg("WatchGasPrice stopped") return } } @@ -485,21 +336,21 @@ func (ob *Observer) PostGasPrice() error { // GAS PRICE gasPrice, err := ob.evmClient.SuggestGasPrice(context.TODO()) if err != nil { - ob.logger.GasPrice.Err(err).Msg("Err SuggestGasPrice:") + ob.Logger().GasPrice.Err(err).Msg("Err SuggestGasPrice:") return err } blockNum, err := ob.evmClient.BlockNumber(context.TODO()) if err != nil { - ob.logger.GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") + ob.Logger().GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") return err } // SUPPLY supply := "100" // lockedAmount on ETH, totalSupply on other chains - zetaHash, err := ob.zetacoreClient.PostGasPrice(ob.chain, gasPrice.Uint64(), supply, blockNum) + zetaHash, err := ob.ZetacoreClient().PostGasPrice(ob.Chain(), gasPrice.Uint64(), supply, blockNum) if err != nil { - ob.logger.GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") + ob.Logger().GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") return err } _ = zetaHash @@ -521,21 +372,21 @@ func (ob *Observer) TransactionByHash(txHash string) (*ethrpc.Transaction, bool, } func (ob *Observer) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, error) { - if header, ok := ob.headerCache.Get(blockNumber); ok { + if header, ok := ob.HeaderCache().Get(blockNumber); ok { return header.(*ethtypes.Header), nil } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) if err != nil { return nil, err } - ob.headerCache.Add(blockNumber, header) + ob.HeaderCache().Add(blockNumber, header) return header, nil } // GetBlockByNumberCached get block by number from cache // returns block, ethrpc.Block, isFallback, isSkip, error func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, error) { - if block, ok := ob.blockCache.Get(blockNumber); ok { + if block, ok := ob.BlockCache().Get(blockNumber); ok { return block.(*ethrpc.Block), nil } if blockNumber > math.MaxInt32 { @@ -546,13 +397,13 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, e if err != nil { return nil, err } - ob.blockCache.Add(blockNumber, block) + ob.BlockCache().Add(blockNumber, block) return block, nil } // RemoveCachedBlock remove block from cache func (ob *Observer) RemoveCachedBlock(blockNumber uint64) { - ob.blockCache.Remove(blockNumber) + ob.BlockCache().Remove(blockNumber) } // BlockByNumber query block by number via JSON-RPC @@ -570,92 +421,60 @@ func (ob *Observer) BlockByNumber(blockNumber int) (*ethrpc.Block, error) { return block, nil } -// LoadLastScannedBlock loads last scanned block from specified height or from database -// The last scanned block is the height from which the observer should continue scanning for inbound transactions -func (ob *Observer) LoadLastScannedBlock() error { - // get environment variable - envvar := ob.chain.ChainName.String() + "_SCAN_FROM" - scanFromBlock := os.Getenv(envvar) - - // load from environment variable if set - if scanFromBlock != "" { - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: envvar %s is set; scan from block %s", envvar, scanFromBlock) - if scanFromBlock == base.EnvVarLatestBlock { - header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(header.Number.Uint64()) - } else { - scanFromBlockInt, err := strconv.ParseUint(scanFromBlock, 10, 64) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(scanFromBlockInt) - } - } else { - // load from DB otherwise - var lastBlock clienttypes.LastBlockSQLType - if err := ob.db.First(&lastBlock, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") - header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(header.Number.Uint64()) - if dbc := ob.db.Save(clienttypes.ToLastBlockSQLType(ob.GetLastBlockHeightScanned())); dbc.Error != nil { - ob.logger.Chain.Error().Err(dbc.Error).Msgf("LoadLastScannedBlock: error writing last scanned block %d to DB", ob.GetLastBlockHeightScanned()) - } - } else { - ob.SetLastBlockHeightScanned(lastBlock.Num) - } +// LoadDB open sql database and load data into EVM observer +func (ob *Observer) LoadDB(dbPath string) error { + if dbPath == "" { + return errors.New("empty db path") } - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) - return nil -} + // open database + err := ob.OpenDB(dbPath, "") + if err != nil { + return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) + } -// LoadDB open sql database and load data into EVM observer -func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { - if dbPath != "" { - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return err - } - } - path := fmt.Sprintf("%s/%s", dbPath, chain.ChainName.String()) //Use "file::memory:?cache=shared" for temp db - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) - if err != nil { - ob.logger.Chain.Error(). - Err(err). - Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) - return err - } + // run auto migration + // transaction and receipt tables are used nowhere but we still run migration in case they are needed in future + err = ob.DB().AutoMigrate( + &clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + ) + if err != nil { + return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) + } - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("error migrating db") - return err - } + // load last block scanned + err = ob.LoadLastBlockScanned() + + return err +} - ob.db = db - err = ob.LoadLastScannedBlock() +// LoadLastBlockScanned loads the last scanned block from the database +func (ob *Observer) LoadLastBlockScanned() error { + err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) + if err != nil { + return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) + } + + // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: + // 1. environment variable is set explicitly to "latest" + // 2. environment variable is empty and last scanned block is not found in DB + if ob.LastBlockScanned() == 0 { + blockNumber, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - return err + return errors.Wrapf(err, "error BlockNumber for chain %d", ob.Chain().ChainId) } + ob.WithLastBlockScanned(blockNumber) } + ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) + return nil } func (ob *Observer) postBlockHeader(tip uint64) error { bn := tip - res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) + res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { // #nosec G701 always positive bn = uint64(res.ChainState.LatestHeight) + 1 // the next header to post @@ -667,23 +486,23 @@ func (ob *Observer) postBlockHeader(tip uint64) error { header, err := ob.GetBlockHeaderCached(bn) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) return err } headerRLP, err := rlp.EncodeToBytes(header) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) return err } - _, err = ob.zetacoreClient.PostVoteBlockHeader( - ob.chain.ChainId, + _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ob.Chain().ChainId, header.Hash().Bytes(), header.Number.Int64(), proofs.NewEthereumHeader(headerRLP), ) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) return err } return nil diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 0601c083f2..9ef7e47e1b 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -1,6 +1,8 @@ package observer_test import ( + "fmt" + "os" "sync" "testing" @@ -21,6 +23,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" + "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -28,17 +31,24 @@ import ( // the relative path to the testdata directory var TestDataDir = "../../../" -// getAppContext creates an app context for unit tests -func getAppContext( +// getZetacoreContext creates a zetacore context for unit tests +func getZetacoreContext( evmChain chains.Chain, + endpoint string, evmChainParams *observertypes.ChainParams, -) (*context.AppContext, config.EVMConfig) { +) (*context.ZetacoreContext, config.EVMConfig) { + // use default endpoint if not provided + if endpoint == "" { + endpoint = "http://localhost:8545" + } + // create config cfg := config.NewConfig() cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ Chain: evmChain, - Endpoint: "http://localhost:8545", + Endpoint: endpoint, } + // create zetacore context coreCtx := context.NewZetacoreContext(cfg) evmChainParamsMap := make(map[int64]*observertypes.ChainParams) @@ -57,8 +67,7 @@ func getAppContext( zerolog.Logger{}, ) // create app context - appCtx := context.NewAppContext(coreCtx, cfg) - return appCtx, cfg.EVMChainConfigs[evmChain.ChainId] + return coreCtx, cfg.EVMChainConfigs[evmChain.ChainId] } // MockEVMObserver creates a mock ChainObserver with custom chain, TSS, params etc @@ -69,8 +78,15 @@ func MockEVMObserver( evmJSONRPC interfaces.EVMJSONRPCClient, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, + dbpath string, lastBlock uint64, - params observertypes.ChainParams) *observer.Observer { + params observertypes.ChainParams, +) *observer.Observer { + // use default mock evm client if not provided + if evmClient == nil { + evmClient = mocks.NewMockEvmClient().WithBlockNumber(1000) + } + // use default mock zetacore client if not provided if zetacoreClient == nil { zetacoreClient = mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) @@ -79,17 +95,203 @@ func MockEVMObserver( if tss == nil { tss = mocks.NewTSSMainnet() } - // create app context - appCtx, evmCfg := getAppContext(chain, ¶ms) + // create zetacore context + coreCtx, evmCfg := getZetacoreContext(chain, "", ¶ms) - // create chain observer - client, err := observer.NewObserver(appCtx, zetacoreClient, tss, "", base.Logger{}, evmCfg, nil) + // create observer + ob, err := observer.NewObserver(evmCfg, evmClient, params, coreCtx, zetacoreClient, tss, dbpath, base.Logger{}, nil) require.NoError(t, err) - client.WithEvmClient(evmClient) - client.WithEvmJSONRPC(evmJSONRPC) - client.SetLastBlockHeight(lastBlock) + ob.WithEvmJSONRPC(evmJSONRPC) + ob.WithLastBlock(lastBlock) + + return ob +} + +func Test_NewObserver(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) + + // test cases + tests := []struct { + name string + evmCfg config.EVMConfig + chainParams observertypes.ChainParams + evmClient interfaces.EVMRPCClient + tss interfaces.TSSSigner + dbpath string + logger base.Logger + ts *metrics.TelemetryServer + fail bool + message string + }{ + { + name: "should be able to create observer", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: false, + }, + { + name: "should fail on invalid dbpath", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + tss: mocks.NewTSSMainnet(), + dbpath: "/invalid/dbpath", // invalid dbpath + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error creating db path", + }, + { + name: "should fail if RPC call fails", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + evmClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error RPC", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create zetacore context, client and tss + zetacoreCtx, _ := getZetacoreContext(tt.evmCfg.Chain, tt.evmCfg.Endpoint, ¶ms) + zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) + + // create observer + ob, err := observer.NewObserver( + tt.evmCfg, + tt.evmClient, + tt.chainParams, + zetacoreCtx, + zetacoreClient, + tt.tss, + tt.dbpath, + tt.logger, + tt.ts, + ) + + // check result + if tt.fail { + require.ErrorContains(t, err, tt.message) + require.Nil(t, ob) + } else { + require.NoError(t, err) + require.NotNil(t, ob) + } + }) + } +} + +func Test_LoadDB(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) + dbpath := testutils.CreateTempDir(t) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, dbpath, 1, params) + + t.Run("should load db successfully", func(t *testing.T) { + err := ob.LoadDB(dbpath) + require.NoError(t, err) + require.EqualValues(t, 1000, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid dbpath", func(t *testing.T) { + // load db with empty dbpath + err := ob.LoadDB("") + require.ErrorContains(t, err, "empty db path") + + // load db with invalid dbpath + err = ob.LoadDB("/invalid/dbpath") + require.ErrorContains(t, err, "error OpenDB") + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load db + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer + tempClient := mocks.NewMockEvmClient() + ob := MockEVMObserver(t, chain, tempClient, nil, nil, nil, dbpath, 1, params) + + // set RPC error + tempClient.WithError(fmt.Errorf("error RPC")) + + // load db + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error RPC") + }) +} + +func Test_LoadLastBlockScanned(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) - return client + // create observer using mock evm client + evmClient := mocks.NewMockEvmClient().WithBlockNumber(100) + dbpath := testutils.CreateTempDir(t) + ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, dbpath, 1, params) + + t.Run("should load last block scanned", func(t *testing.T) { + // create db and write 123 as last block scanned + ob.WriteLastBlockScannedToDB(123) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, 123, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer on separate path, as we need to reset last block scanned + otherPath := testutils.CreateTempDir(t) + obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, otherPath, 1, params) + + // reset last block scanned to 0 so that it will be loaded from RPC + obOther.WithLastBlockScanned(0) + + // set RPC error + evmClient.WithError(fmt.Errorf("error RPC")) + + // load last block scanned + err := obOther.LoadLastBlockScanned() + require.ErrorContains(t, err, "error RPC") + }) } func Test_BlockCache(t *testing.T) { diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 8dda7ac034..9c3bd1c66b 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -29,33 +29,33 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.Tss.EVMAddress().String() - return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) + tssAddr := ob.TSS().EVMAddress().String() + return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) } // WatchOutbound watches evm chain for outgoing txs status func (ob *Observer) WatchOutbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchOutbound_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchOutbound_%d", ob.Chain().ChainId), ob.GetChainParams().OutboundTicker, ) if err != nil { - ob.logger.Outbound.Error().Err(err).Msg("error creating ticker") + ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") return } - ob.logger.Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.chain.ChainId) - sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.Logger().Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.Chain().ChainId) + sampledLogger := ob.Logger().Outbound.Sample(&zerolog.BasicSampler{N: 10}) defer ticker.Stop() for { select { case <-ticker.C(): - if !clientcontext.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.Chain().ChainId) continue } - trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ob.Chain().ChainId, interfaces.Ascending) if err != nil { continue } @@ -72,23 +72,23 @@ func (ob *Observer) WatchOutbound() { txCount++ outboundReceipt = receipt outbound = tx - ob.logger.Outbound.Info(). - Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, nonceInt) + ob.Logger().Outbound.Info(). + Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.Chain().ChainId, nonceInt) if txCount > 1 { - ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.chain.ChainId, nonceInt, outboundReceipt, outbound) + ob.Logger().Outbound.Error().Msgf( + "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.Chain().ChainId, nonceInt, outboundReceipt, outbound) } } } if txCount == 1 { // should be only one txHash confirmed for each nonce. ob.SetTxNReceipt(nonceInt, outboundReceipt, outbound) } else if txCount > 1 { // should not happen. We can't tell which txHash is true. It might happen (e.g. glitchy/hacked endpoint) - ob.logger.Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, nonceInt) + ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.Chain().ChainId, nonceInt) } } - ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.stop: - ob.logger.Outbound.Info().Msg("WatchOutbound: stopped") + ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) + case <-ob.StopChannel(): + ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") return } } @@ -105,8 +105,8 @@ func (ob *Observer) PostVoteOutbound( cointype coin.CoinType, logger zerolog.Logger, ) { - chainID := ob.chain.ChainId - zetaTxHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( + chainID := ob.Chain().ChainId + zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( cctxIndex, receipt.TxHash.Hex(), receipt.BlockNumber.Uint64(), @@ -115,7 +115,7 @@ func (ob *Observer) PostVoteOutbound( transaction.Gas(), receiveValue, receiveStatus, - ob.chain, + ob.Chain(), nonce, cointype, ) @@ -137,17 +137,17 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg return false, false, nil } receipt, transaction := ob.GetTxNReceipt(nonce) - sendID := fmt.Sprintf("%d-%d", ob.chain.ChainId, nonce) + sendID := fmt.Sprintf("%d-%d", ob.Chain().ChainId, nonce) logger = logger.With().Str("sendID", sendID).Logger() // get connector and erce20Custody contracts connectorAddr, connector, err := ob.GetConnectorContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.chain.ChainId) + return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.Chain().ChainId) } custodyAddr, custody, err := ob.GetERC20CustodyContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.chain.ChainId) + return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.Chain().ChainId) } // define a few common variables @@ -181,7 +181,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg if err != nil { logger.Error(). Err(err). - Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.chain.ChainId, receipt.TxHash) + Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.Chain().ChainId, receipt.TxHash) return false, false, err } @@ -346,7 +346,7 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.chain.ChainId) + Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.Chain().ChainId) return nil, nil, false } if transaction == nil { // should not happen @@ -355,17 +355,17 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check tx sender and nonce - signer := ethtypes.NewLondonSigner(big.NewInt(ob.chain.ChainId)) + signer := ethtypes.NewLondonSigner(big.NewInt(ob.Chain().ChainId)) from, err := signer.Sender(transaction) if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.chain.ChainId) + Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.Chain().ChainId) return nil, nil, false } - if from != ob.Tss.EVMAddress() { // must be TSS address + if from != ob.TSS().EVMAddress() { // must be TSS address log.Error().Msgf("confirmTxByHash: sender %s for outbound %s chain %d is not TSS address %s", - from.Hex(), transaction.Hash().Hex(), ob.chain.ChainId, ob.Tss.EVMAddress().Hex()) + from.Hex(), transaction.Hash().Hex(), ob.Chain().ChainId, ob.TSS().EVMAddress().Hex()) return nil, nil, false } if transaction.Nonce() != nonce { // must match cctx nonce @@ -394,10 +394,10 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check confirmations - if !ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()) { + if !ob.HasEnoughConfirmations(receipt, ob.LastBlock()) { log.Debug(). Msgf("confirmTxByHash: txHash %s nonce %d included but not confirmed: receipt block %d, current block %d", - txHash, nonce, receipt.BlockNumber, ob.GetLastBlockHeight()) + txHash, nonce, receipt.BlockNumber, ob.LastBlock()) return nil, nil, false } diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index e0806d6086..df9a642a8e 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -13,12 +13,17 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) +const ( + memDBPath = base.TempSQLiteDBPath +) + // getContractsByChainID is a helper func to get contracts and addresses by chainID func getContractsByChainID( t *testing.T, @@ -57,11 +62,12 @@ func Test_IsOutboundProcessed(t *testing.T) { ) t.Run("should post vote and return true if outbound is processed", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) + // post outbound vote - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) @@ -73,9 +79,9 @@ func Test_IsOutboundProcessed(t *testing.T) { cctx := testutils.LoadCctxByNonce(t, chainID, nonce) cctx.InboundParams.Sender = sample.EthAddress().Hex() - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) // modify compliance config to restrict sender address cfg := config.Config{ @@ -85,29 +91,29 @@ func Test_IsOutboundProcessed(t *testing.T) { config.LoadComplianceConfig(cfg) // post outbound vote - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) }) t.Run("should return false if outbound is not confirmed", func(t *testing.T) { - // create evm client and DO NOT set outbound as confirmed - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + // create evm observer and DO NOT set outbound as confirmed + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) }) t.Run("should fail if unable to parse ZetaReceived event", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail - chainParamsNew := client.GetChainParams() + chainParamsNew := ob.GetChainParams() chainParamsNew.ConnectorContractAddress = sample.EthAddress().Hex() - client.SetChainParams(chainParamsNew) - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + ob.SetChainParams(chainParamsNew) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.Error(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) @@ -147,15 +153,15 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { ) t.Run("should fail if unable to get connector/custody contract", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) abiConnector := zetaconnector.ZetaConnectorNonEthMetaData.ABI abiCustody := erc20custody.ERC20CustodyMetaData.ABI // set invalid connector ABI zetaconnector.ZetaConnectorNonEthMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) zetaconnector.ZetaConnectorNonEthMetaData.ABI = abiConnector // reset connector ABI require.ErrorContains(t, err, "error getting zeta connector") require.False(t, isIncluded) @@ -163,7 +169,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { // set invalid custody ABI erc20custody.ERC20CustodyMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err = client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err = ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.ErrorContains(t, err, "error getting erc20 custody") require.False(t, isIncluded) require.False(t, isConfirmed) @@ -193,8 +199,8 @@ func Test_PostVoteOutbound(t *testing.T) { // create evm client using mock zetacore client and post outbound vote zetacoreClient := mocks.NewMockZetacoreClient() - client := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, 1, observertypes.ChainParams{}) - client.PostVoteOutbound( + ob := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, memDBPath, 1, observertypes.ChainParams{}) + ob.PostVoteOutbound( cctx.Index, receipt, outbound, @@ -207,7 +213,7 @@ func Test_PostVoteOutbound(t *testing.T) { // pause the mock zetacore client to simulate error posting vote zetacoreClient.Pause() - client.PostVoteOutbound( + ob.PostVoteOutbound( cctx.Index, receipt, outbound, diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index fbb4d5ef88..d7df5a33d1 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -70,7 +70,7 @@ func TestSigner_NewOutboundData(t *testing.T) { evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) t.Run("NewOutboundData success", func(t *testing.T) { diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index 410ea5adf3..bdce93f60c 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -59,22 +59,34 @@ func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { } // getNewEvmChainObserver creates a new EVM chain observer for testing -func getNewEvmChainObserver(tss interfaces.TSSSigner) (*observer.Observer, error) { +func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.Observer, error) { // use default mock TSS if not provided if tss == nil { tss = mocks.NewTSSMainnet() } - - logger := base.Logger{} - ts := &metrics.TelemetryServer{} cfg := config.NewConfig() + // prepare mock arguments to create observer evmcfg := config.EVMConfig{Chain: chains.BscMainnet, Endpoint: "http://localhost:8545"} + evmClient := mocks.NewMockEvmClient().WithBlockNumber(1000) + params := mocks.MockChainParams(evmcfg.Chain.ChainId, 10) cfg.EVMChainConfigs[chains.BscMainnet.ChainId] = evmcfg coreCTX := context.NewZetacoreContext(cfg) - appCTX := context.NewAppContext(coreCTX, cfg) + dbpath := testutils.CreateTempDir(t) + logger := base.Logger{} + ts := &metrics.TelemetryServer{} - return observer.NewObserver(appCTX, mocks.NewMockZetacoreClient(), tss, "", logger, evmcfg, ts) + return observer.NewObserver( + evmcfg, + evmClient, + params, + coreCTX, + mocks.NewMockZetacoreClient(), + tss, + dbpath, + logger, + ts, + ) } func getNewOutboundProcessor() *outboundprocessor.Processor { @@ -145,7 +157,7 @@ func TestSigner_TryProcessOutbound(t *testing.T) { require.NoError(t, err) cctx := getCCTX(t) processor := getNewOutboundProcessor() - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) // Test with mock client that has keys @@ -166,7 +178,7 @@ func TestSigner_SignOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -200,7 +212,7 @@ func TestSigner_SignRevertTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -238,7 +250,7 @@ func TestSigner_SignCancelTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -276,7 +288,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -312,7 +324,7 @@ func TestSigner_SignCommandTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -357,7 +369,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -395,7 +407,7 @@ func TestSigner_BroadcastOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -445,7 +457,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -488,7 +500,7 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index f975c7eb88..e27d64fb12 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -123,7 +123,7 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { zetaTokenAddressString := externalEvmChainParams.ZetaTokenContractAddress zetaTokenAddress := ethcommon.HexToAddress(zetaTokenAddressString) - zetatokenNonEth, err := observer.FetchZetaZetaNonEthTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) + zetatokenNonEth, err := observer.FetchZetaTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) if err != nil { return err } From 625e6334a881f6e5615ec99bf56f149cdf8f3cea Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Thu, 20 Jun 2024 13:35:45 -0500 Subject: [PATCH 14/21] moved pure RPC methods to rpc package --- cmd/zetaclientd/utils.go | 54 ++++----- .../chains/bitcoin/observer/observer.go | 73 ------------ .../chains/bitcoin/observer/outbound.go | 9 +- zetaclient/chains/bitcoin/rpc/rpc.go | 109 ++++++++++++++++++ .../live_test.go => rpc/rpc_live_test.go} | 44 ++++--- 5 files changed, 159 insertions(+), 130 deletions(-) create mode 100644 zetaclient/chains/bitcoin/rpc/rpc.go rename zetaclient/chains/bitcoin/{observer/live_test.go => rpc/rpc_live_test.go} (95%) diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 31444cf5e7..647e2520e1 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -3,7 +3,6 @@ package main import ( "fmt" - "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" @@ -11,6 +10,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" btcobserver "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + btcrpc "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" btcsigner "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/signer" evmobserver "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" evmsigner "github.com/zeta-chain/zetacore/zetaclient/chains/evm/signer" @@ -168,41 +168,27 @@ func CreateChainObserverMap( // create BTC chain observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - // create BTC client - connCfg := &rpcclient.ConnConfig{ - Host: btcConfig.RPCHost, - User: btcConfig.RPCUsername, - Pass: btcConfig.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcConfig.RPCParams, - } - btcClient, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - err = btcClient.Ping() + btcClient, err := btcrpc.NewRPCClient(btcConfig) if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) - } - - // create BTC chain observer - co, err := btcobserver.NewObserver( - btcChain, - btcClient, - *chainParams, - zetacoreContext, - zetacoreClient, - tss, - dbpath, - logger, - ts, - ) - if err != nil { - logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) - + logger.Std.Error().Err(err).Msgf("error creating rpc client for bitcoin chain %s", btcChain.String()) } else { - observerMap[btcChain.ChainId] = co + // create BTC chain observer + co, err := btcobserver.NewObserver( + btcChain, + btcClient, + *chainParams, + zetacoreContext, + zetacoreClient, + tss, + dbpath, + logger, + ts, + ) + if err != nil { + logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) + } else { + observerMap[btcChain.ChainId] = co + } } } diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 0cf1766895..b1a8e2b671 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -531,79 +531,6 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { ob.logger.Outbound.Info().Msgf("SaveBroadcastedTx: saved broadcasted txHash %s for outbound %s", txHash, outboundID) } -// GetTxResultByHash gets the transaction result by hash -func GetTxResultByHash( - rpcClient interfaces.BTCRPCClient, - txID string, -) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { - hash, err := chainhash.NewHashFromStr(txID) - if err != nil { - return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error NewHashFromStr: %s", txID) - } - - // The Bitcoin node has to be configured to watch TSS address - txResult, err := rpcClient.GetTransaction(hash) - if err != nil { - return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error GetTransaction %s", hash.String()) - } - return hash, txResult, nil -} - -// GetBlockHeightByHash gets the block height by block hash -func GetBlockHeightByHash( - rpcClient interfaces.BTCRPCClient, - hash string, -) (int64, error) { - // decode the block hash - var blockHash chainhash.Hash - err := chainhash.Decode(&blockHash, hash) - if err != nil { - return 0, errors.Wrapf(err, "GetBlockHeightByHash: error decoding block hash %s", hash) - } - - // get block by hash - block, err := rpcClient.GetBlockVerbose(&blockHash) - if err != nil { - return 0, errors.Wrapf(err, "GetBlockHeightByHash: error GetBlockVerbose %s", hash) - } - return block.Height, nil -} - -// GetRawTxResult gets the raw tx result -func GetRawTxResult( - rpcClient interfaces.BTCRPCClient, - hash *chainhash.Hash, - res *btcjson.GetTransactionResult, -) (btcjson.TxRawResult, error) { - if res.Confirmations == 0 { // for pending tx, we query the raw tx directly - rawResult, err := rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf( - err, - "getRawTxResult: error GetRawTransactionVerbose %s", - res.TxID, - ) - } - return *rawResult, nil - } else if res.Confirmations > 0 { // for confirmed tx, we query the block - blkHash, err := chainhash.NewHashFromStr(res.BlockHash) - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error NewHashFromStr for block hash %s", res.BlockHash) - } - block, err := rpcClient.GetBlockVerboseTx(blkHash) - if err != nil { - return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: error GetBlockVerboseTx %s", res.BlockHash) - } - if res.BlockIndex < 0 || res.BlockIndex >= int64(len(block.Tx)) { - return btcjson.TxRawResult{}, errors.Wrapf(err, "getRawTxResult: invalid outbound with invalid block index, TxID %s, BlockIndex %d", res.TxID, res.BlockIndex) - } - return block.Tx[res.BlockIndex], nil - } - - // res.Confirmations < 0 (meaning not included) - return btcjson.TxRawResult{}, fmt.Errorf("getRawTxResult: tx %s not included yet", hash) -} - // GetBlockByNumberCached gets cached block (and header) by block number func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { if result, ok := ob.BlockCache().Get(blockNumber); ok { diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 6e80e1ea5c..bddbee223a 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -13,6 +13,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" crosschaintypes "github.com/zeta-chain/zetacore/x/crosschain/types" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/compliance" "github.com/zeta-chain/zetacore/zetaclient/context" @@ -163,7 +164,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // Get outbound block height - blockHeight, err := GetBlockHeightByHash(ob.btcClient, res.BlockHash) + blockHeight, err := rpc.GetBlockHeightByHash(ob.btcClient, res.BlockHash) if err != nil { return true, false, errors.Wrapf( err, @@ -346,7 +347,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return "", fmt.Errorf("getOutboundIDByNonce: cannot find outbound txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := GetTxResultByHash(ob.btcClient, txid) + _, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txid) if err != nil { return "", errors.Wrapf( err, @@ -387,7 +388,7 @@ func (ob *Observer) checkIncludedTx( txHash string, ) (*btcjson.GetTransactionResult, bool) { outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) - hash, getTxResult, err := GetTxResultByHash(ob.btcClient, txHash) + hash, getTxResult, err := rpc.GetTxResultByHash(ob.btcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -471,7 +472,7 @@ func (ob *Observer) checkTssOutboundResult( ) error { params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce - rawResult, err := GetRawTxResult(ob.btcClient, hash, res) + rawResult, err := rpc.GetRawTxResult(ob.btcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } diff --git a/zetaclient/chains/bitcoin/rpc/rpc.go b/zetaclient/chains/bitcoin/rpc/rpc.go new file mode 100644 index 0000000000..32144d77f2 --- /dev/null +++ b/zetaclient/chains/bitcoin/rpc/rpc.go @@ -0,0 +1,109 @@ +package rpc + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/pkg/errors" + + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" + "github.com/zeta-chain/zetacore/zetaclient/config" +) + +// NewRPCClient creates a new RPC client by the given config. +func NewRPCClient(btcConfig config.BTCConfig) (*rpcclient.Client, error) { + connCfg := &rpcclient.ConnConfig{ + Host: btcConfig.RPCHost, + User: btcConfig.RPCUsername, + Pass: btcConfig.RPCPassword, + HTTPPostMode: true, + DisableTLS: true, + Params: btcConfig.RPCParams, + } + + rpcClient, err := rpcclient.New(connCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating rpc client: %s", err) + } + + err = rpcClient.Ping() + if err != nil { + return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + } + return rpcClient, nil +} + +// GetTxResultByHash gets the transaction result by hash +func GetTxResultByHash( + rpcClient interfaces.BTCRPCClient, + txID string, +) (*chainhash.Hash, *btcjson.GetTransactionResult, error) { + hash, err := chainhash.NewHashFromStr(txID) + if err != nil { + return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error NewHashFromStr: %s", txID) + } + + // The Bitcoin node has to be configured to watch TSS address + txResult, err := rpcClient.GetTransaction(hash) + if err != nil { + return nil, nil, errors.Wrapf(err, "GetTxResultByHash: error GetTransaction %s", hash.String()) + } + return hash, txResult, nil +} + +// GetBlockHeightByHash gets the block height by block hash +func GetBlockHeightByHash( + rpcClient interfaces.BTCRPCClient, + hash string, +) (int64, error) { + // decode the block hash + var blockHash chainhash.Hash + err := chainhash.Decode(&blockHash, hash) + if err != nil { + return 0, errors.Wrapf(err, "GetBlockHeightByHash: error decoding block hash %s", hash) + } + + // get block by hash + block, err := rpcClient.GetBlockVerbose(&blockHash) + if err != nil { + return 0, errors.Wrapf(err, "GetBlockHeightByHash: error GetBlockVerbose %s", hash) + } + return block.Height, nil +} + +// GetRawTxResult gets the raw tx result +func GetRawTxResult( + rpcClient interfaces.BTCRPCClient, + hash *chainhash.Hash, + res *btcjson.GetTransactionResult, +) (btcjson.TxRawResult, error) { + if res.Confirmations == 0 { // for pending tx, we query the raw tx directly + rawResult, err := rpcClient.GetRawTransactionVerbose(hash) // for pending tx, we query the raw tx + if err != nil { + return btcjson.TxRawResult{}, errors.Wrapf( + err, + "GetRawTxResult: error GetRawTransactionVerbose %s", + res.TxID, + ) + } + return *rawResult, nil + } else if res.Confirmations > 0 { // for confirmed tx, we query the block + blkHash, err := chainhash.NewHashFromStr(res.BlockHash) + if err != nil { + return btcjson.TxRawResult{}, errors.Wrapf(err, "GetRawTxResult: error NewHashFromStr for block hash %s", res.BlockHash) + } + block, err := rpcClient.GetBlockVerboseTx(blkHash) + if err != nil { + return btcjson.TxRawResult{}, errors.Wrapf(err, "GetRawTxResult: error GetBlockVerboseTx %s", res.BlockHash) + } + if res.BlockIndex < 0 || res.BlockIndex >= int64(len(block.Tx)) { + return btcjson.TxRawResult{}, errors.Wrapf(err, "GetRawTxResult: invalid outbound with invalid block index, TxID %s, BlockIndex %d", res.TxID, res.BlockIndex) + } + return block.Tx[res.BlockIndex], nil + } + + // res.Confirmations < 0 (meaning not included) + return btcjson.TxRawResult{}, fmt.Errorf("GetRawTxResult: tx %s not included yet", hash) +} diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go similarity index 95% rename from zetaclient/chains/bitcoin/observer/live_test.go rename to zetaclient/chains/bitcoin/rpc/rpc_live_test.go index c5512a1fae..26355ad837 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go @@ -1,4 +1,4 @@ -package observer_test +package rpc_test import ( "context" @@ -24,6 +24,8 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/rpc" + "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -207,26 +209,11 @@ func (suite *BitcoinObserverTestSuite) Test2() { suite.Require().Equal(0, len(inbounds)) } -func (suite *BitcoinObserverTestSuite) Test3() { - client := suite.rpcClient - res, err := client.EstimateSmartFee(1, &btcjson.EstimateModeConservative) - suite.Require().NoError(err) - suite.T().Logf("fee: %f", *res.FeeRate) - suite.T().Logf("blocks: %d", res.Blocks) - suite.T().Logf("errors: %s", res.Errors) - gasPrice := big.NewFloat(0) - gasPriceU64, _ := gasPrice.Mul(big.NewFloat(*res.FeeRate), big.NewFloat(1e8)).Uint64() - suite.T().Logf("gas price: %d", gasPriceU64) - - bn, err := client.GetBlockCount() - suite.Require().NoError(err) - suite.T().Logf("block number %d", bn) -} - // TestBitcoinObserverLive is a phony test to run each live test individually func TestBitcoinObserverLive(t *testing.T) { // suite.Run(t, new(BitcoinClientTestSuite)) + // LiveTestNewRPCClient(t) // LiveTestGetBlockHeightByHash(t) // LiveTestBitcoinFeeRate(t) // LiveTestAvgFeeRateMainnetMempoolSpace(t) @@ -234,6 +221,25 @@ func TestBitcoinObserverLive(t *testing.T) { // LiveTestGetSenderByVin(t) } +// LiveTestNewRPCClient creates a new Bitcoin RPC client +func LiveTestNewRPCClient(t *testing.T) { + btcConfig := config.BTCConfig{ + RPCUsername: "user", + RPCPassword: "pass", + RPCHost: "bitcoin.rpc.zetachain.com/6315704c-49bc-4649-8b9d-e9278a1dfeb3", + RPCParams: "mainnet", + } + + // create Bitcoin RPC client + client, err := rpc.NewRPCClient(btcConfig) + require.NoError(t, err) + + // get block count + bn, err := client.GetBlockCount() + require.NoError(t, err) + require.Greater(t, bn, int64(0)) +} + // LiveTestGetBlockHeightByHash queries Bitcoin block height by hash func LiveTestGetBlockHeightByHash(t *testing.T) { // setup Bitcoin client @@ -246,11 +252,11 @@ func LiveTestGetBlockHeightByHash(t *testing.T) { invalidHash := "invalidhash" // get block by invalid hash - _, err = observer.GetBlockHeightByHash(client, invalidHash) + _, err = rpc.GetBlockHeightByHash(client, invalidHash) require.ErrorContains(t, err, "error decoding block hash") // get block height by block hash - height, err := observer.GetBlockHeightByHash(client, hash) + height, err := rpc.GetBlockHeightByHash(client, hash) require.NoError(t, err) require.Equal(t, expectedHeight, height) } From 45d1a9ca8c030597fe7ed868268c0d745acabb67 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 21 Jun 2024 10:57:20 -0500 Subject: [PATCH 15/21] moved Mutex to base Observer struct --- cmd/zetaclientd/debug.go | 9 ++--- zetaclient/chains/base/observer.go | 11 +++++++ .../chains/bitcoin/observer/observer.go | 25 ++++++-------- .../chains/bitcoin/observer/outbound.go | 32 +++++++++--------- zetaclient/chains/evm/observer/observer.go | 33 ++++++++----------- .../chains/evm/observer/observer_test.go | 5 ++- zetaclient/chains/evm/signer/signer.go | 3 +- 7 files changed, 57 insertions(+), 61 deletions(-) diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index 4e10b24c25..56535bec1f 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -5,7 +5,6 @@ import ( "fmt" "strconv" "strings" - "sync" "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" @@ -86,9 +85,7 @@ func DebugCmd() *cobra.Command { // get ballot identifier according to the chain type if chains.IsEVMChain(chain.ChainId) { - evmObserver := evmobserver.Observer{ - Mu: &sync.Mutex{}, - } + evmObserver := evmobserver.Observer{} evmObserver.WithZetacoreClient(client) var ethRPC *ethrpc.EthRPC var client *ethclient.Client @@ -164,9 +161,7 @@ func DebugCmd() *cobra.Command { } fmt.Println("CoinType : ", coinType) } else if chains.IsBitcoinChain(chain.ChainId) { - btcObserver := btcobserver.Observer{ - Mu: &sync.Mutex{}, - } + btcObserver := btcobserver.Observer{} btcObserver.WithZetacoreClient(client) btcObserver.WithChain(*chains.GetChainFromChainID(chainID)) connCfg := &rpcclient.ConnConfig{ diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index b2aa983bbd..6bf2ec9e6a 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "sync" "sync/atomic" lru "github.com/hashicorp/golang-lru" @@ -76,6 +77,10 @@ type Observer struct { // logger contains the loggers used by observer logger ObserverLogger + // mu protects fields from concurrent access + // Note: base observer simply provides the mutex. It's the sub-struct's responsibility to use it to be thread-safe + mu *sync.Mutex + // stop is the channel to signal the observer to stop stop chan struct{} } @@ -101,6 +106,7 @@ func NewObserver( lastBlock: 0, lastBlockScanned: 0, ts: ts, + mu: &sync.Mutex{}, stop: make(chan struct{}), } @@ -272,6 +278,11 @@ func (ob *Observer) WithLogger(logger Logger) *Observer { return ob } +// Mu returns the mutex for the observer. +func (ob *Observer) Mu() *sync.Mutex { + return ob.mu +} + // StopChannel returns the stop channel for the observer. func (ob *Observer) StopChannel() chan struct{} { return ob.stop diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index b1a8e2b671..a3d63bae42 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -7,7 +7,6 @@ import ( "math" "math/big" "sort" - "sync" "time" "github.com/btcsuite/btcd/btcjson" @@ -104,9 +103,6 @@ type Observer struct { // logger contains the loggers used by the bitcoin observer logger Logger - - // Mu protects the maps, utxos and chain params from concurrent access - Mu *sync.Mutex } // NewObserver returns a new Bitcoin chain observer @@ -157,7 +153,6 @@ func NewObserver( ObserverLogger: *baseObserver.Logger(), UTXOs: baseObserver.Logger().Chain.With().Str("module", "utxos").Logger(), }, - Mu: &sync.Mutex{}, } // load btc chain observer DB @@ -182,16 +177,16 @@ func (ob *Observer) WithBtcClient(client interfaces.BTCRPCClient) { // SetChainParams sets the chain params for the observer // Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() ob.WithChainParams(params) } // GetChainParams returns the chain params for the observer // Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() return ob.ChainParams() } @@ -283,8 +278,8 @@ func (ob *Observer) WatchRPCStatus() { // GetPendingNonce returns the artificial pending nonce // Note: pending nonce is accessed concurrently func (ob *Observer) GetPendingNonce() uint64 { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() return ob.pendingNonce } @@ -508,19 +503,19 @@ func (ob *Observer) FetchUTXOs() error { utxosFiltered = append(utxosFiltered, utxo) } - ob.Mu.Lock() + ob.Mu().Lock() ob.TelemetryServer().SetNumberOfUTXOs(len(utxosFiltered)) ob.utxos = utxosFiltered - ob.Mu.Unlock() + ob.Mu().Unlock() return nil } // SaveBroadcastedTx saves successfully broadcasted transaction func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { outboundID := ob.GetTxID(nonce) - ob.Mu.Lock() + ob.Mu().Lock() ob.broadcastedTx[outboundID] = txHash - ob.Mu.Unlock() + ob.Mu().Unlock() broadcastEntry := clienttypes.ToOutboundHashSQLType(txHash, outboundID) if err := ob.DB().Save(&broadcastEntry).Error; err != nil { diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index bddbee223a..7ea56a057a 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -120,10 +120,10 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg outboundID := ob.GetTxID(nonce) logger.Info().Msgf("IsOutboundProcessed %s", outboundID) - ob.Mu.Lock() + ob.Mu().Lock() txnHash, broadcasted := ob.broadcastedTx[outboundID] res, included := ob.includedTxResults[outboundID] - ob.Mu.Unlock() + ob.Mu().Unlock() if !included { if !broadcasted { @@ -223,16 +223,16 @@ func (ob *Observer) SelectUTXOs( idx := -1 if nonce == 0 { // for nonce = 0; make exception; no need to include nonce-mark utxo - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() } else { // for nonce > 0; we proceed only when we see the nonce-mark utxo preTxid, err := ob.getOutboundIDByNonce(nonce-1, test) if err != nil { return nil, 0, 0, 0, err } - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() idx, err = ob.findNonceMarkUTXO(nonce-1, preTxid) if err != nil { return nil, 0, 0, 0, err @@ -307,9 +307,9 @@ func (ob *Observer) refreshPendingNonce() { } // increase pending nonce if lagged behind - ob.Mu.Lock() + ob.Mu().Lock() pendingNonce := ob.pendingNonce - ob.Mu.Unlock() + ob.Mu().Unlock() // #nosec G701 always non-negative nonceLow := uint64(p.NonceLow) @@ -321,8 +321,8 @@ func (ob *Observer) refreshPendingNonce() { } // set 'NonceLow' as the new pending nonce - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() ob.pendingNonce = nonceLow ob.logger.Chain.Info(). Msgf("refreshPendingNonce: increase pending nonce to %d with txid %s", ob.pendingNonce, txid) @@ -418,8 +418,8 @@ func (ob *Observer) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransact txHash := getTxResult.TxID outboundID := ob.GetTxID(nonce) - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() res, found := ob.includedTxResults[outboundID] if !found { // not found. @@ -444,15 +444,15 @@ func (ob *Observer) setIncludedTx(nonce uint64, getTxResult *btcjson.GetTransact // getIncludedTx gets the receipt and transaction from memory func (ob *Observer) getIncludedTx(nonce uint64) *btcjson.GetTransactionResult { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() return ob.includedTxResults[ob.GetTxID(nonce)] } // removeIncludedTx removes included tx from memory func (ob *Observer) removeIncludedTx(nonce uint64) { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() txResult, found := ob.includedTxResults[ob.GetTxID(nonce)] if found { delete(ob.includedTxHashes, txResult.TxID) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 158b3e326c..0a8e212e3e 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -6,7 +6,6 @@ import ( "math" "math/big" "strings" - "sync" "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -51,9 +50,6 @@ type Observer struct { // outboundConfirmedTransactions is the map to index confirmed transactions by hash outboundConfirmedTransactions map[string]*ethtypes.Transaction - - // Mu protects the maps and chain params from concurrent access - Mu *sync.Mutex } // NewObserver returns a new EVM chain observer @@ -92,7 +88,6 @@ func NewObserver( outboundPendingTransactions: make(map[string]*ethtypes.Transaction), outboundConfirmedReceipts: make(map[string]*ethtypes.Receipt), outboundConfirmedTransactions: make(map[string]*ethtypes.Transaction), - Mu: &sync.Mutex{}, } // open database and load data @@ -117,16 +112,16 @@ func (ob *Observer) WithEvmJSONRPC(client interfaces.EVMJSONRPCClient) { // SetChainParams sets the chain params for the observer // Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() ob.WithChainParams(params) } // GetChainParams returns the chain params for the observer // Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() return ob.ChainParams() } @@ -230,22 +225,22 @@ func (ob *Observer) WatchRPCStatus() { // SetPendingTx sets the pending transaction in memory func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() ob.outboundPendingTransactions[ob.GetTxID(nonce)] = transaction } // GetPendingTx gets the pending transaction from memory func (ob *Observer) GetPendingTx(nonce uint64) *ethtypes.Transaction { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() return ob.outboundPendingTransactions[ob.GetTxID(nonce)] } // SetTxNReceipt sets the receipt and transaction in memory func (ob *Observer) SetTxNReceipt(nonce uint64, receipt *ethtypes.Receipt, transaction *ethtypes.Transaction) { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() delete(ob.outboundPendingTransactions, ob.GetTxID(nonce)) // remove pending transaction, if any ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] = receipt ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] = transaction @@ -253,8 +248,8 @@ func (ob *Observer) SetTxNReceipt(nonce uint64, receipt *ethtypes.Receipt, trans // GetTxNReceipt gets the receipt and transaction from memory func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Transaction) { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() receipt := ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] transaction := ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] return receipt, transaction @@ -262,8 +257,8 @@ func (ob *Observer) GetTxNReceipt(nonce uint64) (*ethtypes.Receipt, *ethtypes.Tr // IsTxConfirmed returns true if there is a confirmed tx for 'nonce' func (ob *Observer) IsTxConfirmed(nonce uint64) bool { - ob.Mu.Lock() - defer ob.Mu.Unlock() + ob.Mu().Lock() + defer ob.Mu().Unlock() return ob.outboundConfirmedReceipts[ob.GetTxID(nonce)] != nil && ob.outboundConfirmedTransactions[ob.GetTxID(nonce)] != nil } diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 9ef7e47e1b..2ad34132c1 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -3,7 +3,6 @@ package observer_test import ( "fmt" "os" - "sync" "testing" "cosmossdk.io/math" @@ -298,7 +297,7 @@ func Test_BlockCache(t *testing.T) { // create client blockCache, err := lru.New(1000) require.NoError(t, err) - ob := &observer.Observer{Mu: &sync.Mutex{}} + ob := &observer.Observer{} ob.WithBlockCache(blockCache) // delete non-existing block should not panic @@ -337,7 +336,7 @@ func Test_CheckTxInclusion(t *testing.T) { // create client blockCache, err := lru.New(1000) require.NoError(t, err) - ob := &observer.Observer{Mu: &sync.Mutex{}} + ob := &observer.Observer{} // save block to cache blockCache.Add(blockNumber, block) diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index d503be8f38..26921ad412 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -532,7 +532,8 @@ func (signer *Signer) BroadcastOutbound( logger zerolog.Logger, myID sdk.AccAddress, zetacoreClient interfaces.ZetacoreClient, - txData *OutboundData) { + txData *OutboundData, +) { // Get destination chain for logging toChain := chains.GetChainFromChainID(txData.toChainID.Int64()) if tx == nil { From 642e54e5838a5c18a449342cdb6788a546d6a5d0 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 21 Jun 2024 14:35:34 -0500 Subject: [PATCH 16/21] type check on cached block, header --- cmd/zetaclientd/utils.go | 8 +- testutil/sample/crypto.go | 6 + zetaclient/chains/base/observer_test.go | 21 ++- .../chains/bitcoin/observer/observer.go | 6 +- .../chains/bitcoin/observer/observer_test.go | 64 ++++++++- zetaclient/chains/evm/observer/observer.go | 14 +- .../chains/evm/observer/observer_test.go | 132 ++++++++++++++---- zetaclient/chains/evm/signer/signer_test.go | 2 +- zetaclient/testutils/mocks/btc_rpc.go | 39 +++++- zetaclient/testutils/mocks/evm_rpc.go | 8 +- zetaclient/testutils/os.go | 15 -- 11 files changed, 241 insertions(+), 74 deletions(-) delete mode 100644 zetaclient/testutils/os.go diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 647e2520e1..99dd26c487 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -141,7 +141,7 @@ func CreateChainObserverMap( } // create EVM chain observer - co, err := evmobserver.NewObserver( + observer, err := evmobserver.NewObserver( evmConfig, evmClient, *chainParams, @@ -156,7 +156,7 @@ func CreateChainObserverMap( logger.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) continue } - observerMap[evmConfig.Chain.ChainId] = co + observerMap[evmConfig.Chain.ChainId] = observer } // BTC observer @@ -173,7 +173,7 @@ func CreateChainObserverMap( logger.Std.Error().Err(err).Msgf("error creating rpc client for bitcoin chain %s", btcChain.String()) } else { // create BTC chain observer - co, err := btcobserver.NewObserver( + observer, err := btcobserver.NewObserver( btcChain, btcClient, *chainParams, @@ -187,7 +187,7 @@ func CreateChainObserverMap( if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) } else { - observerMap[btcChain.ChainId] = co + observerMap[btcChain.ChainId] = observer } } } diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index 153f1a2a80..a5b62d7154 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -7,6 +7,7 @@ import ( "strconv" "testing" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/cometbft/cometbft/crypto/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" @@ -60,6 +61,11 @@ func Hash() ethcommon.Hash { return EthAddress().Hash() } +// BtcHash returns a sample btc hash +func BtcHash() chainhash.Hash { + return chainhash.Hash(Hash()) +} + // PubKey returns a sample account PubKey func PubKey(r *rand.Rand) cryptotypes.PubKey { seed := []byte(strconv.Itoa(r.Int())) diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 870f88715e..5ba2044505 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -16,7 +16,6 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" - "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -212,7 +211,7 @@ func TestObserverGetterAndSetter(t *testing.T) { }) t.Run("should be able to get database", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) ob.OpenDB(dbPath, "") @@ -242,7 +241,7 @@ func TestObserverGetterAndSetter(t *testing.T) { } func TestOpenCloseDB(t *testing.T) { - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) t.Run("should be able to open/close db", func(t *testing.T) { @@ -275,7 +274,7 @@ func TestLoadLastBlockScanned(t *testing.T) { t.Run("should be able to load last block scanned", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) @@ -290,7 +289,7 @@ func TestLoadLastBlockScanned(t *testing.T) { }) t.Run("latest block scanned should be 0 if not found in db", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) @@ -302,7 +301,7 @@ func TestLoadLastBlockScanned(t *testing.T) { }) t.Run("should overwrite last block scanned if env var is set", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) @@ -320,7 +319,7 @@ func TestLoadLastBlockScanned(t *testing.T) { }) t.Run("last block scanned should remain 0 if env var is set to latest", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) @@ -338,7 +337,7 @@ func TestLoadLastBlockScanned(t *testing.T) { }) t.Run("should return error on invalid env var", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) @@ -355,7 +354,7 @@ func TestLoadLastBlockScanned(t *testing.T) { func TestSaveLastBlockScanned(t *testing.T) { t.Run("should be able to save last block scanned", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) @@ -377,7 +376,7 @@ func TestSaveLastBlockScanned(t *testing.T) { func TestReadWriteLastBlockScannedToDB(t *testing.T) { t.Run("should be able to write and read last block scanned to db", func(t *testing.T) { // create observer and open db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) @@ -392,7 +391,7 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { }) t.Run("should return error when last block scanned not found in db", func(t *testing.T) { // create empty db - dbPath := testutils.CreateTempDir(t) + dbPath := sample.CreateTempDir(t) ob := createObserver(t) err := ob.OpenDB(dbPath, "") require.NoError(t, err) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index a3d63bae42..5d6e308dfe 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -529,8 +529,12 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { // GetBlockByNumberCached gets cached block (and header) by block number func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { if result, ok := ob.BlockCache().Get(blockNumber); ok { - return result.(*BTCBlockNHeader), nil + if block, ok := result.(*BTCBlockNHeader); ok { + return block, nil + } + return nil, errors.New("cached value is not of type *BTCBlockNHeader") } + // Get the block hash hash, err := ob.btcClient.GetBlockHash(blockNumber) if err != nil { diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index eeead6d8ae..ed9802567c 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -8,18 +8,20 @@ import ( "testing" "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/wire" + lru "github.com/hashicorp/golang-lru" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" - "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) @@ -127,7 +129,7 @@ func Test_NewObserver(t *testing.T) { coreContext: nil, coreClient: nil, tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), + dbpath: sample.CreateTempDir(t), logger: base.Logger{}, ts: nil, fail: false, @@ -140,7 +142,7 @@ func Test_NewObserver(t *testing.T) { coreContext: nil, coreClient: nil, tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), + dbpath: sample.CreateTempDir(t), logger: base.Logger{}, ts: nil, fail: true, @@ -190,6 +192,56 @@ func Test_NewObserver(t *testing.T) { } } +func Test_BlockCache(t *testing.T) { + t.Run("should add and get block from cache", func(t *testing.T) { + // create observer + ob := &observer.Observer{} + blockCache, err := lru.New(100) + require.NoError(t, err) + ob.WithBlockCache(blockCache) + + // create mock btc client + btcClient := mocks.NewMockBTCRPCClient() + ob.WithBtcClient(btcClient) + + // feed block hash, header and block to btc client + hash := sample.BtcHash() + header := &wire.BlockHeader{Version: 1} + block := &btcjson.GetBlockVerboseTxResult{Version: 1} + btcClient.WithBlockHash(&hash) + btcClient.WithBlockHeader(header) + btcClient.WithBlockVerboseTx(block) + + // get block and header from observer, fallback to btc client + result, err := ob.GetBlockByNumberCached(100) + require.NoError(t, err) + require.EqualValues(t, header, result.Header) + require.EqualValues(t, block, result.Block) + + // get block header from cache + result, err = ob.GetBlockByNumberCached(100) + require.NoError(t, err) + require.EqualValues(t, header, result.Header) + require.EqualValues(t, block, result.Block) + }) + t.Run("should fail if stored type is not BlockNHeader", func(t *testing.T) { + // create observer + ob := &observer.Observer{} + blockCache, err := lru.New(100) + require.NoError(t, err) + ob.WithBlockCache(blockCache) + + // add a string to cache + blockNumber := int64(100) + blockCache.Add(blockNumber, "a string value") + + // get result from cache + result, err := ob.GetBlockByNumberCached(blockNumber) + require.ErrorContains(t, err, "cached value is not of type *BTCBlockNHeader") + require.Nil(t, result) + }) +} + func Test_LoadDB(t *testing.T) { // use Bitcoin mainnet chain for testing chain := chains.BitcoinMainnet @@ -200,7 +252,7 @@ func Test_LoadDB(t *testing.T) { tss := mocks.NewTSSMainnet() // create observer - dbpath := testutils.CreateTempDir(t) + dbpath := sample.CreateTempDir(t) ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, dbpath, base.Logger{}, nil) require.NoError(t, err) @@ -237,7 +289,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { // create observer using mock btc client btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(200) - dbpath := testutils.CreateTempDir(t) + dbpath := sample.CreateTempDir(t) t.Run("should load last block scanned", func(t *testing.T) { // create observer and write 199 as last block scanned @@ -264,7 +316,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { }) t.Run("should fail on RPC error", func(t *testing.T) { // create observer on separate path, as we need to reset last block scanned - otherPath := testutils.CreateTempDir(t) + otherPath := sample.CreateTempDir(t) obOther := MockBTCObserver(t, chain, params, btcClient, otherPath) // reset last block scanned to 0 so that it will be loaded from RPC diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 0a8e212e3e..f7ac03b5af 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -367,8 +367,11 @@ func (ob *Observer) TransactionByHash(txHash string) (*ethrpc.Transaction, bool, } func (ob *Observer) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, error) { - if header, ok := ob.HeaderCache().Get(blockNumber); ok { - return header.(*ethtypes.Header), nil + if result, ok := ob.HeaderCache().Get(blockNumber); ok { + if header, ok := result.(*ethtypes.Header); ok { + return header, nil + } + return nil, errors.New("cached value is not of type *ethtypes.Header") } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) if err != nil { @@ -381,8 +384,11 @@ func (ob *Observer) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, // GetBlockByNumberCached get block by number from cache // returns block, ethrpc.Block, isFallback, isSkip, error func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, error) { - if block, ok := ob.BlockCache().Get(blockNumber); ok { - return block.(*ethrpc.Block), nil + if result, ok := ob.BlockCache().Get(blockNumber); ok { + if block, ok := result.(*ethrpc.Block); ok { + return block, nil + } + return nil, errors.New("cached value is not of type *ethrpc.Block") } if blockNumber > math.MaxInt32 { return nil, fmt.Errorf("block number %d is too large", blockNumber) diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 2ad34132c1..f149d1bae2 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -2,10 +2,12 @@ package observer_test import ( "fmt" + "math/big" "os" "testing" "cosmossdk.io/math" + ethtypes "github.com/ethereum/go-ethereum/core/types" lru "github.com/hashicorp/golang-lru" "github.com/onrik/ethrpc" "github.com/rs/zerolog" @@ -133,7 +135,7 @@ func Test_NewObserver(t *testing.T) { chainParams: params, evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), + dbpath: sample.CreateTempDir(t), logger: base.Logger{}, ts: nil, fail: false, @@ -162,7 +164,7 @@ func Test_NewObserver(t *testing.T) { chainParams: params, evmClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), tss: mocks.NewTSSMainnet(), - dbpath: testutils.CreateTempDir(t), + dbpath: sample.CreateTempDir(t), logger: base.Logger{}, ts: nil, fail: true, @@ -206,7 +208,7 @@ func Test_LoadDB(t *testing.T) { // use Ethereum chain for testing chain := chains.Ethereum params := mocks.MockChainParams(chain.ChainId, 10) - dbpath := testutils.CreateTempDir(t) + dbpath := sample.CreateTempDir(t) ob := MockEVMObserver(t, chain, nil, nil, nil, nil, dbpath, 1, params) t.Run("should load db successfully", func(t *testing.T) { @@ -254,7 +256,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { // create observer using mock evm client evmClient := mocks.NewMockEvmClient().WithBlockNumber(100) - dbpath := testutils.CreateTempDir(t) + dbpath := sample.CreateTempDir(t) ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, dbpath, 1, params) t.Run("should load last block scanned", func(t *testing.T) { @@ -278,7 +280,7 @@ func Test_LoadLastBlockScanned(t *testing.T) { }) t.Run("should fail on RPC error", func(t *testing.T) { // create observer on separate path, as we need to reset last block scanned - otherPath := testutils.CreateTempDir(t) + otherPath := sample.CreateTempDir(t) obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, otherPath, 1, params) // reset last block scanned to 0 so that it will be loaded from RPC @@ -294,30 +296,110 @@ func Test_LoadLastBlockScanned(t *testing.T) { } func Test_BlockCache(t *testing.T) { - // create client - blockCache, err := lru.New(1000) - require.NoError(t, err) - ob := &observer.Observer{} - ob.WithBlockCache(blockCache) + t.Run("should get block from cache", func(t *testing.T) { + // create observer + ob := &observer.Observer{} + blockCache, err := lru.New(100) + require.NoError(t, err) + ob.WithBlockCache(blockCache) - // delete non-existing block should not panic - blockNumber := uint64(10388180) - ob.RemoveCachedBlock(blockNumber) + // create mock evm client + JSONRPC := mocks.NewMockJSONRPCClient() + ob.WithEvmJSONRPC(JSONRPC) - // add a block - block := ðrpc.Block{ - // #nosec G701 always in range - Number: int(blockNumber), - } - blockCache.Add(blockNumber, block) - ob.WithBlockCache(blockCache) + // feed block to JSON rpc client + block := ðrpc.Block{Number: 100} + JSONRPC.WithBlock(block) - // block should be in cache - _, err = ob.GetBlockByNumberCached(blockNumber) - require.NoError(t, err) + // get block header from observer, fallback to JSON RPC + result, err := ob.GetBlockByNumberCached(uint64(100)) + require.NoError(t, err) + require.EqualValues(t, block, result) + + // get block header from cache + result, err = ob.GetBlockByNumberCached(uint64(100)) + require.NoError(t, err) + require.EqualValues(t, block, result) + }) + t.Run("should fail if stored type is not block", func(t *testing.T) { + // create observer + ob := &observer.Observer{} + blockCache, err := lru.New(100) + require.NoError(t, err) + ob.WithBlockCache(blockCache) + + // add a string to cache + blockNumber := uint64(100) + blockCache.Add(blockNumber, "a string value") + + // get result header from cache + result, err := ob.GetBlockByNumberCached(blockNumber) + require.ErrorContains(t, err, "cached value is not of type *ethrpc.Block") + require.Nil(t, result) + }) + t.Run("should be able to remove block from cache", func(t *testing.T) { + // create observer + ob := &observer.Observer{} + blockCache, err := lru.New(100) + require.NoError(t, err) + ob.WithBlockCache(blockCache) + + // delete non-existing block should not panic + blockNumber := uint64(123) + ob.RemoveCachedBlock(blockNumber) + + // add a block + block := ðrpc.Block{Number: 123} + blockCache.Add(blockNumber, block) + ob.WithBlockCache(blockCache) - // delete the block should not panic - ob.RemoveCachedBlock(blockNumber) + // block should be in cache + result, err := ob.GetBlockByNumberCached(blockNumber) + require.NoError(t, err) + require.EqualValues(t, block, result) + + // delete the block should not panic + ob.RemoveCachedBlock(blockNumber) + }) +} + +func Test_HeaderCache(t *testing.T) { + t.Run("should get block header from cache", func(t *testing.T) { + // create observer + ob := &observer.Observer{} + headerCache, err := lru.New(100) + require.NoError(t, err) + ob.WithHeaderCache(headerCache) + + // create mock evm client + evmClient := mocks.NewMockEvmClient() + ob.WithEvmClient(evmClient) + + // feed block header to evm client + header := ðtypes.Header{Number: big.NewInt(100)} + evmClient.WithHeader(header) + + // get block header from observer + resHeader, err := ob.GetBlockHeaderCached(uint64(100)) + require.NoError(t, err) + require.EqualValues(t, header, resHeader) + }) + t.Run("should fail if stored type is not block header", func(t *testing.T) { + // create observer + ob := &observer.Observer{} + headerCache, err := lru.New(100) + require.NoError(t, err) + ob.WithHeaderCache(headerCache) + + // add a string to cache + blockNumber := uint64(100) + headerCache.Add(blockNumber, "a string value") + + // get block header from cache + header, err := ob.GetBlockHeaderCached(blockNumber) + require.ErrorContains(t, err, "cached value is not of type *ethtypes.Header") + require.Nil(t, header) + }) } func Test_CheckTxInclusion(t *testing.T) { diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index bdce93f60c..ea27152b97 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -72,7 +72,7 @@ func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.O params := mocks.MockChainParams(evmcfg.Chain.ChainId, 10) cfg.EVMChainConfigs[chains.BscMainnet.ChainId] = evmcfg coreCTX := context.NewZetacoreContext(cfg) - dbpath := testutils.CreateTempDir(t) + dbpath := sample.CreateTempDir(t) logger := base.Logger{} ts := &metrics.TelemetryServer{} diff --git a/zetaclient/testutils/mocks/btc_rpc.go b/zetaclient/testutils/mocks/btc_rpc.go index 01d286d31a..6809de664a 100644 --- a/zetaclient/testutils/mocks/btc_rpc.go +++ b/zetaclient/testutils/mocks/btc_rpc.go @@ -17,9 +17,12 @@ var _ interfaces.BTCRPCClient = &MockBTCRPCClient{} // MockBTCRPCClient is a mock implementation of the BTCRPCClient interface type MockBTCRPCClient struct { - err error - blockCount int64 - Txs []*btcutil.Tx + err error + blockCount int64 + blockHash *chainhash.Hash + blockHeader *wire.BlockHeader + blockVerboseTx *btcjson.GetBlockVerboseTxResult + Txs []*btcutil.Tx } // NewMockBTCRPCClient creates a new mock BTC RPC client @@ -108,7 +111,10 @@ func (c *MockBTCRPCClient) GetBlockCount() (int64, error) { } func (c *MockBTCRPCClient) GetBlockHash(_ int64) (*chainhash.Hash, error) { - return nil, errors.New("not implemented") + if c.err != nil { + return nil, c.err + } + return c.blockHash, nil } func (c *MockBTCRPCClient) GetBlockVerbose(_ *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { @@ -116,11 +122,17 @@ func (c *MockBTCRPCClient) GetBlockVerbose(_ *chainhash.Hash) (*btcjson.GetBlock } func (c *MockBTCRPCClient) GetBlockVerboseTx(_ *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { - return nil, errors.New("not implemented") + if c.err != nil { + return nil, c.err + } + return c.blockVerboseTx, nil } func (c *MockBTCRPCClient) GetBlockHeader(_ *chainhash.Hash) (*wire.BlockHeader, error) { - return nil, errors.New("not implemented") + if c.err != nil { + return nil, c.err + } + return c.blockHeader, nil } // ---------------------------------------------------------------------------- @@ -137,6 +149,21 @@ func (c *MockBTCRPCClient) WithBlockCount(blkCnt int64) *MockBTCRPCClient { return c } +func (c *MockBTCRPCClient) WithBlockHash(hash *chainhash.Hash) *MockBTCRPCClient { + c.blockHash = hash + return c +} + +func (c *MockBTCRPCClient) WithBlockHeader(header *wire.BlockHeader) *MockBTCRPCClient { + c.blockHeader = header + return c +} + +func (c *MockBTCRPCClient) WithBlockVerboseTx(block *btcjson.GetBlockVerboseTxResult) *MockBTCRPCClient { + c.blockVerboseTx = block + return c +} + func (c *MockBTCRPCClient) WithRawTransaction(tx *btcutil.Tx) *MockBTCRPCClient { c.Txs = append(c.Txs, tx) return c diff --git a/zetaclient/testutils/mocks/evm_rpc.go b/zetaclient/testutils/mocks/evm_rpc.go index fa40357592..e6daff0f13 100644 --- a/zetaclient/testutils/mocks/evm_rpc.go +++ b/zetaclient/testutils/mocks/evm_rpc.go @@ -33,6 +33,7 @@ var _ interfaces.EVMRPCClient = &MockEvmClient{} type MockEvmClient struct { err error blockNumber uint64 + header *ethtypes.Header Receipts []*ethtypes.Receipt } @@ -70,7 +71,7 @@ func (e *MockEvmClient) HeaderByNumber(_ context.Context, _ *big.Int) (*ethtypes if e.err != nil { return nil, e.err } - return ðtypes.Header{}, nil + return e.header, nil } func (e *MockEvmClient) PendingCodeAt(_ context.Context, _ ethcommon.Address) ([]byte, error) { @@ -189,6 +190,11 @@ func (e *MockEvmClient) WithBlockNumber(blockNumber uint64) *MockEvmClient { return e } +func (e *MockEvmClient) WithHeader(header *ethtypes.Header) *MockEvmClient { + e.header = header + return e +} + func (e *MockEvmClient) WithReceipt(receipt *ethtypes.Receipt) *MockEvmClient { e.Receipts = append(e.Receipts, receipt) return e diff --git a/zetaclient/testutils/os.go b/zetaclient/testutils/os.go deleted file mode 100644 index 714b650bb5..0000000000 --- a/zetaclient/testutils/os.go +++ /dev/null @@ -1,15 +0,0 @@ -package testutils - -import ( - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -// create a temporary directory for testing -func CreateTempDir(t *testing.T) string { - tempPath, err := os.MkdirTemp("", "tempdir-") - require.NoError(t, err) - return tempPath -} From 0ee1e6046c00eda7be9b8607f035463450b0ecd9 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Fri, 21 Jun 2024 15:58:09 -0500 Subject: [PATCH 17/21] update changelog PR number and added unit test for Stop() method --- changelog.md | 2 +- zetaclient/chains/base/observer.go | 8 +++++--- zetaclient/chains/base/observer_test.go | 11 +++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 4c8998411a..c601f74028 100644 --- a/changelog.md +++ b/changelog.md @@ -51,7 +51,7 @@ * [2340](https://github.com/zeta-chain/node/pull/2340) - add ValidateInbound method for cctx orchestrator * [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs * [2357](https://github.com/zeta-chain/node/pull/2357) - integrate base Signer structure into EVM/Bitcoin Signer -* [2359](https://github.com/zeta-chain/node/pull/2357) - integrate base Observer structure into EVM/Bitcoin Observer +* [2359](https://github.com/zeta-chain/node/pull/2359) - integrate base Observer structure into EVM/Bitcoin Observer ### Tests diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 6bf2ec9e6a..2716637656 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -135,9 +135,11 @@ func (ob *Observer) Stop() { close(ob.stop) // close database - err := ob.CloseDB() - if err != nil { - ob.Logger().Chain.Error().Err(err).Msgf("CloseDB failed for chain %d", ob.Chain().ChainId) + if ob.db != nil { + err := ob.CloseDB() + if err != nil { + ob.Logger().Chain.Error().Err(err).Msgf("CloseDB failed for chain %d", ob.Chain().ChainId) + } } ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 5ba2044505..065283d25d 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -132,6 +132,17 @@ func TestNewObserver(t *testing.T) { } } +func TestStop(t *testing.T) { + t.Run("should be able to stop observer", func(t *testing.T) { + // create observer and initialize db + ob := createObserver(t) + ob.OpenDB(sample.CreateTempDir(t), "") + + // stop observer + ob.Stop() + }) +} + func TestObserverGetterAndSetter(t *testing.T) { t.Run("should be able to update chain", func(t *testing.T) { ob := createObserver(t) From 0652bf43f5d1021dc969171ee301d42702f1ac08 Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 24 Jun 2024 09:11:38 -0500 Subject: [PATCH 18/21] replace magic numbers with constants --- zetaclient/chains/bitcoin/signer/signer.go | 19 ++++++++++++------- zetaclient/chains/evm/signer/signer.go | 12 ++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index d39b8a8f9d..63c5aa75d9 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "fmt" "math/big" - "math/rand" "time" "github.com/btcsuite/btcd/btcec" @@ -37,6 +36,12 @@ const ( // the rank below (or equal to) which we consolidate UTXOs consolidationRank = 10 + + // broadcastBackoff is the initial backoff duration for retrying broadcast + broadcastBackoff = 1000 * time.Millisecond + + // broadcastRetries is the maximum number of retries for broadcasting a transaction + broadcastRetries = 5 ) var _ interfaces.ChainSigner = &Signer{} @@ -422,17 +427,17 @@ func (signer *Signer) TryProcessOutbound( outboundHash := tx.TxHash().String() logger.Info(). Msgf("on chain %s nonce %d, outboundHash %s signer %s", chain.ChainName, outboundTssNonce, outboundHash, signerAddress) - // TODO: pick a few broadcasters. - //if len(signers) == 0 || myid == signers[send.OutboundParams.Broadcaster] || myid == signers[int(send.OutboundParams.Broadcaster+1)%len(signers)] { - // retry loop: 1s, 2s, 4s, 8s, 16s in case of RPC error - for i := 0; i < 5; i++ { - // #nosec G404 randomness is not a security issue here - time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond) //random delay to avoid sychronized broadcast + + // try broacasting tx with increasing backoff (1s, 2s, 4s, 8s, 16s) in case of RPC error + backOff := broadcastBackoff + for i := 0; i < broadcastRetries; i++ { + time.Sleep(backOff) err := signer.Broadcast(tx) if err != nil { logger.Warn(). Err(err). Msgf("broadcasting tx %s to chain %s: nonce %d, retry %d", outboundHash, chain.ChainName, outboundTssNonce, i) + backOff *= 2 continue } logger.Info(). diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index 26921ad412..a1e0c9e4c3 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -38,6 +38,14 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) +const ( + // broadcastBackoff is the initial backoff duration for retrying broadcast + broadcastBackoff = 1000 * time.Millisecond + + // broadcastRetries is the maximum number of retries for broadcasting a transaction + broadcastRetries = 5 +) + var ( _ interfaces.ChainSigner = &Signer{} @@ -545,8 +553,8 @@ func (signer *Signer) BroadcastOutbound( outboundHash := tx.Hash().Hex() // try broacasting tx with increasing backoff (1s, 2s, 4s, 8s, 16s) in case of RPC error - backOff := 1000 * time.Millisecond - for i := 0; i < 5; i++ { + backOff := broadcastBackoff + for i := 0; i < broadcastRetries; i++ { time.Sleep(backOff) err := signer.Broadcast(tx) if err != nil { From f5914d8fc2db7d4d14c80e0c22f1c841922bfa0c Mon Sep 17 00:00:00 2001 From: Charlie Chen Date: Mon, 24 Jun 2024 09:14:18 -0500 Subject: [PATCH 19/21] updated method name in logging --- zetaclient/chains/bitcoin/observer/outbound.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 7ea56a057a..b6712a8ba6 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -36,7 +36,7 @@ func (ob *Observer) WatchOutbound() { defer ticker.Stop() chainID := ob.Chain().ChainId - ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", chainID) + ob.logger.Outbound.Info().Msgf("WatchOutbound started for chain %d", chainID) sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) for { From a96898d6474136ee60d3e437477d0119088c424e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 25 Jun 2024 17:02:10 +0200 Subject: [PATCH 20/21] Move sqlite-mem db to testutils --- zetaclient/chains/base/observer.go | 8 +++----- zetaclient/chains/base/observer_test.go | 3 ++- zetaclient/chains/bitcoin/observer/observer_test.go | 3 ++- zetaclient/chains/bitcoin/observer/outbound_test.go | 2 +- zetaclient/chains/bitcoin/rpc/rpc_live_test.go | 2 +- zetaclient/chains/evm/observer/outbound_test.go | 5 +---- zetaclient/testutils/constant.go | 3 +++ 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 2716637656..1f3fc2d0ca 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "strings" "sync" "sync/atomic" @@ -33,9 +34,6 @@ const ( // DefaultHeaderCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) // Cached headers can be used to get header information DefaultHeaderCacheSize = 1000 - - // TempSQLiteDBPath is the temporary in-memory SQLite database used for testing - TempSQLiteDBPath = "file::memory:?cache=shared" ) // Observer is the base structure for chain observers, grouping the common logic for each chain observer client. @@ -307,8 +305,8 @@ func (ob *Observer) OpenDB(dbPath string, dbName string) error { path := fmt.Sprintf("%s/%s", dbPath, dbName) // use memory db if specified - if dbPath == TempSQLiteDBPath { - path = TempSQLiteDBPath + if strings.Contains(dbPath, ":memory:") { + path = dbPath } // open db diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 065283d25d..a04a48fcc3 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -7,6 +7,7 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/testutil/sample" @@ -266,7 +267,7 @@ func TestOpenCloseDB(t *testing.T) { }) t.Run("should use memory db if specified", func(t *testing.T) { // open db with memory - err := ob.OpenDB(base.TempSQLiteDBPath, "") + err := ob.OpenDB(testutils.SQLiteMemory, "") require.NoError(t, err) // close db diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index ed9802567c..c79209e3fc 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/wire" lru "github.com/hashicorp/golang-lru" "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "gorm.io/driver/sqlite" "gorm.io/gorm" @@ -35,7 +36,7 @@ var ( func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransactionResult) { submittedTx := map[string]btcjson.GetTransactionResult{} - db, err := gorm.Open(sqlite.Open(base.TempSQLiteDBPath), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(testutils.SQLiteMemory), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}) diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index 910384ef9a..0aaeb7b600 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -27,7 +27,7 @@ func MockBTCObserverMainnet(t *testing.T) *Observer { tss := mocks.NewTSSMainnet() // create Bitcoin observer - ob, err := NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, base.Logger{}, nil) + ob, err := NewObserver(chain, btcClient, params, nil, nil, tss, testutils.SQLiteMemory, base.Logger{}, nil) require.NoError(t, err) return ob diff --git a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go index 26355ad837..97a373f94d 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc_live_test.go +++ b/zetaclient/chains/bitcoin/rpc/rpc_live_test.go @@ -55,7 +55,7 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { btcClient := mocks.NewMockBTCRPCClient() // create observer - ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, testutils.SQLiteMemory, base.DefaultLogger(), nil) suite.Require().NoError(err) suite.Require().NotNil(ob) diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index df9a642a8e..72023d8f57 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -13,16 +13,13 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" - "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -const ( - memDBPath = base.TempSQLiteDBPath -) +const memDBPath = testutils.SQLiteMemory // getContractsByChainID is a helper func to get contracts and addresses by chainID func getContractsByChainID( diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index ad8302577d..3d4f6e2a03 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -27,6 +27,9 @@ const ( EventZetaReverted = "ZetaReverted" EventERC20Deposit = "Deposited" EventERC20Withdraw = "Withdrawn" + + // SQLiteMemory is a SQLite in-memory database connection string. + SQLiteMemory = "file::memory:?cache=shared" ) // ConnectorAddresses contains constants ERC20 connector addresses for testing From ab0c960e115e1fa5f29b93d7bc2e3e80bc7e1429 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 25 Jun 2024 17:26:06 +0200 Subject: [PATCH 21/21] Add base signer's Lock & Unlock --- zetaclient/chains/base/signer.go | 14 +++++---- zetaclient/chains/base/signer_test.go | 4 --- zetaclient/chains/bitcoin/signer/signer.go | 5 ++-- zetaclient/chains/evm/signer/signer.go | 34 +++++++++------------- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/zetaclient/chains/base/signer.go b/zetaclient/chains/base/signer.go index a33f116e48..bc5ad7934f 100644 --- a/zetaclient/chains/base/signer.go +++ b/zetaclient/chains/base/signer.go @@ -29,7 +29,7 @@ type Signer struct { // mu protects fields from concurrent access // Note: base signer simply provides the mutex. It's the sub-struct's responsibility to use it to be thread-safe - mu *sync.Mutex + mu sync.Mutex } // NewSigner creates a new base signer @@ -49,7 +49,6 @@ func NewSigner( Std: logger.Std.With().Int64("chain", chain.ChainId).Str("module", "signer").Logger(), Compliance: logger.Compliance, }, - mu: &sync.Mutex{}, } } @@ -102,7 +101,12 @@ func (s *Signer) Logger() *Logger { return &s.logger } -// Mu returns the mutex for the signer -func (s *Signer) Mu() *sync.Mutex { - return s.mu +// Lock locks the signer +func (s *Signer) Lock() { + s.mu.Lock() +} + +// Unlock unlocks the signer +func (s *Signer) Unlock() { + s.mu.Unlock() } diff --git a/zetaclient/chains/base/signer_test.go b/zetaclient/chains/base/signer_test.go index 3de1d18d4a..960c508d6e 100644 --- a/zetaclient/chains/base/signer_test.go +++ b/zetaclient/chains/base/signer_test.go @@ -71,8 +71,4 @@ func TestSignerGetterAndSetter(t *testing.T) { logger.Std.Info().Msg("print standard log") logger.Compliance.Info().Msg("print compliance log") }) - t.Run("should be able to get mutex", func(t *testing.T) { - signer := createSigner(t) - require.NotNil(t, signer.Mu()) - }) } diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index 63c5aa75d9..6ad9bfc2a6 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -48,8 +48,7 @@ var _ interfaces.ChainSigner = &Signer{} // Signer deals with signing BTC transactions and implements the ChainSigner interface type Signer struct { - // base.Signer implements the base chain signer - base.Signer + *base.Signer // client is the RPC client to interact with the Bitcoin chain client interfaces.BTCRPCClient @@ -81,7 +80,7 @@ func NewSigner( } return &Signer{ - Signer: *baseSigner, + Signer: baseSigner, client: client, }, nil } diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index a1e0c9e4c3..d50b214b03 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -7,7 +7,6 @@ import ( "math/big" "strconv" "strings" - "sync" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -55,8 +54,7 @@ var ( // Signer deals with the signing EVM transactions and implements the ChainSigner interface type Signer struct { - // base.Signer implements the base chain signer - base.Signer + *base.Signer // client is the EVM RPC client to interact with the EVM chain client interfaces.EVMRPCClient @@ -78,9 +76,6 @@ type Signer struct { // outboundHashBeingReported is a map of outboundHash being reported outboundHashBeingReported map[string]bool - - // mu protects fields from concurrent access - mu *sync.Mutex } // NewSigner creates a new EVM signer @@ -116,7 +111,7 @@ func NewSigner( } return &Signer{ - Signer: *baseSigner, + Signer: baseSigner, client: client, ethSigner: ethSigner, zetaConnectorABI: connectorABI, @@ -124,35 +119,34 @@ func NewSigner( zetaConnectorAddress: zetaConnectorAddress, er20CustodyAddress: erc20CustodyAddress, outboundHashBeingReported: make(map[string]bool), - mu: &sync.Mutex{}, }, nil } // SetZetaConnectorAddress sets the zeta connector address func (signer *Signer) SetZetaConnectorAddress(addr ethcommon.Address) { - signer.Mu().Lock() - defer signer.Mu().Unlock() + signer.Lock() + defer signer.Unlock() signer.zetaConnectorAddress = addr } // SetERC20CustodyAddress sets the erc20 custody address func (signer *Signer) SetERC20CustodyAddress(addr ethcommon.Address) { - signer.Mu().Lock() - defer signer.Mu().Unlock() + signer.Lock() + defer signer.Unlock() signer.er20CustodyAddress = addr } // GetZetaConnectorAddress returns the zeta connector address func (signer *Signer) GetZetaConnectorAddress() ethcommon.Address { - signer.Mu().Lock() - defer signer.Mu().Unlock() + signer.Lock() + defer signer.Unlock() return signer.zetaConnectorAddress } // GetERC20CustodyAddress returns the erc20 custody address func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address { - signer.Mu().Lock() - defer signer.Mu().Unlock() + signer.Lock() + defer signer.Unlock() return signer.er20CustodyAddress } @@ -703,8 +697,8 @@ func (signer *Signer) reportToOutboundTracker( logger zerolog.Logger, ) { // skip if already being reported - signer.Mu().Lock() - defer signer.Mu().Unlock() + signer.Lock() + defer signer.Unlock() if _, found := signer.outboundHashBeingReported[outboundHash]; found { logger.Info(). Msgf("reportToOutboundTracker: outboundHash %s for chain %d nonce %d is being reported", outboundHash, chainID, nonce) @@ -715,9 +709,9 @@ func (signer *Signer) reportToOutboundTracker( // report to outbound tracker with goroutine go func() { defer func() { - signer.Mu().Lock() + signer.Lock() delete(signer.outboundHashBeingReported, outboundHash) - signer.Mu().Unlock() + signer.Unlock() }() // try monitoring tx inclusion status for 10 minutes