diff --git a/changelog.md b/changelog.md index 3ee291c6e6..72110564a5 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 +* [2357](https://github.com/zeta-chain/node/pull/2357) - 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 }