diff --git a/changelog.md b/changelog.md index 72110564a5..37eeac2ff7 100644 --- a/changelog.md +++ b/changelog.md @@ -49,6 +49,7 @@ * [2344](https://github.com/zeta-chain/node/pull/2344) - group common data of EVM/Bitcoin signer and observer using base structs * [2317](https://github.com/zeta-chain/node/pull/2317) - add ValidateOutbound method for cctx orchestrator * [2357](https://github.com/zeta-chain/node/pull/2357) - integrated base Signer structure into EVM/Bitcoin Signer +* [2359](https://github.com/zeta-chain/node/pull/2357) - integrated base Observer structure into EVM/Bitcoin Observer ### Tests diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 64db1c3efa..31444cf5e7 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -1,8 +1,12 @@ package main import ( + "fmt" + + "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/zeta-chain/zetacore/zetaclient/authz" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -116,28 +120,84 @@ func CreateChainObserverMap( logger base.Logger, ts *metrics.TelemetryServer, ) (map[int64]interfaces.ChainObserver, error) { + zetacoreContext := appContext.ZetacoreContext() observerMap := make(map[int64]interfaces.ChainObserver) // EVM observers for _, evmConfig := range appContext.Config().GetAllEVMConfigs() { if evmConfig.Chain.IsZetaChain() { continue } - _, found := appContext.ZetacoreContext().GetEVMChainParams(evmConfig.Chain.ChainId) + chainParams, found := zetacoreContext.GetEVMChainParams(evmConfig.Chain.ChainId) if !found { logger.Std.Error().Msgf("ChainParam not found for chain %s", evmConfig.Chain.String()) continue } - co, err := evmobserver.NewObserver(appContext, zetacoreClient, tss, dbpath, logger, evmConfig, ts) + + // create EVM client + evmClient, err := ethclient.Dial(evmConfig.Endpoint) + if err != nil { + logger.Std.Error().Err(err).Msgf("error dailing endpoint %s", evmConfig.Endpoint) + continue + } + + // create EVM chain observer + co, err := evmobserver.NewObserver( + evmConfig, + evmClient, + *chainParams, + zetacoreContext, + zetacoreClient, + tss, + dbpath, + logger, + ts, + ) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for evm chain %s", evmConfig.Chain.String()) continue } observerMap[evmConfig.Chain.ChainId] = co } + // BTC observer + _, chainParams, found := zetacoreContext.GetBTCChainParams() + if !found { + return nil, fmt.Errorf("bitcoin chains params not found") + } + + // create BTC chain observer btcChain, btcConfig, enabled := appContext.GetBTCChainAndConfig() if enabled { - co, err := btcobserver.NewObserver(appContext, btcChain, zetacoreClient, tss, dbpath, logger, btcConfig, ts) + // create BTC client + connCfg := &rpcclient.ConnConfig{ + Host: btcConfig.RPCHost, + User: btcConfig.RPCUsername, + Pass: btcConfig.RPCPassword, + HTTPPostMode: true, + DisableTLS: true, + Params: btcConfig.RPCParams, + } + btcClient, err := rpcclient.New(connCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating rpc client: %s", err) + } + err = btcClient.Ping() + if err != nil { + return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + } + + // create BTC chain observer + co, err := btcobserver.NewObserver( + btcChain, + btcClient, + *chainParams, + zetacoreContext, + zetacoreClient, + tss, + dbpath, + logger, + ts, + ) if err != nil { logger.Std.Error().Err(err).Msgf("NewObserver error for bitcoin chain %s", btcChain.String()) diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 905775b2b2..b2aa983bbd 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -11,6 +11,7 @@ import ( "github.com/rs/zerolog" "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" observertypes "github.com/zeta-chain/zetacore/x/observer/types" @@ -28,9 +29,12 @@ const ( // Cached blocks can be used to get block information and verify transactions DefaultBlockCacheSize = 1000 - // DefaultHeadersCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) + // DefaultHeaderCacheSize is the default number of headers that the observer will keep in cache for performance (without RPC calls) // Cached headers can be used to get header information - DefaultHeadersCacheSize = 1000 + DefaultHeaderCacheSize = 1000 + + // TempSQLiteDBPath is the temporary in-memory SQLite database used for testing + TempSQLiteDBPath = "file::memory:?cache=shared" ) // Observer is the base structure for chain observers, grouping the common logic for each chain observer client. @@ -84,8 +88,7 @@ func NewObserver( zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, blockCacheSize int, - headersCacheSize int, - dbPath string, + headerCacheSize int, ts *metrics.TelemetryServer, logger Logger, ) (*Observer, error) { @@ -112,18 +115,25 @@ func NewObserver( } // create header cache - ob.headerCache, err = lru.New(headersCacheSize) + ob.headerCache, err = lru.New(headerCacheSize) if err != nil { return nil, errors.Wrap(err, "error creating header cache") } - // open database - err = ob.OpenDB(dbPath) + return &ob, nil +} + +// Stop notifies all goroutines to stop and closes the database. +func (ob *Observer) Stop() { + ob.logger.Chain.Info().Msgf("observer is stopping for chain %d", ob.Chain().ChainId) + close(ob.stop) + + // close database + err := ob.CloseDB() if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("error opening observer db for chain: %s", chain.ChainName)) + ob.Logger().Chain.Error().Err(err).Msgf("CloseDB failed for chain %d", ob.Chain().ChainId) } - - return &ob, nil + ob.Logger().Chain.Info().Msgf("observer stopped for chain %d", ob.Chain().ChainId) } // Chain returns the chain for the observer. @@ -232,9 +242,20 @@ func (ob *Observer) DB() *gorm.DB { return ob.db } +// WithTelemetryServer attaches a new telemetry server to the observer. +func (ob *Observer) WithTelemetryServer(ts *metrics.TelemetryServer) *Observer { + ob.ts = ts + return ob +} + +// TelemetryServer returns the telemetry server for the observer. +func (ob *Observer) TelemetryServer() *metrics.TelemetryServer { + return ob.ts +} + // Logger returns the logger for the observer. -func (ob *Observer) Logger() ObserverLogger { - return ob.logger +func (ob *Observer) Logger() *ObserverLogger { + return &ob.logger } // WithLogger attaches a new logger to the observer. @@ -251,45 +272,64 @@ func (ob *Observer) WithLogger(logger Logger) *Observer { return ob } -// Stop returns the stop channel for the observer. -func (ob *Observer) Stop() chan struct{} { +// StopChannel returns the stop channel for the observer. +func (ob *Observer) StopChannel() chan struct{} { return ob.stop } // OpenDB open sql database in the given path. -func (ob *Observer) OpenDB(dbPath string) error { - if dbPath != "" { - // create db path if not exist - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return errors.Wrap(err, "error creating db path") - } - } - - // open db by chain name - chainName := ob.chain.ChainName.String() - path := fmt.Sprintf("%s/%s", dbPath, chainName) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) +func (ob *Observer) OpenDB(dbPath string, dbName string) error { + // create db path if not exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + err := os.MkdirAll(dbPath, os.ModePerm) if err != nil { - return errors.Wrap(err, "error opening db") + return errors.Wrapf(err, "error creating db path: %s", dbPath) } + } - // migrate db - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return errors.Wrap(err, "error migrating db") - } - ob.db = db + // use custom dbName or chain name if not provided + if dbName == "" { + dbName = ob.chain.ChainName.String() + } + path := fmt.Sprintf("%s/%s", dbPath, dbName) + + // use memory db if specified + if dbPath == TempSQLiteDBPath { + path = TempSQLiteDBPath + } + + // open db + db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + return errors.Wrap(err, "error opening db") + } + + // migrate db + err = db.AutoMigrate(&clienttypes.LastBlockSQLType{}) + if err != nil { + return errors.Wrap(err, "error migrating db") + } + ob.db = db + + return nil +} + +// CloseDB close the database. +func (ob *Observer) CloseDB() error { + dbInst, err := ob.db.DB() + if err != nil { + return fmt.Errorf("error getting database instance: %w", err) + } + err = dbInst.Close() + if err != nil { + return fmt.Errorf("error closing database: %w", err) } return nil } // LoadLastBlockScanned loads last scanned block from environment variable or from database. // The last scanned block is the height from which the observer should continue scanning. -func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool, err error) { +func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) error { // get environment variable envvar := EnvVarLatestBlockByChain(ob.chain) scanFromBlock := os.Getenv(envvar) @@ -299,27 +339,33 @@ func (ob *Observer) LoadLastBlockScanned(logger zerolog.Logger) (fromLatest bool logger.Info(). Msgf("LoadLastBlockScanned: envvar %s is set; scan from block %s", envvar, scanFromBlock) if scanFromBlock == EnvVarLatestBlock { - return true, nil + return nil } blockNumber, err := strconv.ParseUint(scanFromBlock, 10, 64) if err != nil { - return false, err + return err } ob.WithLastBlockScanned(blockNumber) - return false, nil + return nil } // load from DB otherwise. If not found, start from latest block blockNumber, err := ob.ReadLastBlockScannedFromDB() if err != nil { - logger.Info().Msgf("LoadLastBlockScanned: chain %d starts scanning from latest block", ob.chain.ChainId) - return true, nil + logger.Info().Msgf("LoadLastBlockScanned: last scanned block not found in db for chain %d", ob.chain.ChainId) + return nil } ob.WithLastBlockScanned(blockNumber) logger.Info(). Msgf("LoadLastBlockScanned: chain %d starts scanning from block %d", ob.chain.ChainId, ob.LastBlockScanned()) - return false, nil + return nil +} + +// SaveLastBlockScanned saves the last scanned block to memory and database. +func (ob *Observer) SaveLastBlockScanned(blockNumber uint64) error { + ob.WithLastBlockScanned(blockNumber) + return ob.WriteLastBlockScannedToDB(blockNumber) } // WriteLastBlockScannedToDB saves the last scanned block to the database. @@ -339,5 +385,5 @@ func (ob *Observer) ReadLastBlockScannedFromDB() (uint64, error) { // EnvVarLatestBlock returns the environment variable for the latest block by chain. func EnvVarLatestBlockByChain(chain chains.Chain) string { - return chain.ChainName.String() + "_SCAN_FROM" + return fmt.Sprintf("CHAIN_%d_SCAN_FROM", chain.ChainId) } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 7dd2f18081..870f88715e 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -15,18 +15,13 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -// create a temporary directory for testing -func createTempDir(t *testing.T) string { - tempPath, err := os.MkdirTemp("", "tempdir-") - require.NoError(t, err) - return tempPath -} - // createObserver creates a new observer for testing -func createObserver(t *testing.T, dbPath string) *base.Observer { +func createObserver(t *testing.T) *base.Observer { // constructor parameters chain := chains.Ethereum chainParams := *sample.ChainParams(chain.ChainId) @@ -43,8 +38,7 @@ func createObserver(t *testing.T, dbPath string) *base.Observer { zetacoreClient, tss, base.DefaultBlockCacheSize, - base.DefaultHeadersCacheSize, - dbPath, + base.DefaultHeaderCacheSize, nil, logger, ) @@ -61,73 +55,55 @@ func TestNewObserver(t *testing.T) { zetacoreClient := mocks.NewMockZetacoreClient() tss := mocks.NewTSSMainnet() blockCacheSize := base.DefaultBlockCacheSize - headersCacheSize := base.DefaultHeadersCacheSize - dbPath := createTempDir(t) + headersCacheSize := base.DefaultHeaderCacheSize // test cases tests := []struct { - name string - chain chains.Chain - chainParams observertypes.ChainParams - zetacoreContext *context.ZetacoreContext - zetacoreClient interfaces.ZetacoreClient - tss interfaces.TSSSigner - blockCacheSize int - headersCacheSize int - dbPath string - fail bool - message string + name string + chain chains.Chain + chainParams observertypes.ChainParams + zetacoreContext *context.ZetacoreContext + zetacoreClient interfaces.ZetacoreClient + tss interfaces.TSSSigner + blockCacheSize int + headerCacheSize int + fail bool + message string }{ { - name: "should be able to create new observer", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headersCacheSize: headersCacheSize, - dbPath: dbPath, - fail: false, + name: "should be able to create new observer", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + headerCacheSize: headersCacheSize, + fail: false, }, { - name: "should return error on invalid block cache size", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: 0, - headersCacheSize: headersCacheSize, - dbPath: dbPath, - fail: true, - message: "error creating block cache", + name: "should return error on invalid block cache size", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: 0, + headerCacheSize: headersCacheSize, + fail: true, + message: "error creating block cache", }, { - name: "should return error on invalid header cache size", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headersCacheSize: 0, - dbPath: dbPath, - fail: true, - message: "error creating header cache", - }, - { - name: "should return error on invalid db path", - chain: chain, - chainParams: chainParams, - zetacoreContext: zetacoreContext, - zetacoreClient: zetacoreClient, - tss: tss, - blockCacheSize: blockCacheSize, - headersCacheSize: headersCacheSize, - dbPath: "/invalid/123db", - fail: true, - message: "error opening observer db", + name: "should return error on invalid header cache size", + chain: chain, + chainParams: chainParams, + zetacoreContext: zetacoreContext, + zetacoreClient: zetacoreClient, + tss: tss, + blockCacheSize: blockCacheSize, + headerCacheSize: 0, + fail: true, + message: "error creating header cache", }, } @@ -141,8 +117,7 @@ func TestNewObserver(t *testing.T) { tt.zetacoreClient, tt.tss, tt.blockCacheSize, - tt.headersCacheSize, - tt.dbPath, + tt.headerCacheSize, nil, base.DefaultLogger(), ) @@ -159,10 +134,8 @@ func TestNewObserver(t *testing.T) { } func TestObserverGetterAndSetter(t *testing.T) { - dbPath := createTempDir(t) - t.Run("should be able to update chain", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update chain newChain := chains.BscMainnet @@ -170,7 +143,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newChain, ob.Chain()) }) t.Run("should be able to update chain params", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update chain params newChainParams := *sample.ChainParams(chains.BscMainnet.ChainId) @@ -178,7 +151,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.True(t, observertypes.ChainParamsEqual(newChainParams, ob.ChainParams())) }) t.Run("should be able to update zetacore context", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update zetacore context newZetacoreContext := context.NewZetacoreContext(config.NewConfig()) @@ -186,7 +159,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreContext, ob.ZetacoreContext()) }) t.Run("should be able to update zetacore client", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update zetacore client newZetacoreClient := mocks.NewMockZetacoreClient() @@ -194,7 +167,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newZetacoreClient, ob.ZetacoreClient()) }) t.Run("should be able to update tss", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update tss newTSS := mocks.NewTSSAthens3() @@ -202,7 +175,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newTSS, ob.TSS()) }) t.Run("should be able to update last block", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update last block newLastBlock := uint64(100) @@ -210,7 +183,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newLastBlock, ob.LastBlock()) }) t.Run("should be able to update last block scanned", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update last block scanned newLastBlockScanned := uint64(100) @@ -218,7 +191,7 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newLastBlockScanned, ob.LastBlockScanned()) }) t.Run("should be able to replace block cache", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) // update block cache newBlockCache, err := lru.New(200) @@ -227,8 +200,8 @@ func TestObserverGetterAndSetter(t *testing.T) { ob = ob.WithBlockCache(newBlockCache) require.Equal(t, newBlockCache, ob.BlockCache()) }) - t.Run("should be able to replace headers cache", func(t *testing.T) { - ob := createObserver(t, dbPath) + t.Run("should be able to replace header cache", func(t *testing.T) { + ob := createObserver(t) // update headers cache newHeadersCache, err := lru.New(200) @@ -238,13 +211,24 @@ func TestObserverGetterAndSetter(t *testing.T) { require.Equal(t, newHeadersCache, ob.HeaderCache()) }) t.Run("should be able to get database", func(t *testing.T) { - ob := createObserver(t, dbPath) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + ob.OpenDB(dbPath, "") db := ob.DB() require.NotNil(t, db) }) + t.Run("should be able to update telemetry server", func(t *testing.T) { + ob := createObserver(t) + + // update telemetry server + newServer := metrics.NewTelemetryServer() + ob = ob.WithTelemetryServer(newServer) + require.Equal(t, newServer, ob.TelemetryServer()) + }) t.Run("should be able to get logger", func(t *testing.T) { - ob := createObserver(t, dbPath) + ob := createObserver(t) logger := ob.Logger() // should be able to print log @@ -257,16 +241,30 @@ func TestObserverGetterAndSetter(t *testing.T) { }) } -func TestOpenDB(t *testing.T) { - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) +func TestOpenCloseDB(t *testing.T) { + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + + t.Run("should be able to open/close db", func(t *testing.T) { + // open db + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // close db + err = ob.CloseDB() + require.NoError(t, err) + }) + t.Run("should use memory db if specified", func(t *testing.T) { + // open db with memory + err := ob.OpenDB(base.TempSQLiteDBPath, "") + require.NoError(t, err) - t.Run("should be able to open db", func(t *testing.T) { - err := ob.OpenDB(dbPath) + // close db + err = ob.CloseDB() require.NoError(t, err) }) t.Run("should return error on invalid db path", func(t *testing.T) { - err := ob.OpenDB("/invalid/123db") + err := ob.OpenDB("/invalid/123db", "") require.ErrorContains(t, err, "error creating db path") }) } @@ -276,71 +274,116 @@ func TestLoadLastBlockScanned(t *testing.T) { envvar := base.EnvVarLatestBlockByChain(chain) t.Run("should be able to load last block scanned", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 100, ob.LastBlockScanned()) - require.False(t, fromLatest) }) - t.Run("should use latest block if last block scanned not found", func(t *testing.T) { - // create empty db - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + t.Run("latest block scanned should be 0 if not found in db", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.True(t, fromLatest) + require.EqualValues(t, 0, ob.LastBlockScanned()) }) t.Run("should overwrite last block scanned if env var is set", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) ob.WriteLastBlockScannedToDB(100) // set env var os.Setenv(envvar, "101") // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) require.EqualValues(t, 101, ob.LastBlockScanned()) - require.False(t, fromLatest) + }) + t.Run("last block scanned should remain 0 if env var is set to latest", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // create db and write 100 as last block scanned + ob.WriteLastBlockScannedToDB(100) // set env var to 'latest' os.Setenv(envvar, base.EnvVarLatestBlock) - // read last block scanned - fromLatest, err = ob.LoadLastBlockScanned(log.Logger) + // last block scanned should remain 0 + err = ob.LoadLastBlockScanned(log.Logger) require.NoError(t, err) - require.True(t, fromLatest) + require.EqualValues(t, 0, ob.LastBlockScanned()) }) t.Run("should return error on invalid env var", func(t *testing.T) { - // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) // set invalid env var os.Setenv(envvar, "invalid") // read last block scanned - fromLatest, err := ob.LoadLastBlockScanned(log.Logger) + err = ob.LoadLastBlockScanned(log.Logger) require.Error(t, err) - require.False(t, fromLatest) + }) +} + +func TestSaveLastBlockScanned(t *testing.T) { + t.Run("should be able to save last block scanned", func(t *testing.T) { + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // save 100 as last block scanned + err = ob.SaveLastBlockScanned(100) + require.NoError(t, err) + + // check last block scanned in memory + require.EqualValues(t, 100, ob.LastBlockScanned()) + + // read last block scanned from db + lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() + require.NoError(t, err) + require.EqualValues(t, 100, lastBlockScanned) }) } func TestReadWriteLastBlockScannedToDB(t *testing.T) { t.Run("should be able to write and read last block scanned to db", func(t *testing.T) { - // create db and write 100 as last block scanned - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) - err := ob.WriteLastBlockScannedToDB(100) + // create observer and open db + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) + + // write last block scanned + err = ob.WriteLastBlockScannedToDB(100) require.NoError(t, err) lastBlockScanned, err := ob.ReadLastBlockScannedFromDB() @@ -349,8 +392,10 @@ func TestReadWriteLastBlockScannedToDB(t *testing.T) { }) t.Run("should return error when last block scanned not found in db", func(t *testing.T) { // create empty db - dbPath := createTempDir(t) - ob := createObserver(t, dbPath) + dbPath := testutils.CreateTempDir(t) + ob := createObserver(t) + err := ob.OpenDB(dbPath, "") + require.NoError(t, err) lastScannedBlock, err := ob.ReadLastBlockScannedFromDB() require.Error(t, err) diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index 54c5294745..2438411b5b 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -25,7 +25,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) -// WatchInbound watches Bitcoin chain for incoming txs and post votes to zetacore +// WatchInbound watches Bitcoin chain for inbounds on a ticker func (ob *Observer) WatchInbound() { ticker, err := types.NewDynamicTicker("Bitcoin_WatchInbound", ob.GetChainParams().InboundTicker) if err != nil { @@ -34,15 +34,15 @@ func (ob *Observer) WatchInbound() { } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) + ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } err := ob.ObserveInbound() @@ -50,47 +50,49 @@ func (ob *Observer) WatchInbound() { ob.logger.Inbound.Error().Err(err).Msg("WatchInbound error observing in tx") } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) return } } } +// ObserveInbound observes the Bitcoin chain for inbounds and post votes to zetacore func (ob *Observer) ObserveInbound() error { // get and update latest block height - cnt, err := ob.rpcClient.GetBlockCount() + cnt, err := ob.btcClient.GetBlockCount() if err != nil { return fmt.Errorf("observeInboundBTC: error getting block number: %s", err) } if cnt < 0 { return fmt.Errorf("observeInboundBTC: block number is negative: %d", cnt) } - if cnt < ob.GetLastBlockHeight() { + // #nosec G701 checked positive + lastBlock := uint64(cnt) + if lastBlock < ob.LastBlock() { return fmt.Errorf( "observeInboundBTC: block number should not decrease: current %d last %d", cnt, - ob.GetLastBlockHeight(), + ob.LastBlock(), ) } - ob.SetLastBlockHeight(cnt) + ob.WithLastBlock(lastBlock) // skip if current height is too low - // #nosec G701 always in range - confirmedBlockNum := cnt - int64(ob.GetChainParams().ConfirmationCount) - if confirmedBlockNum < 0 { + if lastBlock < ob.GetChainParams().ConfirmationCount { return fmt.Errorf("observeInboundBTC: skipping observer, current block number %d is too low", cnt) } // skip if no new block is confirmed - lastScanned := ob.GetLastBlockHeightScanned() - if lastScanned >= confirmedBlockNum { + lastScanned := ob.LastBlockScanned() + if lastScanned >= lastBlock-ob.GetChainParams().ConfirmationCount { return nil } // query incoming gas asset to TSS address blockNumber := lastScanned + 1 - res, err := ob.GetBlockByNumberCached(blockNumber) + // #nosec G701 always in range + res, err := ob.GetBlockByNumberCached(int64(blockNumber)) if err != nil { ob.logger.Inbound.Error().Err(err).Msgf("observeInboundBTC: error getting bitcoin block %d", blockNumber) return err @@ -103,9 +105,10 @@ func (ob *Observer) ObserveInbound() error { // https://github.com/zeta-chain/node/issues/1847 // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 - blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) + blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) if found && blockHeaderVerification.Enabled { - err = ob.postBlockHeader(blockNumber) + // #nosec G701 always in range + err = ob.postBlockHeader(int64(blockNumber)) if err != nil { ob.logger.Inbound.Warn().Err(err).Msgf("observeInboundBTC: error posting block header %d", blockNumber) } @@ -113,14 +116,14 @@ func (ob *Observer) ObserveInbound() error { if len(res.Block.Tx) > 1 { // get depositor fee - depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) + depositorFee := bitcoin.CalcDepositorFee(res.Block, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) // filter incoming txs to TSS address - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() // #nosec G701 always positive inbounds, err := FilterAndParseIncomingTx( - ob.rpcClient, + ob.btcClient, res.Block.Tx, uint64(res.Block.Height), tssAddress, @@ -139,7 +142,7 @@ func (ob *Observer) ObserveInbound() error { for _, inbound := range inbounds { msg := ob.GetInboundVoteMessageFromBtcEvent(inbound) if msg != nil { - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -157,11 +160,8 @@ func (ob *Observer) ObserveInbound() error { } } - // Save LastBlockHeight - ob.SetLastBlockHeightScanned(blockNumber) - - // #nosec G701 always positive - if err := ob.db.Save(types.ToLastBlockSQLType(uint64(blockNumber))).Error; err != nil { + // save last scanned block to both memory and db + if err := ob.SaveLastBlockScanned(blockNumber); err != nil { ob.logger.Inbound.Error(). Err(err). Msgf("observeInboundBTC: error writing last scanned block %d to db", blockNumber) @@ -182,18 +182,18 @@ func (ob *Observer) WatchInboundTracker() { for { select { case <-ticker.C(): - if !context.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { ob.logger.Inbound.Error(). Err(err). - Msgf("error observing inbound tracker for chain %d", ob.chain.ChainId) + Msgf("error observing inbound tracker for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return } } @@ -201,7 +201,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) if err != nil { return err } @@ -214,7 +214,7 @@ func (ob *Observer) ProcessInboundTrackers() error { return err } ob.logger.Inbound.Info(). - Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.chain.ChainName, ballotIdentifier, coin.CoinType_Gas.String()) + Msgf("Vote submitted for inbound Tracker, Chain : %s,Ballot Identifier : %s, coin-type %s", ob.Chain().ChainName, ballotIdentifier, coin.CoinType_Gas.String()) } return nil @@ -227,7 +227,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - tx, err := ob.rpcClient.GetRawTransactionVerbose(hash) + tx, err := ob.btcClient.GetRawTransactionVerbose(hash) if err != nil { return "", err } @@ -237,7 +237,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", err } - blockVb, err := ob.rpcClient.GetBlockVerboseTx(blockHash) + blockVb, err := ob.btcClient.GetBlockVerboseTx(blockHash) if err != nil { return "", err } @@ -246,15 +246,15 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return "", fmt.Errorf("block %d has no transactions", blockVb.Height) } - depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.chain.ChainId, ob.netParams, ob.logger.Inbound) - tss, err := ob.zetacoreClient.GetBtcTssAddress(ob.chain.ChainId) + depositorFee := bitcoin.CalcDepositorFee(blockVb, ob.Chain().ChainId, ob.netParams, ob.logger.Inbound) + tss, err := ob.ZetacoreClient().GetBtcTssAddress(ob.Chain().ChainId) if err != nil { return "", err } // #nosec G701 always positive event, err := GetBtcEvent( - ob.rpcClient, + ob.btcClient, *tx, tss, uint64(blockVb.Height), @@ -279,7 +279,7 @@ func (ob *Observer) CheckReceiptForBtcTxHash(txHash string, vote bool) (string, return msg.Digest(), nil } - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound( zetacore.PostVoteInboundGasLimit, zetacore.PostVoteInboundExecutionGasLimit, msg, @@ -343,10 +343,10 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) return zetacore.GetInboundVoteMessage( inbound.FromAddress, - ob.chain.ChainId, + ob.Chain().ChainId, inbound.FromAddress, inbound.FromAddress, - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, cosmosmath.NewUintFromBigInt(amountInt), message, inbound.TxHash, @@ -354,7 +354,7 @@ func (ob *Observer) GetInboundVoteMessageFromBtcEvent(inbound *BTCInboundEvent) 0, coin.CoinType_Gas, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, ) } @@ -368,7 +368,7 @@ func (ob *Observer) DoesInboundContainsRestrictedAddress(inTx *BTCInboundEvent) } if config.ContainRestrictedAddress(inTx.FromAddress, receiver) { compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") + false, ob.Chain().ChainId, inTx.TxHash, inTx.FromAddress, receiver, "BTC") return true } return false diff --git a/zetaclient/chains/bitcoin/observer/inbound_test.go b/zetaclient/chains/bitcoin/observer/inbound_test.go index b67b7d7b50..63042e8b85 100644 --- a/zetaclient/chains/bitcoin/observer/inbound_test.go +++ b/zetaclient/chains/bitcoin/observer/inbound_test.go @@ -1,4 +1,4 @@ -package observer +package observer_test import ( "bytes" @@ -18,6 +18,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" @@ -198,7 +199,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1px3peqcd60hk7wqyqk36697u9hzugq0pd5lzvney93yzzrqy4fkpq6cj7m3", sender) }) @@ -210,7 +211,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q79kmcyc706d6nh7tpzhnn8lzp76rp0tepph3hqwrhacqfcy4lwxqft0ppq", sender) }) @@ -222,7 +223,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", sender) }) @@ -234,7 +235,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 0} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "3MqRRSP76qxdVD9K4cfFnVtSLVwaaAjm3t", sender) }) @@ -246,7 +247,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Equal(t, "1ESQp1WQi7fzSpzCNs2oBTqaUBmNjLQLoV", sender) }) @@ -272,7 +273,7 @@ func TestGetSenderAddressByVin(t *testing.T) { // get sender address txVin := btcjson.Vin{Txid: txHash, Vout: 1} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.NoError(t, err) require.Empty(t, sender) }) @@ -288,7 +289,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // use invalid tx hash txVin := btcjson.Vin{Txid: "invalid tx hash", Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.Error(t, err) require.Empty(t, sender) }) @@ -296,7 +297,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { // create mock rpc client without preloaded tx rpcClient := mocks.NewMockBTCRPCClient() txVin := btcjson.Vin{Txid: txHash, Vout: 2} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "error getting raw transaction") require.Empty(t, sender) }) @@ -305,7 +306,7 @@ func TestGetSenderAddressByVinErrors(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, txHash) // invalid output index txVin := btcjson.Vin{Txid: txHash, Vout: 3} - sender, err := GetSenderAddressByVin(rpcClient, txVin, net) + sender, err := observer.GetSenderAddressByVin(rpcClient, txVin, net) require.ErrorContains(t, err, "out of range") require.Empty(t, sender) }) @@ -328,7 +329,7 @@ func TestGetBtcEvent(t *testing.T) { // expected result memo, err := hex.DecodeString(tx.Vout[1].ScriptPubKey.Hex[4:]) require.NoError(t, err) - eventExpected := &BTCInboundEvent{ + eventExpected := &observer.BTCInboundEvent{ FromAddress: "bc1q68kxnq52ahz5vd6c8czevsawu0ux9nfrzzrh6e", ToAddress: tssAddress, Value: tx.Vout[0].Value - depositorFee, // 7008 sataoshis @@ -347,7 +348,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -362,7 +363,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -377,7 +378,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -392,7 +393,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -407,7 +408,7 @@ func TestGetBtcEvent(t *testing.T) { rpcClient := createRPCClientAndLoadTx(t, chain.ChainId, preHash) // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Equal(t, eventExpected, event) }) @@ -418,7 +419,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -429,13 +430,13 @@ func TestGetBtcEvent(t *testing.T) { // modify the tx to have Vout[0] a P2SH output tx.Vout[0].ScriptPubKey.Hex = strings.Replace(tx.Vout[0].ScriptPubKey.Hex, "0014", "a914", 1) - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) // append 1 byte to script to make it longer than 22 bytes tx.Vout[0].ScriptPubKey.Hex = tx.Vout[0].ScriptPubKey.Hex + "00" - event, err = GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err = observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -446,7 +447,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -457,7 +458,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -468,7 +469,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -479,7 +480,7 @@ func TestGetBtcEvent(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.NoError(t, err) require.Nil(t, event) }) @@ -502,7 +503,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -513,7 +514,7 @@ func TestGetBtcEventErrors(t *testing.T) { // get BTC event rpcClient := mocks.NewMockBTCRPCClient() - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) @@ -523,7 +524,7 @@ func TestGetBtcEventErrors(t *testing.T) { rpcClient := mocks.NewMockBTCRPCClient() // get BTC event - event, err := GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) + event, err := observer.GetBtcEvent(rpcClient, *tx, tssAddress, blockNumber, log.Logger, net, depositorFee) require.Error(t, err) require.Nil(t, event) }) diff --git a/zetaclient/chains/bitcoin/observer/live_test.go b/zetaclient/chains/bitcoin/observer/live_test.go index dd1053f620..c5512a1fae 100644 --- a/zetaclient/chains/bitcoin/observer/live_test.go +++ b/zetaclient/chains/bitcoin/observer/live_test.go @@ -1,4 +1,4 @@ -package observer +package observer_test import ( "context" @@ -23,8 +23,7 @@ import ( "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" - "github.com/zeta-chain/zetacore/zetaclient/config" - clientcontext "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -47,34 +46,39 @@ func (suite *BitcoinObserverTestSuite) SetupTest() { tss := &mocks.TSS{ PrivKey: privateKey, } - appContext := clientcontext.NewAppContext(&clientcontext.ZetacoreContext{}, config.Config{}) - client, err := NewObserver(appContext, chains.BitcoinRegtest, nil, tss, tempSQLiteDbPath, - base.DefaultLogger(), config.BTCConfig{}, nil) + + // create mock arguments for constructor + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + btcClient := mocks.NewMockBTCRPCClient() + + // create observer + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, + base.DefaultLogger(), nil) suite.Require().NoError(err) + suite.Require().NotNil(ob) suite.rpcClient, err = getRPCClient(18332) suite.Require().NoError(err) skBytes, err := hex.DecodeString(skHex) suite.Require().NoError(err) suite.T().Logf("skBytes: %d", len(skBytes)) - btc := client.rpcClient - - _, err = btc.CreateWallet("e2e") + _, err = btcClient.CreateWallet("e2e") suite.Require().NoError(err) - addr, err := btc.GetNewAddress("test") + addr, err := btcClient.GetNewAddress("test") suite.Require().NoError(err) suite.T().Logf("deployer address: %s", addr) //err = btc.ImportPrivKey(privkeyWIF) //suite.Require().NoError(err) - btc.GenerateToAddress(101, addr, nil) + btcClient.GenerateToAddress(101, addr, nil) suite.Require().NoError(err) - bal, err := btc.GetBalance("*") + bal, err := btcClient.GetBalance("*") suite.Require().NoError(err) suite.T().Logf("balance: %f", bal.ToBTC()) - utxo, err := btc.ListUnspent() + utxo, err := btcClient.ListUnspent() suite.Require().NoError(err) suite.T().Logf("utxo: %d", len(utxo)) for _, u := range utxo { @@ -153,7 +157,7 @@ func (suite *BitcoinObserverTestSuite) Test1() { suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := FilterAndParseIncomingTx( + inbounds, err := observer.FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -190,7 +194,7 @@ func (suite *BitcoinObserverTestSuite) Test2() { suite.T().Logf("block height %d", block.Height) suite.T().Logf("block txs len %d", len(block.Tx)) - inbounds, err := FilterAndParseIncomingTx( + inbounds, err := observer.FilterAndParseIncomingTx( suite.rpcClient, block.Tx, uint64(block.Height), @@ -242,11 +246,11 @@ func LiveTestGetBlockHeightByHash(t *testing.T) { invalidHash := "invalidhash" // get block by invalid hash - _, err = GetBlockHeightByHash(client, invalidHash) + _, err = observer.GetBlockHeightByHash(client, invalidHash) require.ErrorContains(t, err, "error decoding block hash") // get block height by block hash - height, err := GetBlockHeightByHash(client, hash) + height, err := observer.GetBlockHeightByHash(client, hash) require.NoError(t, err) require.Equal(t, expectedHeight, height) } @@ -460,7 +464,7 @@ BLOCKLOOP: Txid: mpvin.TxID, Vout: mpvin.Vout, } - senderAddr, err := GetSenderAddressByVin(client, vin, net) + senderAddr, err := observer.GetSenderAddressByVin(client, vin, net) if err != nil { fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) time.Sleep(3 * time.Second) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index 6b93ba12cd..0cf1766895 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -6,24 +6,17 @@ import ( "fmt" "math" "math/big" - "os" "sort" "sync" - "sync/atomic" "time" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" "github.com/rs/zerolog" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" @@ -31,47 +24,34 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin" "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" - "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" - "github.com/zeta-chain/zetacore/zetaclient/zetacore" ) const ( // btcBlocksPerDay represents Bitcoin blocks per days for LRU block cache size btcBlocksPerDay = 144 - // bigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC - bigValueSats = 200000000 + // RegnetStartBlock is the hardcoded start block for regnet + RegnetStartBlock = 100 - // bigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations - bigValueConfirmationCount = 6 + // BigValueSats contains the threshold to determine a big value in Bitcoin represents 2 BTC + BigValueSats = 200000000 + + // BigValueConfirmationCount represents the number of confirmation necessary for bigger values: 6 confirmations + BigValueConfirmationCount = 6 ) var _ interfaces.ChainObserver = &Observer{} // Logger contains list of loggers used by Bitcoin chain observer -// TODO: Merge this logger with the one in evm -// https://github.com/zeta-chain/node/issues/2022 type Logger struct { - // Chain is the parent logger for the chain - Chain zerolog.Logger - - // Inbound is the logger for incoming transactions - Inbound zerolog.Logger // The logger for incoming transactions - - // Outbound is the logger for outgoing transactions - Outbound zerolog.Logger // The logger for outgoing transactions + // base.Logger contains a list of base observer loggers + base.ObserverLogger // UTXOs is the logger for UTXOs management - UTXOs zerolog.Logger // The logger for UTXOs management - - // GasPrice is the logger for gas price - GasPrice zerolog.Logger // The logger for gas price - - // Compliance is the logger for compliance checks - Compliance zerolog.Logger // The logger for compliance checks + UTXOs zerolog.Logger } // BTCInboundEvent represents an incoming transaction event @@ -98,23 +78,20 @@ type BTCBlockNHeader struct { // Observer is the Bitcoin chain observer type Observer struct { - BlockCache *lru.Cache + // base.Observer implements the base chain observer + base.Observer - // Mu is lock for all the maps, utxos and core params - Mu *sync.Mutex + // netParams contains the Bitcoin network parameters + netParams *chaincfg.Params + + // btcClient is the Bitcoin RPC client that interacts with the Bitcoin node + btcClient interfaces.BTCRPCClient - Tss interfaces.TSSSigner + // pendingNonce is the outbound artificial pending nonce + pendingNonce uint64 - chain chains.Chain - netParams *chaincfg.Params - rpcClient interfaces.BTCRPCClient - zetacoreClient interfaces.ZetacoreClient - lastBlock int64 - lastBlockScanned int64 - pendingNonce uint64 - utxos []btcjson.ListUnspentResult - params observertypes.ChainParams - coreContext *context.ZetacoreContext + // utxos contains the UTXOs owned by the TSS address + utxos []btcjson.ListUnspentResult // includedTxHashes indexes included tx with tx hash includedTxHashes map[string]bool @@ -125,157 +102,120 @@ type Observer struct { // broadcastedTx indexes the outbound hash with the outbound tx identifier broadcastedTx map[string]string - db *gorm.DB - stop chan struct{} + // logger contains the loggers used by the bitcoin observer logger Logger - ts *metrics.TelemetryServer + + // Mu protects the maps, utxos and chain params from concurrent access + Mu *sync.Mutex } // NewObserver returns a new Bitcoin chain observer func NewObserver( - appcontext *context.AppContext, chain chains.Chain, + btcClient interfaces.BTCRPCClient, + chainParams observertypes.ChainParams, + zetacoreContext *context.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, - btcCfg config.BTCConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - // initialize the observer - ob := Observer{ - ts: ts, - } - ob.stop = make(chan struct{}) - ob.chain = chain - - // get the bitcoin network params - netParams, err := chains.BitcoinNetParamsFromChainID(ob.chain.ChainId) - if err != nil { - return nil, fmt.Errorf("error getting net params for chain %d: %s", ob.chain.ChainId, err) - } - ob.netParams = netParams - - ob.Mu = &sync.Mutex{} - - chainLogger := logger.Std.With().Str("chain", chain.ChainName.String()).Logger() - ob.logger = Logger{ - Chain: chainLogger, - Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), - Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), - UTXOs: chainLogger.With().Str("module", "WatchUTXOs").Logger(), - GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: logger.Compliance, - } - - ob.zetacoreClient = zetacoreClient - ob.Tss = tss - ob.coreContext = appcontext.ZetacoreContext() - ob.includedTxHashes = make(map[string]bool) - ob.includedTxResults = make(map[string]*btcjson.GetTransactionResult) - ob.broadcastedTx = make(map[string]string) - - // set the Bitcoin chain params - _, chainParams, found := appcontext.ZetacoreContext().GetBTCChainParams() - if !found { - return nil, fmt.Errorf("btc chains params not initialized") - } - ob.params = *chainParams - - // create the RPC client - ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) - connCfg := &rpcclient.ConnConfig{ - Host: btcCfg.RPCHost, - User: btcCfg.RPCUsername, - Pass: btcCfg.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcCfg.RPCParams, - } - rpcClient, err := rpcclient.New(connCfg, nil) - if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - - // try connection - ob.rpcClient = rpcClient - err = rpcClient.Ping() + // create base observer + baseObserver, err := base.NewObserver( + chain, + chainParams, + zetacoreContext, + zetacoreClient, + tss, + btcBlocksPerDay, + base.DefaultHeaderCacheSize, + ts, + logger, + ) if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + return nil, err } - ob.BlockCache, err = lru.New(btcBlocksPerDay) + // get the bitcoin network params + netParams, err := chains.BitcoinNetParamsFromChainID(chain.ChainId) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create bitcoin block cache") - return nil, err + return nil, fmt.Errorf("error getting net params for chain %d: %s", chain.ChainId, err) + } + + // create bitcoin observer + ob := &Observer{ + Observer: *baseObserver, + netParams: netParams, + btcClient: btcClient, + pendingNonce: 0, + utxos: []btcjson.ListUnspentResult{}, + includedTxHashes: make(map[string]bool), + includedTxResults: make(map[string]*btcjson.GetTransactionResult), + broadcastedTx: make(map[string]string), + logger: Logger{ + ObserverLogger: *baseObserver.Logger(), + UTXOs: baseObserver.Logger().Chain.With().Str("module", "utxos").Logger(), + }, + Mu: &sync.Mutex{}, } // load btc chain observer DB - err = ob.loadDB(dbpath) + err = ob.LoadDB(dbpath) if err != nil { return nil, err } - return &ob, nil + return ob, nil } -func (ob *Observer) WithZetacoreClient(client *zetacore.Client) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.zetacoreClient = client -} - -func (ob *Observer) WithLogger(logger zerolog.Logger) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.logger = Logger{ - Chain: logger, - Inbound: logger.With().Str("module", "WatchInbound").Logger(), - Outbound: logger.With().Str("module", "WatchOutbound").Logger(), - UTXOs: logger.With().Str("module", "WatchUTXOs").Logger(), - GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), - } -} - -func (ob *Observer) WithBtcClient(client *rpcclient.Client) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.rpcClient = client -} - -func (ob *Observer) WithChain(chain chains.Chain) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.chain = chain +// BtcClient returns the btc client +func (ob *Observer) BtcClient() interfaces.BTCRPCClient { + return ob.btcClient } -func (ob *Observer) Chain() chains.Chain { - ob.Mu.Lock() - defer ob.Mu.Unlock() - return ob.chain +// WithBtcClient attaches a new btc client to the observer +func (ob *Observer) WithBtcClient(client interfaces.BTCRPCClient) { + ob.btcClient = client } +// SetChainParams sets the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.params = params + ob.WithChainParams(params) } +// GetChainParams returns the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.params + return ob.ChainParams() } // Start starts the Go routine to observe the Bitcoin chain func (ob *Observer) Start() { - ob.logger.Chain.Info().Msgf("Bitcoin client is starting") - go ob.WatchInbound() // watch bitcoin chain for incoming txs and post votes to zetacore - go ob.WatchOutbound() // watch bitcoin chain for outgoing txs status - go ob.WatchUTXOs() // watch bitcoin chain for UTXOs owned by the TSS address - go ob.WatchGasPrice() // watch bitcoin chain for gas rate and post to zetacore - go ob.WatchInboundTracker() // watch zetacore for bitcoin inbound trackers - go ob.WatchRPCStatus() // watch the RPC status of the bitcoin chain + ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + + // watch bitcoin chain for incoming txs and post votes to zetacore + go ob.WatchInbound() + + // watch bitcoin chain for outgoing txs status + go ob.WatchOutbound() + + // watch bitcoin chain for UTXOs owned by the TSS address + go ob.WatchUTXOs() + + // watch bitcoin chain for gas rate and post to zetacore + go ob.WatchGasPrice() + + // watch zetacore for bitcoin inbound trackers + go ob.WatchInboundTracker() + + // watch the RPC status of the bitcoin chain + go ob.WatchRPCStatus() } // WatchRPCStatus watches the RPC status of the Bitcoin chain @@ -290,19 +230,19 @@ func (ob *Observer) WatchRPCStatus() { continue } - bn, err := ob.rpcClient.GetBlockCount() + bn, err := ob.btcClient.GetBlockCount() if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - hash, err := ob.rpcClient.GetBlockHash(bn) + hash, err := ob.btcClient.GetBlockHash(bn) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue } - header, err := ob.rpcClient.GetBlockHeader(hash) + header, err := ob.btcClient.GetBlockHeader(hash) if err != nil { ob.logger.Chain.Error().Err(err).Msg("RPC status check: RPC down? ") continue @@ -315,8 +255,8 @@ func (ob *Observer) WatchRPCStatus() { continue } - tssAddr := ob.Tss.BTCAddressWitnessPubkeyHash() - res, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) + tssAddr := ob.TSS().BTCAddressWitnessPubkeyHash() + res, err := ob.btcClient.ListUnspentMinMaxAddresses(0, 1000000, []btcutil.Address{tssAddr}) if err != nil { ob.logger.Chain.Error(). Err(err). @@ -334,56 +274,27 @@ func (ob *Observer) WatchRPCStatus() { ob.logger.Chain.Info(). Msgf("[OK] RPC status check: latest block number %d, timestamp %s (%.fs ago), tss addr %s, #utxos: %d", bn, blockTime, elapsedSeconds, tssAddr, len(res)) - case <-ob.stop: + case <-ob.StopChannel(): return } } } -func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) - close(ob.stop) // this notifies all goroutines to stop - ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) -} - -func (ob *Observer) SetLastBlockHeight(height int64) { - atomic.StoreInt64(&ob.lastBlock, height) -} - -func (ob *Observer) GetLastBlockHeight() int64 { - return atomic.LoadInt64(&ob.lastBlock) -} - -func (ob *Observer) SetLastBlockHeightScanned(height int64) { - atomic.StoreInt64(&ob.lastBlockScanned, height) - // #nosec G701 checked as positive - ob.ts.SetLastScannedBlockNumber(ob.chain, uint64(height)) -} - -func (ob *Observer) GetLastBlockHeightScanned() int64 { - return atomic.LoadInt64(&ob.lastBlockScanned) -} - +// GetPendingNonce returns the artificial pending nonce +// Note: pending nonce is accessed concurrently func (ob *Observer) GetPendingNonce() uint64 { ob.Mu.Lock() defer ob.Mu.Unlock() return ob.pendingNonce } -// GetBaseGasPrice ... -// TODO: implement -// https://github.com/zeta-chain/node/issues/868 -func (ob *Observer) GetBaseGasPrice() *big.Int { - return big.NewInt(0) -} - // ConfirmationsThreshold returns number of required Bitcoin confirmations depending on sent BTC amount. func (ob *Observer) ConfirmationsThreshold(amount *big.Int) int64 { - if amount.Cmp(big.NewInt(bigValueSats)) >= 0 { - return bigValueConfirmationCount + if amount.Cmp(big.NewInt(BigValueSats)) >= 0 { + return BigValueConfirmationCount } - if bigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { - return bigValueConfirmationCount + if BigValueConfirmationCount < ob.GetChainParams().ConfirmationCount { + return BigValueConfirmationCount } // #nosec G701 always in range @@ -395,7 +306,7 @@ func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } // start gas price ticker @@ -405,7 +316,7 @@ func (ob *Observer) WatchGasPrice() { return } ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) + ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -416,25 +327,27 @@ func (ob *Observer) WatchGasPrice() { } err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.stop: - ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.GasPrice.Info().Msgf("WatchGasPrice stopped for chain %d", ob.Chain().ChainId) return } } } +// PostGasPrice posts gas price to zetacore func (ob *Observer) PostGasPrice() error { - if ob.chain.ChainId == 18444 { //bitcoin regtest; hardcode here since this RPC is not available on regtest - blockNumber, err := ob.rpcClient.GetBlockCount() + // hardcode gas price here since this RPC is not available on regtest + if chains.IsBitcoinRegnet(ob.Chain().ChainId) { + blockNumber, err := ob.btcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always in range - _, err = ob.zetacoreClient.PostGasPrice(ob.chain, 1, "100", uint64(blockNumber)) + _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), 1, "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -443,7 +356,7 @@ func (ob *Observer) PostGasPrice() error { } // EstimateSmartFee returns the fees per kilobyte (BTC/kb) targeting given block confirmation - feeResult, err := ob.rpcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) + feeResult, err := ob.btcClient.EstimateSmartFee(1, &btcjson.EstimateModeEconomical) if err != nil { return err } @@ -455,13 +368,13 @@ func (ob *Observer) PostGasPrice() error { } feeRatePerByte := bitcoin.FeeRateToSatPerByte(*feeResult.FeeRate) - blockNumber, err := ob.rpcClient.GetBlockCount() + blockNumber, err := ob.btcClient.GetBlockCount() if err != nil { return err } // #nosec G701 always positive - _, err = ob.zetacoreClient.PostGasPrice(ob.chain, feeRatePerByte.Uint64(), "100", uint64(blockNumber)) + _, err = ob.ZetacoreClient().PostGasPrice(ob.Chain(), feeRatePerByte.Uint64(), "100", uint64(blockNumber)) if err != nil { ob.logger.GasPrice.Err(err).Msg("PostGasPrice:") return err @@ -532,13 +445,14 @@ func (ob *Observer) WatchUTXOs() { ob.logger.UTXOs.Error().Err(err).Msg("error fetching btc utxos") } ticker.UpdateInterval(ob.GetChainParams().WatchUtxoTicker, ob.logger.UTXOs) - case <-ob.stop: - ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.UTXOs.Info().Msgf("WatchUTXOs stopped for chain %d", ob.Chain().ChainId) return } } } +// FetchUTXOs fetches TSS-owned UTXOs from the Bitcoin node func (ob *Observer) FetchUTXOs() error { defer func() { if err := recover(); err != nil { @@ -550,19 +464,19 @@ func (ob *Observer) FetchUTXOs() error { ob.refreshPendingNonce() // get the current block height. - bh, err := ob.rpcClient.GetBlockCount() + bh, err := ob.btcClient.GetBlockCount() if err != nil { return fmt.Errorf("btc: error getting block height : %v", err) } maxConfirmations := int(bh) // List all unspent UTXOs (160ms) - tssAddr := ob.Tss.BTCAddress() - address, err := chains.DecodeBtcAddress(tssAddr, ob.chain.ChainId) + tssAddr := ob.TSS().BTCAddress() + address, err := chains.DecodeBtcAddress(tssAddr, ob.Chain().ChainId) if err != nil { return fmt.Errorf("btc: error decoding wallet address (%s) : %s", tssAddr, err.Error()) } - utxos, err := ob.rpcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) + utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) if err != nil { return err } @@ -595,7 +509,7 @@ func (ob *Observer) FetchUTXOs() error { } ob.Mu.Lock() - ob.ts.SetNumberOfUTXOs(len(utxosFiltered)) + ob.TelemetryServer().SetNumberOfUTXOs(len(utxosFiltered)) ob.utxos = utxosFiltered ob.Mu.Unlock() return nil @@ -609,7 +523,7 @@ func (ob *Observer) SaveBroadcastedTx(txHash string, nonce uint64) { ob.Mu.Unlock() broadcastEntry := clienttypes.ToOutboundHashSQLType(txHash, outboundID) - if err := ob.db.Save(&broadcastEntry).Error; err != nil { + if err := ob.DB().Save(&broadcastEntry).Error; err != nil { ob.logger.Outbound.Error(). Err(err). Msgf("SaveBroadcastedTx: error saving broadcasted txHash %s for outbound %s", txHash, outboundID) @@ -690,67 +604,23 @@ func GetRawTxResult( return btcjson.TxRawResult{}, fmt.Errorf("getRawTxResult: tx %s not included yet", hash) } -func (ob *Observer) BuildBroadcastedTxMap() error { - var broadcastedTransactions []clienttypes.OutboundHashSQLType - if err := ob.db.Find(&broadcastedTransactions).Error; err != nil { - ob.logger.Chain.Error().Err(err).Msg("error iterating over db") - return err - } - for _, entry := range broadcastedTransactions { - ob.broadcastedTx[entry.Key] = entry.Hash - } - return nil -} - -// LoadLastScannedBlock loads last scanned block from database -// The last scanned block is the height from which the observer should continue scanning for inbound transactions -func (ob *Observer) LoadLastScannedBlock() error { - // Get the latest block number from node - bn, err := ob.rpcClient.GetBlockCount() - if err != nil { - return err - } - if bn < 0 { - return fmt.Errorf("LoadLastScannedBlock: negative block number %d", bn) - } - - //Load persisted block number - var lastBlockNum clienttypes.LastBlockSQLType - if err := ob.db.First(&lastBlockNum, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") - ob.SetLastBlockHeightScanned(bn) - } else { - // #nosec G701 always in range - lastBN := int64(lastBlockNum.Num) - ob.SetLastBlockHeightScanned(lastBN) - } - - // bitcoin regtest starts from block 100 - if chains.IsBitcoinRegnet(ob.chain.ChainId) { - ob.SetLastBlockHeightScanned(100) - } - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) - - return nil -} - +// GetBlockByNumberCached gets cached block (and header) by block number func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, error) { - if result, ok := ob.BlockCache.Get(blockNumber); ok { + if result, ok := ob.BlockCache().Get(blockNumber); ok { return result.(*BTCBlockNHeader), nil } // Get the block hash - hash, err := ob.rpcClient.GetBlockHash(blockNumber) + hash, err := ob.btcClient.GetBlockHash(blockNumber) if err != nil { return nil, err } // Get the block header - header, err := ob.rpcClient.GetBlockHeader(hash) + header, err := ob.btcClient.GetBlockHeader(hash) if err != nil { return nil, err } // Get the block with verbose transactions - block, err := ob.rpcClient.GetBlockVerboseTx(hash) + block, err := ob.btcClient.GetBlockVerboseTx(hash) if err != nil { return nil, err } @@ -758,11 +628,85 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber int64) (*BTCBlockNHeader, Header: header, Block: block, } - ob.BlockCache.Add(blockNumber, blockNheader) - ob.BlockCache.Add(hash, blockNheader) + ob.BlockCache().Add(blockNumber, blockNheader) + ob.BlockCache().Add(hash, blockNheader) return blockNheader, nil } +// LoadDB open sql database and load data into Bitcoin observer +func (ob *Observer) LoadDB(dbPath string) error { + if dbPath == "" { + return errors.New("empty db path") + } + + // open database, the custom dbName is used here for backward compatibility + err := ob.OpenDB(dbPath, "btc_chain_client") + if err != nil { + return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) + } + + // run auto migration + // transaction result table is used nowhere but we still run migration in case they are needed in future + err = ob.DB().AutoMigrate( + &clienttypes.TransactionResultSQLType{}, + &clienttypes.OutboundHashSQLType{}, + ) + if err != nil { + return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) + } + + // load last scanned block + err = ob.LoadLastBlockScanned() + if err != nil { + return err + } + + // load broadcasted transactions + err = ob.LoadBroadcastedTxMap() + return err +} + +// LoadLastBlockScanned loads the last scanned block from the database +func (ob *Observer) LoadLastBlockScanned() error { + err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) + if err != nil { + return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) + } + + // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: + // 1. environment variable is set explicitly to "latest" + // 2. environment variable is empty and last scanned block is not found in DB + if ob.LastBlockScanned() == 0 { + blockNumber, err := ob.btcClient.GetBlockCount() + if err != nil { + return errors.Wrapf(err, "error GetBlockCount for chain %d", ob.Chain().ChainId) + } + // #nosec G701 always positive + ob.WithLastBlockScanned(uint64(blockNumber)) + } + + // bitcoin regtest starts from hardcoded block 100 + if chains.IsBitcoinRegnet(ob.Chain().ChainId) { + ob.WithLastBlockScanned(RegnetStartBlock) + } + ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) + + return nil +} + +// LoadBroadcastedTxMap loads broadcasted transactions from the database +func (ob *Observer) LoadBroadcastedTxMap() error { + var broadcastedTransactions []clienttypes.OutboundHashSQLType + if err := ob.DB().Find(&broadcastedTransactions).Error; err != nil { + ob.logger.Chain.Error().Err(err).Msgf("error iterating over db for chain %d", ob.Chain().ChainId) + return err + } + for _, entry := range broadcastedTransactions { + ob.broadcastedTx[entry.Key] = entry.Hash + } + return nil +} + // isTssTransaction checks if a given transaction was sent by TSS itself. // An unconfirmed transaction is safe to spend only if it was sent by TSS and verified by ourselves. func (ob *Observer) isTssTransaction(txid string) bool { @@ -774,7 +718,7 @@ func (ob *Observer) isTssTransaction(txid string) bool { func (ob *Observer) postBlockHeader(tip int64) error { ob.logger.Inbound.Info().Msgf("postBlockHeader: tip %d", tip) bn := tip - res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) + res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { bn = res.ChainState.LatestHeight + 1 } @@ -793,8 +737,8 @@ func (ob *Observer) postBlockHeader(tip int64) error { return err } blockHash := res2.Header.BlockHash() - _, err = ob.zetacoreClient.PostVoteBlockHeader( - ob.chain.ChainId, + _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ob.Chain().ChainId, blockHash[:], res2.Block.Height, proofs.NewBitcoinHeader(headerBuf.Bytes()), @@ -805,37 +749,3 @@ func (ob *Observer) postBlockHeader(tip int64) error { } return err } - -func (ob *Observer) loadDB(dbpath string) error { - if _, err := os.Stat(dbpath); os.IsNotExist(err) { - err := os.MkdirAll(dbpath, os.ModePerm) - if err != nil { - return err - } - } - path := fmt.Sprintf("%s/btc_chain_client", dbpath) - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) - if err != nil { - ob.logger.Chain.Error().Err(err).Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) - return err - } - ob.db = db - - err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}, - &clienttypes.OutboundHashSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - return err - } - - // Load last scanned block - err = ob.LoadLastScannedBlock() - if err != nil { - return err - } - - //Load broadcasted transactions - err = ob.BuildBroadcastedTxMap() - - return err -} diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 2d2494dd16..eeead6d8ae 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -1,9 +1,10 @@ -package observer +package observer_test import ( + "fmt" "math/big" + "os" "strconv" - "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -12,21 +13,17 @@ import ( "gorm.io/gorm" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" - "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/chains/bitcoin/observer" + "github.com/zeta-chain/zetacore/zetaclient/chains/interfaces" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/metrics" + "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) -const ( - // tempSQLiteDbPath is the temporary SQLite database used for testing - tempSQLiteDbPath = "file::memory:?cache=shared" -) - var ( // the relative path to the testdata directory TestDataDir = "../../../" @@ -36,7 +33,7 @@ var ( func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransactionResult) { submittedTx := map[string]btcjson.GetTransactionResult{} - db, err := gorm.Open(sqlite.Open(tempSQLiteDbPath), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open(base.TempSQLiteDBPath), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate(&clienttypes.TransactionResultSQLType{}) @@ -67,26 +64,236 @@ func setupDBTxResults(t *testing.T) (*gorm.DB, map[string]btcjson.GetTransaction return db, submittedTx } -func TestNewBitcoinObserver(t *testing.T) { - t.Run("should return error because zetacore doesn't update zetacore context", func(t *testing.T) { - cfg := config.NewConfig() - coreContext := context.NewZetacoreContext(cfg) - appContext := context.NewAppContext(coreContext, cfg) - chain := chains.BitcoinMainnet - zetacoreClient := mocks.NewMockZetacoreClient() - tss := mocks.NewMockTSS(chains.BitcoinTestnet, sample.EthAddress().String(), "") - logger := base.Logger{} - btcCfg := cfg.BitcoinConfig - ts := metrics.NewTelemetryServer() - - client, err := NewObserver(appContext, chain, zetacoreClient, tss, tempSQLiteDbPath, logger, btcCfg, ts) - require.ErrorContains(t, err, "btc chains params not initialized") - require.Nil(t, client) +// MockBTCObserver creates a mock Bitcoin observer for testing +func MockBTCObserver( + t *testing.T, + chain chains.Chain, + params observertypes.ChainParams, + btcClient interfaces.BTCRPCClient, + dbpath string, +) *observer.Observer { + // use default mock btc client if not provided + if btcClient == nil { + btcClient = mocks.NewMockBTCRPCClient().WithBlockCount(100) + } + + // use memory db if dbpath is empty + if dbpath == "" { + dbpath = "file::memory:?cache=shared" + } + + // create observer + ob, err := observer.NewObserver( + chain, + btcClient, + params, + nil, + nil, + nil, + dbpath, + base.Logger{}, + nil, + ) + require.NoError(t, err) + + return ob +} + +func Test_NewObserver(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // test cases + tests := []struct { + name string + chain chains.Chain + btcClient interfaces.BTCRPCClient + chainParams observertypes.ChainParams + coreContext *context.ZetacoreContext + coreClient interfaces.ZetacoreClient + tss interfaces.TSSSigner + dbpath string + logger base.Logger + ts *metrics.TelemetryServer + fail bool + message string + }{ + { + name: "should be able to create observer", + chain: chain, + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + chainParams: params, + coreContext: nil, + coreClient: nil, + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: false, + }, + { + name: "should fail if net params is not found", + chain: chains.Chain{ChainId: 111}, // invalid chain id + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + chainParams: params, + coreContext: nil, + coreClient: nil, + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error getting net params", + }, + { + name: "should fail on invalid dbpath", + chain: chain, + chainParams: params, + coreContext: nil, + coreClient: nil, + btcClient: mocks.NewMockBTCRPCClient().WithBlockCount(100), + tss: mocks.NewTSSMainnet(), + dbpath: "/invalid/dbpath", // invalid dbpath + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error creating db path", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create observer + ob, err := observer.NewObserver( + tt.chain, + tt.btcClient, + tt.chainParams, + tt.coreContext, + tt.coreClient, + tt.tss, + tt.dbpath, + tt.logger, + tt.ts, + ) + + // check result + if tt.fail { + require.ErrorContains(t, err, tt.message) + require.Nil(t, ob) + } else { + require.NoError(t, err) + require.NotNil(t, ob) + } + }) + } +} + +func Test_LoadDB(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create mock btc client, tss and test dbpath + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) + tss := mocks.NewTSSMainnet() + + // create observer + dbpath := testutils.CreateTempDir(t) + ob, err := observer.NewObserver(chain, btcClient, params, nil, nil, tss, dbpath, base.Logger{}, nil) + require.NoError(t, err) + + t.Run("should load db successfully", func(t *testing.T) { + err := ob.LoadDB(dbpath) + require.NoError(t, err) + require.EqualValues(t, 100, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid dbpath", func(t *testing.T) { + // load db with empty dbpath + err := ob.LoadDB("") + require.ErrorContains(t, err, "empty db path") + + // load db with invalid dbpath + err = ob.LoadDB("/invalid/dbpath") + require.ErrorContains(t, err, "error OpenDB") + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load db + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) +} + +func Test_LoadLastBlockScanned(t *testing.T) { + // use Bitcoin mainnet chain for testing + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + + // create observer using mock btc client + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(200) + dbpath := testutils.CreateTempDir(t) + + t.Run("should load last block scanned", func(t *testing.T) { + // create observer and write 199 as last block scanned + ob := MockBTCObserver(t, chain, params, btcClient, dbpath) + ob.WriteLastBlockScannedToDB(199) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, 199, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // create observer + ob := MockBTCObserver(t, chain, params, btcClient, dbpath) + + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer on separate path, as we need to reset last block scanned + otherPath := testutils.CreateTempDir(t) + obOther := MockBTCObserver(t, chain, params, btcClient, otherPath) + + // reset last block scanned to 0 so that it will be loaded from RPC + obOther.WithLastBlockScanned(0) + + // set RPC error + btcClient.WithError(fmt.Errorf("error RPC")) + + // load last block scanned + err := obOther.LoadLastBlockScanned() + require.ErrorContains(t, err, "error RPC") + }) + t.Run("should use hardcode block 100 for regtest", func(t *testing.T) { + // use regtest chain + regtest := chains.BitcoinRegtest + obRegnet := MockBTCObserver(t, regtest, params, btcClient, dbpath) + + // load last block scanned + err := obRegnet.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, observer.RegnetStartBlock, obRegnet.LastBlockScanned()) }) } func TestConfirmationThreshold(t *testing.T) { - ob := &Observer{Mu: &sync.Mutex{}} + chain := chains.BitcoinMainnet + params := mocks.MockChainParams(chain.ChainId, 10) + ob := MockBTCObserver(t, chain, params, nil, "") + t.Run("should return confirmations in chain param", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) require.Equal(t, int64(3), ob.ConfirmationsThreshold(big.NewInt(1000))) @@ -94,12 +301,16 @@ func TestConfirmationThreshold(t *testing.T) { t.Run("should return big value confirmations", func(t *testing.T) { ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: 3}) - require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(bigValueSats))) + require.Equal( + t, + int64(observer.BigValueConfirmationCount), + ob.ConfirmationsThreshold(big.NewInt(observer.BigValueSats)), + ) }) t.Run("big value confirmations is the upper cap", func(t *testing.T) { - ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: bigValueConfirmationCount + 1}) - require.Equal(t, int64(bigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) + ob.SetChainParams(observertypes.ChainParams{ConfirmationCount: observer.BigValueConfirmationCount + 1}) + require.Equal(t, int64(observer.BigValueConfirmationCount), ob.ConfirmationsThreshold(big.NewInt(1000))) }) } diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index a17d46883f..6e80e1ea5c 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -21,8 +21,8 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.Tss.BTCAddress() - return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) + tssAddr := ob.TSS().BTCAddress() + return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) } // WatchOutbound watches Bitcoin chain for outgoing txs status @@ -34,32 +34,33 @@ func (ob *Observer) WatchOutbound() { } defer ticker.Stop() - ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) + chainID := ob.Chain().ChainId + ob.logger.Outbound.Info().Msgf("WatchInbound started for chain %d", chainID) sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !context.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !context.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", chainID) continue } - trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(chainID, interfaces.Ascending) if err != nil { ob.logger.Outbound.Error(). Err(err). - Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: error GetAllOutboundTrackerByChain for chain %d", chainID) continue } for _, tracker := range trackers { // get original cctx parameters outboundID := ob.GetTxID(tracker.Nonce) - cctx, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, tracker.Nonce) + cctx, err := ob.ZetacoreClient().GetCctxByNonce(chainID, tracker.Nonce) if err != nil { ob.logger.Outbound.Info(). Err(err). - Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", ob.chain.ChainId, tracker.Nonce) + Msgf("WatchOutbound: can't find cctx for chain %d nonce %d", chainID, tracker.Nonce) break } @@ -85,10 +86,10 @@ func (ob *Observer) WatchOutbound() { txCount++ txResult = result ob.logger.Outbound.Info(). - Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, tracker.Nonce) + Msgf("WatchOutbound: included outbound %s for chain %d nonce %d", txHash.TxHash, chainID, tracker.Nonce) if txCount > 1 { ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, ob.chain.ChainId, tracker.Nonce, result) + "WatchOutbound: checkIncludedTx passed, txCount %d chain %d nonce %d result %v", txCount, chainID, tracker.Nonce, result) } } } @@ -97,12 +98,12 @@ func (ob *Observer) WatchOutbound() { ob.setIncludedTx(tracker.Nonce, txResult) } else if txCount > 1 { ob.removeIncludedTx(tracker.Nonce) // we can't tell which txHash is true, so we remove all (if any) to be safe - ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, tracker.Nonce) + ob.logger.Outbound.Error().Msgf("WatchOutbound: included multiple (%d) outbound for chain %d nonce %d", txCount, chainID, tracker.Nonce) } } ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.stop: - ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", ob.chain.ChainId) + case <-ob.StopChannel(): + ob.logger.Outbound.Info().Msgf("WatchOutbound stopped for chain %d", chainID) return } } @@ -162,7 +163,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } // Get outbound block height - blockHeight, err := GetBlockHeightByHash(ob.rpcClient, res.BlockHash) + blockHeight, err := GetBlockHeightByHash(ob.btcClient, res.BlockHash) if err != nil { return true, false, errors.Wrapf( err, @@ -172,7 +173,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg } logger.Debug().Msgf("Bitcoin outbound confirmed: txid %s, amount %s\n", res.TxID, amountInSat.String()) - zetaHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( sendHash, res.TxID, // #nosec G701 always positive @@ -182,7 +183,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg 0, // gas limit not used with Bitcoin amountInSat, chains.ReceiveStatus_success, - ob.chain, + ob.Chain(), nonce, coin.CoinType_Gas, ) @@ -299,7 +300,7 @@ func (ob *Observer) SelectUTXOs( // 2. The tracker is missing in zetacore. func (ob *Observer) refreshPendingNonce() { // get pending nonces from zetacore - p, err := ob.zetacoreClient.GetPendingNoncesByChain(ob.chain.ChainId) + p, err := ob.ZetacoreClient().GetPendingNoncesByChain(ob.Chain().ChainId) if err != nil { ob.logger.Chain.Error().Err(err).Msg("refreshPendingNonce: error getting pending nonces") } @@ -336,7 +337,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return res.TxID, nil } if !test { // if not unit test, get cctx from zetacore - send, err := ob.zetacoreClient.GetCctxByNonce(ob.chain.ChainId, nonce) + send, err := ob.ZetacoreClient().GetCctxByNonce(ob.Chain().ChainId, nonce) if err != nil { return "", errors.Wrapf(err, "getOutboundIDByNonce: error getting cctx for nonce %d", nonce) } @@ -345,7 +346,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error return "", fmt.Errorf("getOutboundIDByNonce: cannot find outbound txid for nonce %d", nonce) } // make sure it's a real Bitcoin txid - _, getTxResult, err := GetTxResultByHash(ob.rpcClient, txid) + _, getTxResult, err := GetTxResultByHash(ob.btcClient, txid) if err != nil { return "", errors.Wrapf( err, @@ -363,7 +364,7 @@ func (ob *Observer) getOutboundIDByNonce(nonce uint64, test bool) (string, error } func (ob *Observer) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { - tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() amount := chains.NonceMarkAmount(nonce) for i, utxo := range ob.utxos { sats, err := bitcoin.GetSatoshis(utxo.Amount) @@ -386,7 +387,7 @@ func (ob *Observer) checkIncludedTx( txHash string, ) (*btcjson.GetTransactionResult, bool) { outboundID := ob.GetTxID(cctx.GetCurrentOutboundParam().TssNonce) - hash, getTxResult, err := GetTxResultByHash(ob.rpcClient, txHash) + hash, getTxResult, err := GetTxResultByHash(ob.btcClient, txHash) if err != nil { ob.logger.Outbound.Error().Err(err).Msgf("checkIncludedTx: error GetTxResultByHash: %s", txHash) return nil, false @@ -470,7 +471,7 @@ func (ob *Observer) checkTssOutboundResult( ) error { params := cctx.GetCurrentOutboundParam() nonce := params.TssNonce - rawResult, err := GetRawTxResult(ob.rpcClient, hash, res) + rawResult, err := GetRawTxResult(ob.btcClient, hash, res) if err != nil { return errors.Wrapf(err, "checkTssOutboundResult: error GetRawTxResultByHash %s", hash.String()) } @@ -507,7 +508,7 @@ func (ob *Observer) checkTSSVin(vins []btcjson.Vin, nonce uint64) error { if nonce > 0 && len(vins) <= 1 { return fmt.Errorf("checkTSSVin: len(vins) <= 1") } - pubKeyTss := hex.EncodeToString(ob.Tss.PubKeyCompressedBytes()) + pubKeyTss := hex.EncodeToString(ob.TSS().PubKeyCompressedBytes()) for i, vin := range vins { // The length of the Witness should be always 2 for SegWit inputs. if len(vin.Witness) != 2 { @@ -547,7 +548,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ } nonce := params.TssNonce - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout receiverExpected := tssAddress @@ -555,7 +556,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ // the 2nd output is the payment to recipient receiverExpected = params.Receiver } - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.chain) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, receiverExpected, ob.Chain()) if err != nil { return err } @@ -606,10 +607,10 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams } nonce := params.TssNonce - tssAddress := ob.Tss.BTCAddress() + tssAddress := ob.TSS().BTCAddress() for _, vout := range vouts { // decode receiver and amount from vout - receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.chain) + receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain()) if err != nil { return errors.Wrap(err, "checkTSSVoutCancelled: error decoding P2WPKH vout") } diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index bad0997c0c..910384ef9a 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -3,7 +3,6 @@ package observer import ( "math" "sort" - "sync" "testing" "github.com/btcsuite/btcd/btcjson" @@ -11,22 +10,27 @@ import ( "github.com/stretchr/testify/require" "github.com/zeta-chain/zetacore/pkg/chains" - "github.com/zeta-chain/zetacore/zetaclient/config" - "github.com/zeta-chain/zetacore/zetaclient/context" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) -func MockBTCObserverMainnet() *Observer { - cfg := config.NewConfig() - coreContext := context.NewZetacoreContext(cfg) +// the relative path to the testdata directory +var TestDataDir = "../../../" - return &Observer{ - chain: chains.BitcoinMainnet, - zetacoreClient: mocks.NewMockZetacoreClient(), - Tss: mocks.NewTSSMainnet(), - coreContext: coreContext, - } +// MockBTCObserverMainnet creates a mock Bitcoin mainnet observer for testing +func MockBTCObserverMainnet(t *testing.T) *Observer { + // setup mock arguments + chain := chains.BitcoinMainnet + btcClient := mocks.NewMockBTCRPCClient().WithBlockCount(100) + params := mocks.MockChainParams(chain.ChainId, 10) + tss := mocks.NewTSSMainnet() + + // create Bitcoin observer + ob, err := NewObserver(chain, btcClient, params, nil, nil, tss, base.TempSQLiteDBPath, base.Logger{}, nil) + require.NoError(t, err) + + return ob } // helper function to create a test Bitcoin observer @@ -37,26 +41,27 @@ func createObserverWithPrivateKey(t *testing.T) *Observer { tss := &mocks.TSS{ PrivKey: privateKey, } - return &Observer{ - Tss: tss, - Mu: &sync.Mutex{}, - includedTxResults: make(map[string]*btcjson.GetTransactionResult), - } + + // create Bitcoin observer with mock tss + ob := MockBTCObserverMainnet(t) + ob.WithTSS(tss) + + return ob } // helper function to create a test Bitcoin observer with UTXOs func createObserverWithUTXOs(t *testing.T) *Observer { // Create Bitcoin observer - client := createObserverWithPrivateKey(t) - tssAddress := client.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + ob := createObserverWithPrivateKey(t) + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() // Create 10 dummy UTXOs (22.44 BTC in total) - client.utxos = make([]btcjson.ListUnspentResult, 0, 10) + ob.utxos = make([]btcjson.ListUnspentResult, 0, 10) amounts := []float64{0.01, 0.12, 0.18, 0.24, 0.5, 1.26, 2.97, 3.28, 5.16, 8.72} for _, amount := range amounts { - client.utxos = append(client.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) + ob.utxos = append(ob.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) } - return client + return ob } func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex int) { @@ -65,7 +70,7 @@ func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex i ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark - tssAddress := ob.Tss.BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() nonceMark := btcjson.ListUnspentResult{ TxID: txid, Address: tssAddress, @@ -90,22 +95,22 @@ func TestCheckTSSVout(t *testing.T) { nonce := uint64(148) // create mainnet mock client - btcClient := MockBTCObserverMainnet() + ob := MockBTCObserverMainnet(t) t.Run("valid TSS vout should pass", func(t *testing.T) { rawResult, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 2 or > 3", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVout(params, []btcjson.Vout{{}}) + err := ob.checkTSSVout(params, []btcjson.Vout{{}}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) + err = ob.checkTSSVout(params, []btcjson.Vout{{}, {}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail on invalid TSS vout", func(t *testing.T) { @@ -114,7 +119,7 @@ func TestCheckTSSVout(t *testing.T) { // invalid TSS vout rawResult.Vout[0].ScriptPubKey.Hex = "invalid script" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.Error(t, err) }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -123,7 +128,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -132,7 +137,7 @@ func TestCheckTSSVout(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the receiver address", func(t *testing.T) { @@ -141,7 +146,7 @@ func TestCheckTSSVout(t *testing.T) { // not receiver address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params receiver") }) t.Run("should fail if vout 1 not match payment amount", func(t *testing.T) { @@ -150,7 +155,7 @@ func TestCheckTSSVout(t *testing.T) { // not match payment amount rawResult.Vout[1].Value = 0.00011000 - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match params amount") }) t.Run("should fail if vout 2 is not to the TSS address", func(t *testing.T) { @@ -159,7 +164,7 @@ func TestCheckTSSVout(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[2].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVout(params, rawResult.Vout) + err := ob.checkTSSVout(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } @@ -172,7 +177,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { nonce := uint64(148) // create mainnet mock client - btcClient := MockBTCObserverMainnet() + ob := MockBTCObserverMainnet(t) t.Run("valid TSS vout should pass", func(t *testing.T) { // remove change vout to simulate cancelled tx @@ -181,17 +186,17 @@ func TestCheckTSSVoutCancelled(t *testing.T) { rawResult.Vout = rawResult.Vout[:2] params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.NoError(t, err) }) t.Run("should fail if vout length < 1 or > 2", func(t *testing.T) { _, cctx := testutils.LoadBTCTxRawResultNCctx(t, TestDataDir, chainID, nonce) params := cctx.GetCurrentOutboundParam() - err := btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{}) + err := ob.checkTSSVoutCancelled(params, []btcjson.Vout{}) require.ErrorContains(t, err, "invalid number of vouts") - err = btcClient.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) + err = ob.checkTSSVoutCancelled(params, []btcjson.Vout{{}, {}, {}}) require.ErrorContains(t, err, "invalid number of vouts") }) t.Run("should fail if vout 0 is not to the TSS address", func(t *testing.T) { @@ -203,7 +208,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[0].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) t.Run("should fail if vout 0 not match nonce mark", func(t *testing.T) { @@ -215,7 +220,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not match nonce mark rawResult.Vout[0].Value = 0.00000147 - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match nonce-mark amount") }) t.Run("should fail if vout 1 is not to the TSS address", func(t *testing.T) { @@ -228,7 +233,7 @@ func TestCheckTSSVoutCancelled(t *testing.T) { // not TSS address, bc1qh297vdt8xq6df5xae9z8gzd4jsu9a392mp0dus rawResult.Vout[1].ScriptPubKey.Hex = "0014ba8be635673034d4d0ddc9447409b594385ec4aa" - err := btcClient.checkTSSVoutCancelled(params, rawResult.Vout) + err := ob.checkTSSVoutCancelled(params, rawResult.Vout) require.ErrorContains(t, err, "not match TSS address") }) } diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index 1e261c4d0a..41ada01375 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -36,33 +36,33 @@ import ( // WatchInbound watches evm chain for incoming txs and post votes to zetacore func (ob *Observer) WatchInbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInbound_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchInbound_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error creating ticker") + ob.Logger().Inbound.Error().Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("WatchInbound started for chain %d", ob.chain.ChainId) - sampledLogger := ob.logger.Inbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.Logger().Inbound.Info().Msgf("WatchInbound started for chain %d", ob.Chain().ChainId) + sampledLogger := ob.Logger().Inbound.Sample(&zerolog.BasicSampler{N: 10}) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchInbound: inbound observation is disabled for chain %d", ob.Chain().ChainId) continue } err := ob.ObserveInbound(sampledLogger) if err != nil { - ob.logger.Inbound.Err(err).Msg("WatchInbound: observeInbound error") + ob.Logger().Inbound.Err(err).Msg("WatchInbound: observeInbound error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.chain.ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + case <-ob.StopChannel(): + ob.Logger().Inbound.Info().Msgf("WatchInbound stopped for chain %d", ob.Chain().ChainId) return } } @@ -72,29 +72,29 @@ func (ob *Observer) WatchInbound() { // If it was, it tries to broadcast the confirmation vote. If this zeta client has previously broadcast the vote, the tx would be rejected func (ob *Observer) WatchInboundTracker() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchInboundTracker_%d", ob.Chain().ChainId), ob.GetChainParams().InboundTicker, ) if err != nil { - ob.logger.Inbound.Err(err).Msg("error creating ticker") + ob.Logger().Inbound.Err(err).Msg("error creating ticker") return } defer ticker.Stop() - ob.logger.Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("Inbound tracker watcher started for chain %d", ob.Chain().ChainId) for { select { case <-ticker.C(): - if !clientcontext.IsInboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsInboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { continue } err := ob.ProcessInboundTrackers() if err != nil { - ob.logger.Inbound.Err(err).Msg("ProcessInboundTrackers error") + ob.Logger().Inbound.Err(err).Msg("ProcessInboundTrackers error") } - ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.logger.Inbound) - case <-ob.stop: - ob.logger.Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.chain.ChainId) + ticker.UpdateInterval(ob.GetChainParams().InboundTicker, ob.Logger().Inbound) + case <-ob.StopChannel(): + ob.Logger().Inbound.Info().Msgf("WatchInboundTracker stopped for chain %d", ob.Chain().ChainId) return } } @@ -102,7 +102,7 @@ func (ob *Observer) WatchInboundTracker() { // ProcessInboundTrackers processes inbound trackers from zetacore func (ob *Observer) ProcessInboundTrackers() error { - trackers, err := ob.zetacoreClient.GetInboundTrackersForChain(ob.chain.ChainId) + trackers, err := ob.ZetacoreClient().GetInboundTrackersForChain(ob.Chain().ChainId) if err != nil { return err } @@ -114,15 +114,20 @@ func (ob *Observer) ProcessInboundTrackers() error { err, "error getting transaction for inbound %s chain %d", tracker.TxHash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tracker.TxHash)) if err != nil { - return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) + return errors.Wrapf( + err, + "error getting receipt for inbound %s chain %d", + tracker.TxHash, + ob.Chain().ChainId, + ) } - ob.logger.Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("checking tracker for inbound %s chain %d", tracker.TxHash, ob.Chain().ChainId) // check and vote on inbound tx switch tracker.CoinType { @@ -137,11 +142,11 @@ func (ob *Observer) ProcessInboundTrackers() error { "unknown coin type %s for inbound %s chain %d", tracker.CoinType, tx.Hash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } if err != nil { - return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + return errors.Wrapf(err, "error checking and voting for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } } return nil @@ -153,17 +158,17 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if err != nil { return err } - if blockNumber < ob.GetLastBlockHeight() { + if blockNumber < ob.LastBlock() { return fmt.Errorf( "observeInbound: block number should not decrease: current %d last %d", blockNumber, - ob.GetLastBlockHeight(), + ob.LastBlock(), ) } - ob.SetLastBlockHeight(blockNumber) + ob.WithLastBlock(blockNumber) // increment prom counter - metrics.GetBlockByNumberPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetBlockByNumberPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // skip if current height is too low if blockNumber < ob.GetChainParams().ConfirmationCount { @@ -172,10 +177,10 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { confirmedBlockNum := blockNumber - ob.GetChainParams().ConfirmationCount // skip if no new block is confirmed - lastScanned := ob.GetLastBlockHeightScanned() + lastScanned := ob.LastBlockScanned() if lastScanned >= confirmedBlockNum { sampledLogger.Debug(). - Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.chain.ChainId) + Msgf("observeInbound: skipping observer, no new block is produced for chain %d", ob.Chain().ChainId) return nil } @@ -205,12 +210,11 @@ func (ob *Observer) ObserveInbound(sampledLogger zerolog.Logger) error { if lastScannedLowest > lastScanned { sampledLogger.Info(). Msgf("observeInbound: lasstScanned heights for chain %d ZetaSent %d ERC20Deposited %d TssRecvd %d", - ob.chain.ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) - ob.SetLastBlockHeightScanned(lastScannedLowest) - if err := ob.db.Save(clienttypes.ToLastBlockSQLType(lastScannedLowest)).Error; err != nil { - ob.logger.Inbound.Error(). + ob.Chain().ChainId, lastScannedZetaSent, lastScannedDeposited, lastScannedTssRecvd) + if err := ob.SaveLastBlockScanned(lastScannedLowest); err != nil { + ob.Logger().Inbound.Error(). Err(err). - Msgf("observeInbound: error writing lastScannedLowest %d to db", lastScannedLowest) + Msgf("observeInbound: error saving lastScannedLowest %d to db", lastScannedLowest) } } return nil @@ -222,7 +226,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { // filter ZetaSent logs addrConnector, connector, err := ob.GetConnectorContract() if err != nil { - ob.logger.Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") + ob.Logger().Chain.Warn().Err(err).Msgf("ObserveZetaSent: GetConnectorContract error:") return startBlock - 1 // lastScanned } iter, err := connector.FilterZetaSent(&bind.FilterOpts{ @@ -231,8 +235,8 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}, []*big.Int{}) if err != nil { - ob.logger.Chain.Warn().Err(err).Msgf( - "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) + ob.Logger().Chain.Warn().Err(err).Msgf( + "ObserveZetaSent: FilterZetaSent error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) return startBlock - 1 // lastScanned } @@ -245,10 +249,10 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveZetaSent: invalid ZetaSent event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -261,7 +265,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // post to zetacore beingScanned := uint64(0) @@ -273,7 +277,7 @@ func (ob *Observer) ObserveZetaSent(startBlock, toBlock uint64) uint64 { } // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("ObserveZetaSent: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -301,7 +305,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { // filter ERC20CustodyDeposited logs addrCustody, erc20custodyContract, err := ob.GetERC20CustodyContract() if err != nil { - ob.logger.Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") + ob.Logger().Inbound.Warn().Err(err).Msgf("ObserveERC20Deposited: GetERC20CustodyContract error:") return startBlock - 1 // lastScanned } @@ -311,8 +315,8 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { Context: context.TODO(), }, []ethcommon.Address{}) if err != nil { - ob.logger.Inbound.Warn().Err(err).Msgf( - "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.chain.ChainId) + ob.Logger().Inbound.Warn().Err(err).Msgf( + "ObserveERC20Deposited: FilterDeposited error from block %d to %d for chain %d", startBlock, toBlock, ob.Chain().ChainId) return startBlock - 1 // lastScanned } @@ -325,10 +329,10 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { events = append(events, iter.Event) continue } - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Err(err). Msgf("ObserveERC20Deposited: invalid Deposited event in tx %s on chain %d at height %d", - iter.Event.Raw.TxHash.Hex(), ob.chain.ChainId, iter.Event.Raw.BlockNumber) + iter.Event.Raw.TxHash.Hex(), ob.Chain().ChainId, iter.Event.Raw.BlockNumber) } sort.SliceStable(events, func(i, j int) bool { if events[i].Raw.BlockNumber == events[j].Raw.BlockNumber { @@ -341,7 +345,7 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { }) // increment prom counter - metrics.GetFilterLogsPerChain.WithLabelValues(ob.chain.ChainName.String()).Inc() + metrics.GetFilterLogsPerChain.WithLabelValues(ob.Chain().ChainName.String()).Inc() // post to zeatcore guard := make(map[string]bool) @@ -353,15 +357,15 @@ func (ob *Observer) ObserveERC20Deposited(startBlock, toBlock uint64) uint64 { } tx, _, err := ob.TransactionByHash(event.Raw.TxHash.Hex()) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf( - "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf( + "ObserveERC20Deposited: error getting transaction for inbound %s chain %d", event.Raw.TxHash, ob.Chain().ChainId) return beingScanned - 1 // we have to re-scan from this block next time } sender := ethcommon.HexToAddress(tx.From) // guard against multiple events in the same tx if guard[event.Raw.TxHash.Hex()] { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("ObserveERC20Deposited: multiple remote call events detected in tx %s", event.Raw.TxHash) continue } @@ -387,23 +391,23 @@ func (ob *Observer) ObserverTSSReceive(startBlock, toBlock uint64) uint64 { // post new block header (if any) to zetacore and ignore error // TODO: consider having a independent ticker(from TSS scaning) for posting block headers // https://github.com/zeta-chain/node/issues/1847 - blockHeaderVerification, found := ob.coreContext.GetBlockHeaderEnabledChains(ob.chain.ChainId) + blockHeaderVerification, found := ob.ZetacoreContext().GetBlockHeaderEnabledChains(ob.Chain().ChainId) if found && blockHeaderVerification.Enabled { // post block header for supported chains // TODO: move this logic in its own routine // https://github.com/zeta-chain/node/issues/2204 err := ob.postBlockHeader(toBlock) if err != nil { - ob.logger.Inbound.Error().Err(err).Msg("error posting block header") + ob.Logger().Inbound.Error().Err(err).Msg("error posting block header") } } // observe TSS received gas token in block 'bn' err := ob.ObserveTSSReceiveInBlock(bn) if err != nil { - ob.logger.Inbound.Error(). + ob.Logger().Inbound.Error(). Err(err). - Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.chain.ChainId) + Msgf("ObserverTSSReceive: error observing TSS received token in block %d for chain %d", bn, ob.Chain().ChainId) return bn - 1 // we have to re-scan from this block next time } } @@ -418,7 +422,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -442,7 +446,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( if err == nil { msg = ob.BuildInboundVoteMsgForZetaSentEvent(event) } else { - ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err } break // only one event is allowed per tx @@ -450,7 +454,7 @@ func (ob *Observer) CheckAndVoteInboundTokenZeta( } if msg == nil { // no event, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no ZetaSent event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -467,7 +471,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -492,7 +496,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( if err == nil { msg = ob.BuildInboundVoteMsgForDepositedEvent(zetaDeposited, sender) } else { - ob.logger.Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Error().Err(err).Msgf("CheckEvmTxLog error on inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", err } break // only one event is allowed per tx @@ -500,7 +504,7 @@ func (ob *Observer) CheckAndVoteInboundTokenERC20( } if msg == nil { // no event, donation, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no Deposited event found for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -517,7 +521,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( vote bool, ) (string, error) { // check confirmations - if confirmed := ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()); !confirmed { + if confirmed := ob.HasEnoughConfirmations(receipt, ob.LastBlock()); !confirmed { return "", fmt.Errorf( "inbound %s has not been confirmed yet: receipt block %d", tx.Hash, @@ -526,7 +530,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( } // checks receiver and tx status - if ethcommon.HexToAddress(tx.To) != ob.Tss.EVMAddress() { + if ethcommon.HexToAddress(tx.To) != ob.TSS().EVMAddress() { return "", fmt.Errorf("tx.To %s is not TSS address", tx.To) } if receipt.Status != ethtypes.ReceiptStatusSuccessful { @@ -538,7 +542,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( msg := ob.BuildInboundVoteMsgForTokenSentToTSS(tx, sender, receipt.BlockNumber.Uint64()) if msg == nil { // donation, restricted tx, etc. - ob.logger.Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info().Msgf("no vote message built for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) return "", nil } if vote { @@ -555,16 +559,16 @@ func (ob *Observer) PostVoteInbound( retryGasLimit uint64, ) (string, error) { txHash := msg.InboundHash - chainID := ob.chain.ChainId - zetaHash, ballot, err := ob.zetacoreClient.PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) + chainID := ob.Chain().ChainId + zetaHash, ballot, err := ob.ZetacoreClient().PostVoteInbound(zetacore.PostVoteInboundGasLimit, retryGasLimit, msg) if err != nil { - ob.logger.Inbound.Err(err). + ob.Logger().Inbound.Err(err). Msgf("inbound detected: error posting vote for chain %d token %s inbound %s", chainID, coinType, txHash) return "", err } else if zetaHash != "" { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) + ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s vote %s ballot %s", chainID, coinType, txHash, zetaHash, ballot) } else { - ob.logger.Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) + ob.Logger().Inbound.Info().Msgf("inbound detected: chain %d token %s inbound %s already voted on ballot %s", chainID, coinType, txHash, ballot) } return ballot, err @@ -589,10 +593,10 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( } if config.ContainRestrictedAddress(sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), maybeReceiver) { compliance.PrintComplianceLog( - ob.logger.Inbound, - ob.logger.Compliance, + ob.Logger().Inbound, + ob.Logger().Compliance, false, - ob.chain.ChainId, + ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender.Hex(), clienttypes.BytesToEthHex(event.Recipient), @@ -603,21 +607,22 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( // donation check if bytes.Equal(event.Message, []byte(constant.DonationMessage)) { - ob.logger.Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.chain.ChainId) + ob.Logger().Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", event.Raw.TxHash.Hex(), ob.Chain().ChainId) return nil } message := hex.EncodeToString(event.Message) - ob.logger.Inbound.Info(). + ob.Logger().Inbound.Info(). Msgf("ERC20CustodyDeposited inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) + ob.Chain(). + ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender.Hex(), event.Amount.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.chain.ChainId, + ob.Chain().ChainId, "", clienttypes.BytesToEthHex(event.Recipient), - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, sdkmath.NewUintFromBigInt(event.Amount), hex.EncodeToString(event.Message), event.Raw.TxHash.Hex(), @@ -625,7 +630,7 @@ func (ob *Observer) BuildInboundVoteMsgForDepositedEvent( 1_500_000, coin.CoinType_ERC20, event.Asset.String(), - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -636,7 +641,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( ) *types.MsgVoteInbound { destChain := chains.GetChainFromChainID(event.DestinationChainId.Int64()) if destChain == nil { - ob.logger.Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) + ob.Logger().Inbound.Warn().Msgf("chain id not supported %d", event.DestinationChainId.Int64()) return nil } destAddr := clienttypes.BytesToEthHex(event.DestinationAddress) @@ -644,32 +649,33 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( // compliance check sender := event.ZetaTxSenderAddress.Hex() if config.ContainRestrictedAddress(sender, destAddr, event.SourceTxOriginAddress.Hex()) { - compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, event.Raw.TxHash.Hex(), sender, destAddr, "Zeta") return nil } if !destChain.IsZetaChain() { - paramsDest, found := ob.coreContext.GetEVMChainParams(destChain.ChainId) + paramsDest, found := ob.ZetacoreContext().GetEVMChainParams(destChain.ChainId) if !found { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("chain id not present in EVMChainParams %d", event.DestinationChainId.Int64()) return nil } if strings.EqualFold(destAddr, paramsDest.ZetaTokenContractAddress) { - ob.logger.Inbound.Warn(). + ob.Logger().Inbound.Warn(). Msgf("potential attack attempt: %s destination address is ZETA token contract address %s", destChain, destAddr) return nil } } message := base64.StdEncoding.EncodeToString(event.Message) - ob.logger.Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) + ob.Logger().Inbound.Info().Msgf("ZetaSent inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.Chain(). + ChainId, event.Raw.TxHash.Hex(), event.Raw.BlockNumber, sender, event.ZetaValueAndGas.String(), message) return zetacore.GetInboundVoteMessage( sender, - ob.chain.ChainId, + ob.Chain().ChainId, event.SourceTxOriginAddress.Hex(), destAddr, destChain.ChainId, @@ -680,7 +686,7 @@ func (ob *Observer) BuildInboundVoteMsgForZetaSentEvent( event.DestinationGasLimit.Uint64(), coin.CoinType_Zeta, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), event.Raw.Index, ) } @@ -700,8 +706,8 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( maybeReceiver = parsedAddress.Hex() } if config.ContainRestrictedAddress(sender.Hex(), maybeReceiver) { - compliance.PrintComplianceLog(ob.logger.Inbound, ob.logger.Compliance, - false, ob.chain.ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") + compliance.PrintComplianceLog(ob.Logger().Inbound, ob.Logger().Compliance, + false, ob.Chain().ChainId, tx.Hash, sender.Hex(), sender.Hex(), "Gas") return nil } @@ -709,19 +715,19 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( // #nosec G703 err is already checked data, _ := hex.DecodeString(message) if bytes.Equal(data, []byte(constant.DonationMessage)) { - ob.logger.Inbound.Info(). - Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.chain.ChainId) + ob.Logger().Inbound.Info(). + Msgf("thank you rich folk for your donation! tx %s chain %d", tx.Hash, ob.Chain().ChainId) return nil } - ob.logger.Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", - ob.chain.ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) + ob.Logger().Inbound.Info().Msgf("TSS inbound detected on chain %d tx %s block %d from %s value %s message %s", + ob.Chain().ChainId, tx.Hash, blockNumber, sender.Hex(), tx.Value.String(), message) return zetacore.GetInboundVoteMessage( sender.Hex(), - ob.chain.ChainId, + ob.Chain().ChainId, sender.Hex(), sender.Hex(), - ob.zetacoreClient.Chain().ChainId, + ob.ZetacoreClient().Chain().ChainId, sdkmath.NewUintFromBigInt(&tx.Value), message, tx.Hash, @@ -729,7 +735,7 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( 90_000, coin.CoinType_Gas, "", - ob.zetacoreClient.GetKeys().GetOperatorAddress().String(), + ob.ZetacoreClient().GetKeys().GetOperatorAddress().String(), 0, // not a smart contract call ) } @@ -738,15 +744,15 @@ func (ob *Observer) BuildInboundVoteMsgForTokenSentToTSS( func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { block, err := ob.GetBlockByNumberCached(blockNumber) if err != nil { - return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.chain.ChainId) + return errors.Wrapf(err, "error getting block %d for chain %d", blockNumber, ob.Chain().ChainId) } for i := range block.Transactions { tx := block.Transactions[i] - if ethcommon.HexToAddress(tx.To) == ob.Tss.EVMAddress() { + if ethcommon.HexToAddress(tx.To) == ob.TSS().EVMAddress() { receipt, err := ob.evmClient.TransactionReceipt(context.Background(), ethcommon.HexToHash(tx.Hash)) if err != nil { - return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.chain.ChainId) + return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) } _, err = ob.CheckAndVoteInboundTokenGas(&tx, receipt, true) @@ -755,7 +761,7 @@ func (ob *Observer) ObserveTSSReceiveInBlock(blockNumber uint64) error { err, "error checking and voting inbound gas asset for inbound %s chain %d", tx.Hash, - ob.chain.ChainId, + ob.Chain().ChainId, ) } } diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index 906634e1ad..bb8930f4ca 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -40,7 +40,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -56,7 +56,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -72,7 +72,7 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -89,7 +89,17 @@ func Test_CheckAndVoteInboundTokenZeta(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain connector - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) + ob := MockEVMObserver( + t, + chain, + nil, + nil, + nil, + nil, + memDBPath, + lastBlock, + mocks.MockChainParams(chainID, confirmation), + ) _, err := ob.CheckAndVoteInboundTokenZeta(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -115,7 +125,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -131,7 +141,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -147,7 +157,7 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.NoError(t, err) require.Equal(t, "", ballot) @@ -164,7 +174,17 @@ func Test_CheckAndVoteInboundTokenERC20(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation chainID = 56 // use BSC chain ERC20 custody - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, mocks.MockChainParams(chainID, confirmation)) + ob := MockEVMObserver( + t, + chain, + nil, + nil, + nil, + nil, + memDBPath, + lastBlock, + mocks.MockChainParams(chainID, confirmation), + ) _, err := ob.CheckAndVoteInboundTokenERC20(tx, receipt, true) require.ErrorContains(t, err, "emitter address mismatch") }) @@ -190,7 +210,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, cctx.InboundParams.BallotIndex, ballot) @@ -200,7 +220,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - 1 - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) _, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not been confirmed") }) @@ -210,7 +230,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not TSS address") require.Equal(t, "", ballot) @@ -221,7 +241,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.ErrorContains(t, err, "not a successful tx") require.Equal(t, "", ballot) @@ -232,7 +252,7 @@ func Test_CheckAndVoteInboundTokenGas(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(tx)) lastBlock := receipt.BlockNumber.Uint64() + confirmation - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, lastBlock, chainParam) ballot, err := ob.CheckAndVoteInboundTokenGas(tx, receipt, false) require.NoError(t, err) require.Equal(t, "", ballot) @@ -249,7 +269,7 @@ func Test_BuildInboundVoteMsgForZetaSentEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_Zeta, inboundHash) // parse ZetaSent event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) connector := mocks.MockConnectorNonEth(t, chainID) event := testutils.ParseReceiptZetaSent(receipt, connector) @@ -296,7 +316,7 @@ func Test_BuildInboundVoteMsgForDepositedEvent(t *testing.T) { cctx := testutils.LoadCctxByInbound(t, chainID, coin.CoinType_ERC20, inboundHash) // parse Deposited event - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) custody := mocks.MockERC20Custody(t, chainID) event := testutils.ParseReceiptERC20Deposited(receipt, custody) sender := ethcommon.HexToAddress(tx.From) @@ -354,7 +374,7 @@ func Test_BuildInboundVoteMsgForTokenSentToTSS(t *testing.T) { require.NoError(t, evm.ValidateEvmTransaction(txDonation)) // create test compliance config - ob := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, mocks.MockChainParams(1, 1)) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, mocks.MockChainParams(1, 1)) cfg := config.Config{ ComplianceConfig: config.ComplianceConfig{}, } @@ -424,7 +444,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { lastBlock := receipt.BlockNumber.Uint64() + confirmation t.Run("should observe TSS receive in block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) // feed archived block and receipt evmJSONRPC.WithBlock(block) @@ -433,20 +453,20 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { require.NoError(t, err) }) t.Run("should not observe on error getting block", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock JSONRPC contains no block require.ErrorContains(t, err, "error getting block") }) t.Run("should not observe on error getting receipt", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) evmJSONRPC.WithBlock(block) err := ob.ObserveTSSReceiveInBlock(blockNumber) // error getting block is expected because the mock evmClient contains no receipt require.ErrorContains(t, err, "error getting receipt") }) t.Run("should not observe on error posting vote", func(t *testing.T) { - ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, lastBlock, chainParam) + ob := MockEVMObserver(t, chain, evmClient, evmJSONRPC, zetacoreClient, tss, memDBPath, lastBlock, chainParam) // feed archived block and pause zetacore client evmJSONRPC.WithBlock(block) diff --git a/zetaclient/chains/evm/observer/observer.go b/zetaclient/chains/evm/observer/observer.go index 99db258f5a..158b3e326c 100644 --- a/zetaclient/chains/evm/observer/observer.go +++ b/zetaclient/chains/evm/observer/observer.go @@ -5,29 +5,20 @@ import ( "fmt" "math" "math/big" - "os" - "strconv" "strings" "sync" - "sync/atomic" "time" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rlp" - lru "github.com/hashicorp/golang-lru" "github.com/onrik/ethrpc" "github.com/pkg/errors" - "github.com/rs/zerolog" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/erc20custody.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zeta.non-eth.sol" zetaconnectoreth "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.eth.sol" "github.com/zeta-chain/protocol-contracts/pkg/contracts/evm/zetaconnector.non-eth.sol" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "github.com/zeta-chain/zetacore/pkg/chains" "github.com/zeta-chain/zetacore/pkg/proofs" observertypes "github.com/zeta-chain/zetacore/x/observer/types" "github.com/zeta-chain/zetacore/zetaclient/chains/base" @@ -39,225 +30,128 @@ import ( clienttypes "github.com/zeta-chain/zetacore/zetaclient/types" ) -// Logger is the logger for evm chains -// TODO: Merge this logger with the one in bitcoin -// https://github.com/zeta-chain/node/issues/2022 -type Logger struct { - // Chain is the parent logger for the chain - Chain zerolog.Logger +var _ interfaces.ChainObserver = &Observer{} - // Inbound is the logger for incoming transactions - Inbound zerolog.Logger +// Observer is the observer for evm chains +type Observer struct { + // base.Observer implements the base chain observer + base.Observer - // Outbound is the logger for outgoing transactions - Outbound zerolog.Logger + // evmClient is the EVM client for the observed chain + evmClient interfaces.EVMRPCClient - // GasPrice is the logger for gas prices - GasPrice zerolog.Logger + // evmJSONRPC is the EVM JSON RPC client for the observed chain + evmJSONRPC interfaces.EVMJSONRPCClient - // Compliance is the logger for compliance checks - Compliance zerolog.Logger -} + // outboundPendingTransactions is the map to index pending transactions by hash + outboundPendingTransactions map[string]*ethtypes.Transaction -var _ interfaces.ChainObserver = &Observer{} + // outboundConfirmedReceipts is the map to index confirmed receipts by hash + outboundConfirmedReceipts map[string]*ethtypes.Receipt -// Observer is the observer for evm chains -type Observer struct { - Tss interfaces.TSSSigner + // outboundConfirmedTransactions is the map to index confirmed transactions by hash + outboundConfirmedTransactions map[string]*ethtypes.Transaction + // Mu protects the maps and chain params from concurrent access Mu *sync.Mutex - - chain chains.Chain - evmClient interfaces.EVMRPCClient - evmJSONRPC interfaces.EVMJSONRPCClient - zetacoreClient interfaces.ZetacoreClient - lastBlockScanned uint64 - lastBlock uint64 - db *gorm.DB - outboundPendingTransactions map[string]*ethtypes.Transaction - outboundConfirmedReceipts map[string]*ethtypes.Receipt - outboundConfirmedTransactions map[string]*ethtypes.Transaction - stop chan struct{} - logger Logger - coreContext *clientcontext.ZetacoreContext - chainParams observertypes.ChainParams - ts *metrics.TelemetryServer - - blockCache *lru.Cache - headerCache *lru.Cache } // NewObserver returns a new EVM chain observer func NewObserver( - appContext *clientcontext.AppContext, + evmCfg config.EVMConfig, + evmClient interfaces.EVMRPCClient, + chainParams observertypes.ChainParams, + zetacoreContext *clientcontext.ZetacoreContext, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, dbpath string, logger base.Logger, - evmCfg config.EVMConfig, ts *metrics.TelemetryServer, ) (*Observer, error) { - ob := Observer{ - ts: ts, - } - - chainLogger := logger.Std.With().Str("chain", evmCfg.Chain.ChainName.String()).Logger() - ob.logger = Logger{ - Chain: chainLogger, - Inbound: chainLogger.With().Str("module", "WatchInbound").Logger(), - Outbound: chainLogger.With().Str("module", "WatchOutbound").Logger(), - GasPrice: chainLogger.With().Str("module", "WatchGasPrice").Logger(), - Compliance: logger.Compliance, - } - - ob.coreContext = appContext.ZetacoreContext() - chainParams, found := ob.coreContext.GetEVMChainParams(evmCfg.Chain.ChainId) - if !found { - return nil, fmt.Errorf("evm chains params not initialized for chain %d", evmCfg.Chain.ChainId) - } - - ob.chainParams = *chainParams - ob.stop = make(chan struct{}) - ob.chain = evmCfg.Chain - ob.Mu = &sync.Mutex{} - ob.zetacoreClient = zetacoreClient - ob.Tss = tss - ob.outboundPendingTransactions = make(map[string]*ethtypes.Transaction) - ob.outboundConfirmedReceipts = make(map[string]*ethtypes.Receipt) - ob.outboundConfirmedTransactions = make(map[string]*ethtypes.Transaction) - - ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.ChainName.String(), evmCfg.Endpoint) - client, err := ethclient.Dial(evmCfg.Endpoint) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("eth Client Dial") - return nil, err - } - - ob.evmClient = client - ob.evmJSONRPC = ethrpc.NewEthRPC(evmCfg.Endpoint) - - // create block header and block caches - ob.blockCache, err = lru.New(1000) + // create base observer + baseObserver, err := base.NewObserver( + evmCfg.Chain, + chainParams, + zetacoreContext, + zetacoreClient, + tss, + base.DefaultBlockCacheSize, + base.DefaultHeaderCacheSize, + ts, + logger, + ) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create block cache") return nil, err } - ob.headerCache, err = lru.New(1000) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("failed to create header cache") - return nil, err + // create evm observer + ob := &Observer{ + Observer: *baseObserver, + evmClient: evmClient, + evmJSONRPC: ethrpc.NewEthRPC(evmCfg.Endpoint), + outboundPendingTransactions: make(map[string]*ethtypes.Transaction), + outboundConfirmedReceipts: make(map[string]*ethtypes.Receipt), + outboundConfirmedTransactions: make(map[string]*ethtypes.Transaction), + Mu: &sync.Mutex{}, } - err = ob.LoadDB(dbpath, ob.chain) + // open database and load data + err = ob.LoadDB(dbpath) if err != nil { return nil, err } - ob.logger.Chain.Info().Msgf("%s: start scanning from block %d", ob.chain.String(), ob.GetLastBlockHeightScanned()) - - return &ob, nil -} - -// WithChain attaches a new chain to the observer -func (ob *Observer) WithChain(chain chains.Chain) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.chain = chain -} - -// WithLogger attaches a new logger to the observer -func (ob *Observer) WithLogger(logger zerolog.Logger) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.logger = Logger{ - Chain: logger, - Inbound: logger.With().Str("module", "WatchInbound").Logger(), - Outbound: logger.With().Str("module", "WatchOutbound").Logger(), - GasPrice: logger.With().Str("module", "WatchGasPrice").Logger(), - } + return ob, nil } // WithEvmClient attaches a new evm client to the observer func (ob *Observer) WithEvmClient(client interfaces.EVMRPCClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() ob.evmClient = client } // WithEvmJSONRPC attaches a new evm json rpc client to the observer func (ob *Observer) WithEvmJSONRPC(client interfaces.EVMJSONRPCClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() ob.evmJSONRPC = client } -// WithZetacoreClient attaches a new client to interact with zetacore to the observer -func (ob *Observer) WithZetacoreClient(client interfaces.ZetacoreClient) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.zetacoreClient = client -} - -// WithBlockCache attaches a new block cache to the observer -func (ob *Observer) WithBlockCache(cache *lru.Cache) { - ob.Mu.Lock() - defer ob.Mu.Unlock() - ob.blockCache = cache -} - -// Chain returns the chain for the observer -func (ob *Observer) Chain() chains.Chain { - ob.Mu.Lock() - defer ob.Mu.Unlock() - return ob.chain -} - // SetChainParams sets the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) SetChainParams(params observertypes.ChainParams) { ob.Mu.Lock() defer ob.Mu.Unlock() - ob.chainParams = params + ob.WithChainParams(params) } // GetChainParams returns the chain params for the observer +// Note: chain params is accessed concurrently func (ob *Observer) GetChainParams() observertypes.ChainParams { ob.Mu.Lock() defer ob.Mu.Unlock() - return ob.chainParams + return ob.ChainParams() } +// GetConnectorContract returns the non-Eth connector address and binder func (ob *Observer) GetConnectorContract() (ethcommon.Address, *zetaconnector.ZetaConnectorNonEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) - contract, err := FetchConnectorContract(addr, ob.evmClient) + contract, err := zetaconnector.NewZetaConnectorNonEth(addr, ob.evmClient) return addr, contract, err } +// GetConnectorContractEth returns the Eth connector address and binder func (ob *Observer) GetConnectorContractEth() (ethcommon.Address, *zetaconnectoreth.ZetaConnectorEth, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().ConnectorContractAddress) contract, err := FetchConnectorContractEth(addr, ob.evmClient) return addr, contract, err } -func (ob *Observer) GetZetaTokenNonEthContract() (ethcommon.Address, *zeta.ZetaNonEth, error) { - addr := ethcommon.HexToAddress(ob.GetChainParams().ZetaTokenContractAddress) - contract, err := FetchZetaZetaNonEthTokenContract(addr, ob.evmClient) - return addr, contract, err -} - +// GetERC20CustodyContract returns ERC20Custody contract address and binder func (ob *Observer) GetERC20CustodyContract() (ethcommon.Address, *erc20custody.ERC20Custody, error) { addr := ethcommon.HexToAddress(ob.GetChainParams().Erc20CustodyContractAddress) - contract, err := FetchERC20CustodyContract(addr, ob.evmClient) + contract, err := erc20custody.NewERC20Custody(addr, ob.evmClient) return addr, contract, err } -func FetchConnectorContract( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*zetaconnector.ZetaConnectorNonEth, error) { - return zetaconnector.NewZetaConnectorNonEth(addr, client) -} - +// FetchConnectorContractEth returns the Eth connector address and binder func FetchConnectorContractEth( addr ethcommon.Address, client interfaces.EVMRPCClient, @@ -265,22 +159,18 @@ func FetchConnectorContractEth( return zetaconnectoreth.NewZetaConnectorEth(addr, client) } -func FetchZetaZetaNonEthTokenContract( +// FetchZetaTokenContract returns the non-Eth ZETA token binder +func FetchZetaTokenContract( addr ethcommon.Address, client interfaces.EVMRPCClient, ) (*zeta.ZetaNonEth, error) { return zeta.NewZetaNonEth(addr, client) } -func FetchERC20CustodyContract( - addr ethcommon.Address, - client interfaces.EVMRPCClient, -) (*erc20custody.ERC20Custody, error) { - return erc20custody.NewERC20Custody(addr, client) -} - // Start all observation routines for the evm chain func (ob *Observer) Start() { + ob.Logger().Chain.Info().Msgf("observer is starting for chain %d", ob.Chain().ChainId) + // watch evm chain for incoming txs and post votes to zetacore go ob.WatchInbound() @@ -299,7 +189,7 @@ func (ob *Observer) Start() { // WatchRPCStatus watches the RPC status of the evm chain func (ob *Observer) WatchRPCStatus() { - ob.logger.Chain.Info().Msgf("Starting RPC status check for chain %s", ob.chain.String()) + ob.Logger().Chain.Info().Msgf("Starting RPC status check for chain %d", ob.Chain().ChainId) ticker := time.NewTicker(60 * time.Second) for { select { @@ -309,52 +199,35 @@ func (ob *Observer) WatchRPCStatus() { } bn, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } gasPrice, err := ob.evmClient.SuggestGasPrice(context.Background()) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(bn)) if err != nil { - ob.logger.Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") + ob.Logger().Chain.Error().Err(err).Msg("RPC Status Check error: RPC down?") continue } // #nosec G701 always in range blockTime := time.Unix(int64(header.Time), 0).UTC() elapsedSeconds := time.Since(blockTime).Seconds() if elapsedSeconds > 100 { - ob.logger.Chain.Warn(). + ob.Logger().Chain.Warn(). Msgf("RPC Status Check warning: RPC stale or chain stuck (check explorer)? Latest block %d timestamp is %.0fs ago", bn, elapsedSeconds) continue } - ob.logger.Chain.Info(). + ob.Logger().Chain.Info(). Msgf("[OK] RPC status: latest block num %d, timestamp %s ( %.0fs ago), suggested gas price %d", header.Number, blockTime.String(), elapsedSeconds, gasPrice.Uint64()) - case <-ob.stop: + case <-ob.StopChannel(): return } } } -func (ob *Observer) Stop() { - ob.logger.Chain.Info().Msgf("ob %s is stopping", ob.chain.String()) - close(ob.stop) // this notifies all goroutines to stop - - ob.logger.Chain.Info().Msg("closing ob.db") - dbInst, err := ob.db.DB() - if err != nil { - ob.logger.Chain.Info().Msg("error getting database instance") - } - err = dbInst.Close() - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("error closing database") - } - - ob.logger.Chain.Info().Msgf("%s observer stopped", ob.chain.String()) -} - // SetPendingTx sets the pending transaction in memory func (ob *Observer) SetPendingTx(nonce uint64, transaction *ethtypes.Transaction) { ob.Mu.Lock() @@ -419,47 +292,25 @@ func (ob *Observer) CheckTxInclusion(tx *ethtypes.Transaction, receipt *ethtypes return nil } -// SetLastBlockHeightScanned set last block height scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) SetLastBlockHeightScanned(height uint64) { - atomic.StoreUint64(&ob.lastBlockScanned, height) - ob.ts.SetLastScannedBlockNumber(ob.chain, height) -} - -// GetLastBlockHeightScanned get last block height scanned (not necessarily caught up with external block; could be slow/paused) -func (ob *Observer) GetLastBlockHeightScanned() uint64 { - height := atomic.LoadUint64(&ob.lastBlockScanned) - return height -} - -// SetLastBlockHeight set external last block height -func (ob *Observer) SetLastBlockHeight(height uint64) { - atomic.StoreUint64(&ob.lastBlock, height) -} - -// GetLastBlockHeight get external last block height -func (ob *Observer) GetLastBlockHeight() uint64 { - return atomic.LoadUint64(&ob.lastBlock) -} - // WatchGasPrice watches evm chain for gas prices and post to zetacore func (ob *Observer) WatchGasPrice() { // report gas price right away as the ticker takes time to kick in err := ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } // start gas price ticker ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchGasPrice_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchGasPrice_%d", ob.Chain().ChainId), ob.GetChainParams().GasPriceTicker, ) if err != nil { - ob.logger.GasPrice.Error().Err(err).Msg("NewDynamicTicker error") + ob.Logger().GasPrice.Error().Err(err).Msg("NewDynamicTicker error") return } - ob.logger.GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", - ob.chain.ChainId, ob.GetChainParams().GasPriceTicker) + ob.Logger().GasPrice.Info().Msgf("WatchGasPrice started for chain %d with interval %d", + ob.Chain().ChainId, ob.GetChainParams().GasPriceTicker) defer ticker.Stop() for { @@ -470,11 +321,11 @@ func (ob *Observer) WatchGasPrice() { } err = ob.PostGasPrice() if err != nil { - ob.logger.GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.chain.ChainId) + ob.Logger().GasPrice.Error().Err(err).Msgf("PostGasPrice error for chain %d", ob.Chain().ChainId) } - ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.logger.GasPrice) - case <-ob.stop: - ob.logger.GasPrice.Info().Msg("WatchGasPrice stopped") + ticker.UpdateInterval(ob.GetChainParams().GasPriceTicker, ob.Logger().GasPrice) + case <-ob.StopChannel(): + ob.Logger().GasPrice.Info().Msg("WatchGasPrice stopped") return } } @@ -485,21 +336,21 @@ func (ob *Observer) PostGasPrice() error { // GAS PRICE gasPrice, err := ob.evmClient.SuggestGasPrice(context.TODO()) if err != nil { - ob.logger.GasPrice.Err(err).Msg("Err SuggestGasPrice:") + ob.Logger().GasPrice.Err(err).Msg("Err SuggestGasPrice:") return err } blockNum, err := ob.evmClient.BlockNumber(context.TODO()) if err != nil { - ob.logger.GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") + ob.Logger().GasPrice.Err(err).Msg("Err Fetching Most recent Block : ") return err } // SUPPLY supply := "100" // lockedAmount on ETH, totalSupply on other chains - zetaHash, err := ob.zetacoreClient.PostGasPrice(ob.chain, gasPrice.Uint64(), supply, blockNum) + zetaHash, err := ob.ZetacoreClient().PostGasPrice(ob.Chain(), gasPrice.Uint64(), supply, blockNum) if err != nil { - ob.logger.GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") + ob.Logger().GasPrice.Err(err).Msg("PostGasPrice to zetacore failed") return err } _ = zetaHash @@ -521,21 +372,21 @@ func (ob *Observer) TransactionByHash(txHash string) (*ethrpc.Transaction, bool, } func (ob *Observer) GetBlockHeaderCached(blockNumber uint64) (*ethtypes.Header, error) { - if header, ok := ob.headerCache.Get(blockNumber); ok { + if header, ok := ob.HeaderCache().Get(blockNumber); ok { return header.(*ethtypes.Header), nil } header, err := ob.evmClient.HeaderByNumber(context.Background(), new(big.Int).SetUint64(blockNumber)) if err != nil { return nil, err } - ob.headerCache.Add(blockNumber, header) + ob.HeaderCache().Add(blockNumber, header) return header, nil } // GetBlockByNumberCached get block by number from cache // returns block, ethrpc.Block, isFallback, isSkip, error func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, error) { - if block, ok := ob.blockCache.Get(blockNumber); ok { + if block, ok := ob.BlockCache().Get(blockNumber); ok { return block.(*ethrpc.Block), nil } if blockNumber > math.MaxInt32 { @@ -546,13 +397,13 @@ func (ob *Observer) GetBlockByNumberCached(blockNumber uint64) (*ethrpc.Block, e if err != nil { return nil, err } - ob.blockCache.Add(blockNumber, block) + ob.BlockCache().Add(blockNumber, block) return block, nil } // RemoveCachedBlock remove block from cache func (ob *Observer) RemoveCachedBlock(blockNumber uint64) { - ob.blockCache.Remove(blockNumber) + ob.BlockCache().Remove(blockNumber) } // BlockByNumber query block by number via JSON-RPC @@ -570,92 +421,60 @@ func (ob *Observer) BlockByNumber(blockNumber int) (*ethrpc.Block, error) { return block, nil } -// LoadLastScannedBlock loads last scanned block from specified height or from database -// The last scanned block is the height from which the observer should continue scanning for inbound transactions -func (ob *Observer) LoadLastScannedBlock() error { - // get environment variable - envvar := ob.chain.ChainName.String() + "_SCAN_FROM" - scanFromBlock := os.Getenv(envvar) - - // load from environment variable if set - if scanFromBlock != "" { - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: envvar %s is set; scan from block %s", envvar, scanFromBlock) - if scanFromBlock == base.EnvVarLatestBlock { - header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(header.Number.Uint64()) - } else { - scanFromBlockInt, err := strconv.ParseUint(scanFromBlock, 10, 64) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(scanFromBlockInt) - } - } else { - // load from DB otherwise - var lastBlock clienttypes.LastBlockSQLType - if err := ob.db.First(&lastBlock, clienttypes.LastBlockNumID).Error; err != nil { - ob.logger.Chain.Info().Msg("LoadLastScannedBlock: last scanned block not found in DB, scan from latest") - header, err := ob.evmClient.HeaderByNumber(context.Background(), nil) - if err != nil { - return err - } - ob.SetLastBlockHeightScanned(header.Number.Uint64()) - if dbc := ob.db.Save(clienttypes.ToLastBlockSQLType(ob.GetLastBlockHeightScanned())); dbc.Error != nil { - ob.logger.Chain.Error().Err(dbc.Error).Msgf("LoadLastScannedBlock: error writing last scanned block %d to DB", ob.GetLastBlockHeightScanned()) - } - } else { - ob.SetLastBlockHeightScanned(lastBlock.Num) - } +// LoadDB open sql database and load data into EVM observer +func (ob *Observer) LoadDB(dbPath string) error { + if dbPath == "" { + return errors.New("empty db path") } - ob.logger.Chain.Info(). - Msgf("LoadLastScannedBlock: chain %d starts scanning from block %d", ob.chain.ChainId, ob.GetLastBlockHeightScanned()) - return nil -} + // open database + err := ob.OpenDB(dbPath, "") + if err != nil { + return errors.Wrapf(err, "error OpenDB for chain %d", ob.Chain().ChainId) + } -// LoadDB open sql database and load data into EVM observer -func (ob *Observer) LoadDB(dbPath string, chain chains.Chain) error { - if dbPath != "" { - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - err := os.MkdirAll(dbPath, os.ModePerm) - if err != nil { - return err - } - } - path := fmt.Sprintf("%s/%s", dbPath, chain.ChainName.String()) //Use "file::memory:?cache=shared" for temp db - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) - if err != nil { - ob.logger.Chain.Error(). - Err(err). - Msgf("failed to open observer database for %s", ob.chain.ChainName.String()) - return err - } + // run auto migration + // transaction and receipt tables are used nowhere but we still run migration in case they are needed in future + err = ob.DB().AutoMigrate( + &clienttypes.ReceiptSQLType{}, + &clienttypes.TransactionSQLType{}, + ) + if err != nil { + return errors.Wrapf(err, "error AutoMigrate for chain %d", ob.Chain().ChainId) + } - err = db.AutoMigrate(&clienttypes.ReceiptSQLType{}, - &clienttypes.TransactionSQLType{}, - &clienttypes.LastBlockSQLType{}) - if err != nil { - ob.logger.Chain.Error().Err(err).Msg("error migrating db") - return err - } + // load last block scanned + err = ob.LoadLastBlockScanned() + + return err +} - ob.db = db - err = ob.LoadLastScannedBlock() +// LoadLastBlockScanned loads the last scanned block from the database +func (ob *Observer) LoadLastBlockScanned() error { + err := ob.Observer.LoadLastBlockScanned(ob.Logger().Chain) + if err != nil { + return errors.Wrapf(err, "error LoadLastBlockScanned for chain %d", ob.Chain().ChainId) + } + + // observer will scan from the last block when 'lastBlockScanned == 0', this happens when: + // 1. environment variable is set explicitly to "latest" + // 2. environment variable is empty and last scanned block is not found in DB + if ob.LastBlockScanned() == 0 { + blockNumber, err := ob.evmClient.BlockNumber(context.Background()) if err != nil { - return err + return errors.Wrapf(err, "error BlockNumber for chain %d", ob.Chain().ChainId) } + ob.WithLastBlockScanned(blockNumber) } + ob.Logger().Chain.Info().Msgf("chain %d starts scanning from block %d", ob.Chain().ChainId, ob.LastBlockScanned()) + return nil } func (ob *Observer) postBlockHeader(tip uint64) error { bn := tip - res, err := ob.zetacoreClient.GetBlockHeaderChainState(ob.chain.ChainId) + res, err := ob.ZetacoreClient().GetBlockHeaderChainState(ob.Chain().ChainId) if err == nil && res.ChainState != nil && res.ChainState.EarliestHeight > 0 { // #nosec G701 always positive bn = uint64(res.ChainState.LatestHeight) + 1 // the next header to post @@ -667,23 +486,23 @@ func (ob *Observer) postBlockHeader(tip uint64) error { header, err := ob.GetBlockHeaderCached(bn) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error getting block: %d", bn) return err } headerRLP, err := rlp.EncodeToBytes(header) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error encoding block header: %d", bn) return err } - _, err = ob.zetacoreClient.PostVoteBlockHeader( - ob.chain.ChainId, + _, err = ob.ZetacoreClient().PostVoteBlockHeader( + ob.Chain().ChainId, header.Hash().Bytes(), header.Number.Int64(), proofs.NewEthereumHeader(headerRLP), ) if err != nil { - ob.logger.Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) + ob.Logger().Inbound.Error().Err(err).Msgf("postBlockHeader: error posting block header: %d", bn) return err } return nil diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 0601c083f2..9ef7e47e1b 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -1,6 +1,8 @@ package observer_test import ( + "fmt" + "os" "sync" "testing" @@ -21,6 +23,7 @@ import ( "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/context" "github.com/zeta-chain/zetacore/zetaclient/keys" + "github.com/zeta-chain/zetacore/zetaclient/metrics" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) @@ -28,17 +31,24 @@ import ( // the relative path to the testdata directory var TestDataDir = "../../../" -// getAppContext creates an app context for unit tests -func getAppContext( +// getZetacoreContext creates a zetacore context for unit tests +func getZetacoreContext( evmChain chains.Chain, + endpoint string, evmChainParams *observertypes.ChainParams, -) (*context.AppContext, config.EVMConfig) { +) (*context.ZetacoreContext, config.EVMConfig) { + // use default endpoint if not provided + if endpoint == "" { + endpoint = "http://localhost:8545" + } + // create config cfg := config.NewConfig() cfg.EVMChainConfigs[evmChain.ChainId] = config.EVMConfig{ Chain: evmChain, - Endpoint: "http://localhost:8545", + Endpoint: endpoint, } + // create zetacore context coreCtx := context.NewZetacoreContext(cfg) evmChainParamsMap := make(map[int64]*observertypes.ChainParams) @@ -57,8 +67,7 @@ func getAppContext( zerolog.Logger{}, ) // create app context - appCtx := context.NewAppContext(coreCtx, cfg) - return appCtx, cfg.EVMChainConfigs[evmChain.ChainId] + return coreCtx, cfg.EVMChainConfigs[evmChain.ChainId] } // MockEVMObserver creates a mock ChainObserver with custom chain, TSS, params etc @@ -69,8 +78,15 @@ func MockEVMObserver( evmJSONRPC interfaces.EVMJSONRPCClient, zetacoreClient interfaces.ZetacoreClient, tss interfaces.TSSSigner, + dbpath string, lastBlock uint64, - params observertypes.ChainParams) *observer.Observer { + params observertypes.ChainParams, +) *observer.Observer { + // use default mock evm client if not provided + if evmClient == nil { + evmClient = mocks.NewMockEvmClient().WithBlockNumber(1000) + } + // use default mock zetacore client if not provided if zetacoreClient == nil { zetacoreClient = mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) @@ -79,17 +95,203 @@ func MockEVMObserver( if tss == nil { tss = mocks.NewTSSMainnet() } - // create app context - appCtx, evmCfg := getAppContext(chain, ¶ms) + // create zetacore context + coreCtx, evmCfg := getZetacoreContext(chain, "", ¶ms) - // create chain observer - client, err := observer.NewObserver(appCtx, zetacoreClient, tss, "", base.Logger{}, evmCfg, nil) + // create observer + ob, err := observer.NewObserver(evmCfg, evmClient, params, coreCtx, zetacoreClient, tss, dbpath, base.Logger{}, nil) require.NoError(t, err) - client.WithEvmClient(evmClient) - client.WithEvmJSONRPC(evmJSONRPC) - client.SetLastBlockHeight(lastBlock) + ob.WithEvmJSONRPC(evmJSONRPC) + ob.WithLastBlock(lastBlock) + + return ob +} + +func Test_NewObserver(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) + + // test cases + tests := []struct { + name string + evmCfg config.EVMConfig + chainParams observertypes.ChainParams + evmClient interfaces.EVMRPCClient + tss interfaces.TSSSigner + dbpath string + logger base.Logger + ts *metrics.TelemetryServer + fail bool + message string + }{ + { + name: "should be able to create observer", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: false, + }, + { + name: "should fail on invalid dbpath", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + evmClient: mocks.NewMockEvmClient().WithBlockNumber(1000), + tss: mocks.NewTSSMainnet(), + dbpath: "/invalid/dbpath", // invalid dbpath + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error creating db path", + }, + { + name: "should fail if RPC call fails", + evmCfg: config.EVMConfig{ + Chain: chain, + Endpoint: "http://localhost:8545", + }, + chainParams: params, + evmClient: mocks.NewMockEvmClient().WithError(fmt.Errorf("error RPC")), + tss: mocks.NewTSSMainnet(), + dbpath: testutils.CreateTempDir(t), + logger: base.Logger{}, + ts: nil, + fail: true, + message: "error RPC", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create zetacore context, client and tss + zetacoreCtx, _ := getZetacoreContext(tt.evmCfg.Chain, tt.evmCfg.Endpoint, ¶ms) + zetacoreClient := mocks.NewMockZetacoreClient().WithKeys(&keys.Keys{}) + + // create observer + ob, err := observer.NewObserver( + tt.evmCfg, + tt.evmClient, + tt.chainParams, + zetacoreCtx, + zetacoreClient, + tt.tss, + tt.dbpath, + tt.logger, + tt.ts, + ) + + // check result + if tt.fail { + require.ErrorContains(t, err, tt.message) + require.Nil(t, ob) + } else { + require.NoError(t, err) + require.NotNil(t, ob) + } + }) + } +} + +func Test_LoadDB(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) + dbpath := testutils.CreateTempDir(t) + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, dbpath, 1, params) + + t.Run("should load db successfully", func(t *testing.T) { + err := ob.LoadDB(dbpath) + require.NoError(t, err) + require.EqualValues(t, 1000, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid dbpath", func(t *testing.T) { + // load db with empty dbpath + err := ob.LoadDB("") + require.ErrorContains(t, err, "empty db path") + + // load db with invalid dbpath + err = ob.LoadDB("/invalid/dbpath") + require.ErrorContains(t, err, "error OpenDB") + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load db + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer + tempClient := mocks.NewMockEvmClient() + ob := MockEVMObserver(t, chain, tempClient, nil, nil, nil, dbpath, 1, params) + + // set RPC error + tempClient.WithError(fmt.Errorf("error RPC")) + + // load db + err := ob.LoadDB(dbpath) + require.ErrorContains(t, err, "error RPC") + }) +} + +func Test_LoadLastBlockScanned(t *testing.T) { + // use Ethereum chain for testing + chain := chains.Ethereum + params := mocks.MockChainParams(chain.ChainId, 10) - return client + // create observer using mock evm client + evmClient := mocks.NewMockEvmClient().WithBlockNumber(100) + dbpath := testutils.CreateTempDir(t) + ob := MockEVMObserver(t, chain, evmClient, nil, nil, nil, dbpath, 1, params) + + t.Run("should load last block scanned", func(t *testing.T) { + // create db and write 123 as last block scanned + ob.WriteLastBlockScannedToDB(123) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.NoError(t, err) + require.EqualValues(t, 123, ob.LastBlockScanned()) + }) + t.Run("should fail on invalid env var", func(t *testing.T) { + // set invalid environment variable + envvar := base.EnvVarLatestBlockByChain(chain) + os.Setenv(envvar, "invalid") + defer os.Unsetenv(envvar) + + // load last block scanned + err := ob.LoadLastBlockScanned() + require.ErrorContains(t, err, "error LoadLastBlockScanned") + }) + t.Run("should fail on RPC error", func(t *testing.T) { + // create observer on separate path, as we need to reset last block scanned + otherPath := testutils.CreateTempDir(t) + obOther := MockEVMObserver(t, chain, evmClient, nil, nil, nil, otherPath, 1, params) + + // reset last block scanned to 0 so that it will be loaded from RPC + obOther.WithLastBlockScanned(0) + + // set RPC error + evmClient.WithError(fmt.Errorf("error RPC")) + + // load last block scanned + err := obOther.LoadLastBlockScanned() + require.ErrorContains(t, err, "error RPC") + }) } func Test_BlockCache(t *testing.T) { diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 8dda7ac034..9c3bd1c66b 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -29,33 +29,33 @@ import ( // GetTxID returns a unique id for outbound tx func (ob *Observer) GetTxID(nonce uint64) string { - tssAddr := ob.Tss.EVMAddress().String() - return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddr, nonce) + tssAddr := ob.TSS().EVMAddress().String() + return fmt.Sprintf("%d-%s-%d", ob.Chain().ChainId, tssAddr, nonce) } // WatchOutbound watches evm chain for outgoing txs status func (ob *Observer) WatchOutbound() { ticker, err := clienttypes.NewDynamicTicker( - fmt.Sprintf("EVM_WatchOutbound_%d", ob.chain.ChainId), + fmt.Sprintf("EVM_WatchOutbound_%d", ob.Chain().ChainId), ob.GetChainParams().OutboundTicker, ) if err != nil { - ob.logger.Outbound.Error().Err(err).Msg("error creating ticker") + ob.Logger().Outbound.Error().Err(err).Msg("error creating ticker") return } - ob.logger.Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.chain.ChainId) - sampledLogger := ob.logger.Outbound.Sample(&zerolog.BasicSampler{N: 10}) + ob.Logger().Outbound.Info().Msgf("WatchOutbound started for chain %d", ob.Chain().ChainId) + sampledLogger := ob.Logger().Outbound.Sample(&zerolog.BasicSampler{N: 10}) defer ticker.Stop() for { select { case <-ticker.C(): - if !clientcontext.IsOutboundObservationEnabled(ob.coreContext, ob.GetChainParams()) { + if !clientcontext.IsOutboundObservationEnabled(ob.ZetacoreContext(), ob.GetChainParams()) { sampledLogger.Info(). - Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.chain.ChainId) + Msgf("WatchOutbound: outbound observation is disabled for chain %d", ob.Chain().ChainId) continue } - trackers, err := ob.zetacoreClient.GetAllOutboundTrackerByChain(ob.chain.ChainId, interfaces.Ascending) + trackers, err := ob.ZetacoreClient().GetAllOutboundTrackerByChain(ob.Chain().ChainId, interfaces.Ascending) if err != nil { continue } @@ -72,23 +72,23 @@ func (ob *Observer) WatchOutbound() { txCount++ outboundReceipt = receipt outbound = tx - ob.logger.Outbound.Info(). - Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.chain.ChainId, nonceInt) + ob.Logger().Outbound.Info(). + Msgf("WatchOutbound: confirmed outbound %s for chain %d nonce %d", txHash.TxHash, ob.Chain().ChainId, nonceInt) if txCount > 1 { - ob.logger.Outbound.Error().Msgf( - "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.chain.ChainId, nonceInt, outboundReceipt, outbound) + ob.Logger().Outbound.Error().Msgf( + "WatchOutbound: checkConfirmedTx passed, txCount %d chain %d nonce %d receipt %v transaction %v", txCount, ob.Chain().ChainId, nonceInt, outboundReceipt, outbound) } } } if txCount == 1 { // should be only one txHash confirmed for each nonce. ob.SetTxNReceipt(nonceInt, outboundReceipt, outbound) } else if txCount > 1 { // should not happen. We can't tell which txHash is true. It might happen (e.g. glitchy/hacked endpoint) - ob.logger.Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.chain.ChainId, nonceInt) + ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, ob.Chain().ChainId, nonceInt) } } - ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.logger.Outbound) - case <-ob.stop: - ob.logger.Outbound.Info().Msg("WatchOutbound: stopped") + ticker.UpdateInterval(ob.GetChainParams().OutboundTicker, ob.Logger().Outbound) + case <-ob.StopChannel(): + ob.Logger().Outbound.Info().Msg("WatchOutbound: stopped") return } } @@ -105,8 +105,8 @@ func (ob *Observer) PostVoteOutbound( cointype coin.CoinType, logger zerolog.Logger, ) { - chainID := ob.chain.ChainId - zetaTxHash, ballot, err := ob.zetacoreClient.PostVoteOutbound( + chainID := ob.Chain().ChainId + zetaTxHash, ballot, err := ob.ZetacoreClient().PostVoteOutbound( cctxIndex, receipt.TxHash.Hex(), receipt.BlockNumber.Uint64(), @@ -115,7 +115,7 @@ func (ob *Observer) PostVoteOutbound( transaction.Gas(), receiveValue, receiveStatus, - ob.chain, + ob.Chain(), nonce, cointype, ) @@ -137,17 +137,17 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg return false, false, nil } receipt, transaction := ob.GetTxNReceipt(nonce) - sendID := fmt.Sprintf("%d-%d", ob.chain.ChainId, nonce) + sendID := fmt.Sprintf("%d-%d", ob.Chain().ChainId, nonce) logger = logger.With().Str("sendID", sendID).Logger() // get connector and erce20Custody contracts connectorAddr, connector, err := ob.GetConnectorContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.chain.ChainId) + return false, false, errors.Wrapf(err, "error getting zeta connector for chain %d", ob.Chain().ChainId) } custodyAddr, custody, err := ob.GetERC20CustodyContract() if err != nil { - return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.chain.ChainId) + return false, false, errors.Wrapf(err, "error getting erc20 custody for chain %d", ob.Chain().ChainId) } // define a few common variables @@ -181,7 +181,7 @@ func (ob *Observer) IsOutboundProcessed(cctx *crosschaintypes.CrossChainTx, logg if err != nil { logger.Error(). Err(err). - Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.chain.ChainId, receipt.TxHash) + Msgf("IsOutboundProcessed: error parsing outbound event for chain %d txhash %s", ob.Chain().ChainId, receipt.TxHash) return false, false, err } @@ -346,7 +346,7 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.chain.ChainId) + Msgf("confirmTxByHash: error getting transaction for outbound %s chain %d", txHash, ob.Chain().ChainId) return nil, nil, false } if transaction == nil { // should not happen @@ -355,17 +355,17 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check tx sender and nonce - signer := ethtypes.NewLondonSigner(big.NewInt(ob.chain.ChainId)) + signer := ethtypes.NewLondonSigner(big.NewInt(ob.Chain().ChainId)) from, err := signer.Sender(transaction) if err != nil { log.Error(). Err(err). - Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.chain.ChainId) + Msgf("confirmTxByHash: local recovery of sender address failed for outbound %s chain %d", transaction.Hash().Hex(), ob.Chain().ChainId) return nil, nil, false } - if from != ob.Tss.EVMAddress() { // must be TSS address + if from != ob.TSS().EVMAddress() { // must be TSS address log.Error().Msgf("confirmTxByHash: sender %s for outbound %s chain %d is not TSS address %s", - from.Hex(), transaction.Hash().Hex(), ob.chain.ChainId, ob.Tss.EVMAddress().Hex()) + from.Hex(), transaction.Hash().Hex(), ob.Chain().ChainId, ob.TSS().EVMAddress().Hex()) return nil, nil, false } if transaction.Nonce() != nonce { // must match cctx nonce @@ -394,10 +394,10 @@ func (ob *Observer) checkConfirmedTx(txHash string, nonce uint64) (*ethtypes.Rec } // check confirmations - if !ob.HasEnoughConfirmations(receipt, ob.GetLastBlockHeight()) { + if !ob.HasEnoughConfirmations(receipt, ob.LastBlock()) { log.Debug(). Msgf("confirmTxByHash: txHash %s nonce %d included but not confirmed: receipt block %d, current block %d", - txHash, nonce, receipt.BlockNumber, ob.GetLastBlockHeight()) + txHash, nonce, receipt.BlockNumber, ob.LastBlock()) return nil, nil, false } diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index e0806d6086..df9a642a8e 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -13,12 +13,17 @@ import ( "github.com/zeta-chain/zetacore/pkg/coin" "github.com/zeta-chain/zetacore/testutil/sample" observertypes "github.com/zeta-chain/zetacore/x/observer/types" + "github.com/zeta-chain/zetacore/zetaclient/chains/base" "github.com/zeta-chain/zetacore/zetaclient/chains/evm/observer" "github.com/zeta-chain/zetacore/zetaclient/config" "github.com/zeta-chain/zetacore/zetaclient/testutils" "github.com/zeta-chain/zetacore/zetaclient/testutils/mocks" ) +const ( + memDBPath = base.TempSQLiteDBPath +) + // getContractsByChainID is a helper func to get contracts and addresses by chainID func getContractsByChainID( t *testing.T, @@ -57,11 +62,12 @@ func Test_IsOutboundProcessed(t *testing.T) { ) t.Run("should post vote and return true if outbound is processed", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) + // post outbound vote - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) @@ -73,9 +79,9 @@ func Test_IsOutboundProcessed(t *testing.T) { cctx := testutils.LoadCctxByNonce(t, chainID, nonce) cctx.InboundParams.Sender = sample.EthAddress().Hex() - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) // modify compliance config to restrict sender address cfg := config.Config{ @@ -85,29 +91,29 @@ func Test_IsOutboundProcessed(t *testing.T) { config.LoadComplianceConfig(cfg) // post outbound vote - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.True(t, isIncluded) require.True(t, isConfirmed) }) t.Run("should return false if outbound is not confirmed", func(t *testing.T) { - // create evm client and DO NOT set outbound as confirmed - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + // create evm observer and DO NOT set outbound as confirmed + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.NoError(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) }) t.Run("should fail if unable to parse ZetaReceived event", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) // set connector contract address to an arbitrary address to make event parsing fail - chainParamsNew := client.GetChainParams() + chainParamsNew := ob.GetChainParams() chainParamsNew.ConnectorContractAddress = sample.EthAddress().Hex() - client.SetChainParams(chainParamsNew) - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + ob.SetChainParams(chainParamsNew) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.Error(t, err) require.False(t, isIncluded) require.False(t, isConfirmed) @@ -147,15 +153,15 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { ) t.Run("should fail if unable to get connector/custody contract", func(t *testing.T) { - // create evm client and set outbound and receipt - client := MockEVMObserver(t, chain, nil, nil, nil, nil, 1, chainParam) - client.SetTxNReceipt(nonce, receipt, outbound) + // create evm observer and set outbound and receipt + ob := MockEVMObserver(t, chain, nil, nil, nil, nil, memDBPath, 1, chainParam) + ob.SetTxNReceipt(nonce, receipt, outbound) abiConnector := zetaconnector.ZetaConnectorNonEthMetaData.ABI abiCustody := erc20custody.ERC20CustodyMetaData.ABI // set invalid connector ABI zetaconnector.ZetaConnectorNonEthMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err := client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err := ob.IsOutboundProcessed(cctx, zerolog.Logger{}) zetaconnector.ZetaConnectorNonEthMetaData.ABI = abiConnector // reset connector ABI require.ErrorContains(t, err, "error getting zeta connector") require.False(t, isIncluded) @@ -163,7 +169,7 @@ func Test_IsOutboundProcessed_ContractError(t *testing.T) { // set invalid custody ABI erc20custody.ERC20CustodyMetaData.ABI = "invalid abi" - isIncluded, isConfirmed, err = client.IsOutboundProcessed(cctx, zerolog.Logger{}) + isIncluded, isConfirmed, err = ob.IsOutboundProcessed(cctx, zerolog.Logger{}) require.ErrorContains(t, err, "error getting erc20 custody") require.False(t, isIncluded) require.False(t, isConfirmed) @@ -193,8 +199,8 @@ func Test_PostVoteOutbound(t *testing.T) { // create evm client using mock zetacore client and post outbound vote zetacoreClient := mocks.NewMockZetacoreClient() - client := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, 1, observertypes.ChainParams{}) - client.PostVoteOutbound( + ob := MockEVMObserver(t, chain, nil, nil, zetacoreClient, nil, memDBPath, 1, observertypes.ChainParams{}) + ob.PostVoteOutbound( cctx.Index, receipt, outbound, @@ -207,7 +213,7 @@ func Test_PostVoteOutbound(t *testing.T) { // pause the mock zetacore client to simulate error posting vote zetacoreClient.Pause() - client.PostVoteOutbound( + ob.PostVoteOutbound( cctx.Index, receipt, outbound, diff --git a/zetaclient/chains/evm/signer/outbound_data_test.go b/zetaclient/chains/evm/signer/outbound_data_test.go index fbb4d5ef88..d7df5a33d1 100644 --- a/zetaclient/chains/evm/signer/outbound_data_test.go +++ b/zetaclient/chains/evm/signer/outbound_data_test.go @@ -70,7 +70,7 @@ func TestSigner_NewOutboundData(t *testing.T) { evmSigner, err := getNewEvmSigner(nil) require.NoError(t, err) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) t.Run("NewOutboundData success", func(t *testing.T) { diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index 410ea5adf3..bdce93f60c 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -59,22 +59,34 @@ func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { } // getNewEvmChainObserver creates a new EVM chain observer for testing -func getNewEvmChainObserver(tss interfaces.TSSSigner) (*observer.Observer, error) { +func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.Observer, error) { // use default mock TSS if not provided if tss == nil { tss = mocks.NewTSSMainnet() } - - logger := base.Logger{} - ts := &metrics.TelemetryServer{} cfg := config.NewConfig() + // prepare mock arguments to create observer evmcfg := config.EVMConfig{Chain: chains.BscMainnet, Endpoint: "http://localhost:8545"} + evmClient := mocks.NewMockEvmClient().WithBlockNumber(1000) + params := mocks.MockChainParams(evmcfg.Chain.ChainId, 10) cfg.EVMChainConfigs[chains.BscMainnet.ChainId] = evmcfg coreCTX := context.NewZetacoreContext(cfg) - appCTX := context.NewAppContext(coreCTX, cfg) + dbpath := testutils.CreateTempDir(t) + logger := base.Logger{} + ts := &metrics.TelemetryServer{} - return observer.NewObserver(appCTX, mocks.NewMockZetacoreClient(), tss, "", logger, evmcfg, ts) + return observer.NewObserver( + evmcfg, + evmClient, + params, + coreCTX, + mocks.NewMockZetacoreClient(), + tss, + dbpath, + logger, + ts, + ) } func getNewOutboundProcessor() *outboundprocessor.Processor { @@ -145,7 +157,7 @@ func TestSigner_TryProcessOutbound(t *testing.T) { require.NoError(t, err) cctx := getCCTX(t) processor := getNewOutboundProcessor() - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) // Test with mock client that has keys @@ -166,7 +178,7 @@ func TestSigner_SignOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -200,7 +212,7 @@ func TestSigner_SignRevertTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -238,7 +250,7 @@ func TestSigner_SignCancelTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -276,7 +288,7 @@ func TestSigner_SignWithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -312,7 +324,7 @@ func TestSigner_SignCommandTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -357,7 +369,7 @@ func TestSigner_SignERC20WithdrawTx(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -395,7 +407,7 @@ func TestSigner_BroadcastOutbound(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(nil) + mockObserver, err := getNewEvmChainObserver(t, nil) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -445,7 +457,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) @@ -488,7 +500,7 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { // Setup txData struct cctx := getCCTX(t) - mockObserver, err := getNewEvmChainObserver(tss) + mockObserver, err := getNewEvmChainObserver(t, tss) require.NoError(t, err) txData, skip, err := NewOutboundData(cctx, mockObserver, evmSigner.EvmClient(), zerolog.Logger{}, 123) require.False(t, skip) diff --git a/zetaclient/supplychecker/zeta_supply_checker.go b/zetaclient/supplychecker/zeta_supply_checker.go index f975c7eb88..e27d64fb12 100644 --- a/zetaclient/supplychecker/zeta_supply_checker.go +++ b/zetaclient/supplychecker/zeta_supply_checker.go @@ -123,7 +123,7 @@ func (zs *ZetaSupplyChecker) CheckZetaTokenSupply() error { zetaTokenAddressString := externalEvmChainParams.ZetaTokenContractAddress zetaTokenAddress := ethcommon.HexToAddress(zetaTokenAddressString) - zetatokenNonEth, err := observer.FetchZetaZetaNonEthTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) + zetatokenNonEth, err := observer.FetchZetaTokenContract(zetaTokenAddress, zs.evmClient[chain.ChainId]) if err != nil { return err }